Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ff30f2a0e | ||
|
|
7707c5e558 | ||
|
|
9b7a920e0d | ||
|
|
2e51380be5 | ||
|
|
445f508ea5 | ||
|
|
b82c89c582 | ||
|
|
85a47a8049 | ||
|
|
f8e9798038 | ||
|
|
581d6747ad | ||
|
|
00a1aa7628 | ||
|
|
79a9048db5 | ||
|
|
2bfba2e399 | ||
|
|
484419e526 | ||
|
|
8c18830c97 | ||
|
|
653d370a85 | ||
|
|
316e91cfcf | ||
|
|
f31632c8c8 | ||
|
|
bb52c13f9a | ||
|
|
738d43fd83 | ||
|
|
1c6c8588d9 | ||
|
|
7a97cb2e02 | ||
|
|
5e0d2992c9 | ||
|
|
210859a5ef | ||
|
|
25f6d229dd | ||
|
|
60405abf58 | ||
|
|
8070b11c27 | ||
|
|
7d76ce77c9 | ||
|
|
3d48cbc111 | ||
|
|
36f34da227 | ||
|
|
d06e07542e | ||
|
|
a0c8646923 | ||
|
|
6c924de209 | ||
|
|
500503c069 | ||
|
|
5dd8cd66dd | ||
|
|
41e84e55f1 | ||
|
|
7f8888d7dd | ||
|
|
216928f904 | ||
|
|
345bab3a99 | ||
|
|
064a7a113c | ||
|
|
115d5c7db1 | ||
|
|
8a86ecc38d | ||
|
|
430a2d71e3 | ||
|
|
187c0d2a52 | ||
|
|
8fbe77afb2 | ||
|
|
e43e92f2b0 | ||
|
|
cc6c0ee7df | ||
|
|
3cd0665166 | ||
|
|
a42e696695 | ||
|
|
83886fbcf1 | ||
|
|
f0c5046ccf | ||
|
|
9db2a65b31 | ||
|
|
59c5619b44 | ||
|
|
b39016df7c | ||
|
|
c22fe6320d | ||
|
|
b99ccdf612 | ||
|
|
2d338ffbfb | ||
|
|
739cb9306a | ||
|
|
954fc29613 | ||
|
|
630f71b349 | ||
|
|
2b3f274aae | ||
|
|
e834e46ef6 | ||
|
|
27f10cd10e | ||
|
|
f4b92ee690 | ||
|
|
1c4d3d5f13 | ||
|
|
7bb7a092a4 | ||
|
|
0ed623caf9 | ||
|
|
19cd40cfdd | ||
|
|
7617a1ef8e | ||
|
|
a370afe94b | ||
|
|
ff5a1ac7cb | ||
|
|
893c7e3ab2 | ||
|
|
17698bfeae | ||
|
|
8b90e99658 | ||
|
|
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 |
12
.gitignore
vendored
@@ -1,4 +1,14 @@
|
||||
*.so
|
||||
*.pyc
|
||||
*.pyo
|
||||
__pycache__/
|
||||
*.log
|
||||
|
||||
/lib/Solaar.egg-info/
|
||||
/build/
|
||||
/sdist/
|
||||
/dist/
|
||||
/deb_dist/
|
||||
/MANIFEST
|
||||
|
||||
/docs/captures/
|
||||
/share/logitech_icons/
|
||||
|
||||
2
COPYRIGHT
Normal file
@@ -0,0 +1,2 @@
|
||||
Copyright 2012, 2013
|
||||
Daniel Pavel <daniel.pavel@gmail.com>
|
||||
13
ChangeLog
Normal file
@@ -0,0 +1,13 @@
|
||||
0.8.7:
|
||||
|
||||
* Don't show the "device disconnected" notification, it can be annoying and
|
||||
not very useful.
|
||||
* More robust detection of systray icon visibility.
|
||||
|
||||
0.8.6:
|
||||
|
||||
* Ensure the Gtk application is single-instance.
|
||||
* Fix identifying available dpi values.
|
||||
* Fixed locating application icons when installed in a custom prefix.
|
||||
* Fixed some icon names for the oxygen theme.
|
||||
* Python 3 fixes.
|
||||
2
MANIFEST.in
Normal file
@@ -0,0 +1,2 @@
|
||||
include COPYRIGHT COPYING README.md ChangeLog
|
||||
recursive-include rules.d *
|
||||
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
|
||||
141
README.md
Normal file
@@ -0,0 +1,141 @@
|
||||
**Solaar** is a Linux device manager for Logitech's [Unifying Receiver][unifying]
|
||||
peripherals. It is able to pair/unpair devices to the receiver, and for most
|
||||
devices read battery status.
|
||||
|
||||
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.
|
||||
|
||||
For some devices, extra settings (usually not available through the standard
|
||||
Linux system configuration) are supported:
|
||||
|
||||
* The [K750 Solar Keyboard][K750] 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.
|
||||
|
||||
* The state of the `FN` key can be toggled on some keyboards ([K750][K750],
|
||||
[K800][K800] and [K360][K360]). It changes the way the function keys
|
||||
(`F1`..`F12`) work, i.e. whether holding `FN` while pressing the function keys
|
||||
will generate the standard `Fx` keycodes or the special function (yellow
|
||||
icons) keycodes.
|
||||
|
||||
* The DPI can be changed on the [Performance MX Mouse][P_MX].
|
||||
|
||||
* Smooth scrolling (higher sensitivity on vertical scrolling with the wheel) can
|
||||
be toggled on the [M705 Marathon Mouse][M705] and [Anywhere MX Mouse][A_MX].
|
||||
|
||||
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 directly test on right now.
|
||||
|
||||
|
||||
## Pre-built packages
|
||||
|
||||
* Ubuntu 12.04+ packages are available in my PPA: [ppa:daniel.pavel/Solaar][ppa]
|
||||
* A downloadable Debian package for sid/unstable: [.deb][debian]
|
||||
* A [Gentoo overlay][gentoo] is available courtesy of Carlos Silva
|
||||
|
||||
[ppa]: http://launchpad.net/~daniel.pavel/+archive/solaar
|
||||
[debian]: http://pwr.github.com/Solaar/packages/solaar_0.8.6.2-1_all.deb
|
||||
[gentoo]: http://code.r3pek.org/gentoo-overlay/src
|
||||
|
||||
|
||||
## Manual instalation
|
||||
|
||||
### 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
|
||||
|
||||
- Ubuntu's Unity indicators are not supported at this time. However, if you
|
||||
whitelist 'Solaar' in the systray, you will get an icon (see
|
||||
[Enable more icons to be in the system tray?][ubuntu_systray] for details).
|
||||
|
||||
[ubuntu_systray]: http://askubuntu.com/questions/30742
|
||||
|
||||
- The application only looks at the first Unifying Receiver it finds, even if
|
||||
there's more than one plugged in. Support for multiple receivers is in
|
||||
progress.
|
||||
|
||||
- Devices connected throught a [Nano Receiver][nano] (which is very similar to
|
||||
the Unifying Receiver) are not supported at this time.
|
||||
|
||||
- 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)
|
||||
|
||||
Also thanks to Douglas Wagner and Julien Gascard for helping with application
|
||||
testing and supporting new devices.
|
||||
|
||||
--
|
||||
|
||||
[unifying]: http://logitech.com/en-us/66/6079
|
||||
[nano]: http://logitech.com/mice-pointers/articles/5926
|
||||
[K750]: http://logitech.com/product/k750-keyboard
|
||||
[K800]: http://logitech.com/product/wireless-illuminated-keyboard-k800
|
||||
[K360]: http://logitech.com/product/keyboard-k360
|
||||
[M705]: http://logitech.com/product/marathon-mouse-m705
|
||||
[P_MX]: http://logitech.com/product/performance-mouse-mx
|
||||
[A_MX]: http://logitech.com/product/anywhere-mouse-mx
|
||||
@@ -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()
|
||||
127
app/solaar.py
@@ -1,127 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
NAME = 'Solaar'
|
||||
VERSION = '0.7.2'
|
||||
__author__ = "Daniel Pavel <daniel.pavel@gmail.com>"
|
||||
__version__ = VERSION
|
||||
__license__ = "GPL"
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
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',
|
||||
help='disable desktop notifications (shown only when in systray)')
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
import ui
|
||||
|
||||
# check if the notifications are available and enabled
|
||||
args.notifications &= args.systray
|
||||
if ui.notify.available and ui.notify.init(NAME):
|
||||
ui.action.toggle_notifications.set_active(args.notifications)
|
||||
else:
|
||||
ui.action.toggle_notifications = None
|
||||
|
||||
from receiver import DUMMY
|
||||
window = ui.main_window.create(NAME, DUMMY.name, DUMMY.max_devices, args.systray)
|
||||
if args.systray:
|
||||
menu_actions = (ui.action.toggle_notifications,
|
||||
ui.action.about)
|
||||
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)
|
||||
|
||||
global listener
|
||||
if not listener:
|
||||
GObject.timeout_add(5000, check_for_listener)
|
||||
listener = None
|
||||
|
||||
from receiver import ReceiverListener
|
||||
def check_for_listener(retry=True):
|
||||
global listener, notify_missing
|
||||
|
||||
if listener is None:
|
||||
try:
|
||||
listener = ReceiverListener.open(status_changed)
|
||||
except OSError:
|
||||
ui.show_permissions_warning(window)
|
||||
|
||||
if listener is None:
|
||||
pairing.state = None
|
||||
if notify_missing:
|
||||
status_changed(DUMMY, None, True)
|
||||
notify_missing = False
|
||||
return retry
|
||||
|
||||
# print ("opened receiver", listener, listener.receiver)
|
||||
notify_missing = True
|
||||
pairing.state = pairing.State(listener)
|
||||
status_changed(listener.receiver, None, True)
|
||||
|
||||
GObject.timeout_add(100, check_for_listener, False)
|
||||
Gtk.main()
|
||||
|
||||
if listener is not None:
|
||||
listener.stop()
|
||||
|
||||
ui.notify.uninit()
|
||||
@@ -1,70 +0,0 @@
|
||||
# pass
|
||||
|
||||
from . import (notify, status_icon, main_window, pair_window, action)
|
||||
|
||||
from gi.repository import (GObject, Gtk)
|
||||
GObject.threads_init()
|
||||
|
||||
|
||||
from solaar import NAME as _NAME
|
||||
_APP_ICONS = (_NAME + '-fail', _NAME + '-init', _NAME)
|
||||
def appicon(receiver_status):
|
||||
return (_APP_ICONS[0] if receiver_status < 0 else
|
||||
_APP_ICONS[1] if receiver_status < 1 else
|
||||
_APP_ICONS[2])
|
||||
|
||||
|
||||
_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 show_permissions_warning(window):
|
||||
text = ('Found a possible Unifying Receiver device,\n'
|
||||
'but did not have permission to open it.')
|
||||
|
||||
m = Gtk.MessageDialog(window, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, text)
|
||||
m.set_title('Permissions error')
|
||||
m.run()
|
||||
m.destroy()
|
||||
|
||||
|
||||
def find_children(container, *child_names):
|
||||
assert container is not None
|
||||
|
||||
def _iterate_children(widget, names, result, count):
|
||||
wname = widget.get_name()
|
||||
if wname in names:
|
||||
index = names.index(wname)
|
||||
names[index] = None
|
||||
result[index] = widget
|
||||
count -= 1
|
||||
|
||||
if count > 0 and isinstance(widget, Gtk.Container):
|
||||
for w in widget:
|
||||
count = _iterate_children(w, names, result, count)
|
||||
if count == 0:
|
||||
break
|
||||
|
||||
return count
|
||||
|
||||
names = list(child_names)
|
||||
count = len(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)
|
||||
@@ -1,91 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
# from sys import version as PYTTHON_VERSION
|
||||
from gi.repository import Gtk
|
||||
|
||||
import ui.notify
|
||||
import ui.pair_window
|
||||
from solaar import NAME as _NAME
|
||||
from solaar import VERSION as _VERSION
|
||||
|
||||
|
||||
def _action(name, label, function, *args):
|
||||
action = Gtk.Action(name, label, label, None)
|
||||
action.set_icon_name(name)
|
||||
if function:
|
||||
action.connect('activate', function, *args)
|
||||
return action
|
||||
|
||||
|
||||
def _toggle_action(name, label, function, *args):
|
||||
action = Gtk.ToggleAction(name, label, label, None)
|
||||
action.set_icon_name(name)
|
||||
action.connect('activate', function, *args)
|
||||
return action
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _toggle_notifications(action):
|
||||
if action.get_active():
|
||||
ui.notify.init(_NAME)
|
||||
else:
|
||||
ui.notify.uninit()
|
||||
action.set_sensitive(ui.notify.available)
|
||||
toggle_notifications = _toggle_action('notifications', 'Notifications', _toggle_notifications)
|
||||
|
||||
|
||||
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_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_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)
|
||||
|
||||
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.set_transient_for(window)
|
||||
pair_dialog.set_modal(True)
|
||||
|
||||
window.present()
|
||||
pair_dialog.present()
|
||||
|
||||
def pair(frame):
|
||||
return _action('add', 'Pair new device', _pair_device, frame)
|
||||
|
||||
|
||||
def _unpair_device(action, frame):
|
||||
window = frame.get_toplevel()
|
||||
window.present()
|
||||
device = frame._device
|
||||
qdialog = Gtk.MessageDialog(window, 0,
|
||||
Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO,
|
||||
"Unpair device\n%s ?" % device.name)
|
||||
choice = qdialog.run()
|
||||
qdialog.destroy()
|
||||
if choice == Gtk.ResponseType.YES:
|
||||
pairing.state.unpair(device)
|
||||
|
||||
def unpair(frame):
|
||||
return _action('remove', 'Unpair', _unpair_device, frame)
|
||||
@@ -1,304 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from gi.repository import (Gtk, Gdk)
|
||||
|
||||
import ui
|
||||
from logitech.devices.constants import (STATUS, PROPS)
|
||||
|
||||
|
||||
_SMALL_DEVICE_ICON_SIZE = Gtk.IconSize.BUTTON
|
||||
_DEVICE_ICON_SIZE = Gtk.IconSize.DIALOG
|
||||
_STATUS_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR
|
||||
_PLACEHOLDER = '~'
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _info_text(dev):
|
||||
fw_text = '\n'.join(['%-12s\t<tt>%s%s%s</tt>' %
|
||||
(f.kind, f.name, ' ' if f.name else '', f.version) for f in dev.firmware])
|
||||
return ('<small>'
|
||||
'Serial \t\t<tt>%s</tt>\n'
|
||||
'HID protocol\t<tt>%1.1f</tt>\n'
|
||||
'%s'
|
||||
'</small>' % (dev.serial, dev.protocol, fw_text))
|
||||
|
||||
def _toggle_info(action, label_widget, box_widget, frame):
|
||||
if action.get_active():
|
||||
box_widget.set_visible(True)
|
||||
if not label_widget.get_text():
|
||||
label_widget.set_markup(_info_text(frame._device))
|
||||
else:
|
||||
box_widget.set_visible(False)
|
||||
|
||||
|
||||
def _make_receiver_box(name):
|
||||
frame = Gtk.Frame()
|
||||
frame._device = None
|
||||
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)
|
||||
|
||||
label = Gtk.Label('Scanning...')
|
||||
label.set_name('label')
|
||||
label.set_alignment(0, 0.5)
|
||||
|
||||
toolbar = Gtk.Toolbar()
|
||||
toolbar.set_name('toolbar')
|
||||
toolbar.set_style(Gtk.ToolbarStyle.ICONS)
|
||||
toolbar.set_icon_size(Gtk.IconSize.MENU)
|
||||
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)
|
||||
|
||||
info_label = Gtk.Label()
|
||||
info_label.set_name('info-label')
|
||||
info_label.set_alignment(0, 0.5)
|
||||
info_label.set_padding(8, 2)
|
||||
info_label.set_selectable(True)
|
||||
|
||||
info_box = Gtk.Frame()
|
||||
info_box.add(info_label)
|
||||
info_box.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
|
||||
|
||||
toggle_info_action = ui.action._toggle_action('info', 'Receiver info', _toggle_info, info_label, info_box, frame)
|
||||
toolbar.insert(toggle_info_action.create_tool_item(), 0)
|
||||
toolbar.insert(ui.action.pair(frame).create_tool_item(), -1)
|
||||
|
||||
vbox = Gtk.VBox(homogeneous=False, spacing=2)
|
||||
vbox.set_border_width(4)
|
||||
vbox.pack_start(hbox, True, True, 0)
|
||||
vbox.pack_start(info_box, True, True, 0)
|
||||
|
||||
frame.add(vbox)
|
||||
frame.show_all()
|
||||
info_box.set_visible(False)
|
||||
return frame
|
||||
|
||||
|
||||
def _make_device_box(index):
|
||||
frame = Gtk.Frame()
|
||||
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.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)
|
||||
|
||||
battery_icon = Gtk.Image.new_from_icon_name('battery_unknown', _STATUS_ICON_SIZE)
|
||||
|
||||
battery_label = Gtk.Label()
|
||||
battery_label.set_width_chars(6)
|
||||
battery_label.set_alignment(0, 0.5)
|
||||
|
||||
light_icon = Gtk.Image.new_from_icon_name('light_unknown', _STATUS_ICON_SIZE)
|
||||
|
||||
light_label = Gtk.Label()
|
||||
light_label.set_alignment(0, 0.5)
|
||||
light_label.set_width_chars(8)
|
||||
|
||||
toolbar = Gtk.Toolbar()
|
||||
toolbar.set_name('toolbar')
|
||||
toolbar.set_style(Gtk.ToolbarStyle.ICONS)
|
||||
toolbar.set_icon_size(Gtk.IconSize.MENU)
|
||||
toolbar.set_show_arrow(False)
|
||||
|
||||
status_box = Gtk.HBox(homogeneous=False, spacing=0)
|
||||
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)
|
||||
|
||||
info_label = Gtk.Label()
|
||||
info_label.set_name('info-label')
|
||||
info_label.set_alignment(0, 0.5)
|
||||
info_label.set_padding(8, 2)
|
||||
info_label.set_selectable(True)
|
||||
|
||||
info_box = Gtk.Frame()
|
||||
info_box.add(info_label)
|
||||
|
||||
toggle_info_action = ui.action._toggle_action('info', 'Device info', _toggle_info, info_label, info_box, frame)
|
||||
toolbar.insert(toggle_info_action.create_tool_item(), 0)
|
||||
toolbar.insert(ui.action.unpair(frame).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)
|
||||
|
||||
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()
|
||||
|
||||
frame.add(box)
|
||||
info_box.set_visible(False)
|
||||
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.set_border_width(4)
|
||||
|
||||
rbox = _make_receiver_box(name)
|
||||
vbox.add(rbox)
|
||||
for i in range(1, 1 + max_devices):
|
||||
dbox = _make_device_box(i)
|
||||
vbox.add(dbox)
|
||||
vbox.set_visible(True)
|
||||
|
||||
window.add(vbox)
|
||||
|
||||
geometry = Gdk.Geometry()
|
||||
geometry.min_width = 320
|
||||
geometry.min_height = 32
|
||||
window.set_geometry_hints(vbox, geometry, Gdk.WindowHints.MIN_SIZE)
|
||||
window.set_resizable(False)
|
||||
|
||||
window.toggle_visible = lambda i: toggle(window, i)
|
||||
|
||||
if systray:
|
||||
window.set_keep_above(True)
|
||||
window.connect('delete-event', toggle)
|
||||
else:
|
||||
window.connect('delete-event', Gtk.main_quit)
|
||||
|
||||
return window
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _update_receiver_box(frame, receiver):
|
||||
label, toolbar, info_label = ui.find_children(frame, 'label', 'toolbar', 'info-label')
|
||||
|
||||
label.set_text(receiver.status_text or '')
|
||||
if receiver.status < STATUS.CONNECTED:
|
||||
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
|
||||
|
||||
|
||||
def _update_device_box(frame, dev):
|
||||
frame._device = dev
|
||||
|
||||
icon, label, info_label = ui.find_children(frame, 'icon', 'label', 'info-label')
|
||||
|
||||
if frame.get_name() != dev.name:
|
||||
frame.set_name(dev.name)
|
||||
icon_name = ui.get_icon(dev.name, dev.kind)
|
||||
icon.set_from_icon_name(icon_name, _DEVICE_ICON_SIZE)
|
||||
label.set_markup('<b>' + dev.name + '</b>')
|
||||
|
||||
status = ui.find_children(frame, 'status')
|
||||
status_icons = status.get_children()
|
||||
toolbar = status_icons[-1]
|
||||
if dev.status < STATUS.CONNECTED:
|
||||
icon.set_sensitive(False)
|
||||
label.set_sensitive(False)
|
||||
status.set_sensitive(False)
|
||||
for c in status_icons[1:-1]:
|
||||
c.set_visible(False)
|
||||
toolbar.get_children()[0].set_active(False)
|
||||
else:
|
||||
icon.set_sensitive(True)
|
||||
label.set_sensitive(True)
|
||||
status.set_sensitive(True)
|
||||
|
||||
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)
|
||||
else:
|
||||
icon_name = 'battery_%03d' % (20 * ((battery_level + 10) // 20))
|
||||
battery_icon.set_from_icon_name(icon_name, _STATUS_ICON_SIZE)
|
||||
battery_icon.set_sensitive(True)
|
||||
battery_label.set_text('%d%%' % battery_level)
|
||||
battery_label.set_visible(True)
|
||||
|
||||
battery_status = dev.props.get(PROPS.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)
|
||||
if light_level is None:
|
||||
light_icon.set_visible(False)
|
||||
light_label.set_visible(False)
|
||||
else:
|
||||
icon_name = 'light_%03d' % (20 * ((light_level + 50) // 100))
|
||||
light_icon.set_from_icon_name(icon_name, _STATUS_ICON_SIZE)
|
||||
light_icon.set_visible(True)
|
||||
light_label.set_text('%d lux' % light_level)
|
||||
light_label.set_visible(True)
|
||||
|
||||
for b in toolbar.get_children()[:-1]:
|
||||
b.set_sensitive(True)
|
||||
|
||||
frame.set_visible(True)
|
||||
|
||||
|
||||
def update(window, receiver, device=None):
|
||||
# print ("update", receiver, receiver.status, device)
|
||||
window.set_icon_name(ui.appicon(receiver.status))
|
||||
|
||||
vbox = window.get_child()
|
||||
frames = list(vbox.get_children())
|
||||
|
||||
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
|
||||
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)
|
||||
@@ -1,125 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
# import logging
|
||||
from gi.repository import (Gtk, GObject)
|
||||
|
||||
import ui
|
||||
|
||||
|
||||
def _create_page(assistant, text, kind):
|
||||
p = Gtk.VBox(False, 12)
|
||||
p.set_border_width(8)
|
||||
|
||||
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)
|
||||
|
||||
p.show_all()
|
||||
return p
|
||||
|
||||
|
||||
def _device_confirmed(entry, _2, trigger, assistant, page):
|
||||
assistant.commit()
|
||||
assistant.set_page_complete(page, True)
|
||||
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):
|
||||
index = assistant.get_current_page()
|
||||
# logging.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)
|
||||
else:
|
||||
page = _create_page(assistant,
|
||||
None,
|
||||
Gtk.AssistantPageType.CONFIRM)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
assistant.next_page()
|
||||
|
||||
def _scan_complete(assistant, device):
|
||||
GObject.idle_add(_scan_complete_ui, assistant, device)
|
||||
|
||||
|
||||
def create(action, state):
|
||||
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_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)
|
||||
spinner = Gtk.Spinner()
|
||||
spinner.set_visible(True)
|
||||
page_intro.pack_end(spinner, True, True, 16)
|
||||
|
||||
assistant.scan_complete = _scan_complete
|
||||
|
||||
assistant.connect('prepare', _prepare, state)
|
||||
assistant.connect('cancel', _cancel, state)
|
||||
assistant.connect('close', _finish)
|
||||
assistant.connect('apply', _finish)
|
||||
|
||||
return assistant
|
||||
@@ -1,55 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from gi.repository import Gtk
|
||||
import ui
|
||||
|
||||
|
||||
def create(window, menu_actions=None):
|
||||
icon = Gtk.StatusIcon()
|
||||
icon.set_title(window.get_title())
|
||||
icon.set_name(window.get_title())
|
||||
icon.set_from_icon_name(ui.appicon(0))
|
||||
|
||||
icon.connect('activate', window.toggle_visible)
|
||||
|
||||
menu = Gtk.Menu()
|
||||
for action in menu_actions or ():
|
||||
if action:
|
||||
menu.append(action.create_menu_item())
|
||||
|
||||
menu.append(ui.action.quit.create_menu_item())
|
||||
menu.show_all()
|
||||
|
||||
icon.connect('popup_menu',
|
||||
lambda icon, button, time, menu:
|
||||
menu.popup(None, None, icon.position_menu, icon, button, time),
|
||||
menu)
|
||||
|
||||
return icon
|
||||
|
||||
|
||||
def update(icon, receiver):
|
||||
icon.set_from_icon_name(ui.appicon(receiver.status))
|
||||
|
||||
if receiver.devices:
|
||||
lines = []
|
||||
if receiver.status < 1:
|
||||
lines += (receiver.status_text, '')
|
||||
|
||||
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 + ')')
|
||||
else:
|
||||
lines.append(name)
|
||||
if dev.status > 1:
|
||||
lines.append(' ' + dev.status_text)
|
||||
lines.append('')
|
||||
|
||||
text = '\n'.join(lines).rstrip('\n')
|
||||
icon.set_tooltip_markup(text)
|
||||
else:
|
||||
icon.set_tooltip_text(receiver.status_text)
|
||||
@@ -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 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 "$@"
|
||||
33
bin/solaar
@@ -1,13 +1,26 @@
|
||||
#!/bin/sh
|
||||
#!/usr/bin/env python
|
||||
# -*- python-mode -*-
|
||||
"""Takes care of starting the main function."""
|
||||
|
||||
Z=`readlink -f "$0"`
|
||||
APP=`readlink -f $(dirname "$Z")/../app`
|
||||
LIB=`readlink -f $(dirname "$Z")/../lib`
|
||||
SHARE=`readlink -f $(dirname "$Z")/../share`
|
||||
from __future__ import absolute_import
|
||||
|
||||
#export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`arch`
|
||||
export PYTHONPATH=$APP:$LIB
|
||||
export XDG_DATA_DIRS=$SHARE:$XDG_DATA_DIRS
|
||||
|
||||
PYTHON=`which python python2 python3 | head -n 1`
|
||||
exec $PYTHON -OOu -m solaar "$@"
|
||||
def init_paths():
|
||||
"""Make the app work in the source tree."""
|
||||
import sys
|
||||
import os.path as _path
|
||||
|
||||
prefix = _path.normpath(_path.join(_path.realpath(sys.path[0]), '..'))
|
||||
src_lib = _path.join(prefix, 'lib')
|
||||
share_lib = _path.join(prefix, 'share', 'solaar', 'lib')
|
||||
for location in src_lib, share_lib:
|
||||
init_py = _path.join(location, 'solaar', '__init__.py')
|
||||
if _path.exists(init_py):
|
||||
sys.path[0] = location
|
||||
break
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_paths()
|
||||
import solaar.gtk
|
||||
solaar.gtk.main()
|
||||
|
||||
26
bin/solaar-cli
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- python-mode -*-
|
||||
"""Takes care of starting the main function."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
|
||||
def init_paths():
|
||||
"""Make the app work in the source tree."""
|
||||
import sys
|
||||
import os.path as _path
|
||||
|
||||
prefix = _path.normpath(_path.join(_path.realpath(sys.path[0]), '..'))
|
||||
src_lib = _path.join(prefix, 'lib')
|
||||
share_lib = _path.join(prefix, 'share', 'solaar', 'lib')
|
||||
for location in src_lib, share_lib:
|
||||
init_py = _path.join(location, 'solaar', '__init__.py')
|
||||
if _path.exists(init_py):
|
||||
sys.path[0] = location
|
||||
break
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_paths()
|
||||
import solaar.cli
|
||||
solaar.cli.main()
|
||||
BIN
docs/20121210110342697.pdf
Normal file
35
docs/devices/m510.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
# ?
|
||||
<< ( 0.001) [10 01 8100 000000] '\x10\x01\x81\x00\x00\x00\x00'
|
||||
>> ( 0.062) [10 01 8100 000000] '\x10\x01\x81\x00\x00\x00\x00'
|
||||
|
||||
# ?
|
||||
<< ( 1.063) [10 01 8101 000000] '\x10\x01\x81\x01\x00\x00\x00'
|
||||
>> ( 1.078) [10 01 8101 820000] '\x10\x01\x81\x01\x82\x00\x00'
|
||||
|
||||
# ?
|
||||
<< ( 2.079) [10 01 8102 000000] '\x10\x01\x81\x02\x00\x00\x00'
|
||||
>> ( 2.094) [10 01 8102 000080] '\x10\x01\x81\x02\x00\x00\x80'
|
||||
|
||||
# ?
|
||||
<< ( 7.263) [10 01 8107 000000] '\x10\x01\x81\x07\x00\x00\x00'
|
||||
>> ( 7.278) [10 01 8107 050000] '\x10\x01\x81\x07\x05\x00\x00'
|
||||
|
||||
# ?
|
||||
<< ( 41.121) [10 01 8128 000000] '\x10\x01\x81(\x00\x00\x00'
|
||||
>> ( 41.136) [10 01 8128 000200] '\x10\x01\x81(\x00\x02\x00'
|
||||
|
||||
# ?
|
||||
<< ( 215.788) [10 01 81D0 000000] '\x10\x01\x81\xd0\x00\x00\x00'
|
||||
>> ( 215.802) [10 01 81D0 000000] '\x10\x01\x81\xd0\x00\x00\x00'
|
||||
|
||||
# read-only, 01-04 firmware info
|
||||
<< ( 250.779) [10 01 81F1 000000] '\x10\x01\x81\xf1\x00\x00\x00'
|
||||
>> ( 250.794) [10 01 8F81 F10300] '\x10\x01\x8f\x81\xf1\x03\x00'
|
||||
|
||||
# ?
|
||||
<< ( 252.809) [10 01 81F3 000000] '\x10\x01\x81\xf3\x00\x00\x00'
|
||||
>> ( 252.824) [10 01 81F3 000000] '\x10\x01\x81\xf3\x00\x00\x00'
|
||||
|
||||
# ?
|
||||
<< ( 253.825) [10 01 81F4 000000] '\x10\x01\x81\xf4\x00\x00\x00'
|
||||
>> ( 253.838) [10 01 81F4 800000] '\x10\x01\x81\xf4\x80\x00\x00'
|
||||
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,7 @@
|
||||
"""Generic Human Interface Device API."""
|
||||
|
||||
__author__ = "Daniel Pavel"
|
||||
__license__ = "GPL"
|
||||
__version__ = "0.4"
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
try:
|
||||
from hidapi.udev import *
|
||||
except ImportError:
|
||||
from hidapi.native import *
|
||||
__version__ = "0.5"
|
||||
|
||||
from hidapi.udev import *
|
||||
|
||||
@@ -1,109 +1,220 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
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()
|
||||
import hidapi
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
# no Python 3 support :(
|
||||
read_packet = raw_input
|
||||
interactive = os.isatty(0)
|
||||
start_time = 0
|
||||
try:
|
||||
read_packet = raw_input
|
||||
except:
|
||||
read_packet = input
|
||||
prompt = '?? Input: ' if interactive else ''
|
||||
|
||||
strhex = lambda d: hexlify(d).decode('ascii').upper()
|
||||
start_time = time.time()
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from threading import Lock
|
||||
print_lock = Lock()
|
||||
del 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", device)
|
||||
handle = hidapi.open_path(device)
|
||||
if not handle:
|
||||
sys.exit("!! Failed to open %s, aborting." % device)
|
||||
|
||||
print (".. Opened handle %r, vendor %r product %r serial %r." % (
|
||||
handle,
|
||||
hidapi.get_manufacturer(handle),
|
||||
hidapi.get_product(handle),
|
||||
hidapi.get_serial(handle)))
|
||||
if hidpp:
|
||||
if hidapi.get_manufacturer(handle) != b'Logitech':
|
||||
sys.exit("!! Only Logitech devices support the HID++ protocol.")
|
||||
print (".. HID++ validation enabled.")
|
||||
|
||||
return handle
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _parse_arguments():
|
||||
import argparse
|
||||
arg_parser = argparse.ArgumentParser()
|
||||
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')
|
||||
return arg_parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = _parse_arguments()
|
||||
handle = _open(args.device, args.hidpp)
|
||||
|
||||
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:
|
||||
readline.read_history_file(args.history)
|
||||
except:
|
||||
# file may not exist yet
|
||||
pass
|
||||
|
||||
# re-open stdout unbuffered
|
||||
try:
|
||||
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
|
||||
except:
|
||||
# will fail in python3
|
||||
pass
|
||||
|
||||
try:
|
||||
from threading import Thread
|
||||
t = Thread(target=_continuous_read, args=(handle,))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
if interactive:
|
||||
# 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 %r" % handle)
|
||||
hidapi.close(handle)
|
||||
if interactive:
|
||||
readline.write_history_file(args.history)
|
||||
|
||||
|
||||
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')
|
||||
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.")
|
||||
|
||||
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()
|
||||
|
||||
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:
|
||||
pass
|
||||
except Exception as e:
|
||||
print ('%s: %s' % (type(e).__name__, e))
|
||||
|
||||
print (".. Closing handle %X" % handle)
|
||||
hidapi.close(handle)
|
||||
if interactive:
|
||||
readline.write_history_file(args.history)
|
||||
else:
|
||||
print ("!! Failed to open %s, aborting" % args.device)
|
||||
main()
|
||||
|
||||
@@ -1,380 +1,384 @@
|
||||
"""Generic Human Interface Device API.
|
||||
|
||||
It is little more than a thin ctypes layer over a native hidapi implementation.
|
||||
The docstrings are mostly copied from the hidapi API header, with changes where
|
||||
necessary.
|
||||
|
||||
The native HID API implemenation is available at
|
||||
https://github.com/signal11/hidapi.
|
||||
|
||||
The native implementation comes in two flavors, hidraw (``libhidapi-hidraw.so``)
|
||||
and libusb (``libhidapi-libusb.so``). For this API to work, at least one of them
|
||||
must be in ``LD_LIBRARY_PATH``; otherwise an ImportError will be raised.
|
||||
|
||||
Using the native hidraw implementation is recommended.
|
||||
Currently the native libusb implementation (temporarily) detaches the device's
|
||||
USB driver from the kernel, and it may cause the device to become unresponsive.
|
||||
"""
|
||||
|
||||
__version__ = '0.3-hidapi-0.7.0'
|
||||
# """Generic Human Interface Device API.
|
||||
|
||||
# It is little more than a thin ctypes layer over a native hidapi implementation.
|
||||
# The docstrings are mostly copied from the hidapi API header, with changes where
|
||||
# necessary.
|
||||
|
||||
# The native HID API implemenation is available at
|
||||
# https://github.com/signal11/hidapi.
|
||||
|
||||
# The native implementation comes in two flavors, hidraw (``libhidapi-hidraw.so``)
|
||||
# and libusb (``libhidapi-libusb.so``). For this API to work, at least one of them
|
||||
# must be in ``LD_LIBRARY_PATH``; otherwise an ImportError will be raised.
|
||||
|
||||
# Using the native hidraw implementation is recommended.
|
||||
# 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'
|
||||
|
||||
|
||||
import ctypes as _C
|
||||
from struct import pack as _pack
|
||||
# import ctypes as _C
|
||||
# from struct import pack as _pack
|
||||
|
||||
|
||||
#
|
||||
# look for a native implementation in the same directory as this file
|
||||
#
|
||||
# #
|
||||
# # look for a native implementation in the same directory as this file
|
||||
# #
|
||||
|
||||
# The CDLL native library object.
|
||||
_native = None
|
||||
# # The CDLL native library object.
|
||||
# _native = None
|
||||
|
||||
for native_implementation in ('hidraw', 'libusb'):
|
||||
try:
|
||||
_native = _C.cdll.LoadLibrary('libhidapi-' + native_implementation + '.so')
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
# for native_implementation in ('hidraw', 'libusb'):
|
||||
# try:
|
||||
# _native = _C.cdll.LoadLibrary('libhidapi-' + native_implementation + '.so')
|
||||
# break
|
||||
# except OSError:
|
||||
# pass
|
||||
|
||||
if _native is None:
|
||||
raise ImportError('hidapi: failed to load any HID API native implementation')
|
||||
# if _native is None:
|
||||
# raise ImportError('hidapi: failed to load any HID API native implementation')
|
||||
|
||||
|
||||
#
|
||||
# Structures used by this API.
|
||||
#
|
||||
# #
|
||||
# # Structures used by this API.
|
||||
# #
|
||||
|
||||
|
||||
# used by the native implementation when enumerating, no need to expose it
|
||||
class _NativeDeviceInfo(_C.Structure):
|
||||
pass
|
||||
_NativeDeviceInfo._fields_ = [
|
||||
('path', _C.c_char_p),
|
||||
('vendor_id', _C.c_ushort),
|
||||
('product_id', _C.c_ushort),
|
||||
('serial', _C.c_wchar_p),
|
||||
('release', _C.c_ushort),
|
||||
('manufacturer', _C.c_wchar_p),
|
||||
('product', _C.c_wchar_p),
|
||||
('usage_page', _C.c_ushort),
|
||||
('usage', _C.c_ushort),
|
||||
('interface', _C.c_int),
|
||||
('next_device', _C.POINTER(_NativeDeviceInfo))
|
||||
]
|
||||
# # used by the native implementation when enumerating, no need to expose it
|
||||
# class _NativeDeviceInfo(_C.Structure):
|
||||
# pass
|
||||
# _NativeDeviceInfo._fields_ = [
|
||||
# ('path', _C.c_char_p),
|
||||
# ('vendor_id', _C.c_ushort),
|
||||
# ('product_id', _C.c_ushort),
|
||||
# ('serial', _C.c_wchar_p),
|
||||
# ('release', _C.c_ushort),
|
||||
# ('manufacturer', _C.c_wchar_p),
|
||||
# ('product', _C.c_wchar_p),
|
||||
# ('usage_page', _C.c_ushort),
|
||||
# ('usage', _C.c_ushort),
|
||||
# ('interface', _C.c_int),
|
||||
# ('next_device', _C.POINTER(_NativeDeviceInfo))
|
||||
# ]
|
||||
|
||||
|
||||
# the tuple object we'll expose when enumerating devices
|
||||
from collections import namedtuple
|
||||
DeviceInfo = namedtuple('DeviceInfo', [
|
||||
'path',
|
||||
'vendor_id',
|
||||
'product_id',
|
||||
'serial',
|
||||
'release',
|
||||
'manufacturer',
|
||||
'product',
|
||||
'interface',
|
||||
'driver',
|
||||
])
|
||||
del namedtuple
|
||||
# # the tuple object we'll expose when enumerating devices
|
||||
# from collections import namedtuple
|
||||
# DeviceInfo = namedtuple('DeviceInfo', [
|
||||
# 'path',
|
||||
# 'vendor_id',
|
||||
# 'product_id',
|
||||
# 'serial',
|
||||
# 'release',
|
||||
# 'manufacturer',
|
||||
# 'product',
|
||||
# 'interface',
|
||||
# 'driver',
|
||||
# ])
|
||||
# del namedtuple
|
||||
|
||||
|
||||
# create a DeviceInfo tuple from a hid_device object
|
||||
def _makeDeviceInfo(native_device_info):
|
||||
return DeviceInfo(
|
||||
path=native_device_info.path.decode('ascii'),
|
||||
vendor_id=hex(native_device_info.vendor_id)[2:].zfill(4),
|
||||
product_id=hex(native_device_info.product_id)[2:].zfill(4),
|
||||
serial=native_device_info.serial if native_device_info.serial else None,
|
||||
release=hex(native_device_info.release)[2:],
|
||||
manufacturer=native_device_info.manufacturer,
|
||||
product=native_device_info.product,
|
||||
interface=native_device_info.interface,
|
||||
driver=None)
|
||||
# # create a DeviceInfo tuple from a hid_device object
|
||||
# def _makeDeviceInfo(native_device_info):
|
||||
# return DeviceInfo(
|
||||
# path=native_device_info.path.decode('ascii'),
|
||||
# vendor_id=hex(native_device_info.vendor_id)[2:].zfill(4),
|
||||
# product_id=hex(native_device_info.product_id)[2:].zfill(4),
|
||||
# serial=native_device_info.serial if native_device_info.serial else None,
|
||||
# release=hex(native_device_info.release)[2:],
|
||||
# manufacturer=native_device_info.manufacturer,
|
||||
# product=native_device_info.product,
|
||||
# interface=native_device_info.interface,
|
||||
# driver=None)
|
||||
|
||||
|
||||
#
|
||||
# set-up arguments and return types for each hidapi function
|
||||
#
|
||||
# #
|
||||
# # set-up arguments and return types for each hidapi function
|
||||
# #
|
||||
|
||||
_native.hid_init.argtypes = None
|
||||
_native.hid_init.restype = _C.c_int
|
||||
# _native.hid_init.argtypes = None
|
||||
# _native.hid_init.restype = _C.c_int
|
||||
|
||||
_native.hid_exit.argtypes = None
|
||||
_native.hid_exit.restype = _C.c_int
|
||||
# _native.hid_exit.argtypes = None
|
||||
# _native.hid_exit.restype = _C.c_int
|
||||
|
||||
_native.hid_enumerate.argtypes = [_C.c_ushort, _C.c_ushort]
|
||||
_native.hid_enumerate.restype = _C.POINTER(_NativeDeviceInfo)
|
||||
# _native.hid_enumerate.argtypes = [_C.c_ushort, _C.c_ushort]
|
||||
# _native.hid_enumerate.restype = _C.POINTER(_NativeDeviceInfo)
|
||||
|
||||
_native.hid_free_enumeration.argtypes = [_C.POINTER(_NativeDeviceInfo)]
|
||||
_native.hid_free_enumeration.restype = None
|
||||
# _native.hid_free_enumeration.argtypes = [_C.POINTER(_NativeDeviceInfo)]
|
||||
# _native.hid_free_enumeration.restype = None
|
||||
|
||||
_native.hid_open.argtypes = [_C.c_ushort, _C.c_ushort, _C.c_wchar_p]
|
||||
_native.hid_open.restype = _C.c_void_p
|
||||
# _native.hid_open.argtypes = [_C.c_ushort, _C.c_ushort, _C.c_wchar_p]
|
||||
# _native.hid_open.restype = _C.c_void_p
|
||||
|
||||
_native.hid_open_path.argtypes = [_C.c_char_p]
|
||||
_native.hid_open_path.restype = _C.c_void_p
|
||||
# _native.hid_open_path.argtypes = [_C.c_char_p]
|
||||
# _native.hid_open_path.restype = _C.c_void_p
|
||||
|
||||
_native.hid_close.argtypes = [_C.c_void_p]
|
||||
_native.hid_close.restype = None
|
||||
# _native.hid_close.argtypes = [_C.c_void_p]
|
||||
# _native.hid_close.restype = None
|
||||
|
||||
_native.hid_write.argtypes = [_C.c_void_p, _C.c_char_p, _C.c_size_t]
|
||||
_native.hid_write.restype = _C.c_int
|
||||
# _native.hid_write.argtypes = [_C.c_void_p, _C.c_char_p, _C.c_size_t]
|
||||
# _native.hid_write.restype = _C.c_int
|
||||
|
||||
_native.hid_read.argtypes = [_C.c_void_p, _C.c_char_p, _C.c_size_t]
|
||||
_native.hid_read.restype = _C.c_int
|
||||
# _native.hid_read.argtypes = [_C.c_void_p, _C.c_char_p, _C.c_size_t]
|
||||
# _native.hid_read.restype = _C.c_int
|
||||
|
||||
_native.hid_read_timeout.argtypes = [_C.c_void_p, _C.c_char_p, _C.c_size_t, _C.c_int]
|
||||
_native.hid_read_timeout.restype = _C.c_int
|
||||
# _native.hid_read_timeout.argtypes = [_C.c_void_p, _C.c_char_p, _C.c_size_t, _C.c_int]
|
||||
# _native.hid_read_timeout.restype = _C.c_int
|
||||
|
||||
_native.hid_set_nonblocking.argtypes = [_C.c_void_p, _C.c_int]
|
||||
_native.hid_set_nonblocking.restype = _C.c_int
|
||||
# _native.hid_set_nonblocking.argtypes = [_C.c_void_p, _C.c_int]
|
||||
# _native.hid_set_nonblocking.restype = _C.c_int
|
||||
|
||||
_native.hid_send_feature_report.argtypes = [_C.c_void_p, _C.c_char_p, _C.c_size_t]
|
||||
_native.hid_send_feature_report.restype = _C.c_int
|
||||
# _native.hid_send_feature_report.argtypes = [_C.c_void_p, _C.c_char_p, _C.c_size_t]
|
||||
# _native.hid_send_feature_report.restype = _C.c_int
|
||||
|
||||
_native.hid_get_feature_report.argtypes = [_C.c_void_p, _C.c_char_p, _C.c_size_t]
|
||||
_native.hid_get_feature_report.restype = _C.c_int
|
||||
# _native.hid_get_feature_report.argtypes = [_C.c_void_p, _C.c_char_p, _C.c_size_t]
|
||||
# _native.hid_get_feature_report.restype = _C.c_int
|
||||
|
||||
_native.hid_get_manufacturer_string.argtypes = [_C.c_void_p, _C.c_wchar_p, _C.c_size_t]
|
||||
_native.hid_get_manufacturer_string.restype = _C.c_int
|
||||
# _native.hid_get_manufacturer_string.argtypes = [_C.c_void_p, _C.c_wchar_p, _C.c_size_t]
|
||||
# _native.hid_get_manufacturer_string.restype = _C.c_int
|
||||
|
||||
_native.hid_get_product_string.argtypes = [_C.c_void_p, _C.c_wchar_p, _C.c_size_t]
|
||||
_native.hid_get_product_string.restype = _C.c_int
|
||||
# _native.hid_get_product_string.argtypes = [_C.c_void_p, _C.c_wchar_p, _C.c_size_t]
|
||||
# _native.hid_get_product_string.restype = _C.c_int
|
||||
|
||||
_native.hid_get_serial_number_string.argtypes = [_C.c_void_p, _C.c_wchar_p, _C.c_size_t]
|
||||
_native.hid_get_serial_number_string.restype = _C.c_int
|
||||
# _native.hid_get_serial_number_string.argtypes = [_C.c_void_p, _C.c_wchar_p, _C.c_size_t]
|
||||
# _native.hid_get_serial_number_string.restype = _C.c_int
|
||||
|
||||
_native.hid_get_indexed_string.argtypes = [_C.c_void_p, _C.c_int, _C.c_wchar_p, _C.c_size_t]
|
||||
_native.hid_get_indexed_string.restype = _C.c_int
|
||||
# _native.hid_get_indexed_string.argtypes = [_C.c_void_p, _C.c_int, _C.c_wchar_p, _C.c_size_t]
|
||||
# _native.hid_get_indexed_string.restype = _C.c_int
|
||||
|
||||
_native.hid_error.argtypes = [_C.c_void_p]
|
||||
_native.hid_error.restype = _C.c_wchar_p
|
||||
# _native.hid_error.argtypes = [_C.c_void_p]
|
||||
# _native.hid_error.restype = _C.c_wchar_p
|
||||
|
||||
|
||||
#
|
||||
# exposed API
|
||||
# docstrings mostly copied from hidapi.h
|
||||
#
|
||||
# #
|
||||
# # exposed API
|
||||
# # docstrings mostly copied from hidapi.h
|
||||
# #
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize the HIDAPI library.
|
||||
# def init():
|
||||
# """Initialize the HIDAPI library.
|
||||
|
||||
This function initializes the HIDAPI library. Calling it is not strictly
|
||||
necessary, as it will be called automatically by enumerate() and any of the
|
||||
open_*() functions if it is needed. This function should be called at the
|
||||
beginning of execution however, if there is a chance of HIDAPI handles
|
||||
being opened by different threads simultaneously.
|
||||
# This function initializes the HIDAPI library. Calling it is not strictly
|
||||
# necessary, as it will be called automatically by enumerate() and any of the
|
||||
# open_*() functions if it is needed. This function should be called at the
|
||||
# beginning of execution however, if there is a chance of HIDAPI handles
|
||||
# being opened by different threads simultaneously.
|
||||
|
||||
:returns: ``True`` if successful.
|
||||
"""
|
||||
return _native.hid_init() == 0
|
||||
# :returns: ``True`` if successful.
|
||||
# """
|
||||
# return _native.hid_init() == 0
|
||||
|
||||
|
||||
def exit():
|
||||
"""Finalize the HIDAPI library.
|
||||
# def exit():
|
||||
# """Finalize the HIDAPI library.
|
||||
|
||||
This function frees all of the static data associated with HIDAPI. It should
|
||||
be called at the end of execution to avoid memory leaks.
|
||||
# This function frees all of the static data associated with HIDAPI. It should
|
||||
# be called at the end of execution to avoid memory leaks.
|
||||
|
||||
:returns: ``True`` if successful.
|
||||
"""
|
||||
return _native.hid_exit() == 0
|
||||
# :returns: ``True`` if successful.
|
||||
# """
|
||||
# return _native.hid_exit() == 0
|
||||
|
||||
|
||||
def enumerate(vendor_id=None, product_id=None, interface_number=None):
|
||||
"""Enumerate the HID Devices.
|
||||
# def enumerate(vendor_id=None, product_id=None, interface_number=None):
|
||||
# """Enumerate the HID Devices.
|
||||
|
||||
List all the HID devices attached to the system, optionally filtering by
|
||||
vendor_id, product_id, and/or interface_number.
|
||||
# List all the HID devices attached to the system, optionally filtering by
|
||||
# vendor_id, product_id, and/or interface_number.
|
||||
|
||||
:returns: an iterable of matching ``DeviceInfo`` tuples.
|
||||
"""
|
||||
# :returns: an iterable of matching ``DeviceInfo`` tuples.
|
||||
# """
|
||||
|
||||
devices = _native.hid_enumerate(vendor_id, product_id)
|
||||
d = devices
|
||||
while d:
|
||||
if interface_number is None or interface_number == d.contents.interface:
|
||||
yield _makeDeviceInfo(d.contents)
|
||||
d = d.contents.next_device
|
||||
# devices = _native.hid_enumerate(vendor_id, product_id)
|
||||
# d = devices
|
||||
# while d:
|
||||
# if interface_number is None or interface_number == d.contents.interface:
|
||||
# yield _makeDeviceInfo(d.contents)
|
||||
# d = d.contents.next_device
|
||||
|
||||
if devices:
|
||||
_native.hid_free_enumeration(devices)
|
||||
# if devices:
|
||||
# _native.hid_free_enumeration(devices)
|
||||
|
||||
|
||||
def open(vendor_id, product_id, serial=None):
|
||||
"""Open a HID device by its Vendor ID, Product ID and optional serial number.
|
||||
# def open(vendor_id, product_id, serial=None):
|
||||
# """Open a HID device by its Vendor ID, Product ID and optional serial number.
|
||||
|
||||
If no serial is provided, the first device with the specified IDs is opened.
|
||||
# If no serial is provided, the first device with the specified IDs is opened.
|
||||
|
||||
:returns: an opaque device handle, or ``None``.
|
||||
"""
|
||||
return _native.hid_open(vendor_id, product_id, serial) or None
|
||||
# :returns: an opaque device handle, or ``None``.
|
||||
# """
|
||||
# return _native.hid_open(vendor_id, product_id, serial) or None
|
||||
|
||||
|
||||
def open_path(device_path):
|
||||
"""Open a HID device by its path name.
|
||||
# def open_path(device_path):
|
||||
# """Open a HID device by its path name.
|
||||
|
||||
:param device_path: the path of a ``DeviceInfo`` tuple returned by
|
||||
enumerate().
|
||||
# :param device_path: the path of a ``DeviceInfo`` tuple returned by
|
||||
# enumerate().
|
||||
|
||||
:returns: an opaque device handle, or ``None``.
|
||||
"""
|
||||
if type(device_path) == str:
|
||||
device_path = device_path.encode('ascii')
|
||||
return _native.hid_open_path(device_path) or None
|
||||
# :returns: an opaque device handle, or ``None``.
|
||||
# """
|
||||
# if type(device_path) == str:
|
||||
# device_path = device_path.encode('ascii')
|
||||
# return _native.hid_open_path(device_path) or None
|
||||
|
||||
|
||||
def close(device_handle):
|
||||
"""Close a HID device.
|
||||
# def close(device_handle):
|
||||
# """Close a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
"""
|
||||
_native.hid_close(device_handle)
|
||||
# :param device_handle: a device handle returned by open() or open_path().
|
||||
# """
|
||||
# _native.hid_close(device_handle)
|
||||
|
||||
|
||||
def write(device_handle, data):
|
||||
"""Write an Output report to a HID device.
|
||||
# def write(device_handle, data):
|
||||
# """Write an Output report to a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
:param data: the data bytes to send including the report number as the
|
||||
first byte.
|
||||
# :param device_handle: a device handle returned by open() or open_path().
|
||||
# :param data: the data bytes to send including the report number as the
|
||||
# first byte.
|
||||
|
||||
The first byte of data[] must contain the Report ID. For
|
||||
devices which only support a single report, this must be set
|
||||
to 0x0. The remaining bytes contain the report data. Since
|
||||
the Report ID is mandatory, calls to hid_write() will always
|
||||
contain one more byte than the report contains. For example,
|
||||
if a hid report is 16 bytes long, 17 bytes must be passed to
|
||||
hid_write(), the Report ID (or 0x0, for devices with a
|
||||
single report), followed by the report data (16 bytes). In
|
||||
this example, the length passed in would be 17.
|
||||
# The first byte of data[] must contain the Report ID. For
|
||||
# devices which only support a single report, this must be set
|
||||
# to 0x0. The remaining bytes contain the report data. Since
|
||||
# the Report ID is mandatory, calls to hid_write() will always
|
||||
# contain one more byte than the report contains. For example,
|
||||
# if a hid report is 16 bytes long, 17 bytes must be passed to
|
||||
# hid_write(), the Report ID (or 0x0, for devices with a
|
||||
# single report), followed by the report data (16 bytes). In
|
||||
# this example, the length passed in would be 17.
|
||||
|
||||
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).
|
||||
# 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.
|
||||
"""
|
||||
bytes_written = _native.hid_write(device_handle, _C.c_char_p(data), len(data))
|
||||
return bytes_written > -1
|
||||
# :returns: ``True`` if the write was successful.
|
||||
# """
|
||||
# bytes_written = _native.hid_write(device_handle, _C.c_char_p(data), len(data))
|
||||
# return bytes_written > -1
|
||||
|
||||
|
||||
def read(device_handle, bytes_count, timeout_ms=-1):
|
||||
"""Read an Input report from a HID device.
|
||||
# def read(device_handle, bytes_count, timeout_ms=-1):
|
||||
# """Read an Input report from a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
:param bytes_count: maximum number of bytes to read.
|
||||
:param timeout_ms: can be -1 (default) to wait for data indefinitely, 0 to
|
||||
read whatever is in the device's input buffer, or a positive integer to
|
||||
wait that many milliseconds.
|
||||
# :param device_handle: a device handle returned by open() or open_path().
|
||||
# :param bytes_count: maximum number of bytes to read.
|
||||
# :param timeout_ms: can be -1 (default) to wait for data indefinitely, 0 to
|
||||
# read whatever is in the device's input buffer, or a positive integer to
|
||||
# wait that many milliseconds.
|
||||
|
||||
Input reports are returned to the host through the INTERRUPT IN endpoint.
|
||||
The first byte will contain the Report number if the device uses numbered
|
||||
reports.
|
||||
# Input reports are returned to the host through the INTERRUPT IN endpoint.
|
||||
# The first byte will contain the Report number if the device uses numbered
|
||||
# reports.
|
||||
|
||||
:returns: the data packet read, an empty bytes string if a timeout was
|
||||
reached, or None if there was an error while reading.
|
||||
"""
|
||||
out_buffer = _C.create_string_buffer(b'\x00' * (bytes_count + 1))
|
||||
bytes_read = _native.hid_read_timeout(device_handle, out_buffer, bytes_count, timeout_ms)
|
||||
if bytes_read == -1:
|
||||
return None
|
||||
if bytes_read == 0:
|
||||
return b''
|
||||
return out_buffer[:bytes_read]
|
||||
# :returns: the data packet read, an empty bytes string if a timeout was
|
||||
# reached, or None if there was an error while reading.
|
||||
# """
|
||||
# out_buffer = _C.create_string_buffer(b'\x00' * (bytes_count + 1))
|
||||
# bytes_read = _native.hid_read_timeout(device_handle, out_buffer, bytes_count, timeout_ms)
|
||||
# if bytes_read == -1:
|
||||
# return None
|
||||
# if bytes_read == 0:
|
||||
# return b''
|
||||
# return out_buffer[:bytes_read]
|
||||
|
||||
|
||||
def send_feature_report(device_handle, data, report_number=None):
|
||||
"""Send a Feature report to the device.
|
||||
# def send_feature_report(device_handle, data, report_number=None):
|
||||
# """Send a Feature report to the device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
:param data: the data bytes to send including the report number as the
|
||||
first byte.
|
||||
:param report_number: if set, it is sent as the first byte with the data.
|
||||
# :param device_handle: a device handle returned by open() or open_path().
|
||||
# :param data: the data bytes to send including the report number as the
|
||||
# first byte.
|
||||
# :param report_number: if set, it is sent as the first byte with the data.
|
||||
|
||||
Feature reports are sent over the Control endpoint as a
|
||||
Set_Report transfer. The first byte of data[] must
|
||||
contain the Report ID. For devices which only support a
|
||||
single report, this must be set to 0x0. The remaining bytes
|
||||
contain the report data. Since the Report ID is mandatory,
|
||||
calls to send_feature_report() will always contain one
|
||||
more byte than the report contains. For example, if a hid
|
||||
report is 16 bytes long, 17 bytes must be passed to
|
||||
send_feature_report(): the Report ID (or 0x0, for
|
||||
devices which do not use numbered reports), followed by the
|
||||
report data (16 bytes).
|
||||
# Feature reports are sent over the Control endpoint as a
|
||||
# Set_Report transfer. The first byte of data[] must
|
||||
# contain the Report ID. For devices which only support a
|
||||
# single report, this must be set to 0x0. The remaining bytes
|
||||
# contain the report data. Since the Report ID is mandatory,
|
||||
# calls to send_feature_report() will always contain one
|
||||
# more byte than the report contains. For example, if a hid
|
||||
# report is 16 bytes long, 17 bytes must be passed to
|
||||
# send_feature_report(): the Report ID (or 0x0, for
|
||||
# devices which do not use numbered reports), followed by the
|
||||
# report data (16 bytes).
|
||||
|
||||
:returns: ``True`` if the report was successfully written to the device.
|
||||
"""
|
||||
if report_number is not None:
|
||||
data = _pack('!B', report_number) + data
|
||||
bytes_written = _native.hid_send_feature_report(device_handle, _C.c_char_p(data), len(data))
|
||||
return bytes_written > -1
|
||||
# :returns: ``True`` if the report was successfully written to the device.
|
||||
# """
|
||||
# if report_number is not None:
|
||||
# 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
|
||||
|
||||
|
||||
def get_feature_report(device_handle, bytes_count, report_number=None):
|
||||
"""Get a feature report from a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
:param bytes_count: how many bytes to read.
|
||||
:param report_number: if set, it is sent as the report number.
|
||||
|
||||
:returns: the feature report data.
|
||||
"""
|
||||
out_buffer = _C.create_string_buffer('\x00' * (bytes_count + 2))
|
||||
if report_number is not None:
|
||||
out_buffer[0] = _pack('!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]
|
||||
|
||||
|
||||
def _read_wchar(func, device_handle, index=None):
|
||||
_BUFFER_SIZE = 64
|
||||
buf = _C.create_unicode_buffer('\x00' * _BUFFER_SIZE)
|
||||
if index is None:
|
||||
ok = func(device_handle, buf, _BUFFER_SIZE)
|
||||
else:
|
||||
ok = func(device_handle, index, buf, _BUFFER_SIZE)
|
||||
if ok == 0:
|
||||
return buf.value
|
||||
|
||||
|
||||
def get_manufacturer(device_handle):
|
||||
"""Get the Manufacturer String from a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
"""
|
||||
return _read_wchar(_native.hid_get_manufacturer_string, device_handle)
|
||||
|
||||
|
||||
def get_product(device_handle):
|
||||
"""Get the Product String from a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
"""
|
||||
return _read_wchar(_native.hid_get_product_string, device_handle)
|
||||
|
||||
|
||||
def get_serial(device_handle):
|
||||
"""Get the serial number from a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
"""
|
||||
serial = _read_wchar(_native.hid_get_serial_number_string, device_handle)
|
||||
if serial is not None:
|
||||
return ''.join(hex(ord(c)) for c in serial)
|
||||
|
||||
|
||||
def get_indexed_string(device_handle, index):
|
||||
"""Get a string from a HID device, based on its string index.
|
||||
|
||||
Note: currently not working in the ``hidraw`` native implementation.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
:param index: the index of the string to get.
|
||||
"""
|
||||
return _read_wchar(_native.hid_get_indexed_string, device_handle, index)
|
||||
|
||||
# def get_feature_report(device_handle, bytes_count, report_number=None):
|
||||
# """Get a feature report from a HID device.
|
||||
|
||||
# :param device_handle: a device handle returned by open() or open_path().
|
||||
# :param bytes_count: how many bytes to read.
|
||||
# :param report_number: if set, it is sent as the report number.
|
||||
|
||||
# :returns: the feature report data.
|
||||
# """
|
||||
# out_buffer = _C.create_string_buffer('\x00' * (bytes_count + 2))
|
||||
# if report_number is not None:
|
||||
# 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]
|
||||
|
||||
|
||||
# def _read_wchar(func, device_handle, index=None):
|
||||
# _BUFFER_SIZE = 64
|
||||
# buf = _C.create_unicode_buffer('\x00' * _BUFFER_SIZE)
|
||||
# if index is None:
|
||||
# ok = func(device_handle, buf, _BUFFER_SIZE)
|
||||
# else:
|
||||
# ok = func(device_handle, index, buf, _BUFFER_SIZE)
|
||||
# if ok == 0:
|
||||
# return buf.value
|
||||
|
||||
|
||||
# def get_manufacturer(device_handle):
|
||||
# """Get the Manufacturer String from a HID device.
|
||||
|
||||
# :param device_handle: a device handle returned by open() or open_path().
|
||||
# """
|
||||
# return _read_wchar(_native.hid_get_manufacturer_string, device_handle)
|
||||
|
||||
|
||||
# def get_product(device_handle):
|
||||
# """Get the Product String from a HID device.
|
||||
|
||||
# :param device_handle: a device handle returned by open() or open_path().
|
||||
# """
|
||||
# return _read_wchar(_native.hid_get_product_string, device_handle)
|
||||
|
||||
|
||||
# def get_serial(device_handle):
|
||||
# """Get the serial number from a HID device.
|
||||
|
||||
# :param device_handle: a device handle returned by open() or open_path().
|
||||
# """
|
||||
# serial = _read_wchar(_native.hid_get_serial_number_string, device_handle)
|
||||
# if serial is not None:
|
||||
# return ''.join(hex(ord(c)) for c in serial)
|
||||
|
||||
|
||||
# def get_indexed_string(device_handle, index):
|
||||
# """Get a string from a HID device, based on its string index.
|
||||
|
||||
# Note: currently not working in the ``hidraw`` native implementation.
|
||||
|
||||
# :param device_handle: a device handle returned by open() or open_path().
|
||||
# :param index: the index of the string to get.
|
||||
# """
|
||||
# return _read_wchar(_native.hid_get_indexed_string, device_handle, index)
|
||||
|
||||
@@ -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,7 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
__author__ = "Daniel Pavel"
|
||||
__license__ = "GPL"
|
||||
__version__ = "0.5"
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__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,169 +104,249 @@ 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 %r", handle)
|
||||
return True
|
||||
except:
|
||||
_log.exception("closing receiver handle %X", handle)
|
||||
# _log.exception("closing receiver handle %r", 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.
|
||||
:param data: data to send, up to 5 bytes.
|
||||
|
||||
The first two (required) bytes of data must be the feature index for the
|
||||
device, and a function code for that feature.
|
||||
The first two (required) bytes of data must be the SubId and address.
|
||||
|
||||
: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.
|
||||
"""
|
||||
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 %r no longer available", 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.
|
||||
:returns: a tuple of (devnumber, message data), or `None`
|
||||
|
||||
: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):
|
||||
"""Read an incoming packet from the receiver.
|
||||
|
||||
:returns: a tuple of (report_id, devnumber, data), or `None`.
|
||||
|
||||
: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.
|
||||
"""
|
||||
try:
|
||||
data = _hid.read(int(handle), _MAX_READ_SIZE, timeout)
|
||||
except Exception as reason:
|
||||
_log.error("read failed, assuming handle %r no longer available", 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:]
|
||||
|
||||
|
||||
_MAX_READ_TIMES = 3
|
||||
request_context = None
|
||||
def _skip_incoming(handle):
|
||||
"""Read anything already in the input buffer.
|
||||
|
||||
Used by request() and ping() before their write.
|
||||
"""
|
||||
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 notifications.
|
||||
|
||||
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 notifications from the device (e.g. special keys being
|
||||
pressed, battery charge notifications, etc), at least not in a timely manner.
|
||||
"""
|
||||
notifications_hook = None
|
||||
|
||||
def _unhandled(report_id, devnumber, data):
|
||||
"""Deliver a possible notification to the notifications_hook (if any)."""
|
||||
if notifications_hook:
|
||||
n = make_notification(devnumber, data)
|
||||
if n:
|
||||
notifications_hook(n)
|
||||
|
||||
def make_notification(devnumber, data):
|
||||
"""Guess if this is a notification (and not just a request reply), and
|
||||
return a Notification tuple if it is."""
|
||||
sub_id = ord(data[:1])
|
||||
if sub_id & 0x80 != 0x80:
|
||||
# HID++ 1.0 standard notifications are 0x40 - 0x7F
|
||||
# HID++ 2.0 feature notifications have the SoftwareID 0
|
||||
address = ord(data[1:2])
|
||||
if sub_id >= 0x40 or address & 0x0F == 0x00:
|
||||
return _HIDPP_Notification(devnumber, sub_id, address, data[2:])
|
||||
|
||||
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)
|
||||
_HIDPP_Notification = namedtuple('_HIDPP_Notification', ['devnumber', 'sub_id', 'address', 'data'])
|
||||
_HIDPP_Notification.__str__ = lambda self: 'Notification(%d,%02X,%02X,%s)' % (self.devnumber, self.sub_id, self.address, _strhex(self.data))
|
||||
_HIDPP_Notification.__unicode__ = _HIDPP_Notification.__str__
|
||||
del namedtuple
|
||||
|
||||
def request(handle, devnumber, feature_index_function, params=b'', features=None):
|
||||
|
||||
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
|
||||
device we're requesting for, or the feature specified in the initial
|
||||
request; it will also wait for a matching reply indefinitely.
|
||||
This function will wait for a matching reply indefinitely.
|
||||
|
||||
: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))
|
||||
# import inspect as _inspect
|
||||
# print ('\n '.join(str(s) for s in _inspect.stack()))
|
||||
|
||||
if request_context is None or handle != request_context.handle:
|
||||
context = _DEFAULT_REQUEST_CONTEXT
|
||||
_unhandled = unhandled_hook
|
||||
assert isinstance(request_id, int)
|
||||
if devnumber != 0xFF and request_id < 0x8000:
|
||||
timeout = _DEVICE_REQUEST_TIMEOUT
|
||||
# for HID++ 2.0 feature requests, randomize the SoftwareId to make it
|
||||
# easier to recognize the reply for this request. also, always set the
|
||||
# most significant bit (8) in SoftwareId, to make notifications easier
|
||||
# to distinguish from request replies
|
||||
request_id = (request_id & 0xFFF0) | 0x08 | _random_bits(3)
|
||||
else:
|
||||
context = request_context
|
||||
_unhandled = getattr(context, 'unhandled_hook')
|
||||
timeout = _RECEIVER_REQUEST_TIMEOUT
|
||||
|
||||
context.write(handle, devnumber, feature_index_function + params)
|
||||
params = b''.join(_pack(b'B', p) if isinstance(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))
|
||||
request_data = _pack(b'!H', request_id) + 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_data)
|
||||
|
||||
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_data[:2]:
|
||||
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 0x%02X 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_data[:2]:
|
||||
# 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_data[:2]:
|
||||
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 a notification
|
||||
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 +354,51 @@ 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
|
||||
# import inspect as _inspect
|
||||
# print ('\n '.join(str(s) for s in _inspect.stack()))
|
||||
|
||||
if not reply:
|
||||
# keep waiting...
|
||||
continue
|
||||
# randomize the SoftwareId and mark byte to be able to identify the ping
|
||||
# reply, and set most significant (0x8) bit in SoftwareId so that the reply
|
||||
# is always distinguishable from notifications
|
||||
request_id = 0x0018 | _random_bits(3)
|
||||
request_data = _pack(b'!HBBB', request_id, 0, 0, _random_bits(8))
|
||||
|
||||
reply_code, reply_devnumber, reply_data = reply
|
||||
_skip_incoming(handle)
|
||||
ihandle = int(handle)
|
||||
write(ihandle, devnumber, request_data)
|
||||
|
||||
if reply_devnumber != devnumber:
|
||||
# this message not for the device we're interested in
|
||||
# _l.log(_LOG_LEVEL, "device %d request got reply for unexpected device %d: [%s]", devnumber, reply_devnumber, _hex(reply_data))
|
||||
# worst case scenario, this is a reply for a concurrent request
|
||||
# on this receiver
|
||||
if _unhandled:
|
||||
_unhandled(reply_code, reply_devnumber, reply_data)
|
||||
continue
|
||||
while True:
|
||||
now = _timestamp()
|
||||
reply = _read(ihandle, _PING_TIMEOUT)
|
||||
delta = _timestamp() - now
|
||||
|
||||
if reply_code == 0x11 and reply_data[:2] == b'\x00\x10' and reply_data[4:5] == b'\xAA':
|
||||
major, minor = _unpack('!BB', reply_data[2:4])
|
||||
return major + minor / 10.0
|
||||
if reply:
|
||||
report_id, number, data = reply
|
||||
if number == devnumber:
|
||||
if data[:2] == request_data[:2] and data[4:5] == request_data[-1:]:
|
||||
# HID++ 2.0+ device, currently connected
|
||||
return ord(data[2:3]) + ord(data[3:4]) / 10.0
|
||||
|
||||
if reply_code == 0x10 and reply_data == b'\x8F\x00\x10\x01\x00':
|
||||
return 1.0
|
||||
if report_id == 0x10 and data[:1] == b'\x8F' and data[1:3] == request_data[:2]:
|
||||
assert data[-1:] == b'\x00'
|
||||
error = ord(data[3:4])
|
||||
|
||||
if reply_code == 0x10 and reply_data[:3] == b'\x8F\x00\x10':
|
||||
return None
|
||||
if error == _hidpp10.ERROR.invalid_SubID__command: # a valid reply from a HID++ 1.0 device
|
||||
return 1.0
|
||||
|
||||
_log.warn("don't know how to interpret ping reply %s", reply)
|
||||
if error == _hidpp10.ERROR.resource_error: # device unreachable
|
||||
# raise DeviceUnreachable(number=devnumber, request=request_id)
|
||||
break
|
||||
|
||||
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,192 @@
|
||||
# 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):
|
||||
Caution: comparison with strings will also match this NamedInt's name
|
||||
(case-insensitive)."""
|
||||
|
||||
def __new__(cls, value, name):
|
||||
assert isinstance(name, str) or isinstance(name, unicode)
|
||||
obj = int.__new__(cls, value)
|
||||
obj.name = str(name)
|
||||
return obj
|
||||
|
||||
def bytes(self, count=2):
|
||||
if self.bit_length() > count * 8:
|
||||
raise ValueError('cannot fit %X into %d bytes' % (self, count))
|
||||
return _pack(b'!L', self)[-count:]
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, NamedInt):
|
||||
return int(self) == int(other) and self.name == other.name
|
||||
if isinstance(other, int):
|
||||
return int(self) == int(other)
|
||||
if isinstance(other, str) or isinstance(other, unicode):
|
||||
return self.name.lower() == other.lower()
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return int(self)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
__unicode__ = __str__
|
||||
|
||||
def __repr__(self):
|
||||
return 'NamedInt(%d, %r)' % (int(self), self.name)
|
||||
|
||||
|
||||
class NamedInts(object):
|
||||
"""An ordered set of NamedInt values.
|
||||
|
||||
Indexing can be made by int or string, and will return the corresponding
|
||||
NamedInt if it exists in this set, or `None`.
|
||||
|
||||
Extracting slices will return all present NamedInts in the given interval
|
||||
(extended slices are not supported).
|
||||
|
||||
Assigning a string to an indexed int will create a new NamedInt in this set;
|
||||
if the value already exists in the set (int or string), ValueError will be
|
||||
raised.
|
||||
"""
|
||||
__slots__ = ['__dict__', '_values', '_indexed', '_fallback']
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def _readable_name(n):
|
||||
if not isinstance(n, str) and not isinstance(n, unicode):
|
||||
raise TypeError("expected string, got " + type(n))
|
||||
n = n.replace('__', '/').replace('_', ' ')
|
||||
return str(n)
|
||||
|
||||
values = {k: NamedInt(v, _readable_name(k)) for (k, v) in kwargs.items()}
|
||||
self.__dict__ = values
|
||||
self._values = sorted(list(values.values()))
|
||||
self._indexed = {int(v): v for v in self._values}
|
||||
self._fallback = None
|
||||
|
||||
# print ('%r' % self)
|
||||
|
||||
@classmethod
|
||||
def range(cls, from_value, to_value, name_generator=lambda x: str(x), step=1):
|
||||
values = {name_generator(x): x for x in range(from_value, to_value + 1, step)}
|
||||
return NamedInts(**values)
|
||||
|
||||
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 __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 isinstance(index, str) or isinstance(index, unicode):
|
||||
if index in self.__dict__:
|
||||
return self.__dict__[index]
|
||||
|
||||
elif isinstance(index, slice):
|
||||
if index.start is None and index.stop is None:
|
||||
return self._values[:]
|
||||
|
||||
v_start = int(self._values[0]) if index.start is None else int(index.start)
|
||||
v_stop = (self._values[-1] + 1) if index.stop is None else int(index.stop)
|
||||
|
||||
if v_start > v_stop or v_start > self._values[-1] or v_stop <= self._values[0]:
|
||||
return []
|
||||
|
||||
if v_start <= self._values[0] and v_stop > self._values[-1]:
|
||||
return self._values[:]
|
||||
|
||||
start_index = 0
|
||||
stop_index = len(self._values)
|
||||
for i, value in enumerate(self._values):
|
||||
if value < v_start:
|
||||
start_index = i + 1
|
||||
elif index.stop is None:
|
||||
break
|
||||
if value >= v_stop:
|
||||
stop_index = i
|
||||
break
|
||||
|
||||
return self._values[start_index:stop_index]
|
||||
|
||||
def __setitem__(self, index, name):
|
||||
assert isinstance(index, int), type(index)
|
||||
if isinstance(name, NamedInt):
|
||||
assert int(index) == int(name), repr(index) + ' ' + repr(name)
|
||||
value = name
|
||||
elif isinstance(name, str) or isinstance(name, unicode):
|
||||
value = NamedInt(index, name)
|
||||
else:
|
||||
raise TypeError('name must be a string')
|
||||
|
||||
if str(value) in self.__dict__:
|
||||
raise ValueError('%s (%d) already known' % (value, int(value)))
|
||||
if int(value) in self._indexed:
|
||||
raise ValueError('%d (%s) already known' % (int(value), value))
|
||||
|
||||
self._values = sorted(self._values + [value])
|
||||
self.__dict__[str(value)] = value
|
||||
self._indexed[int(value)] = value
|
||||
|
||||
def __contains__(self, value):
|
||||
if isinstance(value, int):
|
||||
return value in self._indexed
|
||||
if isinstance(value, str) or isinstance(value, unicode):
|
||||
return value in self.__dict__
|
||||
|
||||
def __iter__(self):
|
||||
for v in self._values:
|
||||
yield v
|
||||
|
||||
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 +196,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
|
||||
103
lib/logitech/unifying_receiver/descriptors.py
Normal file
@@ -0,0 +1,103 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from .common import NamedInts as _NamedInts
|
||||
from . import hidpp10 as _hidpp10
|
||||
from . import hidpp20 as _hidpp20
|
||||
from . import settings as _settings
|
||||
|
||||
#
|
||||
# common strings for settings
|
||||
#
|
||||
|
||||
_SMOOTH_SCROLL = ('smooth-scroll', 'Smooth Scrolling', 'High-sensitivity mode for vertical scroll with the wheel.')
|
||||
_DPI = ('dpi', 'Sensitivity (DPI)', None)
|
||||
_FN_SWAP = ('fn-swap', '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 _register_smooth_scroll(register, true_value, mask):
|
||||
return _settings.register_toggle(_SMOOTH_SCROLL[0], register, true_value=true_value, mask=mask,
|
||||
label=_SMOOTH_SCROLL[1], description=_SMOOTH_SCROLL[2])
|
||||
|
||||
|
||||
def _register_dpi(register, choices):
|
||||
return _settings.register_choices(_DPI[0], register, choices,
|
||||
label=_DPI[1], description=_DPI[2])
|
||||
|
||||
|
||||
def check_features(device, already_known):
|
||||
if _hidpp20.FEATURE.FN_STATUS in device.features and not any(s.name == 'fn-swap' for s in already_known):
|
||||
tfn = _settings.feature_toggle(_FN_SWAP[0], _hidpp20.FEATURE.FN_STATUS, write_returns_value=True,
|
||||
label=_FN_SWAP[1], description=_FN_SWAP[2])
|
||||
already_known.append(tfn(device))
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
_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',
|
||||
settings=[
|
||||
_register_smooth_scroll(0x01, true_value=0x40, mask=0x40),
|
||||
# _register_dpi(0x63, _NamedInts.range(9, 11, lambda x: str(x * 100))),
|
||||
],
|
||||
)
|
||||
_D('Wireless Keyboard K230')
|
||||
_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',
|
||||
settings=[
|
||||
_register_smooth_scroll(0x01, true_value=0x40, mask=0x40),
|
||||
],
|
||||
)
|
||||
_D('Performance Mouse MX', codename='Performance MX',
|
||||
settings=[
|
||||
_register_dpi(0x63, _NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 100))),
|
||||
],
|
||||
)
|
||||
|
||||
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
|
||||
134
lib/logitech/unifying_receiver/hidpp10.py
Normal file
@@ -0,0 +1,134 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from logging import getLogger # , DEBUG as _DEBUG
|
||||
_log = getLogger('LUR').getChild('hidpp10')
|
||||
del getLogger
|
||||
|
||||
from .common import (strhex as _strhex,
|
||||
NamedInts as _NamedInts,
|
||||
FirmwareInfo as _FirmwareInfo)
|
||||
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)
|
||||
|
||||
#
|
||||
# functions
|
||||
#
|
||||
|
||||
def get_register(device, name, default_number=-1):
|
||||
known_register = device.registers[name]
|
||||
register = known_register or default_number
|
||||
if register > 0:
|
||||
reply = device.request(0x8100 + (register & 0xFF))
|
||||
if reply:
|
||||
return reply
|
||||
|
||||
if not known_register and device.ping():
|
||||
_log.warn("%s: failed to read '%s' from default register 0x%02X, blacklisting", device, name, default_number)
|
||||
device.registers[-default_number] = name
|
||||
|
||||
|
||||
def get_battery(device):
|
||||
"""Reads a device's battery level, if provided by the HID++ 1.0 protocol."""
|
||||
reply = get_register(device, 'battery', 0x0D)
|
||||
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
|
||||
|
||||
reply = get_register(device, 'battery_status', 0x07)
|
||||
if reply:
|
||||
battery_status = ord(reply[:1])
|
||||
_log.info("%s: battery status %02X", device, battery_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)
|
||||
397
lib/logitech/unifying_receiver/hidpp20.py
Normal file
@@ -0,0 +1,397 @@
|
||||
#
|
||||
# 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 .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')
|
||||
assert int(FEATURE.ROOT) == 0x0000
|
||||
|
||||
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 (self.device, "check")
|
||||
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(0x0000, _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():
|
||||
if isinstance(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]
|
||||
|
||||
elif isinstance(index, slice):
|
||||
indices = index.indices(len(self.features))
|
||||
return [self.__getitem__(i) for i in range(*indices)]
|
||||
|
||||
def __contains__(self, value):
|
||||
if self._check():
|
||||
ivalue = int(value)
|
||||
|
||||
may_have = False
|
||||
for f in self.features:
|
||||
if f is None:
|
||||
may_have = True
|
||||
elif ivalue == int(f):
|
||||
return True
|
||||
elif ivalue < int(f):
|
||||
break
|
||||
|
||||
if may_have:
|
||||
reply = self.device.request(0x0000, _pack(b'!H', ivalue))
|
||||
if reply:
|
||||
index = ord(reply[0:1])
|
||||
if index:
|
||||
self.features[index] = FEATURE[ivalue]
|
||||
return True
|
||||
|
||||
def index(self, value):
|
||||
if self._check():
|
||||
may_have = False
|
||||
ivalue = int(value)
|
||||
for index, f in enumerate(self.features):
|
||||
if f is None:
|
||||
may_have = True
|
||||
elif ivalue == int(f):
|
||||
return index
|
||||
elif ivalue < int(f):
|
||||
raise ValueError("%s not in list" % repr(value))
|
||||
|
||||
if may_have:
|
||||
reply = self.device.request(0x0000, _pack(b'!H', ivalue))
|
||||
if reply:
|
||||
index = ord(reply[0:1])
|
||||
self.features[index] = FEATURE[ivalue]
|
||||
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):
|
||||
if isinstance(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]
|
||||
|
||||
elif isinstance(index, slice):
|
||||
indices = index.indices(len(self.keys))
|
||||
return [self.__getitem__(i) for i in range(*indices)]
|
||||
|
||||
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)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
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,185 @@ 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.
|
||||
|
||||
|
||||
class EventsListener(_Thread):
|
||||
"""Listener thread for events from the Unifying Receiver.
|
||||
|
||||
Incoming packets will be passed to the callback function in sequence, by a
|
||||
separate thread.
|
||||
Closing a ThreadedHandle will close all handles.
|
||||
"""
|
||||
def __init__(self, receiver_handle, events_callback):
|
||||
super(EventsListener, self).__init__(group='Unifying Receiver', name=self.__class__.__name__)
|
||||
|
||||
__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 %r' % 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__
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
# How long to wait during a read for the next packet.
|
||||
# Ideally this should be rather long (10s ?), but the read is blocking
|
||||
# and this means that when the thread is signalled to stop, it would take
|
||||
# a while for it to acknowledge it.
|
||||
_EVENT_READ_TIMEOUT = 500
|
||||
|
||||
# After this many read that did not produce a packet, call the tick() method.
|
||||
_IDLE_READS = 4
|
||||
|
||||
|
||||
class EventsListener(_threading.Thread):
|
||||
"""Listener thread for notifications from the Unifying Receiver.
|
||||
|
||||
Incoming packets will be passed to the callback function in sequence.
|
||||
"""
|
||||
def __init__(self, receiver, notifications_callback):
|
||||
super(EventsListener, self).__init__(name=self.__class__.__name__)
|
||||
|
||||
self.daemon = True
|
||||
self._active = False
|
||||
|
||||
self._handle = receiver_handle
|
||||
self.receiver = receiver
|
||||
self._queued_notifications = _Queue(32)
|
||||
self._notifications_callback = notifications_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
|
||||
|
||||
self._dispatcher.start()
|
||||
# This is necessary because notification packets might be received
|
||||
# during requests made by our callback.
|
||||
_base.notifications_hook = self._notifications_hook
|
||||
|
||||
ihandle = int(self.receiver.handle)
|
||||
_log.info("started with %s (%d)", self.receiver, ihandle)
|
||||
|
||||
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_notifications.empty():
|
||||
try:
|
||||
# _log.debug("read next notification")
|
||||
n = _base.read(ihandle, _EVENT_READ_TIMEOUT)
|
||||
except _base.NoReceiver:
|
||||
_log.warning("receiver disconnected")
|
||||
self.receiver.close()
|
||||
break
|
||||
|
||||
if n:
|
||||
n = _base.make_notification(*n)
|
||||
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 notifications
|
||||
n = self._queued_notifications.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 n:
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("processing %s", n)
|
||||
try:
|
||||
self._notifications_callback(n)
|
||||
except:
|
||||
_log.exception("processing %s", n)
|
||||
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.notifications_hook = None
|
||||
del self._queued_notifications
|
||||
|
||||
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, and before it starts
|
||||
reading notification packets."""
|
||||
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."""
|
||||
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 _notifications_hook(self, n):
|
||||
# Only consider unhandled notifications that were sent from this thread,
|
||||
# i.e. triggered by a callback handling a previous notification.
|
||||
if self._active and _threading.current_thread() == self:
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("queueing unhandled %s", n)
|
||||
self._queued_notifications.put(n)
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self._active and self._handle)
|
||||
return bool(self._active and self.receiver)
|
||||
__nonzero__ = __bool__
|
||||
|
||||
358
lib/logitech/unifying_receiver/receiver.py
Normal file
@@ -0,0 +1,358 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import errno as _errno
|
||||
from weakref import proxy as _proxy
|
||||
|
||||
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, NamedInts as _NamedInts
|
||||
from . import descriptors as _descriptors
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
"""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 _descriptors.DEVICES:
|
||||
self._name, self._kind = _descriptors.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 _descriptors.DEVICES:
|
||||
self._name, self._kind = _descriptors.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 = _descriptors.DEVICES.get(self.codename)
|
||||
if descriptor is None or descriptor.registers is None:
|
||||
self._registers = _NamedInts()
|
||||
else:
|
||||
self._registers = descriptor.registers
|
||||
return self._registers
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
if self._settings is None:
|
||||
descriptor = _descriptors.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 self.features:
|
||||
_descriptors.check_features(self, self._settings)
|
||||
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 __eq__(self, other):
|
||||
return self.serial == other.serial
|
||||
|
||||
def __ne__(self, other):
|
||||
return self.serial != other.serial
|
||||
|
||||
def __hash__(self):
|
||||
return self.serial.__hash__()
|
||||
|
||||
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.
|
||||
"""
|
||||
number = 0xFF
|
||||
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._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 notifications 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 link notifications")
|
||||
|
||||
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
|
||||
209
lib/logitech/unifying_receiver/settings.py
Normal file
@@ -0,0 +1,209 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
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 NamedInt as _NamedInt, NamedInts as _NamedInts
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
KIND = _NamedInts(toggle=0x1, choice=0x02, range=0x12)
|
||||
|
||||
class _Setting(object):
|
||||
__slots__ = ['name', 'label', 'description',
|
||||
'kind', '_rw', '_validator',
|
||||
'_device', '_value']
|
||||
|
||||
def __init__(self, name, rw, validator, kind=None, label=None, description=None):
|
||||
assert name
|
||||
self.name = name
|
||||
self.label = label or name
|
||||
self.description = description
|
||||
|
||||
self._rw = rw
|
||||
self._validator = validator
|
||||
|
||||
assert kind is None or kind & validator.kind != 0
|
||||
self.kind = kind or validator.kind
|
||||
|
||||
def __call__(self, device):
|
||||
o = _copy(self)
|
||||
o._value = None
|
||||
o._device = _proxy(device)
|
||||
return o
|
||||
|
||||
@property
|
||||
def choices(self):
|
||||
return self._validator.choices if self._validator.kind & KIND.choice else None
|
||||
|
||||
def read(self, cached=True):
|
||||
if self._device:
|
||||
if self._value is None or not cached:
|
||||
reply = self._rw.read(self._device)
|
||||
# print ("read reply", repr(reply))
|
||||
if reply:
|
||||
# print ("pre-read", self._value)
|
||||
self._value = self._validator.validate_read(reply)
|
||||
# print ("post-read", self._value)
|
||||
return self._value
|
||||
|
||||
def write(self, value):
|
||||
if self._device:
|
||||
data_bytes = self._validator.prepare_write(value)
|
||||
reply = self._rw.write(self._device, data_bytes)
|
||||
if reply:
|
||||
self._value = self._validator.validate_write(value, reply)
|
||||
return self._value
|
||||
|
||||
def __str__(self):
|
||||
if hasattr(self, '_value'):
|
||||
assert hasattr(self, '_device')
|
||||
return '<Setting([%s:%s] %s:%s=%s)>' % (self._rw.kind, self._validator.kind, self._device.codename, self.name, self._value)
|
||||
return '<Setting([%s:%s] %s)>' % (self._rw.kind, self._validator.kind, self.name)
|
||||
__unicode__ = __repr__ = __str__
|
||||
|
||||
|
||||
class _RegisterRW(object):
|
||||
__slots__ = ['register']
|
||||
|
||||
kind = _NamedInt(0x01, 'register')
|
||||
|
||||
def __init__(self, register):
|
||||
assert isinstance(register, int)
|
||||
self.register = register
|
||||
|
||||
def read(self, device):
|
||||
return device.request(0x8100 | (self.register & 0x2FF))
|
||||
|
||||
def write(self, device, data_bytes):
|
||||
return device.request(0x8000 | (self.register & 0x2FF), data_bytes)
|
||||
|
||||
|
||||
class _FeatureRW(object):
|
||||
__slots__ = ['feature', 'read_fnid', 'write_fnid']
|
||||
|
||||
kind = _NamedInt(0x02, 'feature')
|
||||
default_read_fnid = 0x00
|
||||
default_write_fnid = 0x10
|
||||
|
||||
def __init__(self, feature, read_fnid=default_read_fnid, write_fnid=default_write_fnid):
|
||||
assert isinstance(feature, _NamedInt)
|
||||
self.feature = feature
|
||||
self.read_fnid = read_fnid
|
||||
self.write_fnid = write_fnid
|
||||
|
||||
def read(self, device):
|
||||
assert self.feature is not None
|
||||
return device.feature_request(self.feature, self.read_fnid)
|
||||
|
||||
def write(self, device, data_bytes):
|
||||
assert self.feature is not None
|
||||
return device.feature_request(self.feature, self.write_fnid, data_bytes)
|
||||
|
||||
|
||||
class _BooleanValidator(object):
|
||||
__slots__ = ['true_value', 'false_value', 'mask', 'write_returns_value']
|
||||
|
||||
kind = KIND.toggle
|
||||
default_true = 0x01
|
||||
default_false = 0x00
|
||||
default_mask = 0xFF
|
||||
|
||||
def __init__(self, true_value=default_true, false_value=default_false, mask=default_mask, write_returns_value=False):
|
||||
self.true_value = true_value
|
||||
self.false_value = false_value
|
||||
self.mask = mask
|
||||
self.write_returns_value = write_returns_value
|
||||
|
||||
def validate_read(self, reply_bytes):
|
||||
reply_value = ord(reply_bytes[:1]) & self.mask
|
||||
return reply_value == self.true_value
|
||||
|
||||
def prepare_write(self, value):
|
||||
# FIXME: this does not work right when there is more than one flag in
|
||||
# the same register!
|
||||
return self.true_value if value else self.false_value
|
||||
|
||||
def validate_write(self, value, reply_bytes):
|
||||
if self.write_returns_value:
|
||||
reply_value = ord(reply_bytes[:1]) & self.mask
|
||||
return reply_value == self.true_value
|
||||
|
||||
# just assume the value was written correctly, otherwise there would not
|
||||
# be any reply_bytes to check
|
||||
return bool(value)
|
||||
|
||||
|
||||
class _ChoicesValidator(object):
|
||||
__slots__ = ['choices', 'write_returns_value']
|
||||
|
||||
kind = KIND.choice
|
||||
|
||||
def __init__(self, choices, write_returns_value=False):
|
||||
assert isinstance(choices, _NamedInts)
|
||||
self.choices = choices
|
||||
self.write_returns_value = write_returns_value
|
||||
|
||||
def validate_read(self, reply_bytes):
|
||||
assert self.choices is not None
|
||||
reply_value = ord(reply_bytes[:1])
|
||||
valid_value = self.choices[reply_value]
|
||||
assert valid_value is not None, "%: failed to validate read value %02X" % (self.__class__.__name__, reply_value)
|
||||
return valid_value
|
||||
|
||||
def prepare_write(self, value):
|
||||
assert self.choices is not None
|
||||
choice = self.choices[value]
|
||||
if choice is None:
|
||||
raise ValueError("invalid choice " + repr(value))
|
||||
assert isinstance(choice, _NamedInt)
|
||||
return choice.bytes(1)
|
||||
|
||||
def validate_write(self, value, reply_bytes):
|
||||
assert self.choices is not None
|
||||
if self.write_returns_value:
|
||||
reply_value = ord(reply_bytes[:1])
|
||||
choice = self.choices[reply_value]
|
||||
assert choice is not None, "failed to validate write reply %02X" % reply_value
|
||||
return choice
|
||||
|
||||
# just assume the value was written correctly, otherwise there would not
|
||||
# be any reply_bytes to check
|
||||
return self.choices[value]
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def register_toggle(name, register,
|
||||
true_value=_BooleanValidator.default_true, false_value=_BooleanValidator.default_false,
|
||||
mask=_BooleanValidator.default_mask, write_returns_value=False,
|
||||
label=None, description=None):
|
||||
rw = _RegisterRW(register)
|
||||
validator = _BooleanValidator(true_value=true_value, false_value=false_value, mask=mask, write_returns_value=write_returns_value)
|
||||
return _Setting(name, rw, validator, label=label, description=description)
|
||||
|
||||
|
||||
def register_choices(name, register, choices,
|
||||
kind=KIND.choice, write_returns_value=False,
|
||||
label=None, description=None):
|
||||
assert choices
|
||||
rw = _RegisterRW(register)
|
||||
validator = _ChoicesValidator(choices, write_returns_value=write_returns_value)
|
||||
return _Setting(name, rw, validator, kind=kind, label=label, description=description)
|
||||
|
||||
|
||||
def feature_toggle(name, feature,
|
||||
read_function_id=_FeatureRW.default_read_fnid, write_function_id=_FeatureRW.default_write_fnid,
|
||||
true_value=_BooleanValidator.default_true, false_value=_BooleanValidator.default_false,
|
||||
mask=_BooleanValidator.default_mask, write_returns_value=False,
|
||||
label=None, description=None):
|
||||
rw = _FeatureRW(feature, read_function_id, write_function_id)
|
||||
validator = _BooleanValidator(true_value=true_value, false_value=false_value, mask=mask, write_returns_value=write_returns_value)
|
||||
return _Setting(name, rw, validator, label=label, description=description)
|
||||
332
lib/logitech/unifying_receiver/status.py
Normal file
@@ -0,0 +1,332 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
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_notification(self, n):
|
||||
if n.sub_id == 0x4A:
|
||||
self.lock_open = bool(n.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(n.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):
|
||||
def _item(name, format):
|
||||
value = self.get(name)
|
||||
if value is not None:
|
||||
return format % value
|
||||
|
||||
def _items():
|
||||
battery_level = _item(BATTERY_LEVEL, 'Battery: %d%%')
|
||||
if battery_level:
|
||||
yield battery_level
|
||||
battery_status = _item(BATTERY_STATUS, ' <small>(%s)</small>')
|
||||
if battery_status:
|
||||
yield battery_status
|
||||
|
||||
light_level = _item(LIGHT_LEVEL, 'Light: %d lux')
|
||||
if light_level:
|
||||
if battery_level:
|
||||
yield ', '
|
||||
yield light_level
|
||||
|
||||
return ''.join(i for i in _items())
|
||||
|
||||
__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
|
||||
if not d:
|
||||
_log.error("polling status of invalid device")
|
||||
return
|
||||
|
||||
# read these from the device 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)
|
||||
|
||||
# really unnecessary, if the device has SOLAR_CHARGE it should be
|
||||
# broadcasting it's battery status anyway, it will just take a little while
|
||||
# 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)
|
||||
|
||||
# make sure we know all the features of the device
|
||||
if d.features:
|
||||
d.features[:]
|
||||
|
||||
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, timestamp=timestamp)
|
||||
|
||||
def process_notification(self, n):
|
||||
# incoming packets with SubId >= 0x80 are supposedly replies from
|
||||
# HID++ 1.0 requests, should never get here
|
||||
assert n.sub_id < 0x80
|
||||
|
||||
# 0x40 to 0x7F appear to be HID++ 1.0 notifications
|
||||
if n.sub_id >= 0x40:
|
||||
return self._process_hidpp10_notification(n)
|
||||
|
||||
# assuming 0x00 to 0x3F are feature (HID++ 2.0) notifications
|
||||
try:
|
||||
feature = self._device.features[n.sub_id]
|
||||
except IndexError:
|
||||
_log.warn("%s: notification from invalid feature index %02X: %s", self._device, n.sub_id, n)
|
||||
return False
|
||||
|
||||
return self._process_feature_notification(n, feature)
|
||||
|
||||
def _process_hidpp10_notification(self, n):
|
||||
if n.sub_id == 0x40:
|
||||
if n.address == 0x02:
|
||||
# device un-paired
|
||||
self.clear()
|
||||
self._device.status = None
|
||||
self._changed(False, ALERT.HIGH, 'unpaired')
|
||||
else:
|
||||
_log.warn("%s: disconnection with unknown type %02X: %s", self._device, n.address, n)
|
||||
return True
|
||||
|
||||
if n.sub_id == 0x41:
|
||||
if n.address == 0x04: # unifying protocol
|
||||
# wpid = _strhex(n.data[4:5] + n.data[3:4])
|
||||
# assert wpid == device.wpid
|
||||
|
||||
flags = ord(n.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("%s: connection notification: software=%s, encrypted=%s, link=%s, payload=%s",
|
||||
self._device, sw_present, link_encrypyed, link_established, has_payload)
|
||||
self[ENCRYPTED] = link_encrypyed
|
||||
self._changed(link_established)
|
||||
|
||||
elif n.address == 0x03:
|
||||
_log.warn("%s: connection notification with eQuad protocol, ignored: %s", self._device.number, n)
|
||||
|
||||
else:
|
||||
_log.warn("%s: connection notification with unknown protocol %02X: %s", self._device.number, n.address, n)
|
||||
|
||||
return True
|
||||
|
||||
if n.sub_id == 0x49:
|
||||
# raw input event? just ignore it
|
||||
# if n.address == 0x01, no idea what it is, but they keep on coming
|
||||
# if n.address == 0x03, it's an actual input event
|
||||
return True
|
||||
|
||||
if n.sub_id == 0x4B:
|
||||
if n.address == 0x01:
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s: device powered on", self._device)
|
||||
self._changed(alert=ALERT.LOW, reason='powered on')
|
||||
else:
|
||||
_log.info("%s: unknown %s", self._device, n)
|
||||
return True
|
||||
|
||||
_log.warn("%s: unrecognized %s", self._device, n)
|
||||
|
||||
def _process_feature_notification(self, n, feature):
|
||||
if feature == _hidpp20.FEATURE.BATTERY:
|
||||
if n.address == 0x00:
|
||||
discharge = ord(n.data[:1])
|
||||
battery_status = ord(n.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
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s: battery %d% charged, %s", self._device, discharge, self[BATTERY_STATUS])
|
||||
else:
|
||||
alert = ALERT.MED
|
||||
reason = self[ERROR] = self[BATTERY_STATUS]
|
||||
_log.warn("%s: battery %d% charged, ALERT %s", self._device, discharge, reason)
|
||||
self._changed(alert=alert, reason=reason)
|
||||
else:
|
||||
_log.info("%s: unknown BATTERY %s", self._device, n)
|
||||
return True
|
||||
|
||||
if feature == _hidpp20.FEATURE.REPROGRAMMABLE_KEYS:
|
||||
if n.address == 0x00:
|
||||
_log.info("%s: reprogrammable key: %s", self._device, n)
|
||||
else:
|
||||
_log.info("%s: unknown REPROGRAMMABLE KEYS %s", self._device, n)
|
||||
return True
|
||||
|
||||
if feature == _hidpp20.FEATURE.WIRELESS:
|
||||
if n.address == 0x00:
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("wireless status: %s", n)
|
||||
if n.data[0:3] == b'\x01\x01\x01':
|
||||
self._changed(alert=ALERT.LOW, reason='powered on')
|
||||
else:
|
||||
_log.info("%s: unknown WIRELESS %s", self._device, n)
|
||||
else:
|
||||
_log.info("%s: unknown WIRELESS %s", self._device, n)
|
||||
return True
|
||||
|
||||
if feature == _hidpp20.FEATURE.SOLAR_CHARGE:
|
||||
if n.data[5:9] == b'GOOD':
|
||||
charge, lux, adc = _unpack(b'!BHH', n.data[:5])
|
||||
self[BATTERY_LEVEL] = charge
|
||||
# guesstimate the battery voltage, emphasis on 'guess'
|
||||
self[BATTERY_STATUS] = '%1.2fV' % (adc * 2.67793237653 / 0x0672)
|
||||
if n.address == 0x00:
|
||||
self[LIGHT_LEVEL] = None
|
||||
self._changed()
|
||||
elif n.address == 0x10:
|
||||
self[LIGHT_LEVEL] = lux
|
||||
if lux > 200: # guesstimate
|
||||
self[BATTERY_STATUS] += ', charging'
|
||||
self._changed()
|
||||
elif n.address == 0x20:
|
||||
_log.debug("%s: Solar key pressed", self._device)
|
||||
self._changed(alert=ALERT.MED)
|
||||
# first cancel any reporting
|
||||
self._device.feature_request(_hidpp20.FEATURE.SOLAR_CHARGE)
|
||||
# trigger a new report chain
|
||||
reports_count = 15
|
||||
reports_period = 2 # seconds
|
||||
self._device.feature_request(_hidpp20.FEATURE.SOLAR_CHARGE, 0x00, reports_count, reports_period)
|
||||
else:
|
||||
_log.info("%s: unknown SOLAR CHAGE %s", self._device, n)
|
||||
else:
|
||||
_log.warn("%s: SOLAR CHARGE not GOOD? %s", self._device, n)
|
||||
return True
|
||||
|
||||
if feature == _hidpp20.FEATURE.TOUCH_MOUSE:
|
||||
if n.address == 0x00:
|
||||
_log.info("%s: TOUCH MOUSE points %s", self._device, n)
|
||||
elif n.address == 0x10:
|
||||
touch = ord(n.data[:1])
|
||||
button_down = bool(touch & 0x02)
|
||||
mouse_lifted = bool(touch & 0x01)
|
||||
_log.info("%s: TOUCH MOUSE status: button_down=%s mouse_lifted=%s", self._device, button_down, mouse_lifted)
|
||||
else:
|
||||
_log.info("%s: unknown TOUCH MOUSE %s", self._device, n)
|
||||
return True
|
||||
|
||||
_log.info("%s: unrecognized %s for feature %s (index %02X)", self._device, n, feature, n.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()
|
||||
7
lib/solaar/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__version__ = '0.8.7'
|
||||
386
lib/solaar/cli.py
Normal file
@@ -0,0 +1,386 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
NAME = 'solaar-cli'
|
||||
from solaar import __version__
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
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, may_be_receiver=False):
|
||||
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", number)
|
||||
return dev
|
||||
|
||||
if len(name) < 3:
|
||||
_fail("need at least 3 characters to match a device")
|
||||
|
||||
name = name.lower()
|
||||
if may_be_receiver and ('receiver'.startswith(name) or name == receiver.serial.lower()):
|
||||
return receiver
|
||||
|
||||
for dev in receiver:
|
||||
if (name == dev.serial.lower() or
|
||||
name == dev.codename.lower() or
|
||||
name == str(dev.kind).lower() or
|
||||
name in dev.name.lower()):
|
||||
return dev
|
||||
|
||||
_fail("no device found matching '%s'" % name)
|
||||
|
||||
|
||||
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 :", receiver.path)
|
||||
print (" Serial :", receiver.serial)
|
||||
for f in receiver.firmware:
|
||||
print (" %-11s: %s" % (f.kind, f.version))
|
||||
|
||||
print (" Has", paired_count, "paired device(s).")
|
||||
|
||||
notification_flags = receiver.request(0x8100)
|
||||
if notification_flags:
|
||||
notification_flags = ord(notification_flags[0:1]) << 16 | ord(notification_flags[1:2]) << 8
|
||||
if notification_flags:
|
||||
from logitech.unifying_receiver import hidpp10
|
||||
notification_names = hidpp10.NOTIFICATION_FLAG.flag_names(notification_flags)
|
||||
print (" Enabled notifications: 0x%06X = %s." % (notification_flags, ', '.join(notification_names)))
|
||||
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:", ', '.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]" % (dev.number, dev.name, dev.codename, dev.serial), state)
|
||||
return
|
||||
|
||||
print ("%d: %s" % (dev.number, dev.name))
|
||||
print (" Codename :", dev.codename)
|
||||
print (" Kind :", dev.kind)
|
||||
if p == 0:
|
||||
print (" Protocol : unknown (device is inactive)")
|
||||
else:
|
||||
print (" Protocol : HID++ %1.1f" % p)
|
||||
print (" Polling rate :", dev.polling_rate, "ms")
|
||||
print (" Wireless PID :", dev.wpid)
|
||||
print (" Serial number:", dev.serial)
|
||||
for fw in dev.firmware:
|
||||
print (" %-11s:" % fw.kind, (fw.name + ' ' + fw.version).strip())
|
||||
|
||||
if dev.power_switch_location:
|
||||
print (" The power switch is located on the", 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, ', '.join(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, ', '.join(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," % 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, True)
|
||||
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 _notification_handler(n):
|
||||
if n.devnumber == 0xFF:
|
||||
r_status.process_notification(n)
|
||||
if not r_status.lock_open:
|
||||
done[0] = True
|
||||
elif n.sub_id == 0x41 and n.address == 0x04:
|
||||
if n.devnumber not in known_devices:
|
||||
r_status.new_device = receiver[n.devnumber]
|
||||
|
||||
from logitech.unifying_receiver import base
|
||||
base.notifications_hook = _notification_handler
|
||||
|
||||
# check if it's necessary to set the notification flags
|
||||
notification_flags = receiver.request(0x8100)
|
||||
if notification_flags:
|
||||
# just to see if any bits are set
|
||||
notification_flags = ord(notification_flags[:1]) + ord(notification_flags[1:2]) + ord(notification_flags[2:3])
|
||||
if not notification_flags:
|
||||
# if there are any notifications set, just assume the one we need is already set
|
||||
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]:
|
||||
n = base.read(receiver.handle, 2000)
|
||||
if n:
|
||||
n = base.make_notification(*n)
|
||||
if n:
|
||||
_notification_handler(n)
|
||||
|
||||
if not notification_flags:
|
||||
# only clear the flags if they weren't set before, otherwise a
|
||||
# concurrently running Solaar app will stop working properly
|
||||
receiver.enable_notifications(False)
|
||||
base.notifications_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)
|
||||
|
||||
# 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 ("[%s:%s]" % (dev.serial, dev.kind))
|
||||
print ("#", dev.name)
|
||||
for s in dev.settings:
|
||||
print ("")
|
||||
print ("# %s" % s.label)
|
||||
if s.choices:
|
||||
print ("# possible values: one of [", ', '.join(str(v) for v in s.choices), "], or higher/lower/highest/max/lowest/min")
|
||||
else:
|
||||
print ("# possible values: on/true/t/yes/y/1 or off/false/f/no/n/0")
|
||||
value = s.read()
|
||||
if value is None:
|
||||
print ("# %s = ? (failed to read from device)" % s.name)
|
||||
else:
|
||||
print (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', 'on', 't', 'y']:
|
||||
value = True
|
||||
elif value.lower() in ['0', 'false', 'no', 'off', '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)
|
||||
|
||||
if value == 'lower':
|
||||
lower_values = setting.choices[:old_value]
|
||||
value = lower_values[-1] if lower_values else setting.choices[:][0]
|
||||
elif value == 'higher':
|
||||
higher_values = setting.choices[old_value + 1:]
|
||||
value = higher_values[0] if higher_values else setting.choices[:][-1]
|
||||
elif value in ('highest', 'max'):
|
||||
value = setting.choices[:][-1]
|
||||
elif value in ('lowest', 'min'):
|
||||
value = setting.choices[:][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' = '%s' [%s]" % (setting.name, value, repr(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
|
||||
|
||||
|
||||
def main():
|
||||
_require('pyudev', 'python-pyudev')
|
||||
args = _parse_arguments()
|
||||
receiver = _receiver()
|
||||
args.cmd(receiver, args)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
164
lib/solaar/gtk.py
Normal file
@@ -0,0 +1,164 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
|
||||
NAME = 'Solaar'
|
||||
from solaar import __version__
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
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('-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('-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
|
||||
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 _run(args):
|
||||
import solaar.ui as ui
|
||||
|
||||
# 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 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 solaar.listener import DUMMY, ReceiverListener
|
||||
window = ui.main_window.create(NAME, DUMMY.name, DUMMY.max_devices, args.systray)
|
||||
if args.systray:
|
||||
menu_actions = (ui.action.toggle_notifications,
|
||||
ui.action.about)
|
||||
icon = ui.status_icon.create(window, menu_actions)
|
||||
else:
|
||||
icon = None
|
||||
|
||||
listener = [None]
|
||||
|
||||
# initializes the receiver listener
|
||||
def check_for_listener(notify=False):
|
||||
# print ("check_for_listener", notify)
|
||||
listener[0] = None
|
||||
|
||||
try:
|
||||
listener[0] = ReceiverListener.open(status_changed)
|
||||
except OSError:
|
||||
ui.error_dialog(window, 'Permissions error',
|
||||
'Found a possible Unifying Receiver device,\n'
|
||||
'but did not have permission to open it.')
|
||||
|
||||
if listener[0] is None:
|
||||
if notify:
|
||||
status_changed(DUMMY)
|
||||
else:
|
||||
return True
|
||||
|
||||
from gi.repository import Gtk, GObject
|
||||
from logitech.unifying_receiver import status
|
||||
|
||||
# callback delivering status notifications 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)
|
||||
|
||||
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)
|
||||
if icon:
|
||||
GObject.timeout_add(1000, ui.status_icon.check_systray, icon, window)
|
||||
Gtk.main()
|
||||
|
||||
if listener[0]:
|
||||
listener[0].stop()
|
||||
listener[0].join()
|
||||
|
||||
ui.notify.uninit()
|
||||
|
||||
|
||||
def main():
|
||||
_require('pyudev', 'python-pyudev')
|
||||
_require('gi.repository', 'python-gi')
|
||||
_require('gi.repository.Gtk', 'gir1.2-gtk-3.0')
|
||||
args = _parse_arguments()
|
||||
|
||||
# ensure no more than a single instance runs at a time
|
||||
import os.path as _path
|
||||
import os as _os
|
||||
lock_fd = None
|
||||
for p in _os.environ.get('XDG_RUNTIME_DIR'), '/run/lock', '/var/lock', _os.environ.get('TMPDIR', '/tmp'):
|
||||
if p and _path.isdir(p) and _os.access(p, _os.W_OK):
|
||||
lock_path = _path.join(p, 'solaar.single-instance.%d' % _os.getuid())
|
||||
try:
|
||||
lock_fd = open(lock_path, 'wb')
|
||||
# print ("Single instance lock file is %s" % lock_path)
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
if lock_fd:
|
||||
import fcntl as _fcntl
|
||||
try:
|
||||
_fcntl.flock(lock_fd, _fcntl.LOCK_EX | _fcntl.LOCK_NB)
|
||||
except IOError as e:
|
||||
if e.errno == 11:
|
||||
import sys
|
||||
sys.exit("solaar: error: Solaar is already running.")
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
import sys
|
||||
print ("solaar: warning: failed to create single instance lock file, ignoring.", file=sys.stderr)
|
||||
|
||||
try:
|
||||
_run(args)
|
||||
finally:
|
||||
if lock_fd:
|
||||
_fcntl.flock(lock_fd, _fcntl.LOCK_UN)
|
||||
lock_fd.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
157
lib/solaar/listener.py
Normal file
@@ -0,0 +1,157 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from logging import getLogger, DEBUG as _DEBUG
|
||||
_log = getLogger('listener')
|
||||
del getLogger
|
||||
|
||||
from logitech.unifying_receiver import (Receiver,
|
||||
listener as _listener,
|
||||
status as _status)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from collections import namedtuple
|
||||
_GHOST_DEVICE = namedtuple('_GHOST_DEVICE', ['number', 'name', 'kind', 'status', 'max_devices'])
|
||||
_GHOST_DEVICE.__bool__ = lambda self: False
|
||||
_GHOST_DEVICE.__nonzero__ = _GHOST_DEVICE.__bool__
|
||||
del namedtuple
|
||||
|
||||
def _ghost(device):
|
||||
return _GHOST_DEVICE(number=device.number, name=device.name, kind=device.kind, status=None, max_devices=None)
|
||||
|
||||
DUMMY = _GHOST_DEVICE(Receiver.number, Receiver.name, None, 'Receiver not found.', Receiver.max_devices)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
# how often to poll devices that haven't updated their statuses on their own
|
||||
# (through notifications)
|
||||
_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._notifications_handler)
|
||||
self.tick_period = _POLL_TICK
|
||||
self._last_tick = 0
|
||||
|
||||
self.status_changed_callback = status_changed_callback
|
||||
|
||||
# make it a bit similar with the regular devices
|
||||
receiver.kind = None
|
||||
# replace the
|
||||
receiver.handle = _listener.ThreadedHandle(receiver.handle, receiver.path)
|
||||
receiver.status = _status.ReceiverStatus(receiver, self._status_changed)
|
||||
|
||||
def has_started(self):
|
||||
_log.info("notifications 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("notifications 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("polling status: %s %s", self.receiver, list(iter(self.receiver)))
|
||||
|
||||
if self._last_tick > 0 and timestamp - self._last_tick > _POLL_TICK * 3:
|
||||
# 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 %s (%X) %s", device,
|
||||
None if device is None else 'active' if device.status else 'inactive',
|
||||
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:
|
||||
# the status of the receiver changed
|
||||
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)
|
||||
|
||||
self.status_changed_callback(r, device, alert, reason)
|
||||
|
||||
if device.status is None:
|
||||
# the receiver changed status as well
|
||||
self.status_changed_callback(r)
|
||||
|
||||
def _notifications_handler(self, n):
|
||||
assert self.receiver
|
||||
if n.devnumber == 0xFF:
|
||||
# a receiver notification
|
||||
if self.receiver.status is not None:
|
||||
self.receiver.status.process_notification(n)
|
||||
else:
|
||||
# a device notification
|
||||
assert n.devnumber > 0 and n.devnumber <= self.receiver.max_devices
|
||||
already_known = n.devnumber in self.receiver
|
||||
dev = self.receiver[n.devnumber]
|
||||
|
||||
if not dev:
|
||||
_log.warn("received %s for invalid device %d: %r", n, n.devnumber, dev)
|
||||
return
|
||||
|
||||
if not already_known:
|
||||
# read these as soon as possible, they will be used everywhere
|
||||
dev.protocol, dev.codename
|
||||
dev.status = _status.DeviceStatus(dev, self._status_changed)
|
||||
# the receiver changed status as well
|
||||
self._status_changed(self.receiver)
|
||||
|
||||
# status may be None if the device has just been unpaired
|
||||
if dev.status is not None:
|
||||
dev.status.process_notification(n)
|
||||
if self.receiver.status.lock_open and not already_known:
|
||||
# this should be the first notification after a device was paired
|
||||
assert n.sub_id == 0x41 and n.address == 0x04
|
||||
_log.info("pairing detected new device")
|
||||
self.receiver.status.new_device = 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:
|
||||
rl = ReceiverListener(receiver, status_changed_callback)
|
||||
rl.start()
|
||||
return rl
|
||||
47
lib/solaar/ui/__init__.py
Normal file
@@ -0,0 +1,47 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
|
||||
def _look_for_application_icons():
|
||||
import os.path as _path
|
||||
import os as _os
|
||||
|
||||
import sys as _sys
|
||||
# print ("path[0] = %s" % _sys.path[0])
|
||||
prefix_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..'))
|
||||
src_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..', 'share'))
|
||||
local_share = _os.environ.get('XDG_DATA_HOME', _path.expanduser('~/.local/share'))
|
||||
data_dirs = _os.environ.get('XDG_DATA_DIRS', '/usr/local/share:/usr/share')
|
||||
|
||||
share_solaar = [prefix_share] + list(_path.join(x, 'solaar') for x in [src_share, local_share] + data_dirs.split(':'))
|
||||
for location in share_solaar:
|
||||
# print ("checking %s" % location)
|
||||
solaar_png = _path.join(location, 'icons', 'solaar-mask.png')
|
||||
if _path.exists(solaar_png):
|
||||
_os.environ['XDG_DATA_DIRS'] = location + ':' + data_dirs
|
||||
# print ('XDG_DATA_DIRS=%s' % _os.environ['XDG_DATA_DIRS'])
|
||||
break
|
||||
|
||||
del _sys
|
||||
del _os
|
||||
# del _path
|
||||
|
||||
# look for application-specific icons before initializing Gtk
|
||||
_look_for_application_icons()
|
||||
|
||||
|
||||
from gi.repository import GObject, Gtk
|
||||
GObject.threads_init()
|
||||
|
||||
|
||||
def error_dialog(window, title, text):
|
||||
m = Gtk.MessageDialog(window, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, text)
|
||||
m.set_title(title)
|
||||
m.run()
|
||||
m.destroy()
|
||||
|
||||
|
||||
from . import notify, status_icon, main_window
|
||||
115
lib/solaar/ui/action.py
Normal file
@@ -0,0 +1,115 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
from . import notify, pair_window
|
||||
from ..ui import error_dialog
|
||||
|
||||
|
||||
_NAME = 'Solaar'
|
||||
from solaar import __version__
|
||||
|
||||
|
||||
def make(name, label, function, *args):
|
||||
action = Gtk.Action(name, label, label, None)
|
||||
action.set_icon_name(name)
|
||||
if function:
|
||||
action.connect('activate', function, *args)
|
||||
return action
|
||||
|
||||
|
||||
def make_toggle(name, label, function, *args):
|
||||
action = Gtk.ToggleAction(name, label, label, None)
|
||||
action.set_icon_name(name)
|
||||
action.connect('activate', function, *args)
|
||||
return action
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _toggle_notifications(action):
|
||||
if action.get_active():
|
||||
notify.init('Solaar')
|
||||
else:
|
||||
notify.uninit()
|
||||
action.set_sensitive(notify.available)
|
||||
toggle_notifications = make_toggle('notifications', 'Notifications', _toggle_notifications)
|
||||
|
||||
|
||||
def _show_about_window(action):
|
||||
about = Gtk.AboutDialog()
|
||||
|
||||
about.set_icon_name(_NAME.lower())
|
||||
about.set_program_name(_NAME)
|
||||
about.set_logo_icon_name(_NAME.lower())
|
||||
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',))
|
||||
try:
|
||||
about.add_credit_section('Testing', ('Douglas Wagner', 'Julien Gascard'))
|
||||
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')
|
||||
|
||||
about.run()
|
||||
about.destroy()
|
||||
about = make('help-about', 'About ' + _NAME, _show_about_window)
|
||||
|
||||
quit = make('application-exit', 'Quit', Gtk.main_quit)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _pair_device(action, frame):
|
||||
window = frame.get_toplevel()
|
||||
|
||||
pair_dialog = pair_window.create(action, frame._device)
|
||||
pair_dialog.set_transient_for(window)
|
||||
pair_dialog.set_destroy_with_parent(True)
|
||||
pair_dialog.set_modal(True)
|
||||
pair_dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG)
|
||||
pair_dialog.set_position(Gtk.WindowPosition.CENTER)
|
||||
pair_dialog.present()
|
||||
|
||||
def pair(frame):
|
||||
return make('list-add', 'Pair new device', _pair_device, frame)
|
||||
|
||||
|
||||
def _unpair_device(action, frame):
|
||||
window = frame.get_toplevel()
|
||||
# window.present()
|
||||
device = frame._device
|
||||
qdialog = Gtk.MessageDialog(window, 0,
|
||||
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.ACCEPT:
|
||||
try:
|
||||
del device.receiver[device.number]
|
||||
except:
|
||||
error_dialog(window, 'Unpairing failed', 'Failed to unpair device\n%s .' % device.name)
|
||||
|
||||
def unpair(frame):
|
||||
return make('edit-delete', 'Unpair', _unpair_device, frame)
|
||||
202
lib/solaar/ui/config_panel.py
Normal file
@@ -0,0 +1,202 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from gi.repository import Gtk, GObject
|
||||
|
||||
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(4)
|
||||
|
||||
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)
|
||||
# print ("task", *task)
|
||||
if task[0] == 'write':
|
||||
_, setting, value, sbox = task
|
||||
GObject.idle_add(_write_start, sbox, priority=0)
|
||||
value = setting.write(value)
|
||||
elif task[0] == 'read':
|
||||
_, setting, force_read, sbox = task
|
||||
value = setting.read(not force_read)
|
||||
GObject.idle_add(_update_setting_item, sbox, value, priority=99)
|
||||
|
||||
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):
|
||||
# print ("switch notify", switch, switch.get_active(), setting)
|
||||
if switch.get_sensitive():
|
||||
# value = setting.write(switch.get_active() == True)
|
||||
# _update_setting_item(switch.get_parent(), value)
|
||||
_apply_queue.put(('write', setting, switch.get_active() == True, switch.get_parent()))
|
||||
|
||||
|
||||
def _combo_notify(cbbox, setting, spinner):
|
||||
# print ("combo notify", cbbox, cbbox.get_active_id(), setting)
|
||||
if cbbox.get_sensitive():
|
||||
_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()
|
||||
|
||||
# print ("update", control, "with new value", value)
|
||||
if value is None:
|
||||
control.set_sensitive(False)
|
||||
failed.set_visible(True)
|
||||
return
|
||||
|
||||
failed.set_visible(False)
|
||||
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
|
||||
control.set_sensitive(True)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def create():
|
||||
b = Gtk.VBox(homogeneous=False, spacing=4)
|
||||
b.set_property('margin', 8)
|
||||
return b
|
||||
|
||||
|
||||
def update(frame):
|
||||
box = 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
|
||||
_remove_children(box)
|
||||
return
|
||||
|
||||
if not box.get_visible():
|
||||
# no point in doing this right now, is there?
|
||||
return
|
||||
|
||||
if not device.settings:
|
||||
# nothing to do here
|
||||
return
|
||||
|
||||
force_read = False
|
||||
items = box.get_children()
|
||||
if len(device.settings) != len(items):
|
||||
_remove_children(box)
|
||||
if device.status:
|
||||
items = list(_add_settings(box, device))
|
||||
assert len(device.settings) == len(items)
|
||||
force_read = True
|
||||
|
||||
device_active = bool(device.status)
|
||||
# if the device just became active, re-read the settings
|
||||
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))
|
||||
|
||||
|
||||
def _remove_children(container):
|
||||
container.foreach(lambda x, _: container.remove(x), None)
|
||||
|
||||
79
lib/solaar/ui/icons.py
Normal file
@@ -0,0 +1,79 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from gi.repository import Gtk
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
_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)
|
||||
# print ("menu", int(Gtk.IconSize.MENU), Gtk.icon_size_lookup(Gtk.IconSize.MENU))
|
||||
# print ("small toolbar", int(Gtk.IconSize.SMALL_TOOLBAR), Gtk.icon_size_lookup(Gtk.IconSize.SMALL_TOOLBAR))
|
||||
# print ("large toolbar", int(Gtk.IconSize.LARGE_TOOLBAR), Gtk.icon_size_lookup(Gtk.IconSize.LARGE_TOOLBAR))
|
||||
# print ("button", int(Gtk.IconSize.BUTTON), Gtk.icon_size_lookup(Gtk.IconSize.BUTTON))
|
||||
# print ("dnd", int(Gtk.IconSize.DND), Gtk.icon_size_lookup(Gtk.IconSize.DND))
|
||||
# print ("dialog", int(Gtk.IconSize.DIALOG), Gtk.icon_size_lookup(Gtk.IconSize.DIALOG))
|
||||
|
||||
|
||||
APP_ICON = { 1: 'solaar', 2: 'solaar-mask', 0: 'solaar-init', -1: 'solaar-fail' }
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def battery(level):
|
||||
if level < 0:
|
||||
return 'battery_unknown'
|
||||
return 'battery_%03d' % (10 * ((level + 5) // 10))
|
||||
|
||||
|
||||
_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()
|
||||
62
lib/solaar/ui/indicate.py
Normal file
@@ -0,0 +1,62 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
# import logging
|
||||
|
||||
# try:
|
||||
# from gi.repository import Indicate
|
||||
# from time import time as _timestamp
|
||||
|
||||
# # import ui
|
||||
|
||||
# # 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)
|
||||
|
||||
# def init(app_title):
|
||||
# global available
|
||||
|
||||
# try:
|
||||
# s = Indicate.Server()
|
||||
# s.set_type('message.im')
|
||||
# s.set_default()
|
||||
# print s
|
||||
# s.show()
|
||||
# s.connect('server-display', server_display)
|
||||
|
||||
# i = Indicate.Indicator()
|
||||
# i.set_property('sender', 'test message sender')
|
||||
# i.set_property('body', 'test message body')
|
||||
# i.set_property_time('time', _timestamp())
|
||||
# i.set_subtype('im')
|
||||
# print i, i.list_properties()
|
||||
# i.show()
|
||||
# i.connect('user-display', display)
|
||||
|
||||
# pass
|
||||
# except:
|
||||
# available = False
|
||||
|
||||
# init('foo')
|
||||
|
||||
# # assumed to be working since the import succeeded
|
||||
# available = True
|
||||
|
||||
# def server_display(s):
|
||||
# print 'server display', s
|
||||
|
||||
# def display(i):
|
||||
# print "indicator display", i
|
||||
# i.hide()
|
||||
|
||||
# except ImportError:
|
||||
# available = False
|
||||
# init = lambda app_title: False
|
||||
# uninit = lambda: None
|
||||
# show = lambda dev: None
|
||||
438
lib/solaar/ui/main_window.py
Normal file
@@ -0,0 +1,438 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from gi.repository import Gtk, Gdk, GObject
|
||||
|
||||
from logitech.unifying_receiver import status as _status
|
||||
from . import config_panel as _config_panel
|
||||
from . import action as _action, icons as _icons
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
_RECEIVER_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR
|
||||
_DEVICE_ICON_SIZE = Gtk.IconSize.DIALOG
|
||||
_STATUS_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR
|
||||
_TOOLBAR_ICON_SIZE = Gtk.IconSize.MENU
|
||||
_PLACEHOLDER = '~'
|
||||
_FALLBACK_ICON = 'preferences-desktop-peripherals'
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _make_receiver_box(name):
|
||||
frame = Gtk.Frame()
|
||||
frame._device = None
|
||||
frame.set_name(name)
|
||||
|
||||
icon_set = _icons.device_icon_set(name)
|
||||
icon = Gtk.Image.new_from_icon_set(icon_set, _RECEIVER_ICON_SIZE)
|
||||
icon.set_padding(2, 2)
|
||||
frame._icon = icon
|
||||
|
||||
label = Gtk.Label('Scanning...')
|
||||
label.set_alignment(0, 0.5)
|
||||
frame._label = label
|
||||
|
||||
pairing_icon = Gtk.Image.new_from_icon_name('network-wireless', _RECEIVER_ICON_SIZE)
|
||||
pairing_icon.set_tooltip_text('The pairing lock is open.')
|
||||
pairing_icon._tick = 0
|
||||
frame._pairing_icon = pairing_icon
|
||||
|
||||
toolbar = Gtk.Toolbar()
|
||||
toolbar.set_style(Gtk.ToolbarStyle.ICONS)
|
||||
toolbar.set_icon_size(_TOOLBAR_ICON_SIZE)
|
||||
toolbar.set_show_arrow(False)
|
||||
frame._toolbar = toolbar
|
||||
|
||||
hbox = Gtk.HBox(homogeneous=False, spacing=8)
|
||||
hbox.pack_start(icon, False, False, 0)
|
||||
hbox.pack_start(label, True, True, 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_property('margin-left', 36)
|
||||
info_label.set_alignment(0, 0)
|
||||
info_label.set_selectable(True)
|
||||
frame._info_label = info_label
|
||||
|
||||
def _update_info_label(f):
|
||||
device = f._device
|
||||
if f._info_label.get_visible() and '\n' not in f._info_label.get_text():
|
||||
items = [('Path', device.path), ('Serial', device.serial)] + \
|
||||
[(fw.kind, fw.version) for fw in device.firmware]
|
||||
f._info_label.set_markup('<small><tt>%s</tt></small>' % '\n'.join('%-13s: %s' % item for item in items))
|
||||
|
||||
def _toggle_info_label(action, f):
|
||||
active = action.get_active()
|
||||
vb = f.get_child()
|
||||
for c in vb.get_children()[1:]:
|
||||
c.set_visible(active)
|
||||
|
||||
if active:
|
||||
GObject.timeout_add(50, _update_info_label, f)
|
||||
|
||||
toggle_info_action = _action.make_toggle('dialog-information', 'Details', _toggle_info_label, frame)
|
||||
toolbar.insert(toggle_info_action.create_tool_item(), 0)
|
||||
toolbar.insert(_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(2)
|
||||
vbox.pack_start(hbox, 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()
|
||||
|
||||
pairing_icon.set_visible(False)
|
||||
_toggle_info_label(toggle_info_action, frame)
|
||||
return frame
|
||||
|
||||
|
||||
def _make_device_box(index):
|
||||
frame = Gtk.Frame()
|
||||
frame._device = None
|
||||
frame.set_name(_PLACEHOLDER)
|
||||
|
||||
icon = Gtk.Image.new_from_icon_name(_FALLBACK_ICON, _DEVICE_ICON_SIZE)
|
||||
icon.set_alignment(0.5, 0)
|
||||
frame._icon = icon
|
||||
|
||||
label = Gtk.Label('Initializing...')
|
||||
label.set_alignment(0, 0.5)
|
||||
label.set_padding(4, 0)
|
||||
frame._label = label
|
||||
|
||||
battery_icon = Gtk.Image.new_from_icon_name(_icons.battery(-1), _STATUS_ICON_SIZE)
|
||||
|
||||
battery_label = Gtk.Label()
|
||||
battery_label.set_width_chars(6)
|
||||
battery_label.set_alignment(0, 0.5)
|
||||
|
||||
light_icon = Gtk.Image.new_from_icon_name('light_unknown', _STATUS_ICON_SIZE)
|
||||
|
||||
light_label = Gtk.Label()
|
||||
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)
|
||||
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_style(Gtk.ToolbarStyle.ICONS)
|
||||
toolbar.set_icon_size(_TOOLBAR_ICON_SIZE)
|
||||
toolbar.set_show_arrow(False)
|
||||
frame._toolbar = toolbar
|
||||
|
||||
status_box = Gtk.HBox(homogeneous=False, spacing=2)
|
||||
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)
|
||||
frame._status_icons = status_box
|
||||
|
||||
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_property('margin-left', 54)
|
||||
info_label.set_selectable(True)
|
||||
info_label.set_alignment(0, 0)
|
||||
frame._info_label = info_label
|
||||
|
||||
def _update_info_label(f):
|
||||
if frame._info_label.get_text().count('\n') < 4:
|
||||
device = f._device
|
||||
assert device
|
||||
|
||||
items = [None, None, None, None, None, None, None, None]
|
||||
hid = device.protocol
|
||||
items[0] = ('Protocol', 'HID++ %1.1f' % hid if hid else 'unknown')
|
||||
items[1] = ('Polling rate', '%d ms' % device.polling_rate)
|
||||
items[2] = ('Wireless PID', device.wpid)
|
||||
items[3] = ('Serial', device.serial)
|
||||
firmware = device.firmware
|
||||
if firmware:
|
||||
items[4:] = [(fw.kind, (fw.name + ' ' + fw.version).strip()) for fw in firmware]
|
||||
|
||||
frame._info_label.set_markup('<small><tt>%s</tt></small>' % '\n'.join('%-13s: %s' % i for i in items if i))
|
||||
|
||||
def _toggle_info_label(action, f):
|
||||
active = action.get_active()
|
||||
if active:
|
||||
# set config toggle button as inactive
|
||||
f._toolbar.get_children()[-1].set_active(False)
|
||||
|
||||
vb = f.get_child()
|
||||
children = vb.get_children()
|
||||
children[1].set_visible(active) # separator
|
||||
children[2].set_visible(active) # info label
|
||||
|
||||
if active:
|
||||
GObject.timeout_add(30, _update_info_label, f)
|
||||
|
||||
def _toggle_config(action, f):
|
||||
active = action.get_active()
|
||||
if active:
|
||||
# set info toggle button as inactive
|
||||
f._toolbar.get_children()[0].set_active(False)
|
||||
|
||||
vb = f.get_child()
|
||||
children = vb.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, f)
|
||||
|
||||
toggle_info_action = _action.make_toggle('dialog-information', 'Details', _toggle_info_label, frame)
|
||||
toolbar.insert(toggle_info_action.create_tool_item(), 0)
|
||||
toggle_config_action = _action.make_toggle('preferences-system', 'Configuration', _toggle_config, frame)
|
||||
toolbar.insert(toggle_config_action.create_tool_item(), -1)
|
||||
|
||||
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)
|
||||
|
||||
frame._config_box = _config_panel.create()
|
||||
vbox.pack_start(frame._config_box, False, False, 0)
|
||||
|
||||
unpair = Gtk.Button('Unpair')
|
||||
unpair.set_image(Gtk.Image.new_from_icon_name('edit-delete', Gtk.IconSize.BUTTON))
|
||||
unpair.connect('clicked', _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 create(title, name, max_devices, systray=False):
|
||||
window = Gtk.Window()
|
||||
window.set_title(title)
|
||||
window.set_icon_name(_icons.APP_ICON[0])
|
||||
window.set_role('status-window')
|
||||
|
||||
vbox = Gtk.VBox(homogeneous=False, spacing=12)
|
||||
vbox.set_border_width(4)
|
||||
|
||||
rbox = _make_receiver_box(name)
|
||||
vbox.add(rbox)
|
||||
for i in range(1, 1 + max_devices):
|
||||
dbox = _make_device_box(i)
|
||||
vbox.add(dbox)
|
||||
vbox.set_visible(True)
|
||||
|
||||
window.add(vbox)
|
||||
|
||||
geometry = Gdk.Geometry()
|
||||
geometry.min_width = 320
|
||||
geometry.min_height = 32
|
||||
window.set_geometry_hints(vbox, geometry, Gdk.WindowHints.MIN_SIZE)
|
||||
window.set_resizable(False)
|
||||
|
||||
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
|
||||
|
||||
def _set_has_systray(w, systray):
|
||||
# print ("set has systray", systray, w._has_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
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _update_receiver_box(frame, receiver):
|
||||
frame._label.set_text(str(receiver.status))
|
||||
if receiver:
|
||||
frame._device = receiver
|
||||
frame._icon.set_sensitive(True)
|
||||
if receiver.status.lock_open:
|
||||
if frame._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
|
||||
frame._pairing_icon.set_visible(True)
|
||||
GObject.timeout_add(1000, _pairing_tick, frame._pairing_icon, receiver.status)
|
||||
else:
|
||||
frame._pairing_icon.set_visible(False)
|
||||
frame._pairing_icon.set_sensitive(True)
|
||||
frame._pairing_icon._tick = 0
|
||||
frame._toolbar.set_sensitive(True)
|
||||
else:
|
||||
frame._device = None
|
||||
frame._icon.set_sensitive(False)
|
||||
frame._pairing_icon.set_visible(False)
|
||||
frame._toolbar.set_sensitive(False)
|
||||
frame._toolbar.get_children()[0].set_active(False)
|
||||
frame._info_label.set_text('')
|
||||
|
||||
|
||||
def _update_device_box(frame, dev):
|
||||
if dev is None:
|
||||
frame.set_visible(False)
|
||||
frame.set_name(_PLACEHOLDER)
|
||||
frame._device = None
|
||||
_config_panel.update(frame)
|
||||
return
|
||||
|
||||
first_run = frame.get_name() != dev.name
|
||||
if first_run:
|
||||
frame._device = dev
|
||||
frame.set_name(dev.name)
|
||||
icon_set = _icons.device_icon_set(dev.name, dev.kind)
|
||||
frame._icon.set_from_icon_set(icon_set, _DEVICE_ICON_SIZE)
|
||||
frame._label.set_markup('<b>%s</b>' % dev.name)
|
||||
for i in frame._toolbar.get_children():
|
||||
i.set_active(False)
|
||||
|
||||
battery_icon, battery_label, light_icon, light_label, not_encrypted_icon, _ = frame._status_icons
|
||||
battery_level = dev.status.get(_status.BATTERY_LEVEL)
|
||||
|
||||
if dev.status:
|
||||
frame._label.set_sensitive(True)
|
||||
|
||||
if battery_level is None:
|
||||
battery_icon.set_sensitive(False)
|
||||
battery_icon.set_from_icon_name(_icons.battery(-1), _STATUS_ICON_SIZE)
|
||||
battery_label.set_markup('<small>no status</small>')
|
||||
battery_label.set_sensitive(True)
|
||||
else:
|
||||
battery_icon.set_from_icon_name(_icons.battery(battery_level), _STATUS_ICON_SIZE)
|
||||
battery_icon.set_sensitive(True)
|
||||
battery_label.set_text('%d%%' % battery_level)
|
||||
battery_label.set_sensitive(True)
|
||||
|
||||
battery_status = dev.status.get(_status.BATTERY_STATUS)
|
||||
battery_icon.set_tooltip_text(battery_status or '')
|
||||
|
||||
light_level = dev.status.get(_status.LIGHT_LEVEL)
|
||||
if light_level is None:
|
||||
light_icon.set_visible(False)
|
||||
light_label.set_visible(False)
|
||||
else:
|
||||
icon_name = 'light_%03d' % (20 * ((light_level + 50) // 100))
|
||||
light_icon.set_from_icon_name(icon_name, _STATUS_ICON_SIZE)
|
||||
light_icon.set_visible(True)
|
||||
light_label.set_text('%d lux' % light_level)
|
||||
light_label.set_visible(True)
|
||||
|
||||
not_encrypted_icon.set_visible(dev.status.get(_status.ENCRYPTED) == False)
|
||||
|
||||
else:
|
||||
frame._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._toolbar.get_children()[-1].set_active(False)
|
||||
|
||||
frame.set_visible(True)
|
||||
_config_panel.update(frame)
|
||||
|
||||
|
||||
def update(window, receiver, device=None):
|
||||
assert receiver is not None
|
||||
# print ("update", receiver, receiver.status, len(receiver), device)
|
||||
window.set_icon_name(_icons.APP_ICON[1 if receiver else -1])
|
||||
|
||||
vbox = window.get_child()
|
||||
frames = list(vbox.get_children())
|
||||
assert len(frames) == 1 + receiver.max_devices, frames
|
||||
|
||||
if device is None:
|
||||
_update_receiver_box(frames[0], receiver)
|
||||
if not receiver:
|
||||
for frame in frames[1:]:
|
||||
_update_device_box(frame, None)
|
||||
else:
|
||||
_update_device_box(frames[device.number], None if device.status is None else device)
|
||||
@@ -2,30 +2,24 @@
|
||||
# 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
|
||||
from . import icons as _icons
|
||||
|
||||
# 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
|
||||
|
||||
# cache references to shown notifications here, so if another status comes
|
||||
# while its notification is still visible we don't create another one
|
||||
_notifications = {}
|
||||
|
||||
|
||||
def init(app_title):
|
||||
"""Init the notifications system."""
|
||||
global available
|
||||
@@ -47,7 +41,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 +51,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, _icons.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 +67,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
|
||||
198
lib/solaar/ui/pair_window.py
Normal file
@@ -0,0 +1,198 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
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
|
||||
|
||||
from . import icons as _icons
|
||||
from logitech.unifying_receiver import status as _status
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
_PAIRING_TIMEOUT = 30
|
||||
|
||||
|
||||
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)
|
||||
label.set_line_wrap(True)
|
||||
p.pack_start(label, False, False, 0)
|
||||
|
||||
p.show_all()
|
||||
return p
|
||||
|
||||
|
||||
# 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 _prepare(assistant, page, receiver):
|
||||
index = assistant.get_current_page()
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("prepare %s %d %s", assistant, index, page)
|
||||
|
||||
if index == 0:
|
||||
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:
|
||||
assistant.remove_page(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
|
||||
|
||||
|
||||
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()
|
||||
assistant.commit()
|
||||
|
||||
|
||||
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 = _icons.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>%s</b>' % device.name)
|
||||
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(400, 240)
|
||||
assistant.set_resizable(False)
|
||||
assistant.set_role('pair-device')
|
||||
|
||||
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, 24)
|
||||
|
||||
assistant.connect('prepare', _prepare, receiver)
|
||||
assistant.connect('cancel', _finish, receiver)
|
||||
assistant.connect('close', _finish, receiver)
|
||||
|
||||
return assistant
|
||||
132
lib/solaar/ui/status_icon.py
Normal file
@@ -0,0 +1,132 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from gi.repository import Gtk, GObject, GdkPixbuf
|
||||
|
||||
from . import action as _action, icons as _icons
|
||||
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(name)
|
||||
icon.set_name(name)
|
||||
icon.set_from_icon_name(_icons.APP_ICON[0])
|
||||
icon._devices = list(_NO_DEVICES)
|
||||
|
||||
icon.set_tooltip_text(name)
|
||||
icon.connect('activate', window.toggle_visible)
|
||||
|
||||
menu = Gtk.Menu()
|
||||
for a in menu_actions or ():
|
||||
if a:
|
||||
menu.append(a.create_menu_item())
|
||||
|
||||
menu.append(_action.quit.create_menu_item())
|
||||
menu.show_all()
|
||||
|
||||
icon.connect('popup_menu',
|
||||
lambda icon, button, time, menu:
|
||||
menu.popup(None, None, icon.position_menu, icon, button, time),
|
||||
menu)
|
||||
return icon
|
||||
|
||||
|
||||
def check_systray(icon, window):
|
||||
# use size-changed to detect if the systray is available or not
|
||||
def _size_changed(i, size, w):
|
||||
import logging
|
||||
logging.info("size-chagend %s %s", size, w)
|
||||
def _check_systray(i2, w2):
|
||||
logging.info("check_systray %s %s", i2.is_embedded(), i2.get_visible())
|
||||
w2.set_has_systray(i2.is_embedded() and i2.get_visible())
|
||||
# first guess
|
||||
GObject.timeout_add(250, _check_systray, i, w)
|
||||
# just to make sure...
|
||||
# GObject.timeout_add(1000, _check_systray, i, w)
|
||||
|
||||
_size_changed(icon, None, window)
|
||||
icon.connect('size-changed', _size_changed, window)
|
||||
|
||||
|
||||
_PIXMAPS = {}
|
||||
def _icon_with_battery(level, active):
|
||||
battery_icon = _icons.battery(level)
|
||||
name = '%s-%s' % (battery_icon, active)
|
||||
if name not in _PIXMAPS:
|
||||
mask = _icons.icon_file(_icons.APP_ICON[2], 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)
|
||||
|
||||
battery = _icons.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 active:
|
||||
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)
|
||||
if device is not None:
|
||||
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
|
||||
|
||||
def _lines(r, devices):
|
||||
yield '<b>Solaar</b>: %s' % r.status
|
||||
yield ''
|
||||
|
||||
for dev in devices:
|
||||
if dev is None:
|
||||
continue
|
||||
|
||||
yield '<b>%s</b>' % dev.name
|
||||
|
||||
assert hasattr(dev, 'status') and dev.status is not None
|
||||
p = str(dev.status)
|
||||
if p: # does it have any properties to print?
|
||||
if dev.status:
|
||||
yield '\t%s' % p
|
||||
else:
|
||||
yield '\t%s <small>(inactive)</small>' % p
|
||||
else:
|
||||
if dev.status:
|
||||
yield '\t<small>no status</small>'
|
||||
else:
|
||||
yield '\t<small>(inactive)</small>'
|
||||
yield ''
|
||||
|
||||
icon.set_tooltip_markup('\n'.join(_lines(receiver, icon._devices)).rstrip('\n'))
|
||||
|
||||
battery_status = None
|
||||
battery_level = 1000
|
||||
for dev in icon._devices:
|
||||
if dev is not None:
|
||||
level = dev.status.get(_status.BATTERY_LEVEL)
|
||||
if level is not None and level < battery_level:
|
||||
battery_status = dev.status
|
||||
battery_level = level
|
||||
|
||||
if battery_status is None:
|
||||
icon.set_from_icon_name(_icons.APP_ICON[1 if receiver else -1])
|
||||
else:
|
||||
icon.set_from_pixbuf(_icon_with_battery(battery_level, bool(battery_status)))
|
||||
@@ -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 "$@"
|
||||
35
packaging/build_deb.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
if test ! -r "$HOME/.devscripts"; then
|
||||
echo "$HOME/.descripts must exist"
|
||||
fi
|
||||
|
||||
cd `dirname "$0"`/..
|
||||
DEBIAN_FILES="$PWD/packaging/debian"
|
||||
DIST="$PWD/dist/${DISTRIBUTION:=debian}"
|
||||
|
||||
BUILD_DIR="${TMPDIR:-/tmp}/$DIST"
|
||||
rm -rf "$BUILD_DIR"
|
||||
mkdir -m 0700 -p "$BUILD_DIR"
|
||||
python "setup.py" sdist --dist-dir="$BUILD_DIR" --formats=gztar
|
||||
|
||||
cd "$BUILD_DIR"
|
||||
S=`ls -1 solaar-*.tar.gz`
|
||||
VERSION=${S#solaar-}
|
||||
VERSION=${VERSION%.tar.gz}
|
||||
tar xfz "$S"
|
||||
mv "$S" solaar_$VERSION.orig.tar.gz
|
||||
|
||||
cd solaar-$VERSION
|
||||
cp -a "$DEBIAN_FILES" .
|
||||
|
||||
test -n "$DEBIAN_FILES_EXTRA" && cp -a $DEBIAN_FILES_EXTRA/* debian/
|
||||
|
||||
debuild ${DEBUILD_ARGS:-$@}
|
||||
|
||||
rm -rf "$DIST"
|
||||
mkdir -p "$DIST"
|
||||
cp -a ../solaar_$VERSION* "$DIST"
|
||||
cd "$DIST"
|
||||
30
packaging/build_ppa.sh
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
cd `dirname "$0"`/..
|
||||
|
||||
DISTRIBUTION=ubuntu
|
||||
DEBIAN_FILES_EXTRA="$PWD/packaging/ubuntu"
|
||||
|
||||
. "$HOME/.devscripts"
|
||||
|
||||
DEBIAN_CHANGELOG="$PWD/packaging/debian/changelog"
|
||||
PPA_CHANGELOG="$DEBIAN_FILES_EXTRA/changelog"
|
||||
|
||||
latest=`head -n 1 "$DEBIAN_CHANGELOG" | sed -e 's#(\([^)]*\))#(\1ppa1)#; s#UNRELEASED#precise#'`
|
||||
cat - "$DEBIAN_CHANGELOG" > "$PPA_CHANGELOG" <<_CHANGELOG
|
||||
$latest
|
||||
|
||||
* Customized debian/ for ubuntu launchpad ppa.
|
||||
|
||||
-- $DEBFULLNAME <$DEBMAIL> $(date -R)
|
||||
|
||||
_CHANGELOG
|
||||
|
||||
DEBUILD_ARGS="-S -sa"
|
||||
. packaging/build_deb.sh
|
||||
|
||||
rm -f "$PPA_CHANGELOG"
|
||||
|
||||
#dput solaar-ppa solaar_*_source.changes
|
||||
5
packaging/debian/changelog
Normal file
@@ -0,0 +1,5 @@
|
||||
solaar (0.8.7-1) UNRELEASED; urgency=low
|
||||
|
||||
* Debian packaging scripts, supports ubuntu ppa as well.
|
||||
|
||||
-- Daniel Pavel <daniel.pavel@gmail.com> Tue, 18 Jan 2013 18:36:00 +0200
|
||||
1
packaging/debian/compat
Normal file
@@ -0,0 +1 @@
|
||||
8
|
||||
21
packaging/debian/control
Normal file
@@ -0,0 +1,21 @@
|
||||
Source: solaar
|
||||
Section: misc
|
||||
Priority: optional
|
||||
Maintainer: Daniel Pavel <daniel.pavel@gmail.com>
|
||||
Build-Depends: debhelper (>= 8)
|
||||
Build-Depends-Indep: python
|
||||
X-Python-Version: >= 2.7
|
||||
X-Python3-Version: >= 3.2
|
||||
Standards-Version: 3.9.4
|
||||
Homepage: http://pwr.github.com/Solaar
|
||||
Vcs-Git: git://github.com/pwr/Solaar.git
|
||||
Vcs-browser: http://github.com/pwr/Solaar
|
||||
|
||||
Package: solaar
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, ${python:Depends}, python-pyudev (>= 0.13), python-gi (>= 3.2), gir1.2-gtk-3.0 (>= 3.4), adduser, udev
|
||||
Suggests: gir1.2-notify-0.7
|
||||
Description: Logitech Unifying Receiver peripherals manager for Linux
|
||||
Solaar is a Linux device manager for Logitech's Unifying Receiver peripherals.
|
||||
It is able to pair/unpair devices to the receiver, and for some devices read
|
||||
battery status.
|
||||
39
packaging/debian/copyright
Normal file
@@ -0,0 +1,39 @@
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0
|
||||
Upstream-Name: Solaar
|
||||
Upstream-Contact: Daniel Pavel <daniel.pavel@gmail.com>
|
||||
Upstream-Source: http://github.com/pwr/Solaar
|
||||
|
||||
Files: *
|
||||
Copyright: Copyright (C) 2012 Daniel Pavel
|
||||
License: GPL-2
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License version 2 as
|
||||
published by the Free Software Foundation.
|
||||
.
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software Foundation,
|
||||
Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
.
|
||||
On Debian systems, the complete text of the GNU General Public License,
|
||||
version 2, can be found in /usr/share/common-licenses/GPL-2.
|
||||
|
||||
Files: share/icons/solaar*.png
|
||||
Copyright: Copyright (C) 2012 Daniel Pavel
|
||||
License: LGPL
|
||||
.
|
||||
|
||||
Files: share/icons/light_*.png
|
||||
Copyright: GNOME Project
|
||||
License: LGPL
|
||||
These files were copied from the Gnome icon theme (weather-*).
|
||||
|
||||
Files: share/icons/battery_*.png
|
||||
Copyright: Oxygen Icons
|
||||
Copyright (C) 2012 Daniel Pavel
|
||||
License: LGPL
|
||||
Based on icons from the Oxygen icon theme, with some modifications.
|
||||
23
packaging/debian/rules
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/make -f
|
||||
# -*- makefile -*-
|
||||
|
||||
# Uncomment this to turn on verbose mode.
|
||||
#export DH_VERBOSE=1
|
||||
#export DH_OPTIONS=-v
|
||||
|
||||
PREFIX = /usr
|
||||
-include debian/rules.extra
|
||||
|
||||
%:
|
||||
# Adding the required helpers
|
||||
dh $@ --with=python2
|
||||
|
||||
override_dh_auto_install:
|
||||
dh_auto_install -- --prefix=$(PREFIX) --install-lib=$(PREFIX)/share/solaar/lib
|
||||
|
||||
override_dh_python2:
|
||||
dh_python2 $(PREFIX)/share/solaar
|
||||
|
||||
override_dh_installudev:
|
||||
cp rules.d/??-logitech-unifying-receiver.rules debian/solaar.logitech-unifying-receiver.udev
|
||||
dh_installudev --priority=99 --name=logitech-unifying-receiver
|
||||
1
packaging/debian/rules.extra
Normal file
@@ -0,0 +1 @@
|
||||
# this file is included by debian/rules
|
||||
10
packaging/debian/solaar.postinst
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# creating plugdev group if he isn't already there
|
||||
if ! getent group plugdev >/dev/null; then
|
||||
addgroup --system plugdev
|
||||
fi
|
||||
|
||||
#DEBHELPER#
|
||||
1
packaging/debian/source/format
Normal file
@@ -0,0 +1 @@
|
||||
3.0 (quilt)
|
||||
2
packaging/debian/watch
Normal file
@@ -0,0 +1,2 @@
|
||||
version=3
|
||||
http://github.com/pwr/solaar/tags .*/v?(\d.*)\.(?:tgz|tar\.(?:gz|bz2|xz))
|
||||
53
packaging/gentoo/solaar-0.8.6.2.ebuild
Normal file
@@ -0,0 +1,53 @@
|
||||
# Copyright 1999-2012 Gentoo Foundation
|
||||
# Distributed under the terms of the GNU General Public License v2
|
||||
# $Header: $
|
||||
|
||||
EAPI=5
|
||||
|
||||
PYTHON_COMPAT=( python{2_7,3_2} )
|
||||
|
||||
inherit distutils-r1 udev user linux-info gnome2-utils
|
||||
|
||||
DESCRIPTION="Solaar is a Linux device manager for Logitech's Unifying Receiver peripherals"
|
||||
HOMEPAGE="http://pwr.github.com/Solaar/"
|
||||
SRC_URI="https://github.com/pwr/Solaar/archive/${PV}.tar.gz"
|
||||
|
||||
LICENSE="GPL-2"
|
||||
SLOT="0"
|
||||
KEYWORDS="~amd64"
|
||||
IUSE="doc"
|
||||
|
||||
RDEPEND="${PYTHON_DEPS}
|
||||
dev-python/pyudev
|
||||
dev-python/pygobject[${PYTHON_USEDEP}]"
|
||||
|
||||
MY_P="Solaar-${PV}"
|
||||
S="${WORKDIR}/${MY_P}"
|
||||
|
||||
DOCS=( README.md COPYING COPYRIGHT ChangeLog )
|
||||
|
||||
pkg_setup() {
|
||||
enewgroup plugdev
|
||||
|
||||
CONFIG_CHECK="HID_LOGITECH_DJ"
|
||||
linux-info_pkg_setup
|
||||
}
|
||||
|
||||
src_install() {
|
||||
distutils-r1_src_install
|
||||
|
||||
udev_dorules rules.d/*.rules
|
||||
|
||||
if use doc; then
|
||||
dodoc -r docs/*
|
||||
fi
|
||||
}
|
||||
|
||||
pkg_postinst() {
|
||||
gnome2_icon_cache_update
|
||||
elog "To be able to use this application, the user must be on the plugdev group."
|
||||
}
|
||||
|
||||
pkg_preinst() { gnome2_icon_savelist; }
|
||||
pkg_postrm() { gnome2_icon_cache_update; }
|
||||
|
||||
11
packaging/ubuntu/rules.extra
Normal file
@@ -0,0 +1,11 @@
|
||||
# this file is included by debian/rules
|
||||
|
||||
PREFIX = /opt/extras.ubuntu.com/solaar
|
||||
|
||||
# hacky...
|
||||
override_dh_link:
|
||||
dh_link
|
||||
sed -i -e 's#Exec=solaar#Exec=/opt/extras.ubuntu.com/solaar/bin/solaar#' \
|
||||
debian/solaar/opt/extras.ubuntu.com/solaar/share/applications/solaar.desktop
|
||||
sed -i -e 's#Icon=solaar#Icon=/opt/extras.ubuntu.com/solaar/share/icons/solaar.png#' \
|
||||
debian/solaar/opt/extras.ubuntu.com/solaar/share/applications/solaar.desktop
|
||||
1
packaging/ubuntu/solaar.links
Normal file
@@ -0,0 +1 @@
|
||||
opt/extras.ubuntu.com/solaar/share/applications/solaar.desktop usr/share/applications/extras-solaar.desktop
|
||||
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."
|
||||
45
setup.py
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from glob import glob
|
||||
from distutils.core import setup
|
||||
|
||||
|
||||
setup(name='solaar',
|
||||
version='0.8.7',
|
||||
description='Linux devices manager for the Logitech Unifying Receiver.',
|
||||
long_description='''
|
||||
Solaar is a Linux device manager for Logitech's Unifying Receiver peripherals.
|
||||
It is able to pair/unpair devices to the receiver, and for some devices read
|
||||
battery status.
|
||||
'''.strip(),
|
||||
author='Daniel Pavel',
|
||||
author_email='daniel.pavel@gmail.com',
|
||||
license='GPLv2',
|
||||
url='http://pwr.github.com/Solaar/',
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: X11 Applications :: GTK',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: End Users/Desktop',
|
||||
'License :: DFSG approved',
|
||||
'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',
|
||||
'Natural Language :: English',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3.2',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Topic :: Utilities',
|
||||
],
|
||||
|
||||
platforms=['linux'],
|
||||
requires=['pyudev (>= 0.13)', 'gi.repository.GObject (>= 2.0)', 'gi.repository.Gtk (>= 3.0)'],
|
||||
|
||||
package_dir={'': 'lib'},
|
||||
packages=['hidapi', 'logitech', 'logitech.unifying_receiver', 'solaar', 'solaar.ui'],
|
||||
|
||||
data_files=[('share/icons', ['share/solaar/icons/solaar.png']),
|
||||
('share/applications', ['share/applications/solaar.desktop']),
|
||||
('share/solaar/icons', glob('share/solaar/icons/*.png')),
|
||||
],
|
||||
|
||||
scripts=glob('bin/*'),
|
||||
)
|
||||
10
share/applications/solaar.desktop
Normal file
@@ -0,0 +1,10 @@
|
||||
[Desktop Entry]
|
||||
Name=Solaar
|
||||
Comment=Logitech Unifying Receiver peripherals manager
|
||||
Exec=solaar
|
||||
Icon=solaar
|
||||
StartupNotify=false
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Utility;GTK;
|
||||
#Categories=Utility;GTK;Settings;HardwareSettings;
|
||||
|
Before Width: | Height: | Size: 7.9 KiB |
BIN
share/solaar/icons/battery_000.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
share/solaar/icons/battery_010.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
BIN
share/solaar/icons/battery_040.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
share/solaar/icons/battery_050.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
BIN
share/solaar/icons/battery_070.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
BIN
share/solaar/icons/battery_090.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |