Compare commits

...

627 Commits
0.7.2 ... 0.9.2

Author SHA1 Message Date
Daniel Pavel
edc563e0e4 release 0.9.2 2013-07-24 11:40:54 +02:00
Daniel Pavel
969ca59948 translation credits: order languages alphabetically 2013-07-24 11:39:45 +02:00
Daniel Pavel
53abd22176 packaging: add compiled locale files 2013-07-24 11:06:58 +02:00
Daniel Pavel
f449b37bda minor update to the devices doc 2013-07-23 22:50:37 +02:00
Daniel Pavel
c97f4082dd translation credits updated 2013-07-23 22:49:47 +02:00
Daniel Pavel
5e6761c61b use relative paths in .pot/.po comments
otherwise the comments get changed when someone else run po-update
2013-07-23 22:48:59 +02:00
Daniel Pavel
b2c87b4f80 Merge pull request #95 from nexces/master
Added Polish translation
2013-07-23 13:36:30 -07:00
Adrian 'Nexces' Piotrowicz
58fcb074ff Updated po-update.sh script for use in (x)buntu.
Added another location to scan in function unfmt() due to fact that gtk30.mo and gtk30-properties.mo are found in /usr/share/locale-langpack/ instead of /usr/share/locale/.
2013-07-23 10:49:25 +02:00
Adrian 'Nexces' Piotrowicz
dbff65a4d1 Updated POT (translation template) 2013-07-23 10:49:05 +02:00
Adrian 'Nexces' Piotrowicz
a7ac589692 Added Polish translation 2013-07-23 10:46:56 +02:00
Daniel Pavel
3d4570cd1f check the Gtk bindings version at start-up 2013-07-20 06:22:15 +02:00
Daniel Pavel
b6bc35ed3e K800: added support for hand detection 2013-07-19 11:49:30 +02:00
Daniel Pavel
5d4d0c07ad use () instead of [] where possible 2013-07-19 11:48:40 +02:00
Daniel Pavel
15cb97c56e udev devices may not have all the attributes; fixes #93 2013-07-19 11:46:50 +02:00
Daniel Pavel
f902b32755 m515 device scan 2013-07-18 20:40:38 +02:00
Daniel Pavel
fcfc7cd6fc a few more asserts in descriptors
just testing stuff...
2013-07-18 20:38:05 +02:00
Daniel Pavel
c3718d22d6 minor clean-ups 2013-07-18 20:36:27 +02:00
Daniel Pavel
2829acd6f5 added side-scrolling toggle setting 2013-07-18 20:35:39 +02:00
Daniel Pavel
c35ae8ffc9 added wpid to T650 descriptor 2013-07-18 20:34:29 +02:00
Daniel Pavel
f7159e9338 properly mask flags when writing device settings; fixes #86 2013-07-18 20:33:52 +02:00
Daniel Pavel
4074fb7750 trim the codename to the declared length (fixes #90) 2013-07-18 16:23:34 +02:00
Daniel Pavel
444169d84b added descriptor for V550 Nano 2013-07-18 15:26:53 +02:00
Daniel Pavel
5d5f97a776 Gtk.Application.get_dbus_object_path my be missing 2013-07-18 14:06:51 +02:00
Daniel Pavel
a8c5d3bc24 Nano receivers were unsupported on kernel 3.2; fixes #88 2013-07-18 14:01:36 +02:00
Daniel Pavel
dd2a18d60e added documentation for translators 2013-07-17 20:36:39 +02:00
Daniel Pavel
a3f9860181 added descriptor for V450 Nano; fixes #89 2013-07-17 20:02:31 +02:00
Daniel Pavel
1fed000855 added wpid to M515 desccriptor 2013-07-17 19:54:28 +02:00
Daniel Pavel
61dfefde94 internationalized most strings; fixes #79
some might have slipped through the cracks
2013-07-17 19:53:21 +02:00
Daniel Pavel
454fbcbc6e Merge remote-tracking branch 'origin/master' 2013-07-16 16:41:53 +02:00
Daniel Pavel
8112e90ded website: replace the png logo with embedded svg 2013-07-16 16:36:00 +02:00
Daniel Pavel
a4c0ad3d73 added M345 descriptor info 2013-07-16 12:51:01 +02:00
Daniel Pavel
30f2afacb7 debian packaging: cleaned-up the debian/ files
according to feedback on debian bug #715172
(http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=715172)
2013-07-16 12:50:47 +02:00
Daniel Pavel
2a27c1d14e Merge pull request #87 from Lekensteyn/updates
docs/devices: add M345 (HID++ 2.0)
2013-07-16 03:39:28 -07:00
Peter Wu
cccf4ec428 docs/devices: add M345 (HID++ 2.0) 2013-07-16 12:31:56 +02:00
Daniel Pavel
38c76393b1 added copyright notices to all source files 2013-07-15 17:54:42 +02:00
Daniel Pavel
ce9b10dc2d started i18n support 2013-07-15 17:16:44 +02:00
Daniel Pavel
13e89dc219 post-build updates: debian/changelog and jekyll config 2013-07-13 13:23:52 +02:00
Daniel Pavel
16af3a9acc version 0.9.1 2013-07-13 11:24:41 +02:00
Daniel Pavel
c690b2c9e9 minor descriptors clean-up 2013-07-13 11:15:23 +02:00
Daniel Pavel
43ab629a26 moved logitech.unifying_receiver package to logitech_receiver 2013-07-12 22:48:09 +02:00
Daniel Pavel
b83a14e16f minor clean-ups and formatting 2013-07-12 22:39:17 +02:00
Daniel Pavel
a966bbff6f fixed handling of protocol version when the device is offline 2013-07-12 22:23:03 +02:00
Daniel Pavel
141a5c3dee don't ping devices known to be offline 2013-07-12 22:07:58 +02:00
Daniel Pavel
994bb07bc4 descriptors updated 2013-07-12 21:55:32 +02:00
Daniel Pavel
24f658d8fe support multiple Wireless PIDs for same device model 2013-07-12 21:53:23 +02:00
Daniel Pavel
49d77a2f90 handle unknown devices slightly better 2013-07-12 20:30:46 +02:00
Daniel Pavel
876cf35954 automatically detect the latest source.changes file generated 2013-07-12 17:14:15 +02:00
Daniel Pavel
46c0448854 got rid of /opt/extras sillyness in ubuntu packaging 2013-07-12 17:02:17 +02:00
Daniel Pavel
d78c7ed9be make sure items in window tree are sorted by number (fixed #73) 2013-07-12 15:24:49 +02:00
Daniel Pavel
51d0ccf654 only popup the window once per device on battery alerts 2013-07-12 14:35:16 +02:00
Daniel Pavel
9c8252b59f on alert popups, selected the device that triggered the popup 2013-07-12 14:10:26 +02:00
Daniel Pavel
488a78cc52 fixed #75 (pair button disappearing) 2013-07-12 10:35:28 +02:00
Daniel Pavel
0e6d088372 fixed some awkward path joining stuff 2013-07-11 22:46:29 +02:00
Daniel Pavel
d4d1a4f8e2 install the .desktop file in /etc/xdg/autostart (fixes #77) 2013-07-11 22:45:29 +02:00
Daniel Pavel
34801bfd1e install the application icon in hicolor/scalable/apps 2013-07-11 22:24:57 +02:00
Daniel Pavel
948b4c4d51 updated descriptors and devices doc for Anywhere MX 2013-07-11 15:34:36 +02:00
Daniel Pavel
85efe92d30 replaced appinstance with GtkApplication 2013-07-09 17:52:07 +02:00
Daniel Pavel
b01636e05d build-updated files for 0.9.0 2013-07-09 15:26:38 +02:00
Daniel Pavel
c0a7838429 version 0.9.0 2013-07-09 14:42:46 +02:00
Daniel Pavel
eda4112501 removed obsolete old window code 2013-07-08 21:21:53 +02:00
Daniel Pavel
dbb9025e26 replaced _ dummies with _ignore 2013-07-08 21:21:14 +02:00
Daniel Pavel
beee0bc8c2 typo in comment 2013-07-08 20:51:30 +02:00
Daniel Pavel
1bd6fd512b debian changelog updated 2013-07-07 22:15:00 +02:00
Daniel Pavel
ab8421b2f2 simplified (again) the deb build script
This time also check for consistency of .orig archives.
2013-07-07 22:07:31 +02:00
Daniel Pavel
1be56dd072 version 0.8.99.12 2013-07-07 21:58:33 +02:00
Daniel Pavel
c023f81f1a ChangeLog clean-up 2013-07-07 21:58:06 +02:00
Daniel Pavel
47f64d40de explicit encoding in main scripts 2013-07-07 21:54:35 +02:00
Daniel Pavel
d0b8233c9a minor clean-up 2013-07-07 21:40:55 +02:00
Daniel Pavel
39534d11dd make sure all debug and info loggings are if-guarded 2013-07-07 17:18:42 +02:00
Daniel Pavel
ca2f5f927c configuration: save on every change (fixes #80) 2013-07-07 16:06:27 +02:00
Daniel Pavel
2ab040591c major clean-up of debian building script 2013-07-07 14:06:03 +02:00
Daniel Pavel
8b26759731 release 0.8.99.11 2013-07-07 14:04:11 +02:00
Daniel Pavel
0e43287386 added a symlink to README.md to pacify sdist 2013-07-07 12:25:12 +02:00
Daniel Pavel
7221ceb7b4 forget battery status when the device has been offline for too long 2013-07-07 12:23:58 +02:00
Daniel Pavel
66416ff4cc fixed parsing of custom battery messages 2013-07-07 12:23:23 +02:00
Daniel Pavel
0d89d1e6c8 formatting fix in unpairing error dialog 2013-07-07 01:31:22 +02:00
Daniel Pavel
81765ee971 clean-ed up and documented gh_pages build script 2013-07-06 18:10:05 +02:00
Daniel Pavel
b72273b2a9 jekyll: made all image/css links relative 2013-07-06 18:08:52 +02:00
Daniel Pavel
e697a2d6cf updated debian build & upload scripts 2013-07-06 14:47:07 +02:00
Daniel Pavel
94d07eed53 version 0.8.99.10 2013-07-06 14:35:34 +02:00
Daniel Pavel
64ce3872b4 changelog updated 2013-07-06 14:24:17 +02:00
Daniel Pavel
508444526a commented-out the poll-ticking feature
not useful right now, and less code to worry about
2013-07-06 14:23:56 +02:00
Daniel Pavel
faa6de3b75 more explicit error dialogs, when possible 2013-07-06 14:20:50 +02:00
Daniel Pavel
77d2ae5249 omit the date in logging, is not important and the lines are shorter 2013-07-06 14:20:12 +02:00
Daniel Pavel
e2f94a9e0e be more lax as to when to allow pairing new devices 2013-07-06 13:55:22 +02:00
Daniel Pavel
ba55e4d9bd import * cleanups 2013-07-05 16:19:49 +02:00
Daniel Pavel
8e9ff939f3 hidconsole: enable hidpp automatically when talking to a Logitech Recevier 2013-07-05 16:19:24 +02:00
Daniel Pavel
bd3198f6f0 correctly handle out-of-process pairing and unpairing 2013-07-05 16:06:38 +02:00
Daniel Pavel
3319feeb73 replaced type() with isinstance() where possible 2013-07-05 16:02:38 +02:00
Daniel Pavel
47bcd4478c prettier error message if unpair fails 2013-07-05 14:35:06 +02:00
Daniel Pavel
065b8628d2 don't always show Notifications in details 2013-07-05 14:34:37 +02:00
Daniel Pavel
87e2f1cad7 solaar-cli: clearer handling of invalid device numbers 2013-07-05 14:34:18 +02:00
Daniel Pavel
373b07f271 fixed pairing in solaar-cli 2013-07-05 14:33:58 +02:00
Daniel Pavel
58f598fc88 version 0.8.99.9 2013-07-04 13:49:48 +02:00
Daniel Pavel
8f44c294d7 still getting strange messages from some devices 2013-07-04 13:37:39 +02:00
Daniel Pavel
f65079ad6d formatting fix for notifications 2013-07-04 13:35:23 +02:00
Daniel Pavel
edce56cd20 use a single separate thread for all possibly long-running stuff in GUI 2013-07-04 13:23:25 +02:00
Daniel Pavel
cd437c3809 only set NOTIFICATION_FLAGS for 1.0 devices 2013-07-04 13:22:35 +02:00
Daniel Pavel
007cbef086 better coverage of incoming events 2013-07-04 13:20:00 +02:00
Daniel Pavel
a03cc9ce64 more chair switching -- moved notifications handling into own .py 2013-07-03 08:21:40 +02:00
Daniel Pavel
3275aa4c12 allow most receivers to unpair (or at least try) 2013-07-03 08:18:47 +02:00
Daniel Pavel
a0f880fbba fix for NoneType crash 2013-07-02 19:43:18 +02:00
Daniel Pavel
833f087fdf also show polling rate in Hz 2013-07-02 12:23:47 +02:00
Daniel Pavel
9a2a28e0aa replaced hard-coded register numbers with constants
also re-worked the battery reading code a bit
2013-07-02 12:23:12 +02:00
Daniel Pavel
bf5fc42f07 fixed duplicate entries in keyboard tasks 2013-07-02 00:40:34 +02:00
Daniel Pavel
fb495fd2fc version 0.8.99.8 2013-07-01 19:35:46 +02:00
Daniel Pavel
90ab7af069 Anywhere MX and M600 descriptor updates 2013-07-01 19:22:46 +02:00
Daniel Pavel
0914e4c48d scan-registers: more lax interpretation of arguments 2013-07-01 19:01:37 +02:00
Daniel Pavel
3436055c7f re-worked (AGAIN) the way the devices are initially set-up
There is absolutely no consistency between the registers and
features receivers have, even if they're the same product_id!
2013-07-01 19:00:26 +02:00
Daniel Pavel
20aa797e96 configuration: use wpid in device key 2013-07-01 18:53:10 +02:00
Daniel Pavel
5ebc2634ae (hopefully) better serial read-out for devices 2013-07-01 18:40:03 +02:00
Daniel Pavel
94464a40b8 show the battery charge in the devices tree, if available (fixes #71) 2013-07-01 18:39:17 +02:00
Daniel Pavel
1194abf125 don't know yet how to tell if the receiver supports the unifying protocol 2013-07-01 15:28:05 +02:00
Daniel Pavel
ceba698678 moved settings templates into separate .py 2013-07-01 15:24:30 +02:00
Daniel Pavel
b1e9480f5a minor logging tweaks 2013-07-01 15:00:21 +02:00
Daniel Pavel
eaa38c858c properly format battery status string for 0x07 devices 2013-07-01 11:51:20 +02:00
Daniel Pavel
4504045bc6 extended descriptor for M305 2013-07-01 11:49:44 +02:00
Daniel Pavel
a9ea69b9a1 more explicit error message when trying to pair uncompatible devices 2013-07-01 11:41:47 +02:00
Daniel Pavel
9a8d089c73 handle some mystery data packets from older devices 2013-07-01 11:34:54 +02:00
Daniel Pavel
0d56cfbded clean-up in get/set notification flags computation 2013-06-30 17:12:44 +02:00
Daniel Pavel
9896875180 cleaner import 2013-06-30 15:03:39 +02:00
Daniel Pavel
a1c04ce39f renamed 'leds' register to '3leds', in case other may be found later 2013-06-30 15:02:58 +02:00
Daniel Pavel
da541e000a put usb ids into separate py file 2013-06-30 14:34:53 +02:00
Daniel Pavel
e490162e4f the K400 also has the FN swap feature 2013-06-30 14:04:14 +02:00
Daniel Pavel
a0b7d39f83 use the wpid to identify devices, when possible 2013-06-30 13:49:35 +02:00
Daniel Pavel
c464e049bf allow settings to be bound to certain device types
avoids stuff like smooth-scroll being attached to a keyboard
2013-06-29 21:26:25 +02:00
Daniel Pavel
daad9b5d5f cleaned-up NamedInts, added int2bytes and bytes2int generic functions 2013-06-29 20:37:04 +02:00
Daniel Pavel
2682722cb1 cleaned-up config_panel code 2013-06-29 20:35:35 +02:00
Daniel Pavel
8d5718178f load the configuration when the device is detected
and apply it every time the device comes online
2013-06-29 20:35:02 +02:00
Daniel Pavel
1d438f098f handle 0x07 battery notifications with indicating charging, but with no battery level 2013-06-29 20:26:29 +02:00
Daniel Pavel
1d305db29e minor UI layout tweaks 2013-06-29 19:02:19 +02:00
Daniel Pavel
c2a549c114 the K800 also has 3 leds, use them to indicate battery charge 2013-06-28 17:09:05 +02:00
Daniel Pavel
f43e298ac7 clean-up configuration on load and save 2013-06-26 16:05:14 +02:00
Daniel Pavel
c25b769578 repr() formatting cleanups 2013-06-26 13:40:46 +02:00
Daniel Pavel
0cf1f1983d handle backlight notifications from register 0x17 2013-06-26 13:38:36 +02:00
Daniel Pavel
39ec568ab1 descriptors table and devices documentation updated 2013-06-25 18:54:27 +02:00
Daniel Pavel
5b94ebae34 enable the backlight notifications on devices, if available 2013-06-25 18:53:18 +02:00
Daniel Pavel
a857b5fc43 fixed transparency in generated site icons 2013-06-25 17:11:28 +02:00
Daniel Pavel
eb8791ec47 fixed gh_pages script to use solaar.svg 2013-06-25 16:18:05 +02:00
Daniel Pavel
ff6da43007 version 0.8.99.7 2013-06-25 16:08:47 +02:00
Daniel Pavel
d42fdf8a4a updated debian copyright file (added svgs) 2013-06-25 16:06:13 +02:00
Daniel Pavel
adab25ad44 changelog updated 2013-06-25 16:02:21 +02:00
Daniel Pavel
90b057938b recommend python-dbus and upower in debian package control 2013-06-25 16:02:14 +02:00
Daniel Pavel
fc5c72e164 some clean-ups in descriptors 2013-06-25 16:01:29 +02:00
Daniel Pavel
bd5c03812f replaced solaar png icons with svgs 2013-06-25 15:59:49 +02:00
Daniel Pavel
6b0b7ea823 minor clean-ups and formatting 2013-06-24 17:04:19 +02:00
Daniel Pavel
dd24ed5fe0 brought solaar-cli in sync with the latest gui code 2013-06-24 17:02:45 +02:00
Daniel Pavel
f64942b51d if the battery level is approximative, show a string instead of percentage 2013-06-24 17:02:00 +02:00
Daniel Pavel
375db9996f fixed reading correct device kind on link notification 2013-06-24 16:58:41 +02:00
Daniel Pavel
20dfc063ff initial support for performance mx leds
The leds light up when we get updates about the battery level.
Right now they are not (programatically) turned off.
2013-06-23 20:59:16 +02:00
Daniel Pavel
70c341e268 don't use proxies for receiver/device objects
they should be unnecessary now that their status is properly tracked
2013-06-23 18:37:50 +02:00
Daniel Pavel
dc59c79bd3 properly handle tree re-selection when removing a receiver 2013-06-23 18:35:23 +02:00
Daniel Pavel
422cd26b92 some extra asserts on hid++ 1.0 register r/w 2013-06-23 18:16:13 +02:00
Daniel Pavel
cec892ce88 disable poll ticking
It was necessary to periodically check if the peripherals are still
online -- suspend/resume may cause Solaar to (wrongfully) remember the
status of devices after a resume.

Now that is handled by the optional upower module -- the hard way -- by
restarting all listeners on resume.
2013-06-23 18:15:19 +02:00
Daniel Pavel
d5374b9f51 optionally listen for upower suspend/resume events to stop/start all receiver listeners 2013-06-23 18:11:42 +02:00
Daniel Pavel
51e2d965cf version 0.8.99.6 2013-06-23 15:46:16 +02:00
Daniel Pavel
a6c8f2212a suggest appindicator library in solaar package 2013-06-23 15:45:07 +02:00
Daniel Pavel
b3aac40246 dropped activate_on_single_click, requires gtk 3.8 2013-06-23 13:44:34 +02:00
Daniel Pavel
fb138b77ff version 0.8.99.5 2013-06-23 13:09:44 +02:00
Daniel Pavel
273284da39 use number instead of serial to pick devices in ui (faster start-up) 2013-06-23 12:09:46 +02:00
Daniel Pavel
7102229937 properly handle ^C in console 2013-06-22 21:32:19 +02:00
Daniel Pavel
be80ba3b3b version 0.8.99.4 2013-06-21 22:27:18 +02:00
Daniel Pavel
4410cb6dc6 comment-out old main window 2013-06-21 22:25:04 +02:00
Daniel Pavel
df8df2bffa don't force a battery read on link active, will happen anyway 2013-06-21 22:24:28 +02:00
Daniel Pavel
ae4c921300 cleaner start-up of paired devices 2013-06-21 22:23:53 +02:00
Daniel Pavel
3d1aa6698d only check for non-unifying protocol only on nano receviers 2013-06-21 22:19:31 +02:00
Daniel Pavel
613b115eb4 fixed clean-up of config panel when unpairing devices 2013-06-21 16:51:39 +02:00
Daniel Pavel
e19b690bd5 fixed reading of polling rate 2013-06-21 16:47:19 +02:00
Daniel Pavel
888be8e8a8 be a bit more patient when pairing, the device notification may come after the pairing lock is closed 2013-06-21 16:40:00 +02:00
Daniel Pavel
5785896007 status: the device may be invalid when processing the unpair notification 2013-06-21 16:39:30 +02:00
Daniel Pavel
8d522de7e7 drop some unnecessary logging when ignoring request replies 2013-06-21 16:39:04 +02:00
Daniel Pavel
81ae4c8d4d forgot about pairing notification on receiver... 2013-06-21 16:32:29 +02:00
Daniel Pavel
8cd3b8fdef save configuration a little early, better timing on shutdown 2013-06-21 15:21:49 +02:00
Daniel Pavel
c1aa341a7a tweaked timing of enabling receiver notifications 2013-06-21 15:20:52 +02:00
Daniel Pavel
f9c192c47a only look for a different tray battery icon if the changed device is a peripheral 2013-06-21 15:19:07 +02:00
Daniel Pavel
cdc6da844e made the config_panel self-contained 2013-06-21 15:18:21 +02:00
Daniel Pavel
fd35f23af7 one a separate flag to check if the device is active 2013-06-21 15:17:14 +02:00
Daniel Pavel
c04851f64e better handling of timeouts in base.request 2013-06-21 15:05:48 +02:00
Daniel Pavel
3a63c3ad43 fixed reading encrypted link status in pairing window 2013-06-21 15:04:06 +02:00
Daniel Pavel
431f1c97cf added read/write_register to receiver and devices 2013-06-21 15:03:36 +02:00
Daniel Pavel
1b68a3d5a9 fixed request timeouts for devices 2013-06-20 19:51:54 +02:00
Daniel Pavel
c797808a40 fixed status keys in pairing 2013-06-20 19:35:54 +02:00
Daniel Pavel
d810ccba84 tray icon: pick the battery of the lowest _active_ device 2013-06-20 17:25:53 +02:00
Daniel Pavel
2fea5d9af2 disable the unpair button for peripherals of a Nano receiver 2013-06-20 17:21:58 +02:00
Daniel Pavel
1d9a3bf23a fixed checking for features of offline 2.0 devices 2013-06-20 17:21:23 +02:00
Daniel Pavel
3d8c764eb0 relax timeouts for device requests 2013-06-20 17:13:49 +02:00
Daniel Pavel
eb317f0d46 version 0.8.99.2 2013-06-20 14:49:02 +02:00
Daniel Pavel
27de1aea11 tray/window: append new devices rather than inserting them at the top 2013-06-20 14:43:49 +02:00
Daniel Pavel
92f7e761ee don't save the configuration if it hasn't been loaded first 2013-06-20 14:27:18 +02:00
Daniel Pavel
b2f1786eff use consistent naming of offline state for devices 2013-06-20 14:18:36 +02:00
Daniel Pavel
f8369e7c63 properly style the details panel 2013-06-20 14:18:03 +02:00
Daniel Pavel
f095a74f2a make the main window a regular window (visible in tasks list) 2013-06-20 13:51:42 +02:00
Daniel Pavel
4e3fccb009 fixed sizing of details panel 2013-06-20 13:41:05 +02:00
Daniel Pavel
a2995ed3c5 minor build_deb script update 2013-06-20 13:13:14 +02:00
Daniel Pavel
5edff9235b changelog update 2013-06-20 13:12:39 +02:00
Daniel Pavel
31e134c1e0 don't wait so long on the first idle poll 2013-06-20 13:12:29 +02:00
Daniel Pavel
47e22b788f version 0.8.99, will become 0.9 on release 2013-06-20 13:12:12 +02:00
Daniel Pavel
798628fdf8 add the USB id for the receiver in the details panel 2013-06-20 13:10:55 +02:00
Daniel Pavel
ca9825d262 support for the VX Nano mouse 2013-06-20 13:10:30 +02:00
Daniel Pavel
1a36ec65ee faster checking of feature-based settings 2013-06-20 12:55:48 +02:00
Daniel Pavel
d07a20e6f8 improved reading device properties 2013-06-20 12:54:27 +02:00
Daniel Pavel
783b317281 added a protocol field to the device descriptors 2013-06-20 11:04:17 +02:00
Daniel Pavel
4bc525f250 minor clean-ups 2013-06-20 11:02:45 +02:00
Daniel Pavel
11cfc7ea09 enable notification on recevier as soon as possible 2013-06-19 19:46:54 +02:00
Daniel Pavel
a806f2672d group all status keys into one object 2013-06-19 19:45:39 +02:00
Daniel Pavel
2fee88e54b fixed battery charge display in device info 2013-06-19 19:23:18 +02:00
Daniel Pavel
7e1a2a6e4f more fixes for device unpairing 2013-06-19 17:21:28 +02:00
Daniel Pavel
d5b311760e clean obsolete settings controls when devices are unpaired 2013-06-19 17:04:01 +02:00
Daniel Pavel
dffe6f8b91 fix display of firmware in details panel 2013-06-19 17:03:16 +02:00
Daniel Pavel
b4bca4670b cache notification flags when possible
avoids unnecessary reads from devices when the status hasn't changed
2013-06-19 17:03:01 +02:00
Daniel Pavel
150c43f41f some logging in udev 2013-06-19 16:49:20 +02:00
Daniel Pavel
9fc11df229 fixed window update when unpairing 2013-06-19 16:22:44 +02:00
Daniel Pavel
56a7a960c3 polling: it may happen that the receiver is removed mid-poll 2013-06-19 16:14:04 +02:00
Daniel Pavel
cd44cc6396 new single-window UI 2013-06-19 15:28:13 +02:00
Daniel Pavel
4af714f1dd K750: register the FN swap setting statically, no need to detect it at runtime 2013-06-18 16:48:02 +02:00
Daniel Pavel
836719587c renamed NANO receiver id 2013-06-18 16:45:44 +02:00
Daniel Pavel
2d6fab6e14 gh_pages build script updated 2013-06-17 16:48:04 +02:00
Daniel Pavel
0ae58a3346 fixed ubuntu dh_link 2013-06-17 16:47:44 +02:00
Daniel Pavel
f32e958984 version 0.8.9.4 2013-06-17 16:12:32 +02:00
Daniel Pavel
0703d927ca document Logitech receiver USB ids 2013-06-17 16:12:09 +02:00
Daniel Pavel
62b5deb77d debconf translation support (just english for now) 2013-06-17 16:11:46 +02:00
Daniel Pavel
82e718b3d1 create the plugdev group in postinst, not config 2013-06-17 16:11:17 +02:00
Daniel Pavel
41409c9b94 use debconf to check if the plugdev group is necessary 2013-06-17 14:45:45 +02:00
Daniel Pavel
57c759773a attach configuration to device settings objects 2013-06-17 11:00:53 +02:00
Daniel Pavel
9d6402a4f7 shorter thread name on receiver listener 2013-06-17 08:00:17 +02:00
Daniel Pavel
a29609df21 only save the configuration once when Solaar ends 2013-06-17 08:00:00 +02:00
Daniel Pavel
04ea8293a8 persist and restore device settings 2013-06-16 17:41:03 +02:00
Daniel Pavel
bde54aba3b proper shutdown of receiver listeners 2013-06-16 17:10:46 +02:00
Daniel Pavel
687e1be3e5 simplify debian/rules a bit 2013-06-16 17:10:46 +02:00
Daniel Pavel
a44d9b7ca8 fix battery display in main window 2013-06-16 17:10:46 +02:00
Daniel Pavel
febf571d84 account for devices going out-of-range while active 2013-06-16 17:10:46 +02:00
Daniel Pavel
818ece9f10 Merge pull request #67 from Lekensteyn/updates
udev: Use uaccess instead of plugdev (pwr/Solaar#66)
2013-06-16 08:10:04 -07:00
Peter Wu
ec10c9c70c udev: Use uaccess instead of plugdev (pwr/Solaar#66)
Rules are taken from https://git.lekensteyn.nl/ltunify/. The "VX Nano" receivers
(c51a and c526) have been removed as these are only used for Cordless mice and
are not even enabled in Solaar.

The installer is updated to check for the existence of a group and `mktemp` to
create temporary files. (relying on `$TMPDIR` is a bad idea.) This installer is
too fancy, most users will be fine with `install -m644
rules.d/42-logitech-unify-permissions.rules /etc/udev/rules.d/`. If you are lazy
and want to make `solaar-cli` work over SSH, then this installer will help you.
It won't add you to the `plugdev` group, instead it will edit the rule to use
your user's primary group.

Oh, and it does not change this group to "root" as that is pretty useless given
that root is the default.
2013-06-16 11:33:37 +02:00
Daniel Pavel
8852d50971 mark device status as inactive after idling out 2013-06-12 14:31:46 +02:00
Daniel Pavel
908f6763be updated deb/ppa build scripts 2013-06-12 14:31:16 +02:00
Daniel Pavel
03e20842fb cleaned-up gtk ui initialization 2013-06-12 14:29:34 +02:00
Daniel Pavel
084913ac91 updated devices documentation 2013-06-12 14:29:01 +02:00
Daniel Pavel
d9801e2d57 fixed firmware info for some HID++ 2.0 devices 2013-06-12 14:21:31 +02:00
Daniel Pavel
29fe913fd8 version 0.8.9.3 2013-06-08 21:11:39 +02:00
Daniel Pavel
5b68f9623e fix indicator description when there are receivers but no devices 2013-06-08 21:10:17 +02:00
Daniel Pavel
2a6662472d account for all GLib.id_add_watch variants 2013-06-08 21:07:24 +02:00
Daniel Pavel
5cf6777340 more tweaks to idle polling 2013-06-08 21:06:52 +02:00
Daniel Pavel
17863c85b6 fixed querying for known registers 2013-06-08 21:06:22 +02:00
Daniel Pavel
237d0f9d9a keep the battery level if polling the device marks it as offline 2013-06-08 21:05:30 +02:00
Daniel Pavel
b98e27e464 fix permissions error popup 2013-06-08 21:04:32 +02:00
Daniel Pavel
d01d9edb78 tweaked idle polling timings 2013-06-08 16:27:03 +02:00
Daniel Pavel
0a86683392 clean-up in solaar.listener 2013-06-08 16:18:25 +02:00
Daniel Pavel
b2b4febd31 receiver notifications: don't set all flags, might fail 2013-06-08 16:18:10 +02:00
Daniel Pavel
ad67e6eaee fix read timeouts in base.py, use seconds everywhere 2013-06-08 16:17:14 +02:00
Daniel Pavel
ed5ce48f65 fixes to polling receiver/device status 2013-06-08 16:16:12 +02:00
Daniel Pavel
2e351bfc78 read .devscripts if available 2013-06-08 16:08:00 +02:00
Daniel Pavel
f324b97e8b use GLib.io_add_watch_full when available 2013-06-08 15:02:44 +02:00
Daniel Pavel
46544e1cbe listen for udev events on the GLib main loop 2013-06-07 14:39:04 +02:00
Daniel Pavel
ffab6c0e12 move Gtk-specific idle_add() and main() into ui 2013-06-07 14:37:03 +02:00
Daniel Pavel
10e736386b user module __name__ when initializing logger 2013-06-07 14:28:13 +02:00
Daniel Pavel
a1ecebf5bf documentation and logging for 2013-06-07 14:24:05 +02:00
Daniel Pavel
996597dcb1 fixed upload_ppa script 2013-06-06 12:25:17 +02:00
Daniel Pavel
e09ad373b3 version 0.8.9.2 2013-06-06 12:19:44 +02:00
Daniel Pavel
ddf7d34982 debian packaging: always require -gnome3 for unity integration 2013-06-06 12:18:48 +02:00
Daniel Pavel
db9a4e4402 docs & changelog updates 2013-06-06 10:44:45 +02:00
Daniel Pavel
39a75a6792 AppIndicator: scrolling on the icon cycles through all devices 2013-06-06 10:25:35 +02:00
Daniel Pavel
d78484ff38 use githubredir.debian.net in watch 2013-06-05 19:49:23 +02:00
Daniel Pavel
8900ac9a1f tweak main window geometry 2013-06-05 13:21:22 +02:00
Daniel Pavel
7df2e8af97 jekyll config update, tweaked deb/gh scripts 2013-06-05 13:20:26 +02:00
Daniel Pavel
2ed723dfc2 minor doc update 2013-06-05 13:20:26 +02:00
Daniel Pavel
087177274d Merge pull request #63 from Lekensteyn/updates
Device documentation updates
2013-06-05 04:16:58 -07:00
Peter Wu
59459ebbd6 Fix crash in Python3 due to excess space
The error message was:

    TabError: inconsistent use of tabs and spaces in indentation
2013-06-05 12:39:00 +02:00
Peter Wu
2d981cd737 docs: add mk700 2013-06-05 12:39:00 +02:00
Peter Wu
4c4e6fd6f9 doc/k360: add another recv+kbd, fixup recv serial
Bug in ltunify made the serial number shift by one byte.
2013-06-05 12:38:03 +02:00
Daniel Pavel
c52f2fc069 fix descriptor for K700 keyboard 2013-06-05 01:24:19 +02:00
Daniel Pavel
eb3e2566e9 comment-out unnecessary logs 2013-06-05 00:32:01 +02:00
Daniel Pavel
c186573775 fix register blacklisting 2013-06-05 00:31:40 +02:00
Daniel Pavel
3d9d8d6efc replaced light_* icons with smaller ones 2013-06-04 10:03:13 +02:00
Daniel Pavel
1ed82fc7d5 version 0.8.9.1 2013-06-03 22:02:00 +02:00
Daniel Pavel
162228aff3 started configuration persistence implementation 2013-06-03 22:01:16 +02:00
Daniel Pavel
dd051f4c6e check latest github tag when generating gh-pages 2013-06-03 21:58:53 +02:00
Daniel Pavel
e59645b347 documentation updates 2013-06-03 21:40:54 +02:00
Daniel Pavel
cbdc0bd99b very low battery level should trigger an attention event 2013-06-03 21:40:15 +02:00
Daniel Pavel
b4fc36701a added attention() to tray icon 2013-06-03 21:39:29 +02:00
Daniel Pavel
f2f00e3017 re-arranged for easier extension 2013-06-03 11:17:58 +02:00
Daniel Pavel
c706fbc6e7 update icon theme paths directly, don't mess with XDG_DATA_DIRS 2013-06-03 11:17:19 +02:00
Daniel Pavel
88fb8458c1 logging tweaks 2013-06-02 16:16:58 +02:00
Daniel Pavel
982f298fc4 fixed page back-link 2013-06-02 12:20:44 +02:00
Daniel Pavel
4a9a05718e readme updated with Debian repo link 2013-06-02 12:16:24 +02:00
Daniel Pavel
b99a851af3 build deb-repo Packages index in $SITE/packages 2013-06-02 12:14:20 +02:00
Daniel Pavel
f452d6f5b7 jekyll pages: use absolute path for styles/images 2013-06-02 12:13:10 +02:00
Daniel Pavel
b98b94de91 documentation update -- gnome-shell indicators supported 2013-06-02 11:23:53 +02:00
Daniel Pavel
b18aee9dba maintain proper devices order in tray menu 2013-06-02 11:19:49 +02:00
Daniel Pavel
bfe7bcf1b4 avoid possible race condition on device initialization 2013-06-02 11:19:30 +02:00
Daniel Pavel
55bb732bb0 dropped unused battery icons 2013-06-02 01:11:47 +02:00
Daniel Pavel
465fea8a16 dropped solaar-mask unused icon 2013-06-02 01:11:32 +02:00
Daniel Pavel
149758ccab fixed battery icons in systray menu items 2013-06-02 01:11:04 +02:00
Daniel Pavel
16095544cb debian/control: put solaar-gnome3 in 'gnome' section 2013-06-01 23:10:10 +02:00
Daniel Pavel
37265f25d8 support custom icons in notification messages 2013-06-01 22:32:57 +02:00
Daniel Pavel
c61eb3f039 show charging status in device icon 2013-06-01 22:08:14 +02:00
Daniel Pavel
39862034e1 use just the battery icon in systray 2013-06-01 21:09:41 +02:00
Daniel Pavel
f938b29040 doc: icon names for various icon themes 2013-06-01 18:53:49 +02:00
Daniel Pavel
984b92815c use explicit debian/*.install files 2013-06-01 18:53:20 +02:00
Daniel Pavel
82c4b0007c updated solaar version to 0.8.9 2013-06-01 18:19:47 +02:00
Daniel Pavel
f4a3f699e8 debian/control: tweaks for gnome-shell/unity support 2013-06-01 18:19:31 +02:00
Daniel Pavel
9c4055a046 added Keywords entry to .desktop file 2013-06-01 18:01:23 +02:00
Daniel Pavel
088493dc4f guess battery icon set from the current theme 2013-06-01 17:34:54 +02:00
Daniel Pavel
479ab1fb7b generate a -gnome3 package for gnome-shell integration 2013-06-01 17:34:05 +02:00
Daniel Pavel
749cde6e72 require gir1.2-appindicator3 on ubuntu 2013-06-01 15:09:05 +02:00
Daniel Pavel
7317fad754 some debugging when setting icon theme paths 2013-06-01 15:08:23 +02:00
Daniel Pavel
83a29328c7 don't use notification flags with HID++ 2.0 devices 2013-06-01 15:07:39 +02:00
Daniel Pavel
788fb145af use an AppIndicator if available, instead of the status icon 2013-06-01 15:06:43 +02:00
Daniel Pavel
b1b1a9b5de Merge remote-tracking branch 'origin/master' 2013-05-27 16:24:34 +03:00
Daniel Pavel
941a36a385 Merge pull request #61 from Lekensteyn/sandbox
More special keys
2013-05-27 06:23:45 -07:00
Peter Wu
3ef3c23a29 devices/k360: update solaar output (keys, features) 2013-05-27 14:12:44 +02:00
Peter Wu
39e630cece cli: improve alignment for special keys
The following lines have an insane length and are therefore not included in the
longest line:

    WIN7_MONITOR_SWITCH_AS_WIN_SHIFT_LEFTARROW=0x0093,
    WIN7_MONITOR_SWITCH_AS_WIN_SHIFT_RIGHTARROW=0x0094,

While doing this, also fix an obvious typo in the "Lock PC" control.
2013-05-27 14:07:38 +02:00
Peter Wu
b2a62c2dd7 special_keys: update controls list
My previous observation was right, controls and tasks are really different
entities. The following "controls" appears to be invalid and have been removed:

    Home=0x001A,
    Music=0x001D,
    Search=0x0029,
    Sleep=0x002F,
2013-05-27 11:44:20 +02:00
Daniel Pavel
26dd3a2ca8 only need to set the default window icon once 2013-05-27 12:36:39 +03:00
Daniel Pavel
c328d6f6c6 fixed pairing in solaar-cli 2013-05-27 09:08:23 +03:00
Daniel Pavel
44c79d71d7 use new get_notifications_flags api in solaar-cli 2013-05-27 08:04:49 +03:00
Daniel Pavel
ecd90d605a Merge pull request #60 from Lekensteyn/updates
Update feature name list
2013-05-26 14:55:23 -07:00
Peter Wu
b6bd4b9da0 cli: fix alignment with new feature names 2013-05-26 23:51:46 +02:00
Peter Wu
6ae11f862c Add more feature names, rename existing ones
Extracted from SetPoint 6.52.74 software using:

    <FeaturesSupported.xml awk -F\" '/<Feature /{sub(/^LD_FID_/, "", $2); printf("\t%s=%s,\n", $2, $4)}' | sort -t= -k2

Existing names are renamed to the newer, verbose names.
2013-05-26 23:47:40 +02:00
Daniel Pavel
c224141355 Merge pull request #59 from Lekensteyn/special-keys
Special keys and tasks
2013-05-26 13:54:00 -07:00
Peter Wu
28c35633d3 Add more tasks for special keys
Based on tasks.xml from `%ProgramFiles%\\SetPointP\\tasks.xml`.
2013-05-26 22:47:03 +02:00
Peter Wu
6e36e33b22 Decouple controls from tasks (programmable keys)
They are treated differently in the HID++ 2.0 specification. Observations seem
to confirm this difference. For instance, a part of solaar-cli's output:

	0: unknown:0022         => Home                   FN sensitive, is FN, reprogrammable
	1: Mail                 => Mail                   FN sensitive, is FN, reprogrammable
	2: unknown:003E         => Search                 FN sensitive, is FN, reprogrammable
2013-05-26 22:47:03 +02:00
Daniel Pavel
093cca9d21 Merge pull request #57 from Lekensteyn/fixes
K360 docs, Improved solaar-cli argparse fix
2013-05-26 12:18:43 -07:00
Peter Wu
410c19dd78 devices/k360: ltunify+solaar-cli output of k360+receiver 2013-05-26 21:02:08 +02:00
Peter Wu
4a7be89be6 devices.md: add reprog keys to feature list, K360 2013-05-26 20:58:53 +02:00
Peter Wu
51305e0a21 solaar-cli: fix regression in argument parsing
When no hidraw device is given, `solaar-cli` crashes because `None` is being
indirected. Use the correct action (`store`) to avoid indirecting a list.
2013-05-26 20:51:58 +02:00
Daniel Pavel
fda3398440 commented-out stray print 2013-05-26 19:02:17 +03:00
Daniel Pavel
340ff0107c version 0.8.8.2 2013-05-26 18:43:23 +03:00
Daniel Pavel
8374a58dc1 fixed items in the systray menu 2013-05-26 18:40:53 +03:00
Daniel Pavel
3690863a27 fixed receiver icon in systray menu 2013-05-26 02:56:43 +03:00
Daniel Pavel
078d003cfe about window icon was not displayed 2013-05-26 02:56:25 +03:00
Daniel Pavel
b6ab795878 don't add receivers to the status icon tooltip 2013-05-26 02:03:58 +03:00
Daniel Pavel
7e81dede2f non-modal about dialog; fixes #46 2013-05-26 02:01:59 +03:00
Daniel Pavel
3482257b15 fix solaar-cli when no hidraw path given 2013-05-26 01:40:10 +03:00
Daniel Pavel
9f3ab8d3dd Merge remote-tracking branch 'origin/master' 2013-05-26 01:33:13 +03:00
Daniel Pavel
8f2ee555ec updated arch packaging scripts with official ones 2013-05-26 01:32:46 +03:00
Daniel Pavel
db53de2233 updated status_icon to support future appindicator implementation 2013-05-26 01:24:49 +03:00
Daniel Pavel
3596faed2f documentation updated with info about Nano receivers 2013-05-26 01:23:27 +03:00
Daniel Pavel
9e13d134d3 Merge pull request #56 from Lekensteyn/cli-updates
solaar-cli: Python3 fix, add option for custom receiver path
2013-05-25 08:43:53 -07:00
Peter Wu
7f229005a6 solaar-cli: support specifying different receiver 2013-05-25 15:46:13 +02:00
Peter Wu
e3a887f36c solaar-cli: fix argument parsing in Python 3 2013-05-25 15:17:43 +02:00
Daniel Pavel
bca8e64574 added README to packaging/ folder 2013-05-23 16:21:53 +03:00
Daniel Pavel
3873d7099c slightly reduce sleep timings when waiting for reply 2013-05-23 04:59:16 +03:00
Daniel Pavel
48b176f49a show result on reg00 unparsable output 2013-05-23 04:58:35 +03:00
Daniel Pavel
8b8b387f2a minor tweak to guessing device icon 2013-05-22 20:50:49 +03:00
Daniel Pavel
f459d9b953 updated installation docs and udev rule install scrip 2013-05-22 20:50:35 +03:00
Daniel Pavel
b276fbab90 updated scan-registers to support receiver scan 2013-05-22 20:47:04 +03:00
Daniel Pavel
e73f076324 don't support the VX Nano right now 2013-05-22 20:43:29 +03:00
Daniel Pavel
131f8f7f45 disable status polling on devices
the regular flow should be reading the battery on all devices now
2013-05-22 20:42:26 +03:00
Daniel Pavel
c3b73964d5 improved support for some Nano receivers 2013-05-22 20:41:11 +03:00
Daniel Pavel
790fc7c04b improved support for the Nano receiver 2013-05-22 07:31:16 +03:00
Daniel Pavel
cd33314d0b use passwd | addgroup in post-install 2013-05-22 06:55:59 +03:00
Daniel Pavel
4ba50267f1 add favicon when building site 2013-05-22 01:25:45 +03:00
Daniel Pavel
d46e603366 Merge remote-tracking branch 'origin/master' 2013-05-22 01:21:34 +03:00
Daniel Pavel
f8878d73c3 Merge pull request #54 from Lekensteyn/doc-updates
Doc updates
2013-05-21 15:19:46 -07:00
Peter Wu
9c0dac044c hid10: update 07 battery parsing based on spec
This fixes pwr/Solaar#49 partially, at least the charging state will be reported
correctly hereafter. The charge level may still be incorrect though.
2013-05-22 00:10:17 +02:00
Peter Wu
2f8e330b73 doc/logitech: add HID++1.0 battery level info
This should help in https://github.com/pwr/Solaar/issues/49. This information
was shared by Nestor Lopez Casado from Logitech.

What remains unclear are the 0x25 and 0x26 values for r1 (Charging state). It is
also not clear whether r0 (Battery/Charging level) always reports 0x00 while
charging the Performance MX. (Perhaps this is only reported the first few
seconds?)
2013-05-21 23:56:44 +02:00
Daniel Pavel
e6e55aa827 added performance mx LED documentation
supplied by Alexander Hofbauer
2013-05-22 00:31:08 +03:00
Peter Wu
07bff8cb37 docs/installation: typo fix, clarification
- Fix typo in rules path
 - Fix capitalisation.
 - `udevadm control --reload-rules` is unnecessary[1].
 - `adduser` is Debian-specific, use the more portable `gpasswd`.
 - No need to wait 10 seconds before re-inserting.
 - After adding to group, user must re-login.

 [1]: http://unix.stackexchange.com/a/39485/8250
2013-05-21 10:59:46 +02:00
Daniel Pavel
d857144653 added script to build gh-pages with jekyll 2013-05-21 01:12:04 +03:00
Daniel Pavel
17fdd840bb setup.py: use the version declared in the sources 2013-05-21 01:08:34 +03:00
Daniel Pavel
da6711c93c the 'Solar key' on K750 is named 'Light Check button' 2013-05-21 00:48:21 +03:00
Daniel Pavel
36b307eb49 updated debian/ppa build scripts 2013-05-21 00:47:47 +03:00
Daniel Pavel
2d8ec2fd78 added jekyll skeleton for the site 2013-05-21 00:46:49 +03:00
Daniel Pavel
11ba96103c minor updates to the documentation files 2013-05-21 00:46:07 +03:00
Daniel Pavel
843d2a224d add license section into the README 2013-05-20 16:49:44 +03:00
Daniel Pavel
0faf01c194 place the main application icon in share/pixmaps, not share/icons 2013-05-20 16:34:14 +03:00
Daniel Pavel
e868cf6270 use a separate icon for the about logo 2013-05-20 16:32:26 +03:00
Daniel Pavel
255b3d0da7 minor documentation updates 2013-05-20 13:53:39 +03:00
Daniel Pavel
5fad8d0680 minor documentation updates 2013-05-20 13:27:26 +03:00
Daniel Pavel
d3dcdcb92e Merge remote-tracking branch 'origin/master' 2013-05-19 11:54:38 +03:00
Daniel Pavel
44ae5edc56 Merge pull request #48 from Lekensteyn/doc-updates
Doc updates for HID++ 1.0 notifications
2013-05-19 01:52:07 -07:00
Peter Wu
f0542923d7 hid10: update flags description for notifications
The Logitech HID++ 1.0 documentation actually names the fields for devices and
receiver. Clarify that and explain why enabling all of the bits is a bad idea.
2013-05-19 10:30:41 +02:00
Peter Wu
29d0c07164 hid10: Formatting and re-order NOTIFICATION_FLAGs
Align values for easier reading the numeric values of NOTIFICATION_FLAGs.  To
maintain consistency in the ordering of the values, swap keyboard_backlight with
keyboard_present.
2013-05-19 10:20:21 +02:00
Peter Wu
152abb086a hid10: document notifications better
See [registers.txt][1] for 'keyboard_backlight'. The behavior of
keyboard_sleep_raw and keyboard_multimedia_raw is described at
[keyboard.txt][2].

 [1]: https://git.lekensteyn.nl/ltunify/tree/registers.txt
 [2]: https://git.lekensteyn.nl/ltunify/tree/keyboard.txt
2013-05-19 10:14:31 +02:00
Daniel Pavel
d5c36ddc89 about window tweak 2013-05-19 03:54:30 +03:00
Daniel Pavel
226a4ea2f3 version 0.8.8.1 2013-05-19 03:53:49 +03:00
Daniel Pavel
ac0eafe6ab don't crash when failing to load the icon mask 2013-05-19 03:52:01 +03:00
Daniel Pavel
87bc9a5431 depend on gnome-icon-theme or oxygen-icon-theme
It's necessary to make sure all the standard icons are available.
2013-05-19 03:35:42 +03:00
Daniel Pavel
ef62892f60 pop up a desktop notification if the battery is below 5% 2013-05-18 14:35:33 +03:00
Daniel Pavel
7ae24488d9 only notify on connected devices at start-up 2013-05-18 02:20:22 +03:00
Daniel Pavel
6482670fdf minor readme update 2013-05-18 01:31:15 +03:00
Daniel Pavel
317cf6d00f only enable certain notification flags on devices
Also added some documentation to the notification flags, where possible.
2013-05-18 00:15:42 +03:00
Daniel Pavel
a5eeac6e5a enable notifications for peripherals; fixes #27 2013-05-17 23:06:24 +03:00
Daniel Pavel
e2cf9255ac Merge pull request #41 from Lekensteyn/tool-updates
scan-registers: determine available notifications
2013-05-17 12:44:23 -07:00
Daniel Pavel
25941cdbdd debian version update 2013-05-17 20:04:33 +03:00
Daniel Pavel
1764633bc5 status icon menu items open the right device window 2013-05-17 19:55:07 +03:00
Daniel Pavel
f6eb90bd2d version and url fixes 2013-05-17 19:22:05 +03:00
Daniel Pavel
5f46c820e6 re-work the status icon updating 2013-05-17 16:03:37 +03:00
Daniel Pavel
c20b279362 minor tweaks 2013-05-08 12:06:01 +03:00
Daniel Pavel
24ceb8801e window popup fix for kwin
also, only try to position the window next to the status icon if it has never
been shown before
2013-05-07 05:59:29 +02:00
Daniel Pavel
8d5ca66db7 get rid of bad map() calls 2013-05-06 17:51:03 +02:00
Daniel Pavel
f0d250ff15 updated debian/control dependencies 2013-05-06 17:40:17 +02:00
Daniel Pavel
074cafbab1 simpler (and dumber) way to position receiver windows initially 2013-05-06 17:39:27 +02:00
Daniel Pavel
79cd52833c fix hidconsole for python 2/3 2013-05-06 17:38:09 +02:00
Daniel Pavel
6b75286885 Merge remote-tracking branch 'origin/master' 2013-05-06 17:23:38 +02:00
Daniel Pavel
b884ae039c Merge pull request #40 from Lekensteyn/fixes
Fix window close, fix positioning of new windows
2013-05-06 08:19:27 -07:00
Daniel Pavel
3ecfdd027e handle all 07/0D register notifications 2013-05-06 17:08:58 +02:00
Daniel Pavel
55ca9927b0 python 2/3: fix handling of first argument (unicode) of pack/unpack 2013-05-06 16:50:17 +02:00
Daniel Pavel
b67377c41e minor clean-up in hidconsole 2013-05-06 15:35:47 +02:00
Daniel Pavel
15f24eb26b use 'with' lock syntax instead of acquire/release 2013-05-06 15:35:20 +02:00
Daniel Pavel
0283bb0e91 fix str/unicode detection in Python 3.2 2013-05-06 15:33:42 +02:00
Daniel Pavel
d39c0995b6 Merge remote-tracking branch 'origin/master' 2013-05-06 14:55:21 +02:00
Daniel Pavel
db4c088ce9 Merge pull request #37 from Lekensteyn/py3-compat
Py3 compat: replace use of "unicode" (pwr/Solaar#32)

The `u''` syntax fails in Python 3.2; will fix is_string after the merge.
2013-05-06 05:54:53 -07:00
Daniel Pavel
a4898e24b7 extended documentation of descriptors and registers 2013-05-06 14:51:57 +02:00
Daniel Pavel
086db0d52f Merge pull request #42 from Lekensteyn/doc-updates
performance-mx: dump from scan-registers
2013-05-06 05:47:24 -07:00
Peter Wu
7b3523fb24 performance-mx: dump from scan-registers
Source: https://github.com/pwr/Solaar/issues/27#issuecomment-17472543 and
https://github.com/pwr/Solaar/issues/27#issuecomment-17478409 .
2013-05-06 14:37:28 +02:00
Daniel Pavel
74ca91d611 next version will be 0.8.8 2013-05-06 14:33:06 +02:00
Daniel Pavel
e94d4b28b0 build a proper ghost device when unpairing; fixes #36 2013-05-06 12:06:28 +02:00
Peter Wu
a85aa2da52 scan-registers: determine available notifications
00 is documented in HID++ 1.0 specification, it should be safe to set the 00
register. As another program may have written the notifications register
already and since enabling notifications has side-effects such as "disabling"
certaing functions, restore the flags after reading out available notifs.
2013-05-05 18:16:00 +02:00
Peter Wu
dded8504e6 Fix window positioning of main windows
Previously, the intent was likely to position the receiver window near the
status icon. It did so by calling move followed by present. According to the
Gtk documentation, move() may fail if the window is hidden before.

Therefore present the window *after* determining the position, but *before*
moving it. (presenting the window before getting the position gives a Gtk
warning and has unpredictable behavior wrt. the window position).

As window positioning is now enabled, add additional logic to prevent overlap
of windows: position the first window near the status icon and others on the
left. This is not idea, e.g. when the status icon is on the left side of the
screen, but the idea of positioning windows near to each other is broken anyway.
2013-05-05 11:18:29 +02:00
Peter Wu
a2bad425f6 Fix window close, icon toggle logic
"Toggle" should mean "show or hide all windows", not "flip the visibility state
of windows". Case: one receiver window is open. When a new receiver is
connected, I also expect it to be open. Instead it is hidden, so I click the
icon. Now the new receiver window is shown, but the previous one is hidden.
Huh?! Indeed, let's fix that.
2013-05-05 11:18:29 +02:00
Daniel Pavel
a4ec8ec05d better error dialog on receiver permission error; fixes #17
The problem is caused by starting Solaar right after installing it; while udev
does have the new rule loaded, the /dev/hidraw* device nodes already exist
with the old permissions.

Rather than doing a "udevadm --reload-rules" (which could have unknown side-
effects on other devices), instruct the user to remove and re-insert the usb
receiver. The new /dev/hidraw* device nods will be created with the right
permissions.
2013-05-05 10:35:03 +02:00
Daniel Pavel
1b4bf7918b minor fixes in shell scripts 2013-05-04 19:00:14 +02:00
Daniel Pavel
1f48e44cc6 Merge remote-tracking branch 'origin/master' 2013-05-04 20:20:43 +02:00
Daniel Pavel
83613f02a0 disable info label while we're still reading the info from the device 2013-05-04 20:19:33 +02:00
Daniel Pavel
9d8743e765 comment-out descriptor settings for Anywhere MX, not actually tested 2013-05-04 20:18:10 +02:00
Daniel Pavel
84b9b11ef5 Merge pull request #39 from Lekensteyn/doc-updates
descriptors.py: Clarify use of negative values
2013-05-04 11:14:09 -07:00
Peter Wu
2327a0012e descriptors.py: Clarify use of negative values
The negative behavior is not obvious, document it in the descriptors.py file
such that people who edit it to add new devices know how it works.
2013-05-04 20:02:37 +02:00
Daniel Pavel
4cdd796a3d comment-out some debugging prints 2013-05-04 19:50:50 +02:00
Daniel Pavel
e819265e7e fix automatic register blacklisting 2013-05-04 19:50:08 +02:00
Daniel Pavel
070a96c506 removed dox(x) documentation 2013-05-04 19:42:26 +02:00
Daniel Pavel
e1be54823c updated supported devices documentation 2013-05-04 18:26:09 +02:00
Daniel Pavel
d7dd9393ff minor clean-ups 2013-05-04 17:19:48 +02:00
Daniel Pavel
64c36a1562 updated descriptors table and the devices doc 2013-05-04 17:18:39 +02:00
Daniel Pavel
13a54565ba cleaned-up the descriptors table 2013-05-04 15:09:50 +02:00
Daniel Pavel
12eb17cc41 updated supported devices table 2013-05-04 15:06:55 +02:00
Daniel Pavel
9715dfa126 added a couple of more logitech docs 2013-05-04 15:06:38 +02:00
Daniel Pavel
a6e3689e97 split manual instalation procedure into it's own document 2013-05-04 14:13:31 +02:00
Daniel Pavel
2e5cf81231 split the supporte devices section into separate file
docs/devices.md contains now a few tables with supported devices
and their features (in progress)
2013-05-04 14:06:12 +02:00
Daniel Pavel
4eeca12d6a create and destroy windows on demand
based on receiver added/removed events generated by udev
2013-05-04 12:20:51 +02:00
Daniel Pavel
ad577d22d0 handle sleep in udev monitoring, fixes #35 2013-05-04 12:19:29 +02:00
Daniel Pavel
5e68094e87 split the about window into its own module 2013-05-04 12:01:28 +02:00
Peter Wu
10c26fe642 Py3 compat: replace use of "unicode" (pwr/Solaar#32)
Generating "an unknown notification" type bailed out because of an unknown
feature type None. Since `isinstance(other, str)` is False for None, the script
will raise an exception on `isinstance(other, unicode)`.

There is no differentiation between `str` and `bytes` in Python 2, therefore
add another condition to `NamedInt.__eq__` to catch unknown types (like
`bytes`).
2013-05-03 23:46:46 +02:00
Daniel Pavel
49ecd252ed always log exceptions, even without -v 2013-05-03 18:42:45 +02:00
Daniel Pavel
2ee2a5dc46 Merge remote-tracking branch 'origin/master' 2013-05-03 17:08:32 +02:00
Daniel Pavel
430fdf4fcf initial support for the MOUSE_POINTER hid++2.0 feature 2013-05-03 17:08:26 +02:00
Daniel Pavel
abca81ea38 fix device _match-ing in enumerate() 2013-05-03 16:35:54 +02:00
Daniel Pavel
a0a76f738b assert that data read/written on the receiver handle is of type bytes 2013-05-03 16:35:28 +02:00
Daniel Pavel
7d440c2430 replace thrown OSError with IOError 2013-05-03 16:25:32 +02:00
Daniel Pavel
94274fd092 Merge pull request #34 from Lekensteyn/battery-notif
Refactor battery setter, register 07 processing
2013-05-02 20:40:20 -07:00
Peter Wu
266edd80f3 Process reg07 battery notification 2013-05-03 00:10:58 +02:00
Peter Wu
d1b1be32ca Make read_battery use set_battery_info
This allows battery readouts to generate warnings and debug logs for a given
status and level.
2013-05-02 23:56:08 +02:00
Peter Wu
fbdd923d43 Refactor battery info update
- `self[BATTERY_STATUS] = BATTERY_STATUS[battery_status]` should be:
  `self[BATTERY_STATUS] = _hidpp20.BATTERY_STATUS[battery_status]`, otherwise
  the battery status would be a single char from the string `battery-status`.
- Make `_hidpp20.BATTERY_OK` check against strings instead of a number.
- Move setting battery information to a separate function, `set_battery_info`.
  This prepares for notifications when a battery error/warning occurs.
2013-05-02 23:51:40 +02:00
Daniel Pavel
2c30414f88 Merge pull request #33 from Lekensteyn/py3-hidconsole
Python3 compatibility for hidconsole
2013-05-02 14:23:16 -07:00
Peter Wu
d8e469a33a hidpp10: split reg07 (battery) parsing so it can be reused 2013-05-02 23:06:45 +02:00
Peter Wu
a6b89b3ea3 Python3 compatibility for hidconsole
`type(u'')` is 'str' in Python 3, it was `unicode` on Python 2 (with
`unicode_literals` from `__future__`).
2013-05-02 22:58:35 +02:00
Daniel Pavel
0f80901bce quicker detection of matching receivers in udev 2013-05-02 11:11:53 +02:00
Daniel Pavel
8fc45e5590 fix solaar-cli to handle the new Receiver.open() api 2013-05-01 15:51:12 +02:00
Daniel Pavel
c829304e31 use only udev events to detect receiver devices 2013-05-01 15:47:23 +02:00
Daniel Pavel
d3f94ff2fb detect some HID++1.0 custom battery notifications
they're not handled yet
2013-05-01 11:36:15 +02:00
Daniel Pavel
85d9a9dc27 fix for bug #28 2013-04-30 23:05:26 +02:00
Daniel Pavel
04db6d3838 python 3 fixes for #29 2013-04-30 22:31:06 +02:00
Daniel Pavel
e5a28ac64e simplified window/icon code, reworked how device updates are signalled 2013-04-30 19:44:03 +02:00
Daniel Pavel
2397c6c0ea about dialog updated 2013-04-30 17:25:09 +02:00
Daniel Pavel
c3e6c3d1da more flexible icon loading 2013-04-30 15:45:35 +02:00
Daniel Pavel
22da75cb72 replaced some deprecated GObject calls with GLib 2013-04-30 11:47:30 +02:00
Daniel Pavel
22a8ca37b6 updated FSF address in COPYING 2013-04-29 15:38:15 +02:00
Daniel Pavel
692afba3d9 updated FSF address in COPYING 2013-04-29 15:34:00 +02:00
Daniel Pavel
9c621d5816 always try to initialize systray icon and notifications 2013-04-28 15:44:20 +02:00
Daniel Pavel
feedbcf581 some code clean-ups 2013-04-28 15:16:45 +02:00
Daniel Pavel
a57f3be58d renamed event alert levels to be more clear 2013-04-28 15:12:20 +02:00
Daniel Pavel
897dffc426 only dispatch notification events from the specialized listener thread 2013-04-28 15:09:09 +02:00
Daniel Pavel
6f0b61e6d8 better handling of eq/ne/hash in receiver and device objects 2013-04-28 15:02:17 +02:00
Daniel Pavel
674ee9ac9e split read_battery in status into its own function 2013-04-28 15:01:27 +02:00
Daniel Pavel
5eab013cf6 more explicit logging 2013-04-28 14:58:40 +02:00
Daniel Pavel
079ef8d800 incipient support for the Nano receiver 2013-04-28 14:27:16 +02:00
Daniel Pavel
1a9be279c6 move the code for single-instance check into its own file 2013-04-28 14:06:41 +02:00
Daniel Pavel
f5d2eba0c4 hidapi: dropped native, slight update to the python implementation
added an optional filter for driver name when enumerating devices
2013-04-28 14:05:33 +02:00
Daniel Pavel
e7d19c9084 small fixes to hidconsole 2013-04-28 14:00:46 +02:00
Daniel Pavel
638bf38b25 version increased to 0.8.7.1 2013-04-28 13:59:07 +02:00
Daniel Pavel
c4dc49ac5e Merge pull request #24 from Lekensteyn/hid10-fnkey
Add FN key swap support for K800 (and presumably K710)
2013-04-28 02:03:33 -07:00
Daniel Pavel
926600e29d Merge pull request #26 from Lekensteyn/hid10-battery
Fix battery status reading, add "fully charged"
2013-04-27 23:51:18 -07:00
Peter Wu
5e58f1e273 Fix battery status reading, add "fully charged"
Commit 438c501fae introduced support for HID++ 1.0
battery information. That accidentally selected the third parameter instead of
the second one. This commit fixes that and additionally adds a "fully charged"
status too that was found on the K800.
2013-04-27 17:07:55 +02:00
Peter Wu
c79ad65d37 Add K710 with FN key swap support (pwr/Solaar#18)
The K710 keyboard is (according to the issue reporter) part of a MK710 combo
(which also contains a M705 mouse). Observing a succesful 07 register read,
I think that it is a HID++ 1.0 device too that uses the same register for FN
key swapping as K800.
2013-04-27 15:17:05 +02:00
Peter Wu
95a97ad776 Support FN keys swap for K800 2013-04-27 15:13:42 +02:00
Peter Wu
6c28cedf23 Make BooleanValidator accept bytes too besides int
Previously, only the first parameter byte can be read or written. After this
patch, it is possible to select/write more bytes by specifying a mask of type
bytes with the appropriate length.
2013-04-27 15:12:55 +02:00
Daniel Pavel
a3599b53bb Merge pull request #23 from Lekensteyn/hid10-battery
Parse battery status of HID++ 1.0 devices
2013-04-27 05:20:29 -07:00
Peter Wu
438c501fae Parse battery status of HID++ 1.0 devices
This applies to K800 but it seems also to apply to M510. The numbers are based
on the HID++ 2.0 spec that state the following for GetBatteryCapability:

    If number of levels < 10 or if mileage is disabled then report are
    mapped to 4 levels this way.

    0%->10%     critical
    11%->30%    low
    31%->80%    good
    81%->100%   full

    i.e. to report battery low, FW send 25%, to report battery good, FW send 50%.
2013-04-27 11:58:09 +02:00
Daniel Pavel
25cbd55841 Merge pull request #22 from Lekensteyn/doc-regs
Document registers for K800 and M525
2013-04-27 01:53:16 -07:00
Peter Wu
5ef53f251d Document registers for K800 and M525
See also https://lekensteyn.nl/logitech-unifying.html for tools being used to
draw conclusions. Extended dumps and description can be found on
https://git.lekensteyn.nl/ltunify/tree/registers.txt .
2013-04-27 10:17:36 +02:00
Daniel Pavel
92819b6c9e readme updated 2013-02-24 22:43:16 +01:00
Daniel Pavel
d22674f362 Merge pull request #16 from pivoq/master
Arch Linux package
2013-02-20 10:08:28 -08:00
pivoq
0fc83e9255 add Arch Linux package 2013-02-19 17:38:52 +01:00
pivoq
f171c5d59e add Arch Linux package 2013-02-19 17:15:37 +01:00
Daniel Pavel
78b8299130 readme update 2013-01-25 14:54:48 +02:00
Daniel Pavel
2c7fe0c92d updated gentoo ebuild file 2013-01-18 18:41:15 +02:00
Daniel Pavel
6ff30f2a0e release 0.8.7 2013-01-18 18:37:06 +02:00
Daniel Pavel
7707c5e558 don't notify on device disconnection 2013-01-18 17:41:45 +02:00
Daniel Pavel
9b7a920e0d fix for systray visibility (gihub #14) 2013-01-18 17:18:35 +02:00
Daniel Pavel
2e51380be5 log value of register 0x07 when register 0x0D not available 2013-01-18 12:41:01 +02:00
Daniel Pavel
445f508ea5 readme updates 2013-01-14 20:00:52 +02:00
Daniel Pavel
b82c89c582 release 0.8.6.2 2013-01-09 21:47:59 +02:00
Daniel Pavel
85a47a8049 fixed icon name in desktop file 2013-01-09 21:41:50 +02:00
Daniel Pavel
f8e9798038 gentoo ebuild updated 2013-01-09 21:31:47 +02:00
Daniel Pavel
581d6747ad Merge branch 'nano' into 0.9
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
2013-01-09 21:10:39 +02:00
Daniel Pavel
00a1aa7628 release 0.8.6.1 2013-01-09 15:36:15 +02:00
Daniel Pavel
79a9048db5 fixed names for NamedInts numerical values 2013-01-09 15:31:19 +02:00
Daniel Pavel
2bfba2e399 fixed application quit icon 2013-01-09 13:47:30 +02:00
Daniel Pavel
484419e526 release 0.8.6 2013-01-08 00:59:30 +02:00
Daniel Pavel
8c18830c97 added source changelog file 2013-01-08 00:55:47 +02:00
Daniel Pavel
653d370a85 fixed locating application icons when running in a custom prefix 2013-01-08 00:40:54 +02:00
Daniel Pavel
316e91cfcf fixed some icon names 2013-01-08 00:39:13 +02:00
Daniel Pavel
f31632c8c8 debian: make sure the plugdev group exists after installation 2013-01-07 22:09:43 +02:00
Daniel Pavel
bb52c13f9a only allow a single instance of solaar to run at a time 2013-01-07 21:34:47 +02:00
Daniel Pavel
738d43fd83 fix for gihub issue #10
http://github.com/pwr/Solaar/issues/10
2013-01-07 20:14:31 +02:00
Daniel Pavel
1c6c8588d9 release 0.8.5.3, only packaging changes 2013-01-07 13:01:59 +02:00
Daniel Pavel
7a97cb2e02 gentoo ebuild update 2013-01-07 12:43:11 +02:00
Daniel Pavel
5e0d2992c9 small fixes for ubuntu packaging 2013-01-07 12:40:44 +02:00
Daniel Pavel
210859a5ef ignore some development-side directories 2013-01-07 12:05:40 +02:00
Daniel Pavel
25f6d229dd advanced version to account for latest changes in packaging 2013-01-07 12:04:22 +02:00
Daniel Pavel
60405abf58 dropped unnecessary #! in lib/ python files 2013-01-07 11:54:52 +02:00
Daniel Pavel
8070b11c27 customized debian/ and debian packaging script to support ubuntu ppas 2013-01-07 11:46:34 +02:00
Daniel Pavel
7d76ce77c9 packaging updates for debian/ubunutu 2013-01-05 17:40:06 +02:00
Daniel Pavel
3d48cbc111 updated debian packaging script, added ppa build script 2013-01-05 15:50:37 +02:00
Daniel Pavel
36f34da227 proper debian packaging, dropper stdeb 2013-01-05 11:48:35 +02:00
Daniel Pavel
d06e07542e dependency fix for debian package 2013-01-04 12:43:16 +02:00
Daniel Pavel
a0c8646923 Merge branch 'master' into packaging 2013-01-04 11:55:31 +02:00
Daniel Pavel
6c924de209 small readme update 2013-01-04 11:52:17 +02:00
Daniel Pavel
500503c069 release 0.8.5 2013-01-04 11:38:15 +02:00
Daniel Pavel
5dd8cd66dd added smooth scroll setting for the anywhere mx mouse 2013-01-04 11:23:41 +02:00
Daniel Pavel
41e84e55f1 fix hidconsole input 2013-01-04 10:59:00 +02:00
Daniel Pavel
7f8888d7dd added an ebuild script 2013-01-04 10:51:22 +02:00
Daniel Pavel
216928f904 small logging fixes for repr() 2013-01-04 08:25:47 +02:00
Daniel Pavel
345bab3a99 fix code looking for icons 2013-01-04 08:18:22 +02:00
Daniel Pavel
064a7a113c added setup.py for python and debian packaging 2012-12-18 05:03:43 +02:00
Daniel Pavel
115d5c7db1 release 0.8.4 2012-12-14 22:29:44 +02:00
Daniel Pavel
8a86ecc38d smaller icons in toolbars 2012-12-14 21:50:45 +02:00
Daniel Pavel
430a2d71e3 read all device features as soon as the application is idle 2012-12-14 19:51:18 +02:00
Daniel Pavel
187c0d2a52 fixes in the shell scripts 2012-12-14 19:33:01 +02:00
Daniel Pavel
8fbe77afb2 readme updates 2012-12-14 16:57:21 +02:00
Daniel Pavel
e43e92f2b0 commented-out testing register for M705 2012-12-14 16:57:10 +02:00
Daniel Pavel
cc6c0ee7df fix for python3 2012-12-14 16:25:46 +02:00
Daniel Pavel
3cd0665166 fixed list of settings in config panel 2012-12-14 15:48:04 +02:00
Daniel Pavel
a42e696695 hide the confix box when the device goes inactive 2012-12-14 06:45:51 +02:00
Daniel Pavel
83886fbcf1 fix device settings being read on every confix box display 2012-12-14 06:45:31 +02:00
Daniel Pavel
f0c5046ccf re-worked the settings classes 2012-12-14 06:44:44 +02:00
Daniel Pavel
9db2a65b31 solaar-cli: return on the first match when searching for device name
results may be occasionally ambiguous, but the command runs faster
2012-12-13 15:12:51 +02:00
Daniel Pavel
59c5619b44 cleaner output on solaar-cli config 2012-12-13 14:58:32 +02:00
Daniel Pavel
b39016df7c small clean-ups in device status handling 2012-12-13 14:54:31 +02:00
Daniel Pavel
c22fe6320d properly slice NamedInts 2012-12-13 05:26:56 +02:00
Daniel Pavel
b99ccdf612 support slices in FeaturesArray and KeysArray 2012-12-13 03:34:39 +02:00
Daniel Pavel
2d338ffbfb better __str__ of Setting instances 2012-12-13 03:33:19 +02:00
Daniel Pavel
739cb9306a drop MethodType uglyness 2012-12-13 00:30:20 +02:00
Daniel Pavel
954fc29613 fix event storm when toggling a device's property 2012-12-12 22:55:04 +02:00
Daniel Pavel
630f71b349 logging clean-up 2012-12-12 21:45:56 +02:00
Daniel Pavel
2b3f274aae new UI mock-up from Julien Gascard 2012-12-12 21:45:37 +02:00
Daniel Pavel
e834e46ef6 added register scan for m510
nothing interesting there, so far
2012-12-12 21:45:06 +02:00
Daniel Pavel
27f10cd10e small clean-up in FeaturesArray, should be slightly faster 2012-12-12 21:44:37 +02:00
Daniel Pavel
f4b92ee690 print () clean-ups 2012-12-12 21:43:55 +02:00
Daniel Pavel
1c4d3d5f13 simpler NamedInt, more logical NamedInts 2012-12-12 21:42:43 +02:00
Daniel Pavel
7bb7a092a4 descriptors update (new device K230)
also assume by default all devices have battery info in register 0x0D,
and blacklist them when that's not the case
2012-12-12 21:41:29 +02:00
Daniel Pavel
0ed623caf9 made notifications handling clearer in status.py 2012-12-12 21:39:04 +02:00
Daniel Pavel
19cd40cfdd renamed 'events' to 'notifications'
in order to match the name in Logitech's documentation
2012-12-12 21:03:07 +02:00
Daniel Pavel
7617a1ef8e don't look at a device's settings if they aren'y being displayed
avoids unnecessary hid++ calls from the main thread
2012-12-12 20:56:13 +02:00
Daniel Pavel
a370afe94b faster tooltip text composition 2012-12-12 20:55:09 +02:00
Daniel Pavel
ff5a1ac7cb fast mapping of receiver status to app icon name 2012-12-12 20:54:14 +02:00
Daniel Pavel
893c7e3ab2 use direct vars instead of widget names to identify widgets 2012-12-12 20:52:33 +02:00
Daniel Pavel
17698bfeae minor clean-ups in texts 2012-12-12 20:44:29 +02:00
Daniel Pavel
8b90e99658 bin/ scripts updated 2012-12-10 15:36:10 +02:00
Daniel Pavel
fa72b89b3a release 0.8.3 2012-12-08 05:29:50 +02:00
Daniel Pavel
fd3c88cb67 added a few documentation files 2012-12-08 05:27:22 +02:00
Daniel Pavel
8b44ca913f detect when the systray icon is not available and change window state accordingly 2012-12-08 03:11:45 +02:00
Daniel Pavel
7fe79a703e fixed creation of device settings controls 2012-12-08 01:54:08 +02:00
Daniel Pavel
80c36a02a9 improved notifications detection 2012-12-08 01:49:59 +02:00
Daniel Pavel
4bdfe9b9b8 readme update 2012-12-08 00:51:51 +02:00
Daniel Pavel
767e8a0db4 extra description on configurable settings in solaar-cli 2012-12-08 00:51:34 +02:00
Daniel Pavel
d8a2ffa835 better handling of window pop-up and toggling 2012-12-08 00:51:10 +02:00
Daniel Pavel
d38bec39b6 improved hid++ support 2012-12-08 00:41:43 +02:00
Daniel Pavel
33a9ca060d made hidconsole more user-friendly 2012-12-08 00:41:10 +02:00
Daniel Pavel
30fedf418c re-read device settings when they come back online 2012-12-07 21:00:36 +02:00
Daniel Pavel
5bdacb377c cleaner text when no status is known 2012-12-07 20:54:05 +02:00
Daniel Pavel
ee16892481 fixed registers access 2012-12-07 20:38:24 +02:00
Daniel Pavel
e2909f6165 fixed event detection 2012-12-07 20:37:13 +02:00
Daniel Pavel
205d25e341 special support for configuring dpi 2012-12-07 19:40:32 +02:00
Daniel Pavel
f49ced2d92 readme updates 2012-12-07 19:39:40 +02:00
Daniel Pavel
b86dcce381 I come from the __future__, come with me if you want to live. 2012-12-07 17:10:22 +02:00
Daniel Pavel
c4be58f074 dropped bin/scan as deprecated, bin/solaar-cli completely replaces it 2012-12-07 15:31:19 +02:00
Daniel Pavel
b3f0bfa4fb fixed obsolete import 2012-12-07 14:41:00 +02:00
Daniel Pavel
37daf3a192 better handling of terminal in hidconsole 2012-12-07 14:40:48 +02:00
Daniel Pavel
7ada4af31b hidconsole has to be run in unbuffered mode 2012-12-07 14:29:30 +02:00
Daniel Pavel
67db483b0b dropped the unittests, they've been obsolete and nonfunctional for a long time now 2012-12-07 14:00:28 +02:00
Daniel Pavel
357e118ace added configuration support to solaar-cli 2012-12-07 13:56:22 +02:00
Daniel Pavel
f2cdbe26b6 added configuration items to the UI
the window is getting very cramped now... will most likely have to re-
work the entire UI
2012-12-07 13:56:07 +02:00
Daniel Pavel
3569489ce7 added registers and settings to device descriptors 2012-12-07 13:54:03 +02:00
Daniel Pavel
6c3fa224e0 small ui fixes 2012-12-07 13:52:09 +02:00
Daniel Pavel
9066003240 named ints act like proper sequences now 2012-12-07 13:50:44 +02:00
Daniel Pavel
f0007d0a13 updates to the command lines 2012-12-07 13:41:07 +02:00
Daniel Pavel
ff6db1d00a fix for python 3 2012-12-06 14:15:28 +02:00
Daniel Pavel
27403a08d2 improved hid++ 1.0 support 2012-12-05 21:41:02 +02:00
Daniel Pavel
6d70d2aada improved support for hid++ 1.0 devices 2012-12-05 15:10:41 +02:00
Daniel Pavel
0e551383ba added script for udev rule installation 2012-12-05 12:08:45 +02:00
Daniel Pavel
b5b86ab8b8 more reliable pairing window 2012-12-04 15:59:35 +02:00
Daniel Pavel
61d0159e8a release 0.8.2 2012-12-03 15:17:33 +02:00
Daniel Pavel
c41859816b renamed README 2012-12-03 15:13:03 +02:00
Daniel Pavel
5a99e55309 readme updates 2012-12-03 15:07:35 +02:00
Daniel Pavel
1b6e6692c0 maintain notification flags when pairing in command-line 2012-12-03 15:07:07 +02:00
Daniel Pavel
116ba72f37 fixed possible dangling weakrefs on start-up 2012-12-03 12:51:22 +02:00
Daniel Pavel
3fe9caf0e6 added solaar-cli for command-line operations 2012-12-03 11:34:35 +02:00
Daniel Pavel
a403c3b596 release 0.8.1 2012-12-01 23:32:51 +02:00
Daniel Pavel
2a44b0bb5b fixed scan not seeing the devices 2012-12-01 22:34:52 +02:00
Daniel Pavel
130a23dd4f optimized appicon mask 2012-12-01 19:16:52 +02:00
Daniel Pavel
db0d6e8bbc release 0.8.0 2012-12-01 19:14:06 +02:00
Daniel Pavel
1cc532d600 fixed orphaned weakrefs when unpairing a device 2012-12-01 19:12:53 +02:00
Daniel Pavel
8f5fa0cf9a code clean-ups, the app starts faster now 2012-12-01 15:49:52 +02:00
Daniel Pavel
89c6904d69 fixed pairing (again), this time also tested it 2012-11-30 20:28:22 +02:00
Daniel Pavel
14663ca204 re-wrote loading of icons for devices 2012-11-30 15:23:16 +02:00
Daniel Pavel
64d2b35ace some clean-ups 2012-11-30 15:20:41 +02:00
Daniel Pavel
ab5e09db93 pairing fixes 2012-11-29 21:26:03 +02:00
Daniel Pavel
932a015e49 better battery icon in the systray 2012-11-29 20:13:53 +02:00
Daniel Pavel
d6b18cd426 python 3 fixes 2012-11-29 12:34:20 +02:00
Daniel Pavel
84540fb087 re-wrote most of the app, based on latest HID++ docs from Logitech 2012-11-29 04:10:16 +02:00
Daniel Pavel
5b8c983ab3 some speed tweaks to hidconsole batch mode 2012-11-24 22:49:15 +02:00
Daniel Pavel
13a11e78f0 added more known device names and kinds 2012-11-13 09:48:52 +02:00
Daniel Pavel
fb8cf26c51 release 0.7.4 2012-11-12 18:34:27 +02:00
Daniel Pavel
41db725e15 fixed property updates from events 2012-11-12 18:34:11 +02:00
Daniel Pavel
f25d2ba183 small tweaks on how the devices info is displayed 2012-11-12 18:15:29 +02:00
Daniel Pavel
66531635bc scripts clean-up 2012-11-12 15:33:21 +02:00
Daniel Pavel
4c5cf85091 re-worked the UI a bit to give better info on devices status 2012-11-12 15:28:38 +02:00
Daniel Pavel
6db4deafee python 3 fixes 2012-11-11 22:37:42 +02:00
Daniel Pavel
2c312c1a5b show battery icon in tray if any available 2012-11-11 21:59:50 +02:00
Daniel Pavel
bcc2bf123e fixed initialization sequence for newly detected devices 2012-11-11 20:11:30 +02:00
Daniel Pavel
50fedab19e re-worked how fd handles are used in multi-threading 2012-11-11 17:03:13 +02:00
Daniel Pavel
d0ccd3e9c2 ui tweak 2012-11-09 09:20:44 +02:00
Daniel Pavel
4b2d8a8d5a addded custom swids to feature calls 2012-11-09 09:20:28 +02:00
Daniel Pavel
c12364a7c7 fix receiver status when more than 1 device is connected 2012-11-09 01:03:37 +02:00
Daniel Pavel
560400e786 small ui tweaks 2012-11-08 22:32:19 +02:00
Daniel Pavel
f7a4d89467 fixed display of receiver details 2012-11-08 22:05:35 +02:00
178 changed files with 13300 additions and 3785 deletions

14
.gitignore vendored
View File

@@ -1,4 +1,16 @@
*.so
*.pyc
*.pyo
__pycache__/
*.log
*.mo
/lib/Solaar.egg-info/
/build/
/sdist/
/dist/
/deb_dist/
/MANIFEST
/docs/captures/
/share/logitech_icons/
/share/locale/

41
COPYING
View File

@@ -1,12 +1,12 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
@@ -15,7 +15,7 @@ software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Library General Public License instead.) You can apply it to
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
@@ -55,8 +55,8 @@ patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
@@ -110,7 +110,7 @@ above, provided that you also meet all of these conditions:
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
@@ -168,7 +168,7 @@ access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
@@ -225,7 +225,7 @@ impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
@@ -255,7 +255,7 @@ make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
@@ -277,9 +277,9 @@ YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
@@ -303,17 +303,16 @@ the "copyright" line and a pointer to where the full notice is found.
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
@@ -336,5 +335,5 @@ necessary. Here is a sample; alter the names:
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Library General
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

2
COPYRIGHT Normal file
View File

@@ -0,0 +1,2 @@
Copyright 2012, 2013
Daniel Pavel <daniel.pavel@gmail.com>

62
ChangeLog Normal file
View File

@@ -0,0 +1,62 @@
0.9.2:
* Added support for hand detection on the K800.
* Added support for V550 and V450 Nano.
* Fixed side-scrolling wit the M705 Marathon.
* Fixed identification of the T650 Touchpad.
* Added internationalization support and romanian translation.
* Polish translation courtesy of Adrian Piotrowicz.
0.9.1:
* When devices report a battery alert, only show the alert once.
* Make sure devices in the window tree are sorted by registration index.
* Added an autostart .desktop file.
* Replaced single-instance code with GtkApplication.
* Fixed indentification of the M505 mouse.
* Fixed an occasional windowing layout bug with the C52F Nano Receiver.
0.9.0:
* New single-window UI.
* Performance MX leds show the current battery charge.
* Support the VX Nano mouse.
* Faster and more accurate detection of devices.
* If upower is accessible through DBus, handle suspend/resume.
* Replaced Solaar icons with SVGs.
* Running solaar-cli in parallel with solaar is now less likely to cause issues.
* Bugfixes to saving and applying device settings.
* Properly handle ^C when running in console.
0.8.9:
* Improved support for gnome-shell/Unity.
* Persist devices settings between runs.
* Fixed reading of MK700 keyboard battery status.
* Use battery icons from the current theme instead of custom ones.
* Debian/Ubuntu packages now depend on an icon theme, to make sure
no missing icons appear in the application window.
* Fixed missing icons under Kubuntu.
* Many more bug-fixes and reliability improvements.
0.8.8:
* Partial support for some Nano receivers.
* Improved support for some devices: M510, K800, Performance MX.
* Improved battery support for some HID++ 1.0 devices.
* Properly handle device loss on computer sleep/wake.
* Better handling of receiver adding and removal at runtime.
* Removed a few more unhelpful notifications.
* Incipient support for multiple connected receivers.
* More Python 3 fixes.
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.

3
MANIFEST.in Normal file
View File

@@ -0,0 +1,3 @@
include COPYRIGHT COPYING README.md ChangeLog
recursive-include rules.d *
recursive-include share/locale *

60
README
View File

@@ -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

1
README Symbolic link
View File

@@ -0,0 +1 @@
README.md

88
README.md Normal file
View File

@@ -0,0 +1,88 @@
**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.
[unifying]: http://logitech.com/en-us/66/6079
## 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. For a full list of supported devices
and their features, see [docs/devices.md](docs/devices.md).
## Pre-built packages
Pre-built packages are available for a few Linux distros.
* Debian 7 (Wheezy) or higher: packages in this [repository](docs/debian.md)
* Ubuntu/Kubuntu 12.04+: [ppa:daniel.pavel/solaar][ppa]
The `solaar` package uses a standard system tray implementation; to ensure
integration with *gnome-shell* or *Unity*, install `solaar-gnome3`.
* a [Gentoo overlay][gentoo], courtesy of Carlos Silva
* an [OpenSUSE rpm][opensuse], courtesy of Mathias Homann
* an [Arch package][arch], courtesy of Arnaud Taffanel
[ppa]: http://launchpad.net/~daniel.pavel/+archive/solaar
[gentoo]: http://code.r3pek.org/gentoo-overlay/src
[opensuse]: http://software.opensuse.org/package/Solaar
[arch]: http://aur.archlinux.org/packages/solaar
## Manual installation
See [docs/installation.md](docs/installation.md) for the step-by-step
procedure for manual installation.
## Known Issues
- KDE/Kubuntu: if some icons appear broken in the application, make sure you've
properly configured the Gtk theme and icon theme in KDE's control panel.
- Some devices using the [Nano Receiver][nano] (which is very similar to the
Unifying Receiver) are supported, but not all. For details, see
[docs/devices.md](docs/devices.md).
- 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.
[nano]: http://logitech.com/mice-pointers/articles/5926
## License
This software is distributed under the terms of the
[GNU Public License, v2](COPYING).
## 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)
- [Peter Wu](https://lekensteyn.nl/logitech-unifying.html)
- [Nestor Lopez Casado](http://drive.google.com/folderview?id=0BxbRzx7vEV7eWmgwazJ3NUFfQ28)
provided some more Logitech specifications for the HID++ protocol
Also thanks to Douglas Wagner, Julien Gascard and Peter Wu for helping with
application testing and supporting new devices.

View File

@@ -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)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -1,75 +0,0 @@
#
# Optional desktop notifications.
#
import logging
try:
from gi.repository import Notify
import ui
from logitech.devices.constants import STATUS
# necessary because the notifications daemon does not know about our XDG_DATA_DIRS
_icons = {}
def _icon(title):
if title not in _icons:
_icons[title] = ui.icon_file(title)
return _icons.get(title)
# assumed to be working since the import succeeded
available = True
_notifications = {}
def init(app_title):
"""Init the notifications system."""
global available
if available:
if not Notify.is_initted():
logging.info("starting desktop notifications")
try:
return Notify.init(app_title)
except:
logging.exception("initializing desktop notifications")
available = False
return available and Notify.is_initted()
def uninit():
if available and Notify.is_initted():
logging.info("stopping desktop notifications")
_notifications.clear()
Notify.uninit()
def show(dev):
"""Show a notification with title and text."""
if available and Notify.is_initted():
summary = dev.name
# if a notification with same name is already visible, reuse it to avoid spamming
n = _notifications.get(summary)
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
n.set_urgency(urgency)
try:
# logging.debug("showing %s", n)
n.show()
except Exception:
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

View File

@@ -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

View File

@@ -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)

View File

@@ -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 "$@"

View File

@@ -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 "$@"

View File

@@ -1,13 +1,44 @@
#!/bin/sh
#!/usr/bin/env python
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
Z=`readlink -f "$0"`
APP=`readlink -f $(dirname "$Z")/../app`
LIB=`readlink -f $(dirname "$Z")/../lib`
SHARE=`readlink -f $(dirname "$Z")/../share`
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
#export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`arch`
export PYTHONPATH=$APP:$LIB
export XDG_DATA_DIRS=$SHARE:$XDG_DATA_DIRS
from __future__ import absolute_import, unicode_literals
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')
# print ("sys.path[0]: checking", init_py)
if _path.exists(init_py):
# print ("sys.path[0]: found", location, "replacing", sys.path[0])
sys.path[0] = location
break
if __name__ == '__main__':
init_paths()
import solaar.gtk
solaar.gtk.main()

42
bin/solaar-cli Executable file
View File

@@ -0,0 +1,42 @@
#!/usr/bin/env python
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import absolute_import, unicode_literals
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

Binary file not shown.

7
docs/debian.md Normal file
View File

@@ -0,0 +1,7 @@
# Debian repository
To use this repository with your Debian machine, create a file `solaar.list` in
`/etc/apt/sources.list.d/`, with the following contents:
deb http://pwr.github.io/Solaar/packages/ ./
deb-src http://pwr.github.io/Solaar/packages/ ./

139
docs/devices.md Normal file
View File

@@ -0,0 +1,139 @@
# Supported devices
**Solaar** will detect all devices paired with your receiver, and at the very
least display some basic information about them.
At this moment, all [Unifying Receiver][unifying] are supported (devices with
USB ID `046d:c52b` or `046d:c532`), but only some newer [Nano Receiver][nano]s
(devices with USB ID `046d:c52f`). You can check your connected Logitech devices
by running `lsusb -d 046d:` in a console.
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 `Light-Check` button 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 ([K360][K360],
[MK700][K700], [K750][K750] and [K800][K800]). 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 [M510 Wireless Mouse][M510].
# Supported features
These tables list all known Logitech [Unifying][unifying] devices, and to what
degree their features are supported by Solaar. If your device is not listed here
at all, it is very unlikely Solaar would be able to support it.
The information in these tables is incomplete, based on what devices myself and
other users have been able to test Solaar with. If your device works with
Solaar, but its supported features are not specified here, I would love to hear
about it.
Devices marked with an asterisk (*) use a Nano receiver that knows the Unifying
protocol, and should be fully supported by Solaar.
The HID++ column specifies the device's HID++ version.
The Battery column specifies if Solaar is able to read the device's battery
level.
For mice, the DPI column specifies if the mouse's sensitivity is fixed (`-`),
can only be read (`R`), or can be read and changed by Solaar (`R/W`).
The reprog(rammable) keys feature is currently not fully supported by Solaar.
You are able to read this feature using solaar-cli, but it is not possible to
assign different keys.
Keyboards:
| Device | HID++ | Battery | Other supported features |
|------------------|-------|---------|-----------------------------------------|
| K230 | 2.0 | yes | |
| K270 | | | |
| K340 | | | |
| K350 | | | |
| K360 | 2.0 | yes | FN swap, reprog keys |
| K400 Touch | 2.0 | yes | |
| K750 Solar | 2.0 | yes | FN swap, Lux reading, light button |
| K800 Illuminated | 1.0 | yes | FN swap, reprog keys |
| MK700 | 1.0 | yes | FN swap, reprog keys |
Mice:
| Device | HID++ | Battery | DPI | Other supported features |
|------------------|-------|---------|-------|---------------------------------|
| V450 Nano | 1.0 | yes | - | smooth scrolling |
| V550 Nano | 1.0 | yes | - | smooth scrolling |
| VX Nano | 1.0 | yes | - | smooth scrolling |
| M175 * | | yes | | |
| M185 * | | yes | | |
| M187 * | 2.0 | yes | | |
| M215 * | 1.0 | yes | | |
| M235 * | | yes | | |
| M305 * | 1.0 | yes | | |
| M310 * | | yes | | |
| M315 * | | yes | | |
| M317 | | | | |
| M325 | | | | |
| M345 | 2.0 | yes | - | |
| M505 | 1.0 | yes | | |
| M510 | 1.0 | yes | | smooth scrolling |
| M515 Couch | 2.0 | yes | - | |
| M525 | 2.0 | yes | - | |
| M600 Touch | 2.0 | yes | | |
| M705 Marathon | 1.0 | yes | - | smooth scrolling |
| T400 Zone Touch | | | | |
| T620 Touch | 2.0 | | | |
| Performance MX | 1.0 | yes | R/W | |
| Anywhere MX | 1.0 | yes | - | |
| Cube | 2.0 | yes | | |
Trackballs:
| Device | HID++ | Battery | DPI | Other supported features |
|------------------|-------|---------|-------|---------------------------------|
| M570 Trackball | | | | |
Touchpads:
| Device | HID++ | Battery | DPI | Other supported features |
|------------------|-------|---------|-------|---------------------------------|
| Wireless Touch | 2.0 | | | |
| T650 Touchpad | 2.0 | | | |
Mouse-Keyboard combos:
| Device | HID++ | Battery | Other supported features |
|------------------|-------|---------|-----------------------------------------|
| MK330 | | | |
| MK520 | | | |
| MK550 | | | |
| MK710 | 1.0 | yes | FN swap, reprog keys |
[unifying]: http://logitech.com/en-us/66/6079
[nano]: http://logitech.com/mice-pointers/articles/5926
[K360]: http://logitech.com/product/keyboard-k360
[K700]: http://logitech.com/product/wireless-desktop-mk710
[K750]: http://logitech.com/product/k750-keyboard
[K800]: http://logitech.com/product/wireless-illuminated-keyboard-k800
[M510]: http://logitech.com/product/wireless-mouse-m510
[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

93
docs/devices/k360.txt Normal file
View File

@@ -0,0 +1,93 @@
Receiver
LZ22175-DJ
LZ30965-DJ (another receiver)
M/N:C-U0007
(ltunify)
Serial number: 53B19204
Serial number: 82C3964B (another receiver)
Firmware version: 012.001.00019
Bootloader version: BL.002.014
(solaar-cli)
-: Unifying Receiver
Device path : /dev/hidraw2
Serial : 53B19204
Serial : 82C3964B (another receiver)
Firmware : 12.01.B0019
Bootloader : 02.14
Has 1 paired device(s) out of a maximum of 6
Enabled notifications: 0x000900 = wireless, software present.
Keyboard
K360
P/N: 820-003472
S/N: 1223CE0521E8
S/N: 1311CE0097D8 (another keyboard)
M/N: Y-R0017
(ltunify)
HID++ version: 2.0
Device index 1
Keyboard
Name: K360
Wireless Product ID: 4004
Serial number: 60BA944E
Device was unavailable, version information not available.
Total number of HID++ 2.0 features: 12
0: [0000] IRoot
1: [0001] IFeatureSet
2: [0003] IFirmwareInfo
3: [0005] GetDeviceNameType
4: [1000] batteryLevelStatus
5: [1820] H unknown
6: [1B00] SpecialKeysMSEButtons
7: [1D4B] WirelessDeviceStatus
8: [1DF0] H unknown
9: [1DF3] H unknown
10: [40A0] FnInversion
11: [4100] Encryption
12: [4520] KeyboardLayout
(O = obsolete feature; H = SW hidden feature)
(solaar-cli)
1: Wireless Keyboard K360
Codename : K360
Kind : keyboard
Protocol : HID++ 2.0
Polling rate : 20 ms
Wireless PID : 4004
Serial number: 60BA944E
Serial number: 0D2694C9 (another keyboard)
Firmware : RQK 36.00.B0007
The power switch is located on the top case
Supports 13 HID++ 2.0 features:
0: ROOT {0000}
1: FEATURE SET {0001}
2: DEVICE FW VERSION {0003}
3: DEVICE NAME {0005}
4: BATTERY STATUS {1000}
5: unknown:1820 {1820} hidden
6: REPROG CONTROLS {1B00}
7: WIRELESS DEVICE STATUS {1D4B}
8: unknown:1DF0 {1DF0} hidden
9: unknown:1DF3 {1DF3} hidden
10: FN INVERSION {40A0}
11: ENCRYPTION {4100}
12: KEYBOARD LAYOUT {4520}
Has 18 reprogrammable keys:
0: MY HOME => HomePage FN sensitive, is FN, reprogrammable
1: Mail => Mail FN sensitive, is FN, reprogrammable
2: SEARCH => Search FN sensitive, is FN, reprogrammable
3: MEDIA PLAYER => Music FN sensitive, is FN, reprogrammable
4: Application Switcher => Application Switcher FN sensitive, is FN, reprogrammable
5: SHOW DESKTOP => ShowDesktop FN sensitive, is FN, reprogrammable
6: MINIMIZE AS WIN M => WindowsMinimize FN sensitive, is FN, reprogrammable
7: MAXIMIZE AS WIN SHIFT M => WindowsRestore FN sensitive, is FN, reprogrammable
8: MY COMPUTER AS WIN E => My Computer FN sensitive, is FN, reprogrammable
9: Lock PC => WindowsLock FN sensitive, is FN, reprogrammable
10: SLEEP => Sleep FN sensitive, is FN, reprogrammable
11: Calculator => Calculator FN sensitive, is FN, reprogrammable
12: Previous => Previous nonstandard
13: Play/Pause => Play/Pause nonstandard
14: Next => Next nonstandard
15: Mute => Mute nonstandard
16: Volume Down => Volume Down nonstandard
17: Volume Up => Volume Up nonstandard
Battery is 90% charged, discharging

52
docs/devices/k800.txt Normal file
View File

@@ -0,0 +1,52 @@
# 0x00 - Enabled Notifications. rw (see HID++ 1.0 spec)
<< ( 0.055) [10 02 8100 000000] '\x10\x02\x81\x00\x00\x00\x00'
>> ( 0.084) [10 02 8100 000000] '\x10\x02\x81\x00\x00\x00\x00'
# 0x01 - Keyboard hand detection. rw, last param is 00 when hand detection is
# enabled, 30 when disabled. (when enabled, keyboard will light up if not
# already when hovering over the front)
<< ( 1.085) [10 02 8101 000000] '\x10\x02\x81\x01\x00\x00\x00'
>> ( 1.114) [10 02 8101 000000] '\x10\x02\x81\x01\x00\x00\x00'
# 0x07 - Battery status (3 = one bar; 1 = red/critical; 5=two bars; 7=three
# bars/full. Second returned param is 25 when keyboard is charging )
<< ( 7.327) [10 02 8107 000000] '\x10\x02\x81\x07\x00\x00\x00'
>> ( 7.368) [10 02 8107 030000] '\x10\x02\x81\x07\x03\x00\x00'
# 0x09 - F key function. rw (read: status, set/get: 00 01 00 means swap
# functions, 00 00 00 means do not swap functions)
<< ( 9.411) [10 02 8109 000000] '\x10\x02\x81\t\x00\x00\x00'
>> ( 9.440) [10 02 8109 000000] '\x10\x02\x81\t\x00\x00\x00'
# 0x17 - Illumination info r/w. Last param: 02 to disable backlight, 01 to
# enable backlight
<< ( 24.965) [10 02 8117 000000] '\x10\x02\x81\x17\x00\x00\x00'
>> ( 24.988) [10 02 8117 3C0001] '\x10\x02\x81\x17<\x00\x01'
# 0x51 - ?
<< ( 99.294) [10 02 8151 000000] '\x10\x02\x81Q\x00\x00\x00'
>> ( 99.543) [10 02 8151 000000] '\x10\x02\x81Q\x00\x00\x00'
# 0x54 - ?
<< ( 103.046) [10 02 8154 000000] '\x10\x02\x81T\x00\x00\x00'
>> ( 103.295) [10 02 8154 FF0000] '\x10\x02\x81T\xff\x00\x00'
# 0xD0 - ?
<< ( 253.860) [10 02 81D0 000000] '\x10\x02\x81\xd0\x00\x00\x00'
>> ( 253.883) [10 02 81D0 000000] '\x10\x02\x81\xd0\x00\x00\x00'
# 0xF1 - Version info (params 0n 00 00 where n is 1..4)
<< ( 289.991) [10 02 81F1 000000] '\x10\x02\x81\xf1\x00\x00\x00'
>> ( 290.032) [10 02 8F81 F10300] '\x10\x02\x8f\x81\xf1\x03\x00'
# 0xF3 - ?
<< ( 292.075) [10 02 81F3 000000] '\x10\x02\x81\xf3\x00\x00\x00'
>> ( 292.116) [10 02 81F3 000000] '\x10\x02\x81\xf3\x00\x00\x00'
# 0x0F - This changes, the last commented line was observed in an earlier run
<< ( 17.728) [10 02 830F 000000] '\x10\x02\x83\x0f\x00\x00\x00'
>> ( 17.976) [11 02 830F FFFB00000240025C000000000FF90080] '\x11\x02\x83\x0f\xff\xfb\x00\x00\x02@\x02\\\x00\x00\x00\x00\x0f\xf9\x00\x80'
#>> ( 17.999) [11 02 830F FFFC007F0243025D000000000FF60080] '\x11\x02\x83\x0f\xff\xfc\x00\x7f\x02C\x02]\x00\x00\x00\x00\x0f\xf6\x00\x80'
# See also https://git.lekensteyn.nl/ltunify/tree/registers.txt for a verbose
# meaning of registers and params.

56
docs/devices/m345.txt Normal file
View File

@@ -0,0 +1,56 @@
Receiver
LZ141AX-DJ
M/N: C-U0008
(ltunify)
Serial number: 574197D3
Firmware version: 024.000.00018
Bootloader version: BL.000.006
Mouse
HID++ version: 2.0
Device index 1
Mouse
Name: M345
Wireless Product ID: 4017
Serial number: 920DC223
Device was unavailable, version information not available.
Total number of HID++ 2.0 features: 12
0: [0000] IRoot
1: [0001] IFeatureSet
2: [0003] IFirmwareInfo
3: [0005] GetDeviceNameType
4: [1000] batteryLevelStatus
5: [1D4B] WirelessDeviceStatus
6: [1DF3] H unknown
7: [1B00] SpecialKeysMSEButtons
8: [1DF0] H unknown
9: [1F03] H unknown
10: [2100] VerticalScrolling
11: [2120] HiResScrolling
12: [2200] MousePointer
(O = obsolete feature; H = SW hidden feature)
(solaar-cli)
1: Wireless Mouse M345
Codename : M345
Kind : mouse
Wireless PID : 4017
Protocol : HID++ 2.0
Polling rate : 8 ms
Serial number: 920DC223
Firmware: RQM 27.02.B0028
The power switch is located on the base.
Supports 13 HID++ 2.0 features:
0: ROOT {0000}
1: FEATURE SET {0001}
2: DEVICE FW VERSION {0003}
3: DEVICE NAME {0005}
4: BATTERY STATUS {1000}
5: WIRELESS DEVICE STATUS {1D4B}
6: unknown:1DF3 {1DF3} hidden
7: REPROG CONTROLS {1B00}
8: unknown:1DF0 {1DF0} hidden
9: unknown:1F03 {1F03} hidden
10: VERTICAL SCROLLING {2100}
11: HI RES SCROLLING {2120}
12: MOUSE POINTER {2200}
Battery: 90%, discharging,

35
docs/devices/m510.txt Normal file
View File

@@ -0,0 +1,35 @@
# notification flags
<< ( 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'
# smooth scroll
<< ( 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'
# battery status
<< ( 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'

34
docs/devices/m515.txt Normal file
View File

@@ -0,0 +1,34 @@
1: Couch Mouse M515
Codename : M515
Kind : mouse
Wireless PID : 4007
Protocol : HID++ 2.0
Polling rate : 8 ms
Serial number: BED587E9
Firmware: RQM 24.00.B0023
Bootloader: DFU 00.02.B0010
The power switch is located on the base.
Supports 16 HID++ 2.0 features:
0: ROOT {0000}
1: FEATURE SET {0001}
2: DEVICE FW VERSION {0003}
3: DEVICE NAME {0005}
4: DFUCONTROL {00C0}
5: BATTERY STATUS {1000}
6: unknown:1A30 {1A30} hidden
7: REPROG CONTROLS {1B00}
8: WIRELESS DEVICE STATUS {1D4B}
9: unknown:1DF3 {1DF3} hidden
10: VERTICAL SCROLLING {2100}
11: HI RES SCROLLING {2120}
12: MOUSE POINTER {2200}
13: unknown:1F02 {1F02} hidden
14: unknown:1F03 {1F03} hidden
15: unknown:1E80 {1E80} hidden
Has 5 reprogrammable keys:
0: LEFT CLICK => LeftClick mse, reprogrammable
1: RIGHT CLICK => RightClick mse, reprogrammable
2: MIDDLE BUTTON => MiddleMouseButton mse, reprogrammable
3: BACK AS BUTTON 4 => BackEx mse, reprogrammable
4: FORWARD AS BUTTON 5 => BrowserForwardEx mse, reprogrammable
Battery: 65%, discharging,

2
docs/devices/m525.txt Normal file
View File

@@ -0,0 +1,2 @@
No non-error messages received for GET_REG and GET_REG_LONG. Perhaps because
this is a HID++ 2.0 device?

42
docs/devices/m705.txt Normal file
View 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'

26
docs/devices/mk700.txt Normal file
View File

@@ -0,0 +1,26 @@
# Enabled Notifications
# 10 - battery status
# 02 + 01 - remap FN keys (multimedia + power buttons)
>> ( 1.412) [10 02 8100 130000] '\x10\x02\x81\x00\x13\x00\x00'
<< ( 0.011) [10 02 8101 000000] '\x10\x02\x81\x01\x00\x00\x00'
>> ( 0.276) [10 02 8101 000000] '\x10\x02\x81\x01\x00\x00\x00'
# Battery status
<< ( 6.033) [10 02 8107 000000] '\x10\x02\x81\x07\x00\x00\x00'
>> ( 6.344) [10 02 8107 070000] '\x10\x02\x81\x07\x07\x00\x00'
# FN status
<< ( 8.055) [10 02 8109 000000] '\x10\x02\x81\t\x00\x00\x00'
>> ( 8.144) [10 02 8109 000000] '\x10\x02\x81\t\x00\x00\x00'
# ?
<< ( 208.316) [10 02 81D0 000000] '\x10\x02\x81\xd0\x00\x00\x00'
>> ( 208.353) [10 02 81D0 000000] '\x10\x02\x81\xd0\x00\x00\x00'
# version info
<< ( 237.436) [10 02 81F1 000000] '\x10\x02\x81\xf1\x00\x00\x00'
>> ( 237.744) [10 02 8F81 F10300] '\x10\x02\x8f\x81\xf1\x03\x00'
# ?
<< ( 239.459) [10 02 81F3 000000] '\x10\x02\x81\xf3\x00\x00\x00'
>> ( 239.766) [10 02 81F3 000000] '\x10\x02\x81\xf3\x00\x00\x00'

View File

@@ -0,0 +1,40 @@
# Notifications (r1_bit0 = battery status?)
<< ( 0.113) [10 01 8100 000000] '\x10\x01\x81\x00\x00\x00\x00'
>> ( 0.157) [10 01 8100 100000] '\x10\x01\x81\x00\x10\x00\x00'
# ?
<< ( 1.050) [10 01 8101 000000] '\x10\x01\x81\x01\x00\x00\x00'
>> ( 1.097) [10 01 8101 020000] '\x10\x01\x81\x01\x02\x00\x00'
# battery (07 means full)
<< ( 7.335) [10 01 8107 000000] '\x10\x01\x81\x07\x00\x00\x00'
>> ( 7.382) [10 01 8107 070000] '\x10\x01\x81\x07\x07\x00\x00'
# Set LEDS - 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
# below: all leds are off
<< ( 86.592) [10 01 8151 000000] '\x10\x01\x81Q\x00\x00\x00'
>> ( 86.639) [10 01 8151 111100] '\x10\x01\x81Q\x11\x11\x00'
# DPI (values in range 0x81..0x8F; logical value: 100..1500)
<< ( 108.430) [10 01 8163 000000] '\x10\x01\x81c\x00\x00\x00'
>> ( 108.477) [10 01 8163 890000] '\x10\x01\x81c\x89\x00\x00'
# ?
<< ( 240.505) [10 01 81D0 000000] '\x10\x01\x81\xd0\x00\x00\x00'
>> ( 240.550) [10 01 81D0 000000] '\x10\x01\x81\xd0\x00\x00\x00'
# ?
<< ( 245.690) [10 01 81D4 000000] '\x10\x01\x81\xd4\x00\x00\x00'
>> ( 245.737) [10 01 81D4 000012] '\x10\x01\x81\xd4\x00\x00\x12'
# Firmware/bootloader version
<< ( 281.016) [10 01 81F1 000000] '\x10\x01\x81\xf1\x00\x00\x00'
>> ( 282.177) [10 01 8F81 F10300] '\x10\x01\x8f\x81\xf1\x03\x00'
# ?
<< ( 284.106) [10 01 81F3 000000] '\x10\x01\x81\xf3\x00\x00\x00'
>> ( 284.153) [10 01 81F3 000000] '\x10\x01\x81\xf3\x00\x00\x00'

30
docs/i18n.md Normal file
View File

@@ -0,0 +1,30 @@
# Translating Solaar
First, make sure you have installed the `gettext` package.
Here are the steps to add/update a translation (you should run all scripts from
the source root):
1. Get an up-to-date copy of the source files. Preferrably, make a clone on
GitHub and clone it locally on your machine; this way you can later make a
pull request to the main project.
2. Run `./tools/po-update.sh <language>`; it will create/update the file
`./po/<language>.po`.
3. Edit `./po/<language>.po` with your favourite editor (just make sure it saves
the file with the UTF-8 encoding). For each string in english (msgid), edit
the translation (msgstr); if you leave msgstr empty, the string will remain
untranslated.
Alternatively, you can use the excellent `poedit`.
4. Run `./tools/po-compile.sh`. It will bring up-to-date all the compiled
language files, necessary at runtime.
5. Start Solaar (`./bin/solaar`). By default it will pick up the system languge
from your environment; to start it in another language, run
`LANGUAGE=<language> ./bin/solaar`.
You can edit the translation iteratively, just repeat from step 3.
If the upstream changes, do a `git pull` and then repeat from step 2.

34
docs/icons_names.txt Normal file
View File

@@ -0,0 +1,34 @@
# battery icon names across various icon themes
B = 'battery'
CG = 'charging'
GB = 'gpm-battery'
theme (unknown) 0 0-CG 20 20-CG 40 40-CG 60 60-CG 80 80-CG 100 100-CG 100-full
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
gnome B-missing B-empty - B-caution B-caution-CG B-low B-low-CG - - B-good B-good-CG B-full B-full-CG B-full-charged
Humanity GB-missing GB-000 GB-000-CG GB-020 GB-020-CG GB-040 GB-040-CG GB-060 GB-060-CG GB-080 GB-080-CG GB-100 GB-100-CG GB-charged
(gnome) - B_empty - B-caution - B-low - B_two_thirds - B_third_fourth - B_full B_plugged B_charged
elementary B-missing B-000 B-000-CG B-020 B-020-CG B-040 B-040-CG B-060 B-060-CG B-080 B-080-CG B-100 B-100-CG B-charged
(gnome) - B-empty - B-caution B-caution-CG B-low B-low-CG - - B-good B-good-CG B-full B-full-CG B-full-charged
- B_empty - - - - - B_two_thirds - B_third_fourth - B_full B_plugged B_charged
faenza - GB-000 GB-000-CG GB-020 GB-020-CG GB-040 GB-040-CG GB-060 GB-060-CG GB-080 GB-080-CG GB-100 GB-100-CG GB-charged
(gnome) - B_empty - B_caution - B_low - B_two_thirds - B_third_fourth - B_full B_plugged B_charged
ubuntu-mono GB-missing GB-000 GB-000-CG GB-020 GB-020-CG GB-040 GB-040-CG GB-060 GB-060-CG GB-080 GB-080-CG GB-100 GB-100-CG GB-charged
(Humanity) B-000 B-000-CG B-020 B-020-CG B-040 B-040-CG B-060 B-060-CG B-080 B-080-CG B-100 B-100-CG B-charged
B_empty - B-caution - B-low - - - - - B_full - B_charged
oxygen B-missing B-low B-CG-low B-caution B-CG-caution B-040 B-CG-040 B-060 B-CG-060 B-080 B-CG-080 B-100 B-CG -
moblin - - - B-low - B-caution - - - B-good - B-full B-charging -
nuvola B-missing B-low - - - - - - - - - - B-charging -
# weather icons (for lux)

53
docs/installation.md Normal file
View File

@@ -0,0 +1,53 @@
# Manual installation
### 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.
For gnome-shell/Unity support, you also need to have `gir1.2-appindicator3-0.1`
installed.
### 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 (make sure to run it as your regular desktop user, 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-unifying-receiver.rules` from Solaar to
`/etc/udev/rules.d/`. The `udev` daemon will automatically pick up this file
using inotify.
By default, the rule allows all members of the `plugdev` group to have
read/write access to the Unifying Receiver device. (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. Physically remove the Unifying Receiver and re-insert it.
This is necessary because if the receiver is already plugged-in, it already
has a `/dev/hidrawX` device node, but with the old (`root:root`) permissions.
Plugging it again will re-create the device node with the right permissions.
3. Make sure your desktop users are part of the `plugdev` group, by running
`gpasswd <desktop username> plugdev`. If these users were not assigned to the
group before, they must re-login for the changes to take effect.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,26 @@
The battery/charging level and status is reported only if the related
reporting flag in register 0x00 is enabled by the host. The
"Battery/Charging Level" byte indicates the battery level if the
"Charging State" indicates 0x00 ("Not Charging"). If "Charging State"
indicates 0x21 to 0x23 ("Charging"), the "Battery/Charging Level" byte
indicates the level of charging.
10 ix 07 r0 r1 r2 00
r0 -> Battery/Charging Level
0x00 = Reserved/Unknown
0x01 = Critical
0x02 = Critical (legacy value, don't use)
0x03 = Low
0x04 = Low (legacy value, don't use)
0x05 = Good
0x06 = Good (legacy value, don't use)
0x07 = Full
0x08..0xFF = Reserved
r1 -> Charging state
0x00 = Not charging
0x01..0x1F = Reserved (not charging)
0x20 = Unknown charging state
0x21 = Charging
0x22 = Charging complete
0x23 = Charging error
0x24..0xFF = Reserved

278
docs/logitech/hid10.txt Normal file
View 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)

Binary file not shown.

12
docs/usb-ids.txt Normal file
View File

@@ -0,0 +1,12 @@
Unifying receiver:
046d:c52b interface: 2 driver: logitech-djreceiver
046d:c532 interface: 2 driver: logitech-djreceiver
Nano receiver, Advanced/Unifying ready:
046d:c52f interface: 1 driver: hid-generic
Nano receiver:
046d:c51a interface: 1 driver: hid-generic
046d:c526 interface: 1 driver: hid-generic

364
docs/usb.ids.txt Normal file
View File

@@ -0,0 +1,364 @@
#
# List of USB ID's
#
# Maintained by Stephen J. Gowdy <linux.usb.ids@gmail.com>
# If you have any new entries, please submit them via
# http://www.linux-usb.org/usb-ids.html
# or send entries as patches (diff -u old new) in the
# body of your email (a bot will attempt to deal with it).
# The latest version can be obtained from
# http://www.linux-usb.org/usb.ids
#
# Version: 2013.05.24
# Date: 2013-05-24 20:34:03
#
# Vendors, devices and interfaces. Please keep sorted.
# Syntax:
# vendor vendor_name
# device device_name <-- single tab
# interface interface_name <-- two tabs
046d Logitech, Inc.
0082 Acer Aspire 5672 Webcam
0200 WingMan Extreme Joystick
0203 M2452 Keyboard
0301 M4848 Mouse
0401 HP PageScan
0402 NEC PageScan
040f Logitech/Storm PageScan
0430 Mic (Cordless)
0801 QuickCam Home
0802 Webcam C200
0804 Webcam C250
0805 Webcam C300
0807 Webcam B500
0808 Webcam C600
0809 Webcam Pro 9000
080a Portable Webcam C905
080f Webcam C120
0810 QuickCam Pro
0819 Webcam C210
081b Webcam C310
081d HD Webcam C510
0820 QuickCam VC
0821 HD Webcam C910
0825 Webcam C270
0828 HD Webcam B990
082d HD Pro Webcam C920
0830 QuickClip
0840 QuickCam Express
0850 QuickCam Web
0870 QuickCam Express
0890 QuickCam Traveler
0892 OrbiCam
0894 CrystalCam
0895 QuickCam for Dell Notebooks
0896 OrbiCam
0897 QuickCam for Dell Notebooks
0899 QuickCam for Dell Notebooks
089d QuickCam E2500 series
08a0 QuickCam IM
08a1 QuickCam IM with sound
08a2 Labtec Webcam Pro
08a3 QuickCam QuickCam Chat
08a6 QuickCam IM
08a7 QuickCam Image
08a9 Notebook Deluxe
08aa Labtec Notebooks
08ac QuickCam Cool
08ad QuickCam Communicate STX
08ae QuickCam for Notebooks
08af QuickCam Easy/Cool
08b0 QuickCam 3000 Pro [pwc]
08b1 QuickCam Notebook Pro
08b2 QuickCam Pro 4000
08b3 QuickCam Zoom
08b4 QuickCam Zoom
08b5 QuickCam Sphere
08b9 QuickCam IM
08bd Microphone (Pro 4000)
08c0 QuickCam Pro 3000
08c1 QuickCam Fusion
08c2 QuickCam PTZ
08c3 Camera (Notebooks Pro)
08c5 QuickCam Pro 5000
08c6 QuickCam for DELL Notebooks
08c7 QuickCam OEM Cisco VT Camera II
08c9 QuickCam Ultra Vision
08ca Mic (Fusion)
08cb Mic (Notebooks Pro)
08cc Mic (PTZ)
08ce QuickCam Pro 5000
08cf QuickCam UpdateMe
08d0 QuickCam Express
08d7 QuickCam Communicate STX
08d8 QuickCam for Notebook Deluxe
08d9 QuickCam IM/Connect
08da QuickCam Messanger
08dd QuickCam for Notebooks
08e0 QuickCam Express
08e1 Labtec Webcam
08f0 QuickCam Messenger
08f1 QuickCam Express
08f2 Microphone (Messenger)
08f3 QuickCam Express
08f4 Labtec Webcam
08f5 QuickCam Messenger Communicate
08f6 QuickCam Messenger Plus
0900 ClickSmart 310
0901 ClickSmart 510
0903 ClickSmart 820
0905 ClickSmart 820
0910 QuickCam Cordless
0920 QuickCam Express
0921 Labtec Webcam
0922 QuickCam Live
0928 QuickCam Express
0929 Labtec Webcam Pro
092a QuickCam for Notebooks
092b Labtec Webcam Plus
092c QuickCam Chat
092d QuickCam Express / Go
092e QuickCam Chat
092f QuickCam Express Plus
0950 Pocket Camera
0960 ClickSmart 420
0970 Pocket750
0990 QuickCam Pro 9000
0991 QuickCam Pro for Notebooks
0992 QuickCam Communicate Deluxe
0994 QuickCam Orbit/Sphere AF
09a1 QuickCam Communicate MP/S5500
09a2 QuickCam Communicate Deluxe/S7500
09a4 QuickCam E 3500
09a5 Quickcam 3000 For Business
09a6 QuickCam Vision Pro
09b0 Acer OrbiCam
09b2 Fujitsu Webcam
09c0 QuickCam for Dell Notebooks Mic
09c1 QuickCam Deluxe for Notebooks
0a01 USB Headset
0a02 Premium Stereo USB Headset 350
0a03 Logitech USB Microphone
0a04 V20 portable speakers (USB powered)
0a07 Z-10 Speakers
0a0b ClearChat Pro USB
0a0c Clear Chat Comfort USB Headset
0a13 Z-5 Speakers
0a17 G330 Headset
0a1f G930
0b02 C-UV35 [Bluetooth Mini-Receiver] (HID proxy mode)
8801 Video Camera
b305 BT Mini-Receiver
bfe4 Premium Optical Wheel Mouse
c000 N43 [Pilot Mouse]
c001 N48/M-BB48 [FirstMouse Plus]
c002 M-BA47 [MouseMan Plus]
c003 MouseMan
c004 WingMan Gaming Mouse
c005 WingMan Gaming Wheel Mouse
c00b MouseMan Wheel
c00c Optical Wheel Mouse
c00d MouseMan Wheel+
c00e M-BJ58/M-BJ69 Optical Wheel Mouse
c00f MouseMan Traveler/Mobile
c011 Optical MouseMan
c012 Mouseman Dual Optical
c014 Corded Workstation Mouse
c015 Corded Workstation Mouse
c016 Optical Wheel Mouse
c018 Optical Wheel Mouse
c019 Optical Tilt Wheel Mouse
c01a M-BQ85 Optical Wheel Mouse
c01b MX310 Optical Mouse
c01c Optical Mouse
c01d MX510 Optical Mouse
c01e MX518 Optical Mouse
c024 MX300 Optical Mouse
c025 MX500 Optical Mouse
c030 iFeel Mouse
c031 iFeel Mouse+
c032 MouseMan iFeel
c033 iFeel MouseMan+
c034 MouseMan Optical
c035 Mouse
c036 Mouse
c037 Mouse
c038 Mouse
c03d M-BT96a Pilot Optical Mouse
c03e Premium Optical Wheel Mouse (M-BT58)
c03f M-BT85 [UltraX Optical Mouse]
c040 Corded Tilt-Wheel Mouse
c041 G5 Laser Mouse
c042 G3 Laser Mouse
c043 MX320/MX400 Laser Mouse
c044 LX3 Optical Mouse
c045 Optical Mouse
c046 RX1000 Laser Mouse
c047 Laser Mouse M-UAL120
c048 G9 Laser Mouse
c049 G5 Laser Mouse
c050 RX 250 Optical Mouse
c051 G3 (MX518) Optical Mouse
c053 Laser Mouse
c054 Bluetooth mini-receiver
c058 M115 Mouse
c05a M90/M100 Optical Mouse
c05b M-U0004 810-001317 [B110 Optical USB Mouse]
c05d Optical Mouse
c05f M115 Optical Mouse
c061 RX1500 Laser Mouse
c062 M-UAS144 [LS1 Laser Mouse]
c063 DELL Laser Mouse
c068 G500 Laser Mouse
c069 M500 Laser Mouse
c06a USB Optical Mouse
c06b G700 Wireless Gaming Mouse
c06c Optical Mouse
c101 UltraX Media Remote
c110 Harmony 785/885 Remote
c111 Harmony 525 Remote
c112 Harmony 890 Remote
c11f Harmony 900/1100 Remote
c121 Harmony One Remote
c122 Harmony 700 Remote
c124 Harmony 300 Remote
c125 Harmony 200 Remote
c201 WingMan Extreme Joystick with Throttle
c202 WingMan Formula
c207 WingMan Extreme Digital 3D
c208 WingMan Gamepad Extreme
c209 WingMan Gamepad
c20a WingMan RumblePad
c20b WingMan Action Pad
c20c WingMan Precision
c20d WingMan Attack 2
c20e WingMan Formula GP
c211 iTouch Cordless Reciever
c212 WingMan Extreme Digital 3D
c213 J-UH16 (Freedom 2.4 Cordless Joystick)
c214 ATK3 (Attack III Joystick)
c215 Extreme 3D Pro
c216 Dual Action Gamepad
c218 Logitech RumblePad 2 USB
c219 Cordless RumblePad 2
c21a Precision Gamepad
c21c G13 Advanced Gameboard
c21d F310 Gamepad [XInput Mode]
c21e F510 Gamepad [XInput Mode]
c21f F710 Wireless Gamepad [XInput Mode]
c221 G11/G15 Keyboard / Keyboard
c222 G15 Keyboard / LCD
c223 G11/G15 Keyboard / USB Hub
c225 G11/G15 Keyboard / G keys
c226 G15 Refresh Keyboard
c227 G15 Refresh Keyboard
c22a Gaming Keyboard G110
c22b Gaming Keyboard G110 G-keys
c22d G510 Gaming Keyboard
c22e G510 Gaming Keyboard onboard audio
c245 G400 Optical Mouse
c246 Gaming Mouse G300
c281 WingMan Force
c283 WingMan Force 3D
c285 WingMan Strike Force 3D
c286 Force 3D Pro
c287 Flight System G940
c291 WingMan Formula Force
c293 WingMan Formula Force GP
c294 Driving Force
c295 Momo Force Steering Wheel
c298 Driving Force Pro
c299 G25 Racing Wheel
c29b G27 Racing Wheel
c29c Speed Force Wireless Wheel for Wii
c2a0 Wingman Force Feedback Mouse
c2a1 WingMan Force Feedback Mouse
c301 iTouch Keyboard
c302 iTouch Pro Keyboard
c303 iTouch Keyboard
c305 Internet Keyboard
c307 Internet Keyboard
c308 Internet Navigator Keyboard
c309 Internet Keyboard
c30a iTouch Composite
c30b NetPlay Keyboard
c30c Internet Keys (X)
c30d Internet Keys
c30e UltraX Keyboard (Y-BL49)
c30f Logicool HID-Compliant Keyboard (106 key)
c311 Y-UF49 [Internet Pro Keyboard]
c312 DeLuxe 250 Keyboard
c313 Internet 350 Keyboard
c315 Classic Keyboard 200
c316 HID-Compliant Keyboard
c317 Wave Corded Keyboard
c318 Illuminated Keyboard
c31a Comfort Wave 450
c31b Compact Keyboard K300
c31c Keyboard K120 for Business
c31d Media Keyboard K200
c401 TrackMan Marble Wheel
c402 Marble Mouse (2-button)
c403 Turbo TrackMan Marble FX
c404 TrackMan Wheel
c408 Marble Mouse (4-button)
c501 Cordless Mouse Receiver
c502 Cordless Mouse & iTouch Keys
c503 Cordless Mouse+Keyboard Receiver
c504 Cordless Mouse+Keyboard Receiver
c505 Cordless Mouse+Keyboard Receiver
c506 MX700 Cordless Mouse Receiver
c508 Cordless Trackball
c509 Cordless Keyboard & Mouse
c50a Cordless Mouse
c50b Cordless Desktop Optical
c50c Cordless Desktop S510
c50d Cordless Mouse
c50e Cordless Mouse Receiver
c510 Cordless Mouse
c512 LX-700 Cordless Desktop Receiver
c513 MX3000 Cordless Desktop Receiver
c514 Cordless Mouse
c515 Cordless 2.4 GHz Presenter Presentation remote control
c517 LX710 Cordless Desktop Laser
c518 MX610 Laser Cordless Mouse
c51a MX Revolution/G7 Cordless Mouse
c51b V220 Cordless Optical Mouse for Notebooks
c521 Cordless Mouse Receiver
c525 MX Revolution Cordless Mouse
c526 Nano Receiver
c529 Logitech Keyboard + Mice
c52b Unifying Receiver
c52f Unifying Receiver
c532 Unifying Receiver
c623 3Dconnexion Space Traveller 3D Mouse
c625 3Dconnexion Space Pilot 3D Mouse
c626 3Dconnexion Space Navigator 3D Mouse
c627 3Dconnexion Space Explorer 3D Mouse
c702 Cordless Presenter
c703 Elite Keyboard Y-RP20 + Mouse MX900 (Bluetooth)
c704 diNovo Wireless Desktop
c705 MX900 Bluetooth Wireless Hub (C-UJ16A)
c707 Bluetooth wireless hub
c708 Bluetooth wireless hub
c709 BT Mini-Receiver (HCI mode)
c70a MX5000 Cordless Desktop
c70b BT Mini-Receiver (HID proxy mode)
c70c BT Mini-Receiver (HID proxy mode)
c70d Bluetooth wireless hub
c70e MX1000 Bluetooth Laser Mouse
c70f Bluetooth wireless hub
c712 Bluetooth wireless hub
c714 diNovo Edge Keyboard
c715 Bluetooth wireless hub
c71a Bluetooth wireless hub
c71d Bluetooth wireless hub
c71f diNovo Mini Wireless Keyboard
c720 Bluetooth wireless hub
ca03 MOMO Racing
ca04 Formula Vibration Feedback Wheel
cab1 Cordless Keyboard for Wii HID Receiver
d001 QuickCam Pro

12
jekyll/_config.yml Normal file
View File

@@ -0,0 +1,12 @@
title: Solaar
tagline: Linux devices manager for the Logitech Unifying Receiver.
owner: pwr
owner_url: https://github.com/pwr
repository: https://github.com/pwr/Solaar
version: 0.9.1
tar_download: https://github.com/pwr/Solaar/archive/0.9.1.tar.gz
ga_id: UA-36908718-1
pygments: true
safe: true
lsi: false

View File

@@ -0,0 +1,14 @@
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" style="width: 48px; height:48px; margin-bottom: -10px;">
<defs>
<linearGradient id="gradient_blue">
<stop style="stop-color:#009099;stop-opacity:1" offset="0" />
<stop style="stop-color:#00a899;stop-opacity:0.9" offset="1" />
</linearGradient>
<linearGradient x1="5" y1="50" x2="95" y2="50" id="gradient_rect" xlink:href="#gradient_blue" gradientUnits="userSpaceOnUse" />
<linearGradient x1="37" y1="50" x2="63" y2="50" id="gradient_dot" xlink:href="#gradient_blue" gradientUnits="userSpaceOnUse" />
</defs>
<g transform="scale(0.48)">
<path d="M 21.5,5.5 C 12.636,5.5 5.5,12.636 5.5,21.5 L 5.5,78.5 C 5.5,87.364 12.636,94.5 21.5,94.5 L 78.5,94.5 C 87.364,94.5 94.5,87.364 94.5,78.5 L 94.5,21.5 C 94.5,12.636 87.364,5.5 78.5,5.5 L 21.5,5.5 z M 37.6875,16.6875 46.71875,32.3125 C 47.784179,32.115965 48.877705,32 50,32 51.122295,32 52.215821,32.115965 53.28125,32.3125 L 62.3125,16.6875 72.6875,22.6875 63.65625,38.3125 C 65.078123,39.972287 66.191785,41.898777 66.9375,44 L 85,44 85,56 66.9375,56 C 66.191785,58.101223 65.078123,60.027713 63.65625,61.6875 L 72.6875,77.3125 62.3125,83.3125 53.28125,67.6875 C 52.215821,67.884035 51.122295,68 50,68 48.877705,68 47.784179,67.884035 46.71875,67.6875 L 37.6875,83.3125 27.3125,77.3125 36.34375,61.6875 C 34.921877,60.027713 33.808215,58.101223 33.0625,56 L 15,56 15,44 33.0625,44 C 33.808215,41.898777 34.921877,39.972287 36.34375,38.3125 L 27.3125,22.6875 37.6875,16.6875 z" style="fill:url(#gradient_rect);fill-opacity:1;fill-rule:nonzero;stroke:#16161d;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1" />
<path d="M 62,50 A 12,12 0 1 1 38,50 12,12 0 1 1 62,50 z" style="fill:url(#gradient_dot);fill-opacity:1;fill-rule:nonzero;stroke:#16161d;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<meta name="description" content="{{ site.tagline }}" />
<link rel="stylesheet" type="text/css" media="screen" href="style/stylesheet.css">
<link rel="icon" type="image/png" href="images/favicon.png" />
<title>{{ page.title }}</title>
</head>
<body>
<div id="header_wrap" class="outer">
<header class="inner">
<a id="forkme_banner" href="{{ site.repository }}">View on GitHub</a>
<h1 id="project_title">{% include solaar.svg %} {{ site.title }}</h1>
<h2 id="project_tagline">{{ site.tagline }}</h2>
<section id="downloads">
<a class="tar_download_link" href="{{ site.tar_download }}">Solaar {{ site.version }}</a>
<p style="color: #fff">
Latest version:<br/>
<span style="float: right; font-weight: bolder;">{{ site.version }}</span>
</p>
</section>
</header>
</div>
<div id="main_content_wrap" class="outer">
<section id="main_content" class="inner">
{{ content }}
</section>
</div>
<div id="footer_wrap" class="outer">
<footer class="inner">
<p class="copyright"><a href="{{ site.repository }}">{{ site.title }}</a> maintained by <a href="{{ site.owner_url }}">{{ site.owner }}</a></p>
<p><a href="https://github.com/jsncostello/slate">Slate</a> theme by <a href="https://github.com/jsncostello">Jason Costello</a></p>
</footer>
</div>
<script type="text/javascript">
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
</script>
<script type="text/javascript">
try {
var pageTracker = _gat._getTracker("{{ site.ga_id }}");
pageTracker._trackPageview();
} catch(err) {}
</script>
</body>
</html>

47
jekyll/_layouts/page.html Normal file
View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<meta name="description" content="{{ site.tagline }}" />
<link rel="stylesheet" type="text/css" media="screen" href="style/stylesheet.css">
<link rel="icon" type="image/png" href="images/favicon.png" />
<title>{{ page.title }}</title>
</head>
<body>
<div id="header_wrap" class="outer">
<header class="inner">
<h1 id="project_title">
<a href="index.html">{% include solaar.svg %} {{ site.title }}</a>
</h1>
</header>
</div>
<div id="main_content_wrap" class="outer">
<section id="main_content" class="inner">
{{ content }}
</section>
</div>
<div id="footer_wrap" class="outer">
<footer class="inner">
<p class="copyright"><a href="{{ site.repository }}">{{ site.title }}</a> maintained by <a href="{{ site.owner_url }}">{{ site.owner }}</a></p>
<p><a href="https://github.com/jsncostello/slate">Slate</a> theme by <a href="https://github.com/jsncostello">Jason Costello</a></p>
</footer>
</div>
<script type="text/javascript">
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
</script>
<script type="text/javascript">
try {
var pageTracker = _gat._getTracker("{{ site.ga_id }}");
pageTracker._trackPageview();
} catch(err) {}
</script>
</body>
</html>

BIN
jekyll/images/bg_hr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,70 @@
.highlight .hll { background-color: #ffffcc }
.highlight { background: #f0f3f3; }
.highlight .c { color: #0099FF; font-style: italic } /* Comment */
.highlight .err { color: #AA0000; background-color: #FFAAAA } /* Error */
.highlight .k { color: #006699; font-weight: bold } /* Keyword */
.highlight .o { color: #555555 } /* Operator */
.highlight .cm { color: #0099FF; font-style: italic } /* Comment.Multiline */
.highlight .cp { color: #009999 } /* Comment.Preproc */
.highlight .c1 { color: #0099FF; font-style: italic } /* Comment.Single */
.highlight .cs { color: #0099FF; font-weight: bold; font-style: italic } /* Comment.Special */
.highlight .gd { background-color: #FFCCCC; border: 1px solid #CC0000 } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #FF0000 } /* Generic.Error */
.highlight .gh { color: #003300; font-weight: bold } /* Generic.Heading */
.highlight .gi { background-color: #CCFFCC; border: 1px solid #00CC00 } /* Generic.Inserted */
.highlight .go { color: #AAAAAA } /* Generic.Output */
.highlight .gp { color: #000099; font-weight: bold } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #003300; font-weight: bold } /* Generic.Subheading */
.highlight .gt { color: #99CC66 } /* Generic.Traceback */
.highlight .kc { color: #006699; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #006699; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #006699; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #006699 } /* Keyword.Pseudo */
.highlight .kr { color: #006699; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #007788; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #FF6600 } /* Literal.Number */
.highlight .s { color: #CC3300 } /* Literal.String */
.highlight .na { color: #330099 } /* Name.Attribute */
.highlight .nb { color: #336666 } /* Name.Builtin */
.highlight .nc { color: #00AA88; font-weight: bold } /* Name.Class */
.highlight .no { color: #336600 } /* Name.Constant */
.highlight .nd { color: #9999FF } /* Name.Decorator */
.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */
.highlight .ne { color: #CC0000; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #CC00FF } /* Name.Function */
.highlight .nl { color: #9999FF } /* Name.Label */
.highlight .nn { color: #00CCFF; font-weight: bold } /* Name.Namespace */
.highlight .nt { color: #330099; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #003333 } /* Name.Variable */
.highlight .ow { color: #000000; font-weight: bold } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mf { color: #FF6600 } /* Literal.Number.Float */
.highlight .mh { color: #FF6600 } /* Literal.Number.Hex */
.highlight .mi { color: #FF6600 } /* Literal.Number.Integer */
.highlight .mo { color: #FF6600 } /* Literal.Number.Oct */
.highlight .sb { color: #CC3300 } /* Literal.String.Backtick */
.highlight .sc { color: #CC3300 } /* Literal.String.Char */
.highlight .sd { color: #CC3300; font-style: italic } /* Literal.String.Doc */
.highlight .s2 { color: #CC3300 } /* Literal.String.Double */
.highlight .se { color: #CC3300; font-weight: bold } /* Literal.String.Escape */
.highlight .sh { color: #CC3300 } /* Literal.String.Heredoc */
.highlight .si { color: #AA0000 } /* Literal.String.Interpol */
.highlight .sx { color: #CC3300 } /* Literal.String.Other */
.highlight .sr { color: #33AAAA } /* Literal.String.Regex */
.highlight .s1 { color: #CC3300 } /* Literal.String.Single */
.highlight .ss { color: #FFCC33 } /* Literal.String.Symbol */
.highlight .bp { color: #336666 } /* Name.Builtin.Pseudo */
.highlight .vc { color: #003333 } /* Name.Variable.Class */
.highlight .vg { color: #003333 } /* Name.Variable.Global */
.highlight .vi { color: #003333 } /* Name.Variable.Instance */
.highlight .il { color: #FF6600 } /* Literal.Number.Integer.Long */
.type-csharp .highlight .k { color: #0000FF }
.type-csharp .highlight .kt { color: #0000FF }
.type-csharp .highlight .nf { color: #000000; font-weight: normal }
.type-csharp .highlight .nc { color: #2B91AF }
.type-csharp .highlight .nn { color: #000000 }
.type-csharp .highlight .s { color: #A31515 }
.type-csharp .highlight .sc { color: #A31515 }

442
jekyll/style/stylesheet.css Normal file
View File

@@ -0,0 +1,442 @@
/*******************************************************************************
Slate Theme for Github Pages
by Jason Costello, @jsncostello
*******************************************************************************/
@import url(pygment_trac.css);
/*******************************************************************************
MeyerWeb Reset
*******************************************************************************/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
ol, ul {
list-style: none;
}
blockquote, q {
}
table {
border-collapse: collapse;
border-spacing: 0;
}
a:focus {
outline: none;
}
/*******************************************************************************
Theme Styles
*******************************************************************************/
body {
box-sizing: border-box;
color:#373737;
background: #212121;
font-size: 18px;
font-family: 'Myriad Pro', Calibri, Helvetica, Arial, sans-serif;
line-height: 1.4;
-webkit-font-smoothing: antialiased;
}
h1, h2, h3, h4, h5, h6 {
margin: 20px 0;
font-weight: 700;
color:#222222;
font-family: 'Lucida Grande', 'Calibri', Helvetica, Arial, sans-serif;
letter-spacing: -1px;
}
h1 {
font-size: 36px;
font-weight: 700;
}
h2 {
padding-bottom: 0px;
font-size: 32px;
background: url('../images/bg_hr.png') repeat-x bottom;
}
h3 {
font-size: 24px;
}
h4 {
font-size: 21px;
}
h5 {
font-size: 18px;
}
h6 {
font-size: 16px;
}
p {
margin: 10px 0 15px 0;
}
footer p {
color: #f2f2f2;
}
a {
text-decoration: none;
color: #007edf;
text-shadow: none;
transition: color 0.5s ease;
transition: text-shadow 0.5s ease;
-webkit-transition: color 0.5s ease;
-webkit-transition: text-shadow 0.5s ease;
-moz-transition: color 0.5s ease;
-moz-transition: text-shadow 0.5s ease;
-o-transition: color 0.5s ease;
-o-transition: text-shadow 0.5s ease;
-ms-transition: color 0.5s ease;
-ms-transition: text-shadow 0.5s ease;
}
#main_content a:hover {
color: #0069ba;
text-shadow: #0090ff 0px 0px 2px;
}
footer a:hover {
color: #43adff;
text-shadow: #0090ff 0px 0px 2px;
}
em {
font-style: italic;
}
strong {
font-weight: bold;
}
img {
position: relative;
margin: 0 auto;
max-width: 739px;
padding: 5px;
margin: 10px 0 10px 0;
border: 1px solid #ebebeb;
box-shadow: 0 0 5px #ebebeb;
-webkit-box-shadow: 0 0 5px #ebebeb;
-moz-box-shadow: 0 0 5px #ebebeb;
-o-box-shadow: 0 0 5px #ebebeb;
-ms-box-shadow: 0 0 5px #ebebeb;
}
img.logo {
max-width: 48px;
width: 48px;
height: 48px;
margin: 0;
padding: 0;
border: 0;
bottom: -10px;
box-shadow: none;
-webkit-box-shadow: 0;
-moz-box-shadow: 0;
-o-box-shadow: 0;
-ms-box-shadow: 0;
}
pre, code {
width: 100%;
color: #222;
background-color: #fff;
font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace;
font-size: 14px;
border-radius: 2px;
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
}
pre {
width: 100%;
padding: 10px;
box-shadow: 0 0 10px rgba(0,0,0,.1);
overflow: auto;
}
code {
padding: 3px;
margin: 0 3px;
box-shadow: 0 0 10px rgba(0,0,0,.1);
}
pre code {
display: block;
box-shadow: none;
}
blockquote {
color: #666;
margin-bottom: 20px;
padding: 0 0 0 20px;
border-left: 3px solid #bbb;
}
ul, ol, dl {
margin-bottom: 15px
}
ul li {
list-style: inside;
padding-left: 20px;
}
ol li {
list-style: decimal inside;
padding-left: 20px;
}
dl dt {
font-weight: bold;
}
dl dd {
padding-left: 20px;
font-style: italic;
}
dl p {
padding-left: 20px;
font-style: italic;
}
hr {
height: 1px;
margin-bottom: 5px;
border: none;
background: url('../images/bg_hr.png') repeat-x center;
}
table {
border: 1px solid #373737;
margin-bottom: 20px;
text-align: left;
width: 100%;
}
th {
font-family: 'Lucida Grande', 'Helvetica Neue', Helvetica, Arial, sans-serif;
padding: 10px;
background: #373737;
color: #fff;
}
td {
padding: 10px;
border: 1px solid #373737;
}
form {
background: #f2f2f2;
padding: 20px;
}
img {
width: 100%;
max-width: 100%;
}
/*******************************************************************************
Full-Width Styles
*******************************************************************************/
.outer {
width: 100%;
}
.inner {
position: relative;
max-width: 940px;
padding: 20px 10px;
margin: 0 auto;
}
.copyright {
float: right;
}
#forkme_banner {
display: block;
position: absolute;
top:0;
right: 10px;
z-index: 10;
padding: 10px 50px 10px 10px;
color: #fff;
background: url('../images/blacktocat.png') #0090ff no-repeat 95% 50%;
font-weight: 700;
box-shadow: 0 0 10px rgba(0,0,0,.5);
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
#header_wrap {
background: #212121;
}
#header_wrap .inner {
padding 50px 10px 30px 10px;
}
#project_title {
margin: 0;
color: #fff;
font-size: 42px;
font-weight: 700;
text-shadow: #111 0px 0px 10px;
}
#project_tagline {
color: #fff;
font-size: 24px;
font-weight: 300;
background: none;
text-shadow: #111 0px 0px 10px;
}
#downloads {
position: absolute;
width: 240px;
z-index: 10;
bottom: 0px;
right: 0;
height: 80px;
background: url('../images/icon_download.png') no-repeat 15% 70%;
}
.zip_download_link {
display: block;
float: right;
width: 90px;
height:70px;
text-indent: -5000px;
overflow: hidden;
background: url(../images/sprite_download.png) no-repeat bottom left;
}
.tar_download_link {
display: block;
float: right;
width: 90px;
height:70px;
text-indent: -5000px;
overflow: hidden;
background: url(../images/sprite_download.png) no-repeat bottom right;
margin-left: 10px;
}
.zip_download_link:hover {
background: url(../images/sprite_download.png) no-repeat top left;
}
.tar_download_link:hover {
background: url(../images/sprite_download.png) no-repeat top right;
}
#main_content_wrap {
background: #f2f2f2;
border-top: 1px solid #111;
border-bottom: 1px solid #111;
}
#main_content {
padding-top: 40px;
}
#footer_wrap {
background: #212121;
}
/*******************************************************************************
Small Device Styles
*******************************************************************************/
@media screen and (max-width: 480px) {
body {
font-size:14px;
}
#downloads {
display: none;
}
.inner {
min-width: 320px;
max-width: 480px;
}
#project_title {
font-size: 32px;
}
h1 {
font-size: 28px;
}
h2 {
font-size: 24px;
}
h3 {
font-size: 21px;
}
h4 {
font-size: 18px;
}
h5 {
font-size: 14px;
}
h6 {
font-size: 12px;
}
code, pre {
min-width: 320px;
max-width: 480px;
font-size: 11px;
}
}

View File

@@ -1,10 +1,37 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""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.9'
from hidapi.udev import (
enumerate,
open,
close,
open_path,
monitor_glib,
read,
write,
get_manufacturer,
get_product,
get_serial,
)

View File

@@ -1,109 +1,258 @@
#!/usr/bin/env python
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
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 as _hid
#
#
#
interactive = os.isatty(0)
start_time = 0
try:
read_packet = raw_input
except:
except NameError:
# Python 3 equivalent of raw_input
read_packet = input
interactive = os.isatty(0)
prompt = '?? Input: ' if interactive else ''
start_time = time.time()
strhex = lambda d: hexlify(d).decode('ascii').upper()
try:
unicode
# this is certanly Python 2
is_string = lambda d: isinstance(d, unicode)
# no easy way to distinguish between b'' and '' :(
# or (isinstance(d, str) \
# and not any((chr(k) in d for k in range(0x00, 0x1F))) \
# and not any((chr(k) in d for k in range(0x80, 0xFF))) \
# )
except:
# this is certanly Python 3
# In Py3, unicode and str are equal (the unicode object does not exist)
is_string = lambda d: isinstance(d, str)
#
#
#
from threading import Lock
print_lock = Lock()
del Lock
def _print(marker, data, scroll=False):
t = time.time() - start_time
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))
sys.stdout.write(s)
if interactive and scroll:
sys.stdout.write('\033[u')
if is_string(data):
s = marker + ' ' + data
else:
sys.stdout.write('\n')
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))
with print_lock:
# allow only one thread at a time to write to the console, otherwise
# the output gets garbled, especially with ANSI codes.
if interactive and scroll:
# 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')
# flush stdout manually...
# because trying to open stdin/out unbuffered programatically
# works much too differently in Python 2/3
sys.stdout.flush()
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 = _hid.read(handle, 128, timeout)
except OSError as e:
_error("Read failed, aborting: " + str(e), True)
break
elif reply:
assert reply is not None
if reply:
_print('>>', reply, True)
if __name__ == '__main__':
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(args):
device = args.device
if args.hidpp and not device:
for d in _hid.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 = _hid.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,
_hid.get_manufacturer(handle),
_hid.get_product(handle),
_hid.get_serial(handle)))
if args.hidpp:
if _hid.get_manufacturer(handle) != b'Logitech':
sys.exit("!! Only Logitech devices support the HID++ protocol.")
print (".. HID++ validation enabled.")
else:
if (_hid.get_manufacturer(handle) == b'Logitech' and
b'Receiver' in _hid.get_product(handle)):
args.hidpp = True
print (".. Logitech receiver detected, HID++ validation enabled.")
return handle
#
#
#
def _parse_arguments():
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()
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()
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
def main():
args = _parse_arguments()
handle = _open(args)
start_time = time.time()
if interactive:
print (".. Press ^C/^D to exit, or type hex bytes to write to the device.")
import readline
if args.history is None:
import os.path
args.history = os.path.join(os.path.expanduser('~'), '.hidconsole-history')
try:
from threading import Thread
t = Thread(target=_continuous_read, args=(handle,))
t.daemon = True
t.start()
prompt = '?? Input: ' if interactive else ''
while t.is_alive():
line = read_packet(prompt).strip().replace(' ', '')
if line:
try:
data = unhexlify(line.encode('ascii'))
except Exception as e:
print ("!! Invalid input.")
else:
_print('<<', data)
hidapi.write(handle, data)
# wait for some kind of reply
if not interactive:
rlist, wlist, xlist = _select([handle], [], [], 1)
time.sleep(0.1)
except EOFError:
readline.read_history_file(args.history)
except:
# file may not exist yet
pass
except Exception as e:
print ('%s: %s' % (type(e).__name__, e))
print (".. Closing handle %X" % handle)
hidapi.close(handle)
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(' ', '')
# print ("line", line)
if not line:
continue
data = _validate_input(line, args.hidpp)
if data is None:
continue
_print('<<', data)
_hid.write(handle, data)
# wait for some kind of reply
if args.hidpp and not interactive:
rlist, wlist, xlist = _select([handle], [], [], 1)
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
time.sleep(0.700)
except EOFError:
if interactive:
print ("")
else:
time.sleep(1)
finally:
print (".. Closing handle %r" % handle)
_hid.close(handle)
if interactive:
readline.write_history_file(args.history)
else:
print ("!! Failed to open %s, aborting" % args.device)
if __name__ == '__main__':
main()

View File

@@ -1,380 +0,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.
"""
__version__ = '0.3-hidapi-0.7.0'
import ctypes as _C
from struct import pack as _pack
#
# look for a native implementation in the same directory as this file
#
# 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
if _native is None:
raise ImportError('hidapi: failed to load any HID API native implementation')
#
# 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))
]
# 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)
#
# set-up arguments and return types for each hidapi function
#
_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_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_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_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_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_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_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_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_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
#
# exposed API
# docstrings mostly copied from hidapi.h
#
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.
:returns: ``True`` if successful.
"""
return _native.hid_init() == 0
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.
:returns: ``True`` if successful.
"""
return _native.hid_exit() == 0
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.
: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
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.
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
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().
: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.
: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.
: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.
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
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.
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]
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.
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
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)

View File

@@ -1,3 +1,22 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Generic Human Interface Device API.
It is currently a partial pure-Python implementation of the native HID API
@@ -7,10 +26,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, Monitor as _Monitor, Device as _Device
native_implementation = 'udev'
@@ -31,6 +52,7 @@ DeviceInfo = namedtuple('DeviceInfo', [
])
del namedtuple
#
# exposed API
# docstrings mostly copied from hidapi.h
@@ -54,7 +76,118 @@ def exit():
return True
def enumerate(vendor_id=None, product_id=None, interface_number=None):
def _match(action, device, vendor_id=None, product_id=None, interface_number=None, hid_driver=None):
usb_device = device.find_parent('usb', 'usb_device')
# print ("* parent", action, device, "usb:", usb_device)
if not usb_device:
return
vid = usb_device.get('ID_VENDOR_ID')
pid = usb_device.get('ID_MODEL_ID')
if not ((vendor_id is None or vendor_id == int(vid, 16)) and
(product_id is None or product_id == int(pid, 16))):
return
if action == 'add':
hid_device = device.find_parent('hid')
if not hid_device:
return
hid_driver_name = hid_device.get('DRIVER')
# print ("** found hid", action, device, "hid:", hid_device, hid_driver_name)
if hid_driver:
if isinstance(hid_driver, tuple):
if hid_driver_name not in hid_driver:
return
elif hid_driver_name != hid_driver:
return
intf_device = device.find_parent('usb', 'usb_interface')
# print ("*** usb interface", action, device, "usb_interface:", intf_device)
if interface_number is None:
usb_interface = None if intf_device is None else intf_device.attributes.asint('bInterfaceNumber')
else:
usb_interface = None if intf_device is None else intf_device.attributes.asint('bInterfaceNumber')
if usb_interface is None or interface_number != usb_interface:
return
attrs = usb_device.attributes
d_info = DeviceInfo(path=device.device_node,
vendor_id=vid[-4:],
product_id=pid[-4:],
serial=hid_device.get('HID_UNIQ'),
release=attrs.get('bcdDevice'),
manufacturer=attrs.get('manufacturer'),
product=attrs.get('product'),
interface=usb_interface,
driver=hid_driver_name)
return d_info
elif action == 'remove':
# print (dict(device), dict(usb_device))
d_info = DeviceInfo(path=device.device_node,
vendor_id=vid[-4:],
product_id=pid[-4:],
serial=None,
release=None,
manufacturer=None,
product=None,
interface=None,
driver=None)
return d_info
def monitor_glib(callback, *device_filters):
from gi.repository import GLib
c = _Context()
# already existing devices
# for device in c.list_devices(subsystem='hidraw'):
# # print (device, dict(device), dict(device.attributes))
# for filter in device_filters:
# d_info = _match('add', device, *filter)
# if d_info:
# GLib.idle_add(callback, 'add', d_info)
# break
m = _Monitor.from_netlink(c)
m.filter_by(subsystem='hidraw')
def _process_udev_event(monitor, condition, cb, filters):
if condition == GLib.IO_IN:
event = monitor.receive_device()
if event:
action, device = event
# print ("***", action, device)
if action == 'add':
for filter in filters:
d_info = _match(action, device, *filter)
if d_info:
GLib.idle_add(cb, action, d_info)
break
elif action == 'remove':
# the GLib notification does _not_ match!
pass
return True
try:
# io_add_watch_full may not be available...
GLib.io_add_watch_full(m, GLib.PRIORITY_LOW, GLib.IO_IN, _process_udev_event, callback, device_filters)
# print ("did io_add_watch_full")
except AttributeError:
try:
# and the priority parameter appeared later in the API
GLib.io_add_watch(m, GLib.PRIORITY_LOW, GLib.IO_IN, _process_udev_event, callback, device_filters)
# print ("did io_add_watch with priority")
except:
GLib.io_add_watch(m, GLib.IO_IN, _process_udev_event, callback, device_filters)
# print ("did io_add_watch")
m.start()
def enumerate(vendor_id=None, product_id=None, interface_number=None, hid_driver=None):
"""Enumerate the HID Devices.
List all the HID devices attached to the system, optionally filtering by
@@ -63,45 +196,9 @@ def enumerate(vendor_id=None, product_id=None, interface_number=None):
:returns: a list of matching ``DeviceInfo`` tuples.
"""
for dev in _Context().list_devices(subsystem='hidraw'):
hid_dev = dev.find_parent('hid')
if not hid_dev:
continue
assert 'HID_ID' in hid_dev
bus, vid, pid = hid_dev['HID_ID'].split(':')
if vendor_id is not None and vendor_id != int(vid, 16):
continue
if product_id is not None and product_id != int(pid, 16):
continue
if bus == '0003': # USB
intf_dev = dev.find_parent('usb', 'usb_interface')
if not intf_dev:
continue
interface = intf_dev.attributes.asint('bInterfaceNumber')
if interface_number is not None and interface_number != interface:
continue
serial = hid_dev['HID_UNIQ'] if 'HID_UNIQ' in hid_dev else None
usb_dev = dev.find_parent('usb', 'usb_device')
assert usb_dev
attrs = usb_dev.attributes
d_info = DeviceInfo(path=dev.device_node,
vendor_id=vid[-4:],
product_id=pid[-4:],
serial=serial,
release=attrs['bcdDevice'],
manufacturer=attrs['manufacturer'],
product=attrs['product'],
interface=interface,
driver=hid_dev['DRIVER'])
yield d_info
elif bus == '0005': # BLUETOOTH
# TODO
pass
dev_info = _match('add', dev, vendor_id, product_id, interface_number, hid_driver)
if dev_info:
yield dev_info
def open(vendor_id, product_id, serial=None):
@@ -124,6 +221,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 +231,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 +255,13 @@ 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
assert data
assert isinstance(data, bytes), (repr(data), type(data))
bytes_written = _os.write(device_handle, data)
if bytes_written != len(data):
raise IOError(_errno.EIO, 'written %d bytes out of expected %d' % (bytes_written, len(data)))
def read(device_handle, bytes_count, timeout_ms=-1):
@@ -181,15 +280,22 @@ 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 IOError(_errno.EIO, '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
assert isinstance(data, bytes), (repr(data), type(data))
return data
else:
return b''
except OSError:
pass
_DEVICE_STRINGS = {
@@ -236,13 +342,14 @@ 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:
hid_dev = dev.find_parent('hid')
if hid_dev:
assert 'HID_ID' in hid_dev
bus, _, _ = hid_dev['HID_ID'].split(':')
bus, _ignore, _ignore = hid_dev['HID_ID'].split(':')
if bus == '0003': # USB
usb_dev = dev.find_parent('usb', 'usb_device')

View File

@@ -1,5 +0,0 @@
#
__author__ = "Daniel Pavel"
__license__ = "GPL"
__version__ = "0.5"

View File

@@ -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)

View File

@@ -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'),
}

View File

@@ -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)

View File

@@ -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()

View File

@@ -1,35 +0,0 @@
"""Low-level interface for devices connected through a Logitech Universal
Receiver (UR).
Uses the HID api exposed through hidapi.py, a Python thin layer over a native
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/
"""
import logging
if logging.root.level > logging.DEBUG:
log = logging.getLogger('LUR')
log.addHandler(logging.NullHandler())
log.propagate = 0
del logging
from .constants import *
from .exceptions import *
from .api import *

View File

@@ -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

View File

@@ -1,348 +0,0 @@
#
# Base low-level functions used by the API proper.
# Unlikely to be used directly unless you're expanding the API.
#
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 .constants import ERROR_NAME
from .exceptions import (NoReceiver as _NoReceiver,
FeatureCallError as _FeatureCallError)
from logging import getLogger
_log = getLogger('LUR').getChild('base')
del getLogger
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
"""Default timeout on read (in ms)."""
DEFAULT_TIMEOUT = 1500
#
#
#
def _logdebug_hook(reply_code, devnumber, data):
"""Default unhandled hook, logs the reply as DEBUG."""
_log.warn("UNHANDLED [%02X %02X %s %s] (%s)", reply_code, devnumber, _hex(data[:2]), _hex(data[2:]), repr(data))
"""The function that will be called on unhandled incoming events.
The hook must be a function with the signature: ``_(int, int, str)``, where
the parameters are: (reply_code, devnumber, data).
This hook will only be called by the request() function, when it receives
replies that do not match the requested feature call. As such, it is not
suitable for intercepting broadcast events from the device (e.g. special
keys being pressed, battery charge events, etc), at least not in a timely
manner. However, these events *may* be delivered here if they happen while
doing a feature call to the device.
The default implementation logs the unhandled reply as DEBUG.
"""
unhandled_hook = _logdebug_hook
#
#
#
def list_receiver_devices():
"""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':
yield d
_COUNT_DEVICES_REQUEST = b'\x10\xFF\x81\x00\x00\x00\x00'
def try_open(path):
"""Checks if the given Linux device path points to the right UR device.
:param path: the Linux device path.
The UR physical device may expose multiple linux devices with the same
interface, so we have to check for the right one. At this moment the only
way to distinguish betheen them is to do a test ping on an invalid
(attached) device number (i.e., 0), expecting a 'ping failed' reply.
: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)
def open():
"""Opens the first Logitech Unifying Receiver found attached to the machine.
: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)
if handle:
return handle
def close(handle):
"""Closes a HID device handle."""
if handle:
try:
_hid.close(handle)
# _log.info("closed receiver handle %X", handle)
return True
except:
_log.exception("closing receiver handle %X", handle)
return False
def write(handle, devnumber, data):
"""Writes some data 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.
: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)
close(handle)
raise _NoReceiver
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.
: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)
close(handle)
raise _NoReceiver
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])
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[]")
_MAX_READ_TIMES = 3
request_context = None
from collections import namedtuple
_DEFAULT_REQUEST_CONTEXT_CLASS = namedtuple('_DEFAULT_REQUEST_CONTEXT_CLASS', ['write', 'read', 'unhandled_hook'])
_DEFAULT_REQUEST_CONTEXT = _DEFAULT_REQUEST_CONTEXT_CLASS(write=write, read=read, unhandled_hook=unhandled_hook)
del namedtuple
def request(handle, devnumber, feature_index_function, params=b'', features=None):
"""Makes a feature call to a device and waits for a matching reply.
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.
: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 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.
"""
if type(params) == int:
params = _pack('!B', params)
# _log.debug("device %d request {%s} params [%s]", devnumber, _hex(feature_index_function), _hex(params))
if len(feature_index_function) != 2:
raise ValueError('invalid feature_index_function {%s}: it must be a two-byte string' % _hex(feature_index_function))
if request_context is None or handle != request_context.handle:
context = _DEFAULT_REQUEST_CONTEXT
_unhandled = unhandled_hook
else:
context = request_context
_unhandled = getattr(context, 'unhandled_hook')
context.write(handle, devnumber, feature_index_function + 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
if not reply:
# keep waiting...
continue
reply_code, reply_devnumber, reply_data = reply
if reply_devnumber != devnumber:
# this message not for the device we're interested in
# _l.log(_LOG_LEVEL, "device %d request got reply for unexpected device %d: [%s]", devnumber, reply_devnumber, _hex(reply_data))
# worst case scenario, this is a reply for a concurrent request
# on this receiver
if _unhandled:
_unhandled(reply_code, reply_devnumber, reply_data)
continue
if reply_code == 0x10 and reply_data[: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 reply_code == 0x10 and reply_data[:1] == b'\x8F':
# device not present
_log.debug("device %d request failed: [%s]", devnumber, _hex(reply_data))
return None
if reply_code == 0x11 and reply_data[0] == b'\xFF' and reply_data[1:3] == feature_index_function:
# the feature call returned with an error
error_code = ord(reply_data[3])
_log.warn("device %d request feature call error %d = %s: %s", devnumber, error_code, ERROR_NAME[error_code], _hex(reply_data))
feature_index = ord(feature_index_function[:1])
feature_function = feature_index_function[1:2]
feature = None if features is None else features[feature_index] if feature_index < len(features) else None
raise _FeatureCallError(devnumber, feature, feature_index, feature_function, error_code, reply_data)
if reply_code == 0x11 and reply_data[:2] == feature_index_function:
# a matching reply
# _log.debug("device %d matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:]))
return reply_data[2:]
if reply_code == 0x10 and devnumber == 0xFF and reply_data[:2] == feature_index_function:
# direct calls to the receiver (device 0xFF) may also return successfully with reply code 0x10
# _log.debug("device %d matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:]))
return reply_data[2:]
# _log.debug("device %d unmatched reply {%s} (expected {%s})", devnumber, _hex(reply_data[:2]), _hex(feature_index_function))
if _unhandled:
_unhandled(reply_code, reply_devnumber, reply_data)
def ping(handle, devnumber):
"""Check if a device is connected to the UR.
:returns: The HID protocol supported by the device, as a floating point number, if the device is active.
"""
if request_context is None or handle != request_context.handle:
context = _DEFAULT_REQUEST_CONTEXT
_unhandled = unhandled_hook
else:
context = request_context
_unhandled = getattr(context, 'unhandled_hook')
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
if not reply:
# keep waiting...
continue
reply_code, reply_devnumber, reply_data = reply
if reply_devnumber != devnumber:
# this message not for the device we're interested in
# _l.log(_LOG_LEVEL, "device %d request got reply for unexpected device %d: [%s]", devnumber, reply_devnumber, _hex(reply_data))
# worst case scenario, this is a reply for a concurrent request
# on this receiver
if _unhandled:
_unhandled(reply_code, reply_devnumber, reply_data)
continue
if reply_code == 0x11 and reply_data[:2] == b'\x00\x10' and reply_data[4:5] == b'\xAA':
major, minor = _unpack('!BB', reply_data[2:4])
return major + minor / 10.0
if reply_code == 0x10 and reply_data == b'\x8F\x00\x10\x01\x00':
return 1.0
if reply_code == 0x10 and reply_data[:3] == b'\x8F\x00\x10':
return None
_log.warn("don't know how to interpret ping reply %s", reply)

View File

@@ -1,48 +0,0 @@
#
# Some common functions and types.
#
from collections import namedtuple
from binascii import hexlify as _hexlify
_hex = lambda d: _hexlify(d).decode('ascii').upper()
class FallbackDict(dict):
def __init__(self, fallback_function=lambda x: None, *args, **kwargs):
super(FallbackDict, self).__init__(*args, **kwargs)
self.fallback = fallback_function
def __getitem__(self, key):
try:
return super(FallbackDict, self).__getitem__(key)
except KeyError:
return self.fallback(key)
def list2dict(values_list):
return dict(zip(range(0, len(values_list)), values_list))
"""Firmware information."""
FirmwareInfo = namedtuple('FirmwareInfo', [
'level',
'kind',
'name',
'version',
'extras'])
"""Reprogrammable keys informations."""
ReprogrammableKeyInfo = namedtuple('ReprogrammableKeyInfo', [
'index',
'id',
'name',
'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

View File

@@ -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

View File

@@ -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

View File

@@ -1,141 +0,0 @@
#
#
#
from threading import Thread as _Thread
# from time import sleep as _sleep
from . import base as _base
from .exceptions import NoReceiver as _NoReceiver
from .common import Packet as _Packet
# for both Python 2 and 3
try:
from Queue import Queue as _Queue
except ImportError:
from queue import Queue as _Queue
from logging import getLogger
_log = getLogger('LUR').getChild('listener')
del getLogger
_READ_EVENT_TIMEOUT = int(_base.DEFAULT_TIMEOUT / 2) # ms
def _event_dispatch(listener, callback):
while listener._active: # or not listener._events.empty():
try:
event = listener._events.get(True, _READ_EVENT_TIMEOUT * 10)
except:
continue
# _log.debug("delivering event %s", event)
try:
callback(event)
except:
_log.exception("callback for %s", event)
class EventsListener(_Thread):
"""Listener thread for events from the Unifying Receiver.
Incoming packets will be passed to the callback function in sequence, by a
separate thread.
"""
def __init__(self, receiver_handle, events_callback):
super(EventsListener, self).__init__(group='Unifying Receiver', name=self.__class__.__name__)
self.daemon = True
self._active = False
self._handle = receiver_handle
self._tasks = _Queue(1)
self._backup_unhandled_hook = _base.unhandled_hook
_base.unhandled_hook = self.unhandled_hook
self._events = _Queue(32)
self._dispatcher = _Thread(group='Unifying Receiver',
name=self.__class__.__name__ + '-dispatch',
target=_event_dispatch, args=(self, events_callback))
self._dispatcher.daemon = True
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()
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
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])
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)
_base.request_context = None
handle, self._handle = self._handle, 0
_base.close(handle)
_log.debug("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()
@property
def handle(self):
return self._handle
def write(self, handle, devnumber, data):
assert handle == self._handle
# _log.debug("write %02X %s", devnumber, _base._hex(data))
task = [devnumber, data, None]
self._tasks.put(task)
_base.write(self._handle, devnumber, data)
# _log.debug("task queued %s", task)
def read(self, handle, timeout=_base.DEFAULT_TIMEOUT):
assert handle == self._handle
# _log.debug("read %d", timeout)
assert not self._tasks.empty()
self._tasks.join()
task = self._tasks.get(False)
# _log.debug("task ready %s", task)
return task[-1]
def unhandled_hook(self, reply_code, devnumber, data):
event = _Packet(reply_code, devnumber, data)
_log.info("queueing unhandled event %s", event)
self._events.put(event)
def __bool__(self):
return bool(self._active and self._handle)
__nonzero__ = __bool__

View File

@@ -1,3 +0,0 @@
#
# Tests for the logitech.unifying_receiver package.
#

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -0,0 +1,56 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Low-level interface for devices connected through a Logitech Universal
Receiver (UR).
Uses the HID api exposed through hidapi.py, a Python thin layer over a native
implementation.
Incomplete. Based on a bit of documentation, trial-and-error, and guesswork.
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
_DEBUG = logging.DEBUG
_log = logging.getLogger(__name__)
_log.setLevel(logging.root.level)
# if logging.root.level > logging.DEBUG:
# _log.addHandler(logging.NullHandler())
# _log.propagate = 0
del logging
__version__ = '0.9'
from .common import strhex
from .base import NoReceiver, NoSuchDevice, DeviceUnreachable
from .receiver import Receiver, PairedDevice
from .hidpp20 import FeatureNotSupported, FeatureCallError
from . import listener
from . import status

View File

@@ -0,0 +1,482 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
# Base low-level functions used by the API proper.
# 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 random import getrandbits as _random_bits
from logging import getLogger, DEBUG as _DEBUG
_log = getLogger(__name__)
del getLogger
from .common import strhex as _strhex, KwException as _KwException, pack as _pack
from . import hidpp10 as _hidpp10
from . import hidpp20 as _hidpp20
import hidapi as _hid
#
#
#
_SHORT_MESSAGE_SIZE = 7
_LONG_MESSAGE_SIZE = 20
_MEDIUM_MESSAGE_SIZE = 15
_MAX_READ_SIZE = 32
"""Default timeout on read (in seconds)."""
DEFAULT_TIMEOUT = 4
# the receiver itself should reply very fast, within 500ms
_RECEIVER_REQUEST_TIMEOUT = 0.9
# devices may reply a lot slower, as the call has to go wireless to them and come back
_DEVICE_REQUEST_TIMEOUT = DEFAULT_TIMEOUT
# when pinging, be extra patient
_PING_TIMEOUT = DEFAULT_TIMEOUT * 2
#
# 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
#
#
#
from .base_usb import ALL as _RECEIVER_USB_IDS
def receivers():
"""List all the Linux devices exposed by the UR attached to the machine."""
for receiver_usb_id in _RECEIVER_USB_IDS:
for d in _hid.enumerate(*receiver_usb_id):
yield d
def notify_on_receivers_glib(callback):
"""Watch for matching devices and notifies the callback on the GLib thread."""
_hid.monitor_glib(callback, *_RECEIVER_USB_IDS)
#
#
#
def open_path(path):
"""Checks if the given Linux device path points to the right UR device.
:param path: the Linux device path.
The UR physical device may expose multiple linux devices with the same
interface, so we have to check for the right one. At this moment the only
way to distinguish betheen them is to do a test ping on an invalid
(attached) device number (i.e., 0), expecting a 'ping failed' reply.
:returns: an open receiver handle if this is the right Linux device, or
``None``.
"""
return _hid.open_path(path)
def open():
"""Opens the first Logitech Unifying Receiver found attached to the machine.
:returns: An open file handle for the found receiver, or ``None``.
"""
for rawdevice in receivers():
handle = open_path(rawdevice.path)
if handle:
return handle
def close(handle):
"""Closes a HID device handle."""
if handle:
try:
if isinstance(handle, int):
_hid.close(handle)
else:
handle.close()
# _log.info("closed receiver handle %r", handle)
return True
except:
# _log.exception("closing receiver handle %r", handle)
pass
return False
def write(handle, devnumber, data):
"""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 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.
"""
# the data is padded to either 5 or 18 bytes
assert data is not None
assert isinstance(data, bytes), (repr(data), type(data))
if len(data) > _SHORT_MESSAGE_SIZE - 2 or data[:1] == b'\x82':
wdata = _pack('!BB18s', 0x11, devnumber, data)
else:
wdata = _pack('!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(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 open handle to the receiver
:param: timeout how long to wait for a reply, in seconds
: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.
"""
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:
# convert timeout to milliseconds, the hidapi expects it
timeout = int(timeout * 1000)
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(reason=reason)
if data:
assert isinstance(data, bytes), (repr(data), type(data))
report_id = ord(data[:1])
assert ((report_id & 0xF0 == 0) or
(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)), \
"unexpected message size: report_id %02X message %s" % (report_id, _strhex(data))
if report_id & 0xF0 == 0x00:
if _log.isEnabledFor(_DEBUG):
_log.debug("(%s) => r[%02X %s] ignoring unknown report", handle, report_id, _strhex(data[1:]))
return
devnumber = ord(data[1:2])
if _log.isEnabledFor(_DEBUG):
_log.debug("(%s) => r[%02X %02X %s %s]", handle, report_id, devnumber, _strhex(data[2:4]), _strhex(data[4:]))
return report_id, devnumber, data[2:]
#
#
#
def _skip_incoming(handle, ihandle, notifications_hook):
"""Read anything already in the input buffer.
Used by request() and ping() before their write.
"""
while True:
try:
# read whatever is already in the buffer, if any
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:
assert isinstance(data, bytes), (repr(data), type(data))
report_id = ord(data[:1])
if _log.isEnabledFor(_DEBUG):
assert ((report_id & 0xF0 == 0) or
(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)), \
"unexpected message size: report_id %02X message %s" % (report_id, _strhex(data))
if notifications_hook and report_id & 0xF0:
n = make_notification(ord(data[1:2]), data[2:])
if n:
notifications_hook(n)
else:
# nothing in the input buffer, we're done
return
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:
# this is either a HID++1.0 register r/w, or an error reply
return
address = ord(data[1:2])
if (
# standard HID++ 1.0 notification, SubId may be 0x40 - 0x7F
(sub_id >= 0x40)
or
# custom HID++1.0 battery events, where SubId is 0x07/0x0D
(sub_id in (0x07, 0x0D) and len(data) == 5 and data[4:5] == b'\x00')
or
# custom HID++1.0 illumination event, where SubId is 0x17
(sub_id == 0x17 and len(data) == 5)
or
# HID++ 2.0 feature notifications have the SoftwareID 0
(address & 0x0F == 0x00)
):
return _HIDPP_Notification(devnumber, sub_id, address, data[2:])
from collections import namedtuple
_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, request_id, *params):
"""Makes a feature call to a device and waits for a matching reply.
This function will wait for a matching reply indefinitely.
:param handle: an open UR handle.
:param devnumber: attached device number.
:param request_id: a 16-bit integer.
:param params: parameters for the feature call, 3 to 16 bytes.
:returns: the reply data, or ``None`` if some error occured.
"""
# import inspect as _inspect
# print ('\n '.join(str(s) for s in _inspect.stack()))
assert isinstance(request_id, int)
if devnumber != 0xFF and request_id < 0x8000:
# 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.
# This only applies to peripheral requests, ofc.
request_id = (request_id & 0xFFF0) | 0x08 | _random_bits(3)
timeout = _RECEIVER_REQUEST_TIMEOUT if devnumber == 0xFF else _DEVICE_REQUEST_TIMEOUT
# be extra patient on long register read
if request_id & 0xFF00 == 0x8300:
timeout *= 2
if params:
params = b''.join(_pack('B', p) if isinstance(p, int) else p for p in params)
else:
params = b''
# if _log.isEnabledFor(_DEBUG):
# _log.debug("(%s) device %d request_id {%04X} params [%s]", handle, devnumber, request_id, _strhex(params))
request_data = _pack('!H', request_id) + params
ihandle = int(handle)
notifications_hook = getattr(handle, 'notifications_hook', None)
_skip_incoming(handle, ihandle, notifications_hook)
write(ihandle, devnumber, request_data)
# we consider timeout from this point
request_started = _timestamp()
delta = 0
while delta < timeout:
reply = _read(handle, timeout)
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 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 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 _log.isEnabledFor(_DEBUG):
_log.debug("(%s) device 0x%02X error on request {%04X}: %d = %s",
handle, devnumber, request_id, error, _hidpp10.ERROR[error])
return
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_data[:2] == request_data[:2]:
if request_id & 0xFE00 == 0x8200:
# long registry r/w should return a long reply
assert report_id == 0x11
elif request_id & 0xFE00 == 0x8000:
# short registry r/w should return a short reply
assert report_id == 0x10
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:]
else:
# a reply was received, but did not match our request in any way
# reset the timeout starting point
request_started = _timestamp()
if notifications_hook:
n = make_notification(reply_devnumber, reply_data)
if n:
notifications_hook(n)
# elif _log.isEnabledFor(_DEBUG):
# _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))
# elif _log.isEnabledFor(_DEBUG):
# _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))
delta = _timestamp() - request_started
# if _log.isEnabledFor(_DEBUG):
# _log.debug("(%s) still waiting for reply, delta %f", handle, delta)
_log.warn("timeout (%0.2f/%0.2f) on device %d request {%04X} params [%s]",
delta, timeout, devnumber, request_id, _strhex(params))
# raise DeviceUnreachable(number=devnumber, request=request_id)
def ping(handle, devnumber):
"""Check if a device is connected to the receiver.
:returns: The HID protocol supported by the device, as a floating point number, if the device is active.
"""
if _log.isEnabledFor(_DEBUG):
_log.debug("(%s) pinging device %d", handle, devnumber)
# import inspect as _inspect
# print ('\n '.join(str(s) for s in _inspect.stack()))
assert devnumber != 0xFF
assert devnumber > 0x00
assert devnumber < 0x0F
# 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('!HBBB', request_id, 0, 0, _random_bits(8))
ihandle = int(handle)
notifications_hook = getattr(handle, 'notifications_hook', None)
_skip_incoming(handle, ihandle, notifications_hook)
write(ihandle, devnumber, request_data)
# we consider timeout from this point
request_started = _timestamp()
delta = 0
while delta < _PING_TIMEOUT:
reply = _read(handle, _PING_TIMEOUT)
if reply:
report_id, reply_devnumber, reply_data = reply
if reply_devnumber == devnumber:
if reply_data[:2] == request_data[:2] and reply_data[4:5] == request_data[-1:]:
# HID++ 2.0+ device, currently connected
return ord(reply_data[2:3]) + ord(reply_data[3:4]) / 10.0
if report_id == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == request_data[:2]:
assert reply_data[-1:] == b'\x00'
error = ord(reply_data[3:4])
if error == _hidpp10.ERROR.invalid_SubID__command: # a valid reply from a HID++ 1.0 device
return 1.0
if error == _hidpp10.ERROR.resource_error: # device unreachable
return
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)
if notifications_hook:
n = make_notification(reply_devnumber, reply_data)
if n:
notifications_hook(n)
# elif _log.isEnabledFor(_DEBUG):
# _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))
delta = _timestamp() - request_started
_log.warn("(%s) timeout (%0.2f/%0.2f) on device %d ping", handle, delta, _PING_TIMEOUT, devnumber)
# raise DeviceUnreachable(number=devnumber, request=request_id)

View File

@@ -0,0 +1,63 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
# USB ids of Logitech wireless receivers.
# Only receivers supporting the HID++ protocol can go in here.
from __future__ import absolute_import, division, print_function, unicode_literals
_UNIFYING_DRIVER = 'logitech-djreceiver'
_GENERIC_DRIVER = ('hid-generic', 'generic-usb')
# each tuple contains (vendor_id, product_id, usb interface number, hid driver)
# standard Unifying receivers (marked with the orange Unifying logo)
UNIFYING_RECEIVER = (0x046d, 0xc52b, 2, _UNIFYING_DRIVER)
UNIFYING_RECEIVER_2 = (0x046d, 0xc532, 2, _UNIFYING_DRIVER)
# Nano receviers that support the Unifying protocol
NANO_RECEIVER_ADVANCED = (0x046d, 0xc52f, 1, _GENERIC_DRIVER)
# Nano receivers that don't support the Unifying protocol
NANO_RECEIVER_C517 = (0x046d, 0xc517, 1, _GENERIC_DRIVER)
NANO_RECEIVER_C518 = (0x046d, 0xc518, 1, _GENERIC_DRIVER)
NANO_RECEIVER_C51A = (0x046d, 0xc51a, 1, _GENERIC_DRIVER)
NANO_RECEIVER_C51B = (0x046d, 0xc51b, 1, _GENERIC_DRIVER)
NANO_RECEIVER_C521 = (0x046d, 0xc521, 1, _GENERIC_DRIVER)
NANO_RECEIVER_C525 = (0x046d, 0xc525, 1, _GENERIC_DRIVER)
NANO_RECEIVER_C526 = (0x046d, 0xc526, 1, _GENERIC_DRIVER)
ALL = (
UNIFYING_RECEIVER,
UNIFYING_RECEIVER_2,
NANO_RECEIVER_ADVANCED,
NANO_RECEIVER_C517,
NANO_RECEIVER_C518,
NANO_RECEIVER_C51A,
NANO_RECEIVER_C51B,
NANO_RECEIVER_C521,
NANO_RECEIVER_C525,
NANO_RECEIVER_C526,
)

View File

@@ -0,0 +1,277 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
# Some common functions and types.
from __future__ import absolute_import, division, print_function, unicode_literals
from binascii import hexlify as _hexlify
from struct import pack, unpack
try:
unicode
# if Python2, unicode_literals will mess our first (un)pack() argument
_pack_str = pack
_unpack_str = unpack
pack = lambda x, *args: _pack_str(str(x), *args)
unpack = lambda x, *args: _unpack_str(str(x), *args)
is_string = lambda d: isinstance(d, unicode) or isinstance(d, str)
# no easy way to distinguish between b'' and '' :(
# or (isinstance(d, str) \
# and not any((chr(k) in d for k in range(0x00, 0x1F))) \
# and not any((chr(k) in d for k in range(0x80, 0xFF))) \
# )
except:
# this is certanly Python 3
# In Py3, unicode and str are equal (the unicode object does not exist)
is_string = lambda d: isinstance(d, str)
#
#
#
class NamedInt(int):
"""An reqular Python integer with an attached name.
Caution: comparison with strings will also match this NamedInt's name
(case-insensitive)."""
def __new__(cls, value, name):
assert is_string(name)
obj = int.__new__(cls, value)
obj.name = str(name)
return obj
def bytes(self, count=2):
return int2bytes(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 is_string(other):
return self.name.lower() == other.lower()
# this should catch comparisons with bytes in Py3
if other is not None:
raise TypeError('Unsupported type ' + str(type(other)))
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 is_string(n):
raise TypeError("expected (unicode) string, got " + str(type(n)))
return n.replace('__', '/').replace('_', ' ')
# print (repr(kwargs))
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}
# assert len(values) == len(self._indexed), "(%d) %r\n=> (%d) %r" % (len(values), values, len(self._indexed), self._indexed)
self._fallback = None
@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 isinstance(index, int):
value = NamedInt(index, self._fallback(index))
self._indexed[index] = value
self._values = sorted(self._values + [value])
return value
elif is_string(index):
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 is_string(name):
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
elif is_string(value):
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):
assert x is not None
"""Produce a hex-string representation of a sequence of bytes."""
return _hexlify(x).decode('ascii').upper()
def bytes2int(x):
"""Convert a bytes string to an int.
The bytes are assumed to be in most-significant-first order.
"""
assert isinstance(x, bytes)
assert len(x) < 9
qx = (b'\x00' * 8) + x
result, = unpack('!Q', qx[-8:])
# assert x == int2bytes(result, len(x))
return result
def int2bytes(x, count=None):
"""Convert an int to a bytes representation.
The bytes are ordered in most-significant-first order.
If 'count' is not given, the necessary number of bytes is computed.
"""
assert isinstance(x, int)
result = pack('!Q', x)
assert isinstance(result, bytes)
# assert x == bytes2int(result)
if count is None:
return result.lstrip(b'\x00')
assert isinstance(count, int)
assert count > 0
assert x.bit_length() <= count * 8
return result[-count:]
class KwException(Exception):
"""An exception that remembers all arguments passed to the constructor.
They can be later accessed by simple member access.
"""
def __init__(self, **kwargs):
super(KwException, self).__init__(kwargs)
def __getattr__(self, k):
try:
return super(KwException, self).__getattr__(k)
except AttributeError:
return self.args[0][k]
from collections import namedtuple
"""Firmware information."""
FirmwareInfo = namedtuple('FirmwareInfo', [
'kind',
'name',
'version',
'extras'])
"""Reprogrammable keys informations."""
ReprogrammableKeyInfo = namedtuple('ReprogrammableKeyInfo', [
'index',
'key',
'task',
'flags'])
del namedtuple

View File

@@ -0,0 +1,273 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import absolute_import, division, print_function, unicode_literals
from . import hidpp10 as _hidpp10
from .common import NamedInts as _NamedInts
from .settings_templates import RegisterSettings as _RS, FeatureSettings as _FS
_R = _hidpp10.REGISTERS
#
#
#
from collections import namedtuple
_DeviceDescriptor = namedtuple('_DeviceDescriptor',
('name', 'kind', 'wpid', 'codename', 'protocol', 'registers', 'settings'))
del namedtuple
DEVICES = {}
def _D(name, codename=None, kind=None, wpid=None, protocol=None, registers=None, settings=None):
assert name
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, 'descriptor for %s does not have kind set' % name
# heuristic: the codename is the last word in the device name
if codename is None and ' ' in name:
codename = name.split(' ')[-1]
assert codename is not None, 'descriptor for %s does not have codename set' % name
if protocol is not None:
# ? 2.0 devices should not have any registers
if protocol < 2.0:
assert settings is None or all(s._rw.kind == 1 for s in settings)
else:
assert registers is None
assert settings is None or all(s._rw.kind == 2 for s in settings)
if wpid:
for w in wpid if isinstance(wpid, tuple) else (wpid, ):
if protocol > 1.0:
assert w[0:1] == '4', name + ' has protocol ' + protocol + ', wpid ' + w
else:
if w[0:1] == '1':
assert kind == _hidpp10.DEVICE_KIND.mouse, name + ' has protocol ' + protocol + ', wpid ' + w
elif w[0:1] == '2':
assert kind == _hidpp10.DEVICE_KIND.keyboard, name + ' has protocol ' + protocol + ', wpid ' + w
device_descriptor = _DeviceDescriptor(name=name, kind=kind,
wpid=wpid, codename=codename, protocol=protocol,
registers=registers, settings=settings)
assert codename not in DEVICES, 'duplicate codename in device descriptors: %s' % (DEVICES[codename], )
DEVICES[codename] = device_descriptor
if wpid:
if not isinstance(wpid, tuple):
wpid = (wpid, )
for w in wpid:
assert w not in DEVICES, 'duplicate wpid in device descriptors: %s' % (DEVICES[w], )
DEVICES[w] = device_descriptor
#
#
#
_PERFORMANCE_MX_DPIS = _NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 100))
#
#
#
# Some HID++1.0 registers and HID++2.0 features can be discovered at run-time,
# so they are not specified here.
#
# For known registers, however, please do specify them here -- avoids
# unnecessary communication with the device and makes it easier to make certain
# decisions when querying the device's state.
#
# Specify a negative value to blacklist a certain register for a device.
#
# Usually, state registers (battery, leds, some features, etc) are only used by
# HID++ 1.0 devices, while HID++ 2.0 devices use features for the same
# functionalities. This is a rule that's been discovered by trial-and-error,
# so it may change in the future.
# Well-known registers (in hex):
# * 00 - notification flags (all devices)
# 01 - mice: smooth scrolling
# 07 - battery status
# 09 - keyboards: FN swap (if it has the FN key)
# 0D - battery charge
# a device may have either the 07 or 0D register available;
# no known device uses both
# 51 - leds
# 63 - mice: DPI
# * F1 - firmware info
# Some registers appear to be universally supported, no matter the HID++ version
# (marked with *). The rest may or may not be supported, and their values may or
# may not mean the same thing across different devices.
# The 'codename' and 'kind' fields are usually guessed from the device name,
# but in some cases (like the Logitech Cube) that heuristic fails and they have
# to be specified.
#
# The 'protocol' and 'wpid' fields are optional (they can be discovered at
# runtime), but specifying them here speeds up device discovery and reduces the
# USB traffic Solaar has to do to fully identify peripherals.
# Same goes for HID++ 2.0 feature settings (like _feature_fn_swap).
#
# The 'registers' field indicates read-only registers, specifying a state. These
# are valid (AFAIK) only to HID++ 1.0 devices.
# The 'settings' field indicates a read/write register; based on them Solaar
# generates, at runtime, the settings controls in the device panel. HID++ 1.0
# devices may only have register-based settings; HID++ 2.0 devices may only have
# feature-based settings.
# Keyboards
_D('Wireless Keyboard K230', protocol=2.0, wpid='400D')
_D('Wireless Keyboard K270')
_D('Wireless Keyboard MK330')
_D('Wireless Keyboard K340')
_D('Wireless Keyboard K350', wpid='200A')
_D('Wireless Keyboard K360', protocol=2.0, wpid='4004',
settings=[
_FS.fn_swap()
],
)
_D('Wireless Touch Keyboard K400', protocol=2.0, wpid=('400E', '4024'),
settings=[
_FS.fn_swap()
],
)
_D('Wireless Keyboard MK520')
_D('Wireless Keyboard MK550')
_D('Wireless Keyboard MK700', protocol=1.0, wpid='2008',
registers=(_R.battery_status, ),
settings=[
_RS.fn_swap(),
],
)
_D('Wireless Solar Keyboard K750', protocol=2.0, wpid='4002',
settings=[
_FS.fn_swap()
],
)
_D('Wireless Illuminated Keyboard K800', protocol=1.0, wpid='2010',
registers=(_R.battery_status, _R.three_leds, ),
settings=[
_RS.fn_swap(),
_RS.hand_detection(),
],
)
# Mice
_D('Wireless Mouse M175')
_D('Wireless Mouse M185')
_D('Wireless Mouse M187', protocol=2.0, wpid='4019')
_D('Wireless Mouse M215', protocol=1.0, wpid='1020')
_D('Wireless Mouse M235')
_D('Wireless Mouse M305', protocol=1.0, wpid='101F',
registers=(_R.battery_status, ),
settings=[
_RS.side_scroll(),
],
)
_D('Wireless Mouse M310')
_D('Wireless Mouse M315')
_D('Wireless Mouse M317')
_D('Wireless Mouse M325')
_D('Wireless Mouse M345', protocol=2.0, wpid='4017')
_D('Wireless Mouse M505', codename='M505/B605', protocol=1.0, wpid='101D',
registers=(_R.battery_charge, ),
settings=[
_RS.smooth_scroll(),
_RS.side_scroll(),
],
)
_D('Wireless Mouse M510', protocol=1.0, wpid='1025',
registers=(_R.battery_status, ),
settings=[
_RS.smooth_scroll(),
_RS.side_scroll(),
],
)
_D('Couch Mouse M515', protocol=2.0, wpid='4007')
_D('Wireless Mouse M525', protocol=2.0, wpid='4013')
_D('Touch Mouse M600', protocol=2.0, wpid='401A')
_D('Marathon Mouse M705', protocol=1.0, wpid='101B',
registers=(_R.battery_charge, ),
settings=[
_RS.smooth_scroll(),
_RS.side_scroll(),
],
)
_D('Zone Touch Mouse T400')
_D('Touch Mouse T620', protocol=2.0)
_D('Logitech Cube', kind=_hidpp10.DEVICE_KIND.mouse, protocol=2.0)
_D('Anywhere Mouse MX', codename='Anywhere MX', protocol=1.0, wpid='1017',
registers=(_R.battery_charge, ),
settings=[
_RS.smooth_scroll(),
_RS.side_scroll(),
],
)
_D('Performance Mouse MX', codename='Performance MX', protocol=1.0, wpid='101A',
registers=(_R.battery_status, _R.three_leds, ),
settings=[
_RS.dpi(choices=_PERFORMANCE_MX_DPIS),
_RS.smooth_scroll(),
_RS.side_scroll(),
],
)
# Trackballs
_D('Wireless Trackball M570')
# Touchpads
_D('Wireless Rechargeable Touchpad T650', protocol=2.0, wpid='4101')
_D('Wireless Touchpad', codename='Wireless Touch', protocol=2.0, wpid='4011')
#
# Classic Nano peripherals (that don't support the Unifying protocol).
# A wpid is necessary to properly identify them.
#
_D('VX Nano Cordless Laser Mouse', codename='VX Nano', protocol=1.0, wpid='100F',
registers=(_R.battery_charge, ),
settings=[
_RS.smooth_scroll(),
_RS.side_scroll(),
],
)
_D('V450 Nano Cordless Laser Mouse', codename='V450 Nano', protocol=1.0, wpid='1011',
registers=(_R.battery_charge, ),
)
_D('V550 Nano Cordless Laser Mouse', codename='V550 Nano', protocol=1.0, wpid='1013',
registers=(_R.battery_charge, ),
settings=[
_RS.smooth_scroll(),
_RS.side_scroll(),
],
)

View File

@@ -0,0 +1,329 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import absolute_import, division, print_function, unicode_literals
from logging import getLogger # , DEBUG as _DEBUG
_log = getLogger(__name__)
del getLogger
from .common import (strhex as _strhex,
bytes2int as _bytes2int,
int2bytes as _int2bytes,
NamedInts as _NamedInts,
FirmwareInfo as _FirmwareInfo)
from .hidpp20 import FIRMWARE_KIND, BATTERY_STATUS
#
# Constants - most of them as defined by the official Logitech HID++ 1.0
# documentation, some of them guessed.
#
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)
# Some flags are used both by devices and receivers. The Logitech documentation
# mentions that the first and last (third) byte are used for devices while the
# second is used for the receiver. In practise, the second byte is also used for
# some device-specific notifications (keyboard illumination level). Do not
# simply set all notification bits if the software does not support it. For
# example, enabling keyboard_sleep_raw makes the Sleep key a no-operation unless
# the software is updated to handle that event.
# Observations:
# - wireless and software present were seen on receivers, reserved_r1b4 as well
# - the rest work only on devices as far as we can tell right now
# In the future would be useful to have separate enums for receiver and device notification flags,
# but right now we don't know enough.
NOTIFICATION_FLAG = _NamedInts(
battery_status= 0x100000, # send battery charge notifications (0x07 or 0x0D)
keyboard_sleep_raw= 0x020000, # system control keys such as Sleep
keyboard_multimedia_raw=0x010000, # consumer controls such as Mute and Calculator
# reserved_r1b4= 0x001000, # unknown, seen on a unifying receiver
software_present= 0x000800, # .. no idea
keyboard_illumination= 0x000200, # illumination brightness level changes (by pressing keys)
wireless= 0x000100, # notify when the device wireless goes on/off-line
)
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)
BATTERY_APPOX = _NamedInts(
empty = 0,
critical = 5,
low = 20,
good = 50,
full = 90)
"""Known registers.
Devices usually have a (small) sub-set of these. Some registers are only
applicable to certain device kinds (e.g. smooth_scroll only applies to mice."""
REGISTERS = _NamedInts(
# only apply to receivers
receiver_connection=0x02,
receiver_pairing=0xB2,
devices_activity=0x2B3,
receiver_info=0x2B5,
# only apply to devices
mouse_button_flags=0x01,
keyboard_hand_detection=0x01,
battery_status=0x07,
keyboard_fn_swap=0x09,
battery_charge=0x0D,
keyboard_illumination=0x17,
three_leds=0x51,
mouse_dpi=0x63,
# apply to both
notifications=0x00,
firmware=0xF1,
)
#
# functions
#
def read_register(device, register_number, *params):
assert device, 'tried to read register %02X from invalid device %s' % (register_number, device)
# support long registers by adding a 2 in front of the register number
request_id = 0x8100 | (int(register_number) & 0x2FF)
return device.request(request_id, *params)
def write_register(device, register_number, *value):
assert device, 'tried to write register %02X to invalid device %s' % (register_number, device)
# support long registers by adding a 2 in front of the register number
request_id = 0x8000 | (int(register_number) & 0x2FF)
return device.request(request_id, *value)
def get_battery(device):
assert device
assert device.kind is not None
if not device.online:
return
"""Reads a device's battery level, if provided by the HID++ 1.0 protocol."""
if device.protocol and device.protocol >= 2.0:
# let's just assume HID++ 2.0 devices do not provide the battery info in a register
return
for r in (REGISTERS.battery_status, REGISTERS.battery_charge):
if r in device.registers:
reply = read_register(device, r)
if reply:
return parse_battery_status(r, reply)
return
# the descriptor does not tell us which register this device has, try them both
reply = read_register(device, REGISTERS.battery_charge)
if reply:
# remember this for the next time
device.registers.append(REGISTERS.battery_charge)
return parse_battery_status(REGISTERS.battery_charge, reply)
reply = read_register(device, REGISTERS.battery_status)
if reply:
# remember this for the next time
device.registers.append(REGISTERS.battery_status)
return parse_battery_status(REGISTERS.battery_status, reply)
def parse_battery_status(register, reply):
if register == REGISTERS.battery_charge:
charge = ord(reply[:1])
status_byte = ord(reply[2:3]) & 0xF0
status_text = (BATTERY_STATUS.discharging if status_byte == 0x30
else BATTERY_STATUS.recharging if status_byte == 0x50
else BATTERY_STATUS.full if status_byte == 0x90
else None)
return charge, status_text
if register == REGISTERS.battery_status:
status_byte = ord(reply[:1])
charge = (BATTERY_APPOX.full if status_byte == 7 # full
else BATTERY_APPOX.good if status_byte == 5 # good
else BATTERY_APPOX.low if status_byte == 3 # low
else BATTERY_APPOX.critical if status_byte == 1 # critical
# pure 'charging' notifications may come without a status
else BATTERY_APPOX.empty)
charging_byte = ord(reply[1:2])
if charging_byte == 0x00:
status_text = BATTERY_STATUS.discharging
elif charging_byte & 0x21 == 0x21:
status_text = BATTERY_STATUS.recharging
elif charging_byte & 0x22 == 0x22:
status_text = BATTERY_STATUS.full
else:
_log.warn("could not parse 0x07 battery status: %02X (level %02X)", charging_byte, status_byte)
status_text = None
if charging_byte & 0x03 and status_byte == 0:
# some 'charging' notifications may come with no battery level information
charge = None
return charge, status_text
def get_firmware(device):
assert device
firmware = [None, None, None]
reply = read_register(device, REGISTERS.firmware, 0x01)
if not reply:
# won't be able to read any of it now...
return
fw_version = _strhex(reply[1:3])
fw_version = '%s.%s' % (fw_version[0:2], fw_version[2:4])
reply = read_register(device, REGISTERS.firmware, 0x02)
if reply:
fw_version += '.B' + _strhex(reply[1:3])
fw = _FirmwareInfo(FIRMWARE_KIND.Firmware, '', fw_version, None)
firmware[0] = fw
reply = read_register(device, REGISTERS.firmware, 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[1] = bl
reply = read_register(device, REGISTERS.firmware, 0x03)
if reply:
o_version = _strhex(reply[1:3])
o_version = '%s.%s' % (o_version[0:2], o_version[2:4])
o = _FirmwareInfo(FIRMWARE_KIND.Other, '', o_version, None)
firmware[2] = o
if any(firmware):
return tuple(f for f in firmware if f)
def set_3leds(device, battery_level=None, charging=None, warning=None):
assert device
assert device.kind is not None
if not device.online:
return
if REGISTERS.three_leds not in device.registers:
return
if battery_level is not None:
if battery_level < BATTERY_APPOX.critical:
# 1 orange, and force blink
v1, v2 = 0x22, 0x00
warning = True
elif battery_level < BATTERY_APPOX.low:
# 1 orange
v1, v2 = 0x22, 0x00
elif battery_level < BATTERY_APPOX.good:
# 1 green
v1, v2 = 0x20, 0x00
elif battery_level < BATTERY_APPOX.full:
# 2 greens
v1, v2 = 0x20, 0x02
else:
# all 3 green
v1, v2 = 0x20, 0x22
if warning:
# set the blinking flag for the leds already set
v1 |= (v1 >> 1)
v2 |= (v2 >> 1)
elif charging:
# blink all green
v1, v2 = 0x30,0x33
elif warning:
# 1 red
v1, v2 = 0x02, 0x00
else:
# turn off all leds
v1, v2 = 0x11, 0x11
write_register(device, REGISTERS.three_leds, v1, v2)
def get_notification_flags(device):
assert device
# Avoid a call if the device is not online,
# or the device does not support registers.
if device.kind is not None:
# peripherals with protocol >= 2.0 don't support registers
if device.protocol and device.protocol >= 2.0:
return
flags = read_register(device, REGISTERS.notifications)
if flags is not None:
assert len(flags) == 3
return _bytes2int(flags)
def set_notification_flags(device, *flag_bits):
assert device
# Avoid a call if the device is not online,
# or the device does not support registers.
if device.kind is not None:
# peripherals with protocol >= 2.0 don't support registers
if device.protocol and device.protocol >= 2.0:
return
flag_bits = sum(int(b) for b in flag_bits)
assert flag_bits & 0x00FFFFFF == flag_bits
result = write_register(device, REGISTERS.notifications, _int2bytes(flag_bits, 3))
return result is not None

View File

@@ -0,0 +1,429 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
# Logitech Unifying Receiver API.
from __future__ import absolute_import, division, print_function, unicode_literals
from logging import getLogger, DEBUG as _DEBUG
_log = getLogger(__name__)
del getLogger
from .common import (FirmwareInfo as _FirmwareInfo,
ReprogrammableKeyInfo as _ReprogrammableKeyInfo,
KwException as _KwException,
NamedInts as _NamedInts,
pack as _pack,
unpack as _unpack)
from . import special_keys
#
#
#
"""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,
FEATURE_INFO=0x0002,
DEVICE_FW_VERSION=0x0003,
DEVICE_NAME=0x0005,
DEVICE_GROUPS=0x0006,
DFUCONTROL=0x00C0,
BATTERY_STATUS=0x1000,
BACKLIGHT=0x1981,
REPROG_CONTROLS=0x1B00,
REPROG_CONTROLS_V2=0x1B01,
REPROG_CONTROLS_V3=0x1B03,
WIRELESS_DEVICE_STATUS=0x1D4B,
LEFT_RIGHT_SWAP=0x2001,
VERTICAL_SCROLLING=0x2100,
HI_RES_SCROLLING=0x2120,
MOUSE_POINTER=0x2200,
FN_INVERSION=0x40A0,
NEW_FN_INVERSION=0x40A2,
ENCRYPTION=0x4100,
SOLAR_DASHBOARD=0x4301,
KEYBOARD_LAYOUT=0x4520,
TOUCHPAD_FW_ITEMS=0x6010,
TOUCHPAD_SW_ITEMS=0x6011,
TOUCHPAD_WIN8_FW_ITEMS=0x6012,
TOUCHPAD_RAW_XY=0x6100,
TOUCHMOUSE_RAW_POINTS=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 not in (BATTERY_STATUS.invalid_battery, BATTERY_STATUS.thermal_error)
BATTERY_STATUS = _NamedInts(
discharging=0x00,
recharging=0x01,
almost_full=0x02,
full=0x03,
slow_recharge=0x04,
invalid_battery=0x05,
thermal_error=0x06)
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 FEATURE.ROOT == 0x0000
def __init__(self, device):
assert device is not None
self.device = device
self.supported = True
self.features = None
def __del__(self):
self.supported = False
self.device = None
self.features = None
def _check(self):
# print (self.device, "check", self.supported, self.features, self.device.protocol)
if self.supported:
assert self.device
if self.features is not None:
return True
if not self.device.online:
# device is not connected right now, will have to try later
return False
# I _think_ this is universally true
if self.device.protocol and self.device.protocol < 2.0:
self.supported = False
self.device.features = None
self.device = None
return False
reply = self.device.request(0x0000, _pack('!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('!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('!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("%r not in list" % value)
if may_have:
reply = self.device.request(0x0000, _pack('!H', ivalue))
if reply:
index = ord(reply[0:1])
self.features[index] = FEATURE[ivalue]
return index
raise ValueError("%r not in list" % 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 = 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.REPROG_CONTROLS, 0x10, index)
if keydata:
key, key_task, flags = _unpack('!HHB', keydata[:5])
ctrl_id_text = special_keys.CONTROL[key]
ctrl_task_text = special_keys.TASK[key_task]
self.keys[index] = _ReprogrammableKeyInfo(index, ctrl_id_text, ctrl_task_text, 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.online and 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.DEVICE_FW_VERSION)
if count:
count = ord(count[:1])
fw = []
for index in range(0, count):
fw_info = feature_request(device, FEATURE.DEVICE_FW_VERSION, 0x10, index)
if fw_info:
level = ord(fw_info[:1]) & 0x0F
if level == 0 or level == 1:
name, version_major, version_minor, build = _unpack('!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, '', str(ord(fw_info[1:2])), None)
else:
fw_info = _FirmwareInfo(FIRMWARE_KIND.Other, '', '', None)
fw.append(fw_info)
# if _log.isEnabledFor(_DEBUG):
# _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 ``DEVICE_NAME`` feature.
"""
kind = feature_request(device, FEATURE.DEVICE_NAME, 0x20)
if kind:
kind = ord(kind[:1])
# if _log.isEnabledFor(_DEBUG):
# _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 ``DEVICE_NAME`` feature.
"""
name_length = feature_request(device, FEATURE.DEVICE_NAME)
if name_length:
name_length = ord(name_length[:1])
name = b''
while len(name) < name_length:
fragment = feature_request(device, FEATURE.DEVICE_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_STATUS)
if battery:
discharge, dischargeNext, status = _unpack('!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.REPROG_CONTROLS)
if count:
return KeysArray(device, ord(count[:1]))
def get_mouse_pointer_info(device):
pointer_info = feature_request(device, FEATURE.MOUSE_POINTER)
if pointer_info:
dpi, flags = _unpack('!HB', pointer_info[:3])
acceleration = ('none', 'low', 'med', 'high')[flags & 0x3]
suggest_os_ballistics = (flags & 0x04) != 0
suggest_vertical_orientation = (flags & 0x08) != 0
return {
'dpi': dpi,
'acceleration': acceleration,
'suggest_os_ballistics': suggest_os_ballistics,
'suggest_vertical_orientation': suggest_vertical_orientation
}

View File

@@ -0,0 +1,50 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
# Translation support for the Logitech receivers library
from __future__ import absolute_import, division, print_function, unicode_literals
import gettext as _gettext
try:
unicode
_ = lambda x: _gettext.gettext(x).decode('UTF-8')
except:
_ = _gettext.gettext
# A few common strings, not always accessible as such in the code.
_DUMMY = (
# approximative battery levels
_("empty"), _("critical"), _("low"), _("good"), _("full"),
# battery charging statuses
_("discharging"), _("recharging"), _("almost full"), _("full"),
_("slow recharge"), _("invalid battery"), _("thermal error"),
# pairing errors
_("device timeout"), _("device not supported"), _("too many devices"), _("sequence timeout"),
# firmware kinds
_("Firmware"), _("Bootloader"), _("Hardware"), _("Other"),
)

View File

@@ -0,0 +1,229 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import absolute_import, division, print_function, unicode_literals
import threading as _threading
# from time import time as _timestamp
# for both Python 2 and 3
try:
from Queue import Queue as _Queue
except ImportError:
from queue import Queue as _Queue
from logging import getLogger, DEBUG as _DEBUG, INFO as _INFO
_log = getLogger(__name__)
del getLogger
from . import base as _base
#
#
#
class _ThreadedHandle(object):
"""A thread-local wrapper with different open handles for each thread.
Closing a ThreadedHandle will close all handles.
"""
__slots__ = ('path', '_local', '_handles', '_listener')
def __init__(self, listener, path, handle):
assert listener is not None
assert path is not None
assert handle is not None
assert isinstance(handle, int)
self._listener = listener
self.path = path
self._local = _threading.local()
# take over the current handle for the thread doing the replacement
self._local.handle = handle
self._handles = [handle]
def _open(self):
handle = _base.open_path(self.path)
if handle is None:
_log.error("%r failed to open new handle", self)
else:
# if _log.isEnabledFor(_DEBUG):
# _log.debug("%r opened new handle %d", 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("%r closing %s", self, handles)
for h in handles:
_base.close(h)
@property
def notifications_hook(self):
if self._listener:
assert isinstance(self._listener, _threading.Thread)
if _threading.current_thread() == self._listener:
return self._listener._notifications_hook
def __del__(self):
self._listener = None
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, in seconds
# 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.
# Forcibly closing the file handle on another thread does _not_ interrupt the
# read on Linux systems.
_EVENT_READ_TIMEOUT = 0.4 # in seconds
# After this many reads that did not produce a packet, call the tick() method.
# This only happens if tick_period is enabled (>0) for the Listener instance.
# _IDLE_READS = 1 + int(5 // _EVENT_READ_TIMEOUT) # wait at least 5 seconds between ticks
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__ + ':' + receiver.path.split('/')[2])
self.daemon = True
self._active = False
self.receiver = receiver
self._queued_notifications = _Queue(16)
self._notifications_callback = notifications_callback
# self.tick_period = 0
def run(self):
self._active = True
# replace the handle with a threaded one
self.receiver.handle = _ThreadedHandle(self, self.receiver.path, self.receiver.handle)
# get the right low-level handle for this thead
ihandle = int(self.receiver.handle)
if _log.isEnabledFor(_INFO):
_log.info("started with %s (%d)", self.receiver, ihandle)
self.has_started()
# last_tick = 0
# the first idle read -- delay it a bit, and make sure to stagger
# idle reads for multiple receivers
# idle_reads = _IDLE_READS + (ihandle % 5) * 2
while self._active:
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:
# deliver any queued notifications
n = self._queued_notifications.get()
if n:
# if _log.isEnabledFor(_DEBUG):
# _log.debug("%s: processing %s", self.receiver, n)
try:
self._notifications_callback(n)
except:
_log.exception("processing %s", n)
# elif self.tick_period:
# idle_reads -= 1
# if idle_reads <= 0:
# idle_reads = _IDLE_READS
# now = _timestamp()
# if now - last_tick >= self.tick_period:
# last_tick = now
# self.tick(now)
del self._queued_notifications
self.has_stopped()
def stop(self):
"""Tells the listener to stop as soon as possible."""
self._active = False
def has_started(self):
"""Called right after the thread has started, and before it starts
reading notification packets."""
pass
def has_stopped(self):
"""Called right before the thread stops."""
pass
# def tick(self, timestamp):
# """Called about every tick_period seconds."""
# pass
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.
assert _threading.current_thread() == self
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.receiver)
__nonzero__ = __bool__

View File

@@ -0,0 +1,277 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
# Handles incoming events from the receiver/devices, updating the related
# status object as appropiate.
from __future__ import absolute_import, division, print_function, unicode_literals
from logging import getLogger, DEBUG as _DEBUG, INFO as _INFO
_log = getLogger(__name__)
del getLogger
from .i18n import _
from .common import strhex as _strhex, unpack as _unpack
from . import hidpp10 as _hidpp10
from . import hidpp20 as _hidpp20
from .status import KEYS as _K, ALERT as _ALERT
_R = _hidpp10.REGISTERS
_F = _hidpp20.FEATURE
#
#
#
def process(device, notification):
assert device
assert notification
assert hasattr(device, 'status')
status = device.status
assert status is not None
if device.kind is None:
return _process_receiver_notification(device, status, notification)
return _process_device_notification(device, status, notification)
#
#
#
def _process_receiver_notification(receiver, status, n):
# supposedly only 0x4x notifications arrive for the receiver
assert n.sub_id & 0x40 == 0x40
# pairing lock notification
if n.sub_id == 0x4A:
status.lock_open = bool(n.address & 0x01)
reason = _("pairing lock is ") + (_("open") if status.lock_open else _("closed"))
if _log.isEnabledFor(_INFO):
_log.info("%s: %s", receiver, reason)
status[_K.ERROR] = None
if status.lock_open:
status.new_device = None
pair_error = ord(n.data[:1])
if pair_error:
status[_K.ERROR] = error_string = _hidpp10.PAIRING_ERRORS[pair_error]
status.new_device = None
_log.warn("pairing error %d: %s", pair_error, error_string)
status.changed(reason=reason)
return True
_log.warn("%s: unhandled notification %s", receiver, n)
#
#
#
def _process_device_notification(device, status, n):
# incoming packets with SubId >= 0x80 are supposedly replies from
# HID++ 1.0 requests, should never get here
assert n.sub_id & 0x80 == 0
# 0x40 to 0x7F appear to be HID++ 1.0 notifications
if n.sub_id >= 0x40:
return _process_hidpp10_notification(device, status, n)
# At this point, we need to know the device's protocol, otherwise it's
# possible to not know how to handle it.
assert device.protocol is not None
# some custom battery events for HID++ 1.0 devices
if device.protocol < 2.0:
return _process_hidpp10_custom_notification(device, status, n)
# assuming 0x00 to 0x3F are feature (HID++ 2.0) notifications
assert device.features
try:
feature = device.features[n.sub_id]
except IndexError:
_log.warn("%s: notification from invalid feature index %02X: %s", device, n.sub_id, n)
return False
return _process_feature_notification(device, status, n, feature)
def _process_hidpp10_custom_notification(device, status, n):
if _log.isEnabledFor(_DEBUG):
_log.debug("%s (%s) custom notification %s", device, device.protocol, n)
if n.sub_id in (_R.battery_status, _R.battery_charge):
# message layout: 10 ix <register> <xx> <yy> <zz> <00>
assert n.data[-1:] == b'\x00'
data = chr(n.address).encode() + n.data
charge, status_text = _hidpp10.parse_battery_status(n.sub_id, data)
status.set_battery_info(charge, status_text)
return True
if n.sub_id == _R.illumination:
# message layout: 10 ix 17("address") <??> <?> <??> <light level 1=off..5=max>
# TODO anything we can do with this?
if _log.isEnabledFor(_INFO):
_log.info("illumination event: %s", n)
return True
_log.warn("%s: unrecognized %s", device, n)
def _process_hidpp10_notification(device, status, n):
# unpair notification
if n.sub_id == 0x40:
if n.address == 0x02:
# device un-paired
status.clear()
device.wpid = None
device.status = None
if device.number in device.receiver:
del device.receiver[device.number]
status.changed(active=False, alert=_ALERT.ALL, reason='unpaired')
else:
_log.warn("%s: disconnection with unknown type %02X: %s", device, n.address, n)
return True
# wireless link notification
if n.sub_id == 0x41:
protocol_name = ('unifying (eQuad DJ)' if n.address == 0x04
else 'eQuad' if n.address == 0x03
else None)
if protocol_name:
if _log.isEnabledFor(_DEBUG):
wpid = _strhex(n.data[2:3] + n.data[1:2])
assert wpid == device.wpid, "%s wpid mismatch, got %s" % (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: %s connection notification: software=%s, encrypted=%s, link=%s, payload=%s",
device, protocol_name, sw_present, link_encrypyed, link_established, has_payload)
status[_K.LINK_ENCRYPTED] = link_encrypyed
status.changed(active=link_established)
else:
_log.warn("%s: connection notification with unknown protocol %02X: %s", 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, appears to be an actual input event,
# because they only come when input happents
return True
# power notification
if n.sub_id == 0x4B:
if n.address == 0x01:
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: device powered on", device)
reason = str(status) or _("powered on")
status.changed(active=True, alert=_ALERT.NOTIFICATION, reason=reason)
else:
_log.warn("%s: unknown %s", device, n)
return True
_log.warn("%s: unrecognized %s", device, n)
def _process_feature_notification(device, status, n, feature):
if feature == _F.BATTERY_STATUS:
if n.address == 0x00:
discharge = ord(n.data[:1])
battery_status = ord(n.data[1:2])
status.set_battery_info(discharge, _hidpp20.BATTERY_STATUS[battery_status])
else:
_log.warn("%s: unknown BATTERY %s", device, n)
return True
# TODO: what are REPROG_CONTROLS_V{2,3}?
if feature == _F.REPROG_CONTROLS:
if n.address == 0x00:
if _log.isEnabledFor(_INFO):
_log.info("%s: reprogrammable key: %s", device, n)
else:
_log.warn("%s: unknown REPROGRAMMABLE KEYS %s", device, n)
return True
if feature == _F.WIRELESS_DEVICE_STATUS:
if n.address == 0x00:
if _log.isEnabledFor(_DEBUG):
_log.debug("wireless status: %s", n)
if n.data[0:3] == b'\x01\x01\x01':
status.changed(active=True, alert=_ALERT.NOTIFICATION, reason='powered on')
else:
_log.warn("%s: unknown WIRELESS %s", device, n)
else:
_log.warn("%s: unknown WIRELESS %s", device, n)
return True
if feature == _F.SOLAR_DASHBOARD:
if n.data[5:9] == b'GOOD':
charge, lux, adc = _unpack('!BHH', n.data[:5])
# guesstimate the battery voltage, emphasis on 'guess'
# status_text = '%1.2fV' % (adc * 2.67793237653 / 0x0672)
status_text = _hidpp20.BATTERY_STATUS.discharging
if n.address == 0x00:
status[_K.LIGHT_LEVEL] = None
status.set_battery_info(charge, status_text)
elif n.address == 0x10:
status[_K.LIGHT_LEVEL] = lux
if lux > 200:
status_text = _hidpp20.BATTERY_STATUS.recharging
status.set_battery_info(charge, status_text)
elif n.address == 0x20:
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: Light Check button pressed", device)
status.changed(alert=_ALERT.SHOW_WINDOW)
# first cancel any reporting
# device.feature_request(_F.SOLAR_DASHBOARD)
# trigger a new report chain
reports_count = 15
reports_period = 2 # seconds
device.feature_request(_F.SOLAR_DASHBOARD, 0x00, reports_count, reports_period)
else:
_log.warn("%s: unknown SOLAR CHAGE %s", device, n)
else:
_log.warn("%s: SOLAR CHARGE not GOOD? %s", device, n)
return True
if feature == _F.TOUCHMOUSE_RAW_POINTS:
if n.address == 0x00:
if _log.isEnabledFor(_INFO):
_log.info("%s: TOUCH MOUSE points %s", device, n)
elif n.address == 0x10:
touch = ord(n.data[:1])
button_down = bool(touch & 0x02)
mouse_lifted = bool(touch & 0x01)
if _log.isEnabledFor(_INFO):
_log.info("%s: TOUCH MOUSE status: button_down=%s mouse_lifted=%s", device, button_down, mouse_lifted)
else:
_log.warn("%s: unknown TOUCH MOUSE %s", device, n)
return True
_log.warn("%s: unrecognized %s for feature %s (index %02X)", device, n, feature, n.sub_id)

View File

@@ -0,0 +1,531 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import absolute_import, division, print_function, unicode_literals
import errno as _errno
from logging import getLogger, INFO as _INFO
_log = getLogger(__name__)
del getLogger
from .i18n import _
from . import base as _base
from . import hidpp10 as _hidpp10
from . import hidpp20 as _hidpp20
from .common import strhex as _strhex
from .descriptors import DEVICES as _DESCRIPTORS
from .settings_templates import check_feature_settings as _check_feature_settings
_R = _hidpp10.REGISTERS
#
#
#
class PairedDevice(object):
def __init__(self, receiver, number, link_notification=None):
assert receiver
self.receiver = receiver
assert number > 0 and number <= receiver.max_devices
# Device number, 1..6 for unifying devices, 1 otherwise.
self.number = number
# 'device active' flag; requires manual management.
self.online = None
# the Wireless PID is unique per device model
self.wpid = None
self.descriptor = None
# mose, keyboard, etc (see _hidpp10.DEVICE_KIND)
self._kind = None
# Unifying peripherals report a codename.
self._codename = None
# the full name of the model
self._name = None
# HID++ protocol version, 1.0 or 2.0
self._protocol = None
# serial number (an 8-char hex string)
self._serial = None
self._firmware = None
self._keys = None
self._registers = None
self._settings = None
# Misc stuff that's irrelevant to any functionality, but may be
# displayed in the UI and caching it here helps.
self._polling_rate = None
self._power_switch = None
# if _log.isEnabledFor(_DEBUG):
# _log.debug("new PairedDevice(%s, %s, %s)", receiver, number, link_notification)
if link_notification is not None:
self.online = bool(ord(link_notification.data[0:1]) & 0x40)
self.wpid = _strhex(link_notification.data[2:3] + link_notification.data[1:2])
# assert link_notification.address == (0x04 if unifying else 0x03)
kind = ord(link_notification.data[0:1]) & 0x0F
self._kind = _hidpp10.DEVICE_KIND[kind]
else:
# force a reading of the wpid
pair_info = receiver.read_register(_R.receiver_info, 0x20 + number - 1)
if pair_info:
# may be either a Unifying receiver, or an Unifying-ready receiver
self.wpid = _strhex(pair_info[3:5])
kind = ord(pair_info[7:8]) & 0x0F
self._kind = _hidpp10.DEVICE_KIND[kind]
self._polling_rate = ord(pair_info[2:3])
else:
# unifying protocol not supported, must be a Nano receiver
device_info = self.receiver.read_register(_R.receiver_info, 0x04)
if device_info is None:
_log.error("failed to read Nano wpid for device %d of %s", number, receiver)
raise _base.NoSuchDevice(number=number, receiver=receiver, error="read Nano wpid")
self.wpid = _strhex(device_info[3:5])
self._polling_rate = 0
self._power_switch = '(' + _("unknown") + ')'
# the wpid is necessary to properly identify wireless link on/off notifications
# also it gets set to None on this object when the device is unpaired
assert self.wpid is not None, "failed to read wpid: device %d of %s" % (number, receiver)
self.descriptor = _DESCRIPTORS.get(self.wpid)
if self.descriptor is None:
# Last chance to correctly identify the device; many Nano receivers
# do not support this call.
codename = self.receiver.read_register(_R.receiver_info, 0x40 + self.number - 1)
if codename:
codename_length = ord(codename[1:2])
codename = codename[2:2 + codename_length]
self._codename = codename.decode('ascii')
self.descriptor = _DESCRIPTORS.get(self._codename)
if self.descriptor:
self._name = self.descriptor.name
self._protocol = self.descriptor.protocol
if self._codename is None:
self._codename = self.descriptor.codename
if self._kind is None:
self._kind = self.descriptor.kind
if self._protocol is not None:
self.features = None if self._protocol < 2.0 else _hidpp20.FeaturesArray(self)
else:
# may be a 2.0 device; if not, it will fix itself later
self.features = _hidpp20.FeaturesArray(self)
@property
def protocol(self):
if self._protocol is None and self.online is not False:
self._protocol = _base.ping(self.receiver.handle, self.number)
# if the ping failed, the peripheral is (almost) certainly offline
self.online = self._protocol is not None
# if _log.isEnabledFor(_DEBUG):
# _log.debug("device %d protocol %s", self.number, self._protocol)
return self._protocol or 0
@property
def codename(self):
if self._codename is None:
codename = self.receiver.read_register(_R.receiver_info, 0x40 + self.number - 1)
if codename:
codename_length = ord(codename[1:2])
codename = codename[2:2 + codename_length]
self._codename = codename.decode('ascii')
# if _log.isEnabledFor(_DEBUG):
# _log.debug("device %d codename %s", self.number, self._codename)
else:
self._codename = '? (%s)' % self.wpid
return self._codename
@property
def name(self):
if self._name is None:
if self.online and self.protocol >= 2.0:
self._name = _hidpp20.get_name(self)
return self._name or self.codename or ('Unknown device %s' % self.wpid)
@property
def kind(self):
if self._kind is None:
pair_info = self.receiver.read_register(_R.receiver_info, 0x20 + self.number - 1)
if pair_info:
kind = ord(pair_info[7:8]) & 0x0F
self._kind = _hidpp10.DEVICE_KIND[kind]
self._polling_rate = ord(pair_info[2:3])
elif self.online and self.protocol >= 2.0:
self._kind = _hidpp20.get_kind(self)
return self._kind or '?'
@property
def firmware(self):
if self._firmware is None and self.online:
if self.protocol >= 2.0:
self._firmware = _hidpp20.get_firmware(self)
else:
self._firmware = _hidpp10.get_firmware(self)
return self._firmware or ()
@property
def serial(self):
if self._serial is None:
serial = self.receiver.read_register(_R.receiver_info, 0x30 + self.number - 1)
if serial:
ps = ord(serial[9:10]) & 0x0F
self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps]
else:
# some Nano receivers?
serial = self.receiver.read_register(0x2D5)
if serial:
self._serial = _strhex(serial[1:5])
else:
# fallback...
self._serial = self.receiver.serial
return self._serial or '?'
@property
def power_switch_location(self):
if self._power_switch is None:
ps = self.receiver.read_register(_R.receiver_info, 0x30 + self.number - 1)
if ps is not None:
ps = ord(ps[9:10]) & 0x0F
self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps]
else:
self._power_switch = '(unknown)'
return self._power_switch
@property
def polling_rate(self):
if self._polling_rate is None:
pair_info = self.receiver.read_register(_R.receiver_info, 0x20 + self.number - 1)
if pair_info:
self._polling_rate = ord(pair_info[2:3])
else:
self._polling_rate = 0
return self._polling_rate
@property
def keys(self):
if self._keys is None:
if self.online and self.protocol >= 2.0:
self._keys = _hidpp20.get_keys(self) or ()
return self._keys
@property
def registers(self):
if self._registers is None:
if self.descriptor and self.descriptor.registers:
self._registers = list(self.descriptor.registers)
else:
self._registers = []
return self._registers
@property
def settings(self):
if self._settings is None:
if self.descriptor and self.descriptor.settings:
self._settings = [s(self) for s in self.descriptor.settings]
else:
self._settings = []
_check_feature_settings(self, self._settings)
return self._settings
def enable_notifications(self, enable=True):
"""Enable or disable device (dis)connection notifications on this
receiver."""
if not bool(self.receiver) or self.protocol >= 2.0:
return False
if enable:
set_flag_bits = ( _hidpp10.NOTIFICATION_FLAG.battery_status
| _hidpp10.NOTIFICATION_FLAG.keyboard_illumination
| _hidpp10.NOTIFICATION_FLAG.wireless
| _hidpp10.NOTIFICATION_FLAG.software_present )
else:
set_flag_bits = 0
ok = _hidpp10.set_notification_flags(self, set_flag_bits)
if ok is None:
_log.warn("%s: failed to %s device notifications", self, 'enable' if enable else 'disable')
flag_bits = _hidpp10.get_notification_flags(self)
flag_names = None if flag_bits is None else tuple(_hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits))
if _log.isEnabledFor(_INFO):
_log.info("%s: device notifications %s %s", self, 'enabled' if enable else 'disabled', flag_names)
return flag_bits if ok else None
def request(self, request_id, *params):
return _base.request(self.receiver.handle, self.number, request_id, *params)
read_register = _hidpp10.read_register
write_register = _hidpp10.write_register
def feature_request(self, feature, function=0x00, *params):
if self.protocol >= 2.0:
return _hidpp20.feature_request(self, feature, function, *params)
def ping(self):
"""Checks if the device is online, returns True of False"""
protocol = _base.ping(self.receiver.handle, self.number)
self.online = protocol is not None
if protocol is not None:
self._protocol = protocol
return self.online
def __index__(self):
return self.number
__int__ = __index__
def __eq__(self, other):
return other is not None and self.kind == other.kind and self.wpid == other.wpid
def __ne__(self, other):
return other is None or self.kind != other.kind or self.wpid != other.wpid
def __hash__(self):
return self.wpid.__hash__()
__bool__ = __nonzero__ = lambda self: self.wpid is not None and self.number in self.receiver
def __str__(self):
return '<PairedDevice(%d,%s,%s)>' % (self.number, self.wpid, self.codename or '?')
__unicode__ = __repr__ = __str__
#
#
#
class Receiver(object):
"""A Unifying Receiver instance.
The paired devices are available through the sequence interface.
"""
number = 0xFF
kind = None
def __init__(self, handle, device_info):
assert handle
self.handle = handle
assert device_info
self.path = device_info.path
# USB product id, used for some Nano receivers
self.product_id = device_info.product_id
# read the serial immediately, so we can find out max_devices
# this will tell us if it's a Unifying or Nano receiver
serial_reply = self.read_register(_R.receiver_info, 0x03)
assert serial_reply
self.serial = _strhex(serial_reply[1:5])
self.max_devices = ord(serial_reply[6:7])
if self.max_devices == 6:
self.name = 'Unifying Receiver'
elif self.max_devices < 6:
self.name = 'Nano Receiver'
else:
raise Exception("unknown receiver type", self.max_devices)
self._str = '<%s(%s,%s%s)>' % (self.name.replace(' ', ''), self.path, '' if isinstance(self.handle, int) else 'T', self.handle)
# TODO _properly_ figure out which receivers do and which don't support unpairing
self.may_unpair = self.write_register(_R.receiver_pairing) is 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 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_flag_bits = ( _hidpp10.NOTIFICATION_FLAG.battery_status
| _hidpp10.NOTIFICATION_FLAG.wireless
| _hidpp10.NOTIFICATION_FLAG.software_present )
else:
set_flag_bits = 0
ok = _hidpp10.set_notification_flags(self, set_flag_bits)
if ok is None:
_log.warn("%s: failed to %s receiver notifications", self, 'enable' if enable else 'disable')
return None
flag_bits = _hidpp10.get_notification_flags(self)
flag_names = None if flag_bits is None else tuple(_hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits))
if _log.isEnabledFor(_INFO):
_log.info("%s: receiver notifications %s => %s", self, 'enabled' if enable else 'disabled', flag_names)
return flag_bits
def notify_devices(self):
"""Scan all devices."""
if self.handle:
if not self.write_register(_R.receiver_connection, 0x02):
_log.warn("%s: failed to trigger device link notifications", self)
def register_new_device(self, number, notification=None):
if self._devices.get(number) is not None:
raise IndexError("%s: device number %d already registered" % (self, number))
assert notification is None or notification.devnumber == number
assert notification is None or notification.sub_id == 0x41
try:
dev = PairedDevice(self, number, notification)
assert dev.wpid
if _log.isEnabledFor(_INFO):
_log.info("%s: found new device %d (%s)", self, number, dev.wpid)
self._devices[number] = dev
return dev
except _base.NoSuchDevice:
_log.exception("register_new_device")
_log.warning("%s: looked for device %d, not found", self, number)
self._devices[number] = None
def set_lock(self, lock_closed=True, device=0, timeout=0):
if self.handle:
action = 0x02 if lock_closed else 0x01
reply = self.write_register(_R.receiver_pairing, action, device, timeout)
if reply:
return True
_log.warn("%s: failed to %s the receiver lock", self, 'close' if lock_closed else 'open')
def count(self):
count = self.read_register(_R.receiver_connection)
return 0 if count is None else ord(count[1:2])
# def has_devices(self):
# return len(self) > 0 or self.count() > 0
def request(self, request_id, *params):
if bool(self):
return _base.request(self.handle, 0xFF, request_id, *params)
read_register = _hidpp10.read_register
write_register = _hidpp10.write_register
def __iter__(self):
for number in range(1, 1 + self.max_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 bool(self):
return None
dev = self._devices.get(key)
if dev is not None:
return dev
if not isinstance(key, int):
raise TypeError('key must be an integer')
if key < 1 or key > self.max_devices:
raise IndexError(key)
return self.register_new_device(key)
def __delitem__(self, key):
key = int(key)
if self._devices.get(key) is None:
raise IndexError(key)
dev = self._devices[key]
if not dev:
if key in self._devices:
del self._devices[key]
return
action = 0x03
reply = self.write_register(_R.receiver_pairing, action, key)
if reply:
# invalidate the device
dev.online = False
dev.wpid = None
if key in self._devices:
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 isinstance(dev, int):
return self._devices.get(dev) is not None
return self.__contains__(dev.number)
def __eq__(self, other):
return other is not None and self.kind == other.kind and self.path == other.path
def __ne__(self, other):
return other is None or self.kind != other.kind or self.path != other.path
def __hash__(self):
return self.path.__hash__()
def __str__(self):
return self._str
__unicode__ = __repr__ = __str__
__bool__ = __nonzero__ = lambda self: self.handle is not None
@classmethod
def open(self, device_info):
"""Opens a Logitech Receiver found attached to the machine, by Linux device path.
:returns: An open file handle for the found receiver, or ``None``.
"""
try:
handle = _base.open_path(device_info.path)
if handle:
return Receiver(handle, device_info)
except OSError as e:
_log.exception("open %s", device_info)
if e.errno == _errno.EACCES:
raise
except:
_log.exception("open %s", device_info)

View File

@@ -0,0 +1,351 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import absolute_import, division, print_function, unicode_literals
from logging import getLogger, DEBUG as _DEBUG
_log = getLogger(__name__)
del getLogger
from copy import copy as _copy
from .common import (
NamedInt as _NamedInt,
NamedInts as _NamedInts,
bytes2int as _bytes2int,
)
#
#
#
KIND = _NamedInts(toggle=0x01, choice=0x02, range=0x12)
class Setting(object):
"""A setting descriptor.
Needs to be instantiated for each specific device."""
__slots__ = ('name', 'label', 'description', 'kind', 'persister', 'device_kind',
'_rw', '_validator', '_device', '_value')
def __init__(self, name, rw, validator, kind=None, label=None, description=None, device_kind=None):
assert name
self.name = name
self.label = label or name
self.description = description
self.device_kind = device_kind
self._rw = rw
self._validator = validator
assert kind is None or kind & validator.kind != 0
self.kind = kind or validator.kind
self.persister = None
def __call__(self, device):
assert not hasattr(self, '_value')
assert self.device_kind is None or self.device_kind == device.kind
p = device.protocol
if p == 1.0:
# HID++ 1.0 devices do not support features
assert self._rw.kind == RegisterRW.kind
elif p >= 2.0:
# HID++ 2.0 devices do not support registers
assert self._rw.kind == FeatureRW.kind
o = _copy(self)
o._value = None
o._device = device
return o
@property
def choices(self):
assert hasattr(self, '_value')
assert hasattr(self, '_device')
return self._validator.choices if self._validator.kind & KIND.choice else None
def read(self, cached=True):
assert hasattr(self, '_value')
assert hasattr(self, '_device')
if self._value is None and self.persister:
# We haven't read a value from the device yet,
# maybe we have something in the configuration.
self._value = self.persister.get(self.name)
if cached and self._value is not None:
if self.persister and self.name not in self.persister:
# If this is a new device (or a new setting for an old device),
# make sure to save its current value for the next time.
self.persister[self.name] = self._value
return self._value
if self._device.online:
reply = self._rw.read(self._device)
if reply:
self._value = self._validator.validate_read(reply)
if self.persister and self.name not in self.persister:
# Don't update the persister if it already has a value,
# otherwise the first read might overwrite the value we wanted.
self.persister[self.name] = self._value
return self._value
def write(self, value):
assert hasattr(self, '_value')
assert hasattr(self, '_device')
assert value is not None
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: write %r to %s", self.name, value, self._device)
if self._device.online:
# Remember the value we're trying to set, even if the write fails.
# This way even if the device is offline or some other error occurs,
# the last value we've tried to write is remembered in the configuration.
self._value = value
if self.persister:
self.persister[self.name] = value
current_value = None
if self._validator.needs_current_value:
# the validator needs the current value, possibly to merge flag values
current_value = self._rw.read(self._device)
data_bytes = self._validator.prepare_write(value, current_value)
if data_bytes is not None:
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: prepare write(%s) => %r", self.name, value, data_bytes)
reply = self._rw.write(self._device, data_bytes)
if not reply:
# tell whomever is calling that the write failed
return None
return value
def apply(self):
assert hasattr(self, '_value')
assert hasattr(self, '_device')
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: apply %s (%s)", self.name, self._value, self._device)
value = self.read()
if value is not None:
self.write(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__
#
# read/write low-level operators
#
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.read_register(self.register)
def write(self, device, data_bytes):
return device.write_register(self.register, 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)
#
# value validators
# handle the conversion from read bytes, to setting value, and back
#
class BooleanValidator(object):
__slots__ = ('true_value', 'false_value', 'mask', 'needs_current_value')
kind = KIND.toggle
default_true = 0x01
default_false = 0x00
# mask specifies all the affected bits in the value
default_mask = 0xFF
def __init__(self, true_value=default_true, false_value=default_false, mask=default_mask):
if isinstance(true_value, int):
assert isinstance(false_value, int)
if mask is None:
mask = self.default_mask
else:
assert isinstance(mask, int)
assert true_value & false_value == 0
assert true_value & mask == true_value
assert false_value & mask == false_value
self.needs_current_value = (mask != self.default_mask)
elif isinstance(true_value, bytes):
if false_value is None or false_value == self.default_false:
false_value = b'\x00' * len(true_value)
else:
assert isinstance(false_value, bytes)
if mask is None or mask == self.default_mask:
mask = b'\xFF' * len(true_value)
else:
assert isinstance(mask, bytes)
assert len(mask) == len(true_value) == len(false_value)
tv = _bytes2int(true_value)
fv = _bytes2int(false_value)
mv = _bytes2int(mask)
assert tv & fv == 0
assert tv & mv == tv
assert fv & mv == fv
self.needs_current_value = any(m != b'\xFF' for m in mask)
else:
raise Exception("invalid mask '%r', type %s" % (mask, type(mask)))
self.true_value = true_value
self.false_value = false_value
self.mask = mask
def validate_read(self, reply_bytes):
if isinstance(self.mask, int):
reply_value = ord(reply_bytes[:1]) & self.mask
if _log.isEnabledFor(_DEBUG):
_log.debug("BooleanValidator: validate read %r => %02X", reply_bytes, reply_value)
if reply_value == self.true_value:
return True
if reply_value == self.false_value:
return False
_log.warn("BooleanValidator: reply %02X mismatched %02X/%02X/%02X",
reply_value, self.true_value, self.false_value, self.mask)
return False
count = len(self.mask)
mask = _bytes2int(self.mask)
reply_value = _bytes2int(reply_bytes[:count]) & mask
true_value = _bytes2int(self.true_value)
if reply_value == true_value:
return True
false_value = _bytes2int(self.false_value)
if reply_value == false_value:
return False
_log.warn("BooleanValidator: reply %r mismatched %r/%r/%r",
reply_bytes, self.true_value, self.false_value, self.mask)
return False
def prepare_write(self, new_value, current_value=None):
if new_value is None:
new_value = False
else:
assert isinstance(new_value, bool)
to_write = self.true_value if new_value else self.false_value
if isinstance(self.mask, int):
if current_value is not None and self.needs_current_value:
to_write |= ord(current_value[:1]) & (0xFF ^ self.mask)
if current_value is not None and to_write == ord(current_value[:1]):
return None
else:
to_write = list(to_write)
count = len(self.mask)
for i in range(0, count):
b = ord(to_write[i])
m = ord(self.mask[i : i + 1])
assert b & m == b
# b &= m
if current_value is not None and self.needs_current_value:
b |= ord(current_value[i : i + 1]) & (0xFF ^ m)
to_write[i] = chr(b)
to_write = b''.join(to_write)
if current_value is not None and to_write == current_value[:len(to_write)]:
return None
if _log.isEnabledFor(_DEBUG):
_log.debug("BooleanValidator: prepare_write(%s, %s) => %r", new_value, current_value, to_write)
return to_write
class ChoicesValidator(object):
__slots__ = ('choices', 'flag', '_bytes_count', 'needs_current_value')
kind = KIND.choice
def __init__(self, choices):
assert choices is not None
assert isinstance(choices, _NamedInts)
assert len(choices) > 2
self.choices = choices
self.needs_current_value = False
max_bits = max(x.bit_length() for x in choices)
self._bytes_count = (max_bits // 8) + (1 if max_bits % 8 else 0)
assert self._bytes_count < 8
def validate_read(self, reply_bytes):
reply_value = _bytes2int(reply_bytes[:self._bytes_count])
valid_value = self.choices[reply_value]
assert valid_value is not None, "%s: failed to validate read value %02X" % (self.__class__.__name__, reply_value)
return valid_value
def prepare_write(self, new_value, current_value=None):
if new_value is None:
choice = self.choices[:][0]
else:
if isinstance(new_value, int):
choice = self.choices[new_value]
elif new_value in self.choices:
choice = self.choices[new_value]
else:
raise ValueError(new_value)
if choice is None:
raise ValueError("invalid choice %r" % new_value)
assert isinstance(choice, _NamedInt)
return choice.bytes(self._bytes_count)

View File

@@ -0,0 +1,175 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import absolute_import, division, print_function, unicode_literals
from .i18n import _
from . import hidpp10 as _hidpp10
from . import hidpp20 as _hidpp20
from .settings import (
KIND as _KIND,
Setting as _Setting,
RegisterRW as _RegisterRW,
FeatureRW as _FeatureRW,
BooleanValidator as _BooleanV,
ChoicesValidator as _ChoicesV,
)
_DK = _hidpp10.DEVICE_KIND
_R = _hidpp10.REGISTERS
_F = _hidpp20.FEATURE
#
# pre-defined basic setting descriptors
#
def register_toggle(name, register,
true_value=_BooleanV.default_true,
false_value=_BooleanV.default_false,
mask=_BooleanV.default_mask,
label=None, description=None, device_kind=None):
validator = _BooleanV(true_value=true_value, false_value=false_value, mask=mask)
rw = _RegisterRW(register)
return _Setting(name, rw, validator, label=label, description=description, device_kind=device_kind)
def register_choices(name, register, choices,
kind=_KIND.choice,
label=None, description=None, device_kind=None):
assert choices
validator = _ChoicesV(choices)
rw = _RegisterRW(register)
return _Setting(name, rw, validator, kind=kind, label=label, description=description, device_kind=device_kind)
def feature_toggle(name, feature,
read_function_id=_FeatureRW.default_read_fnid,
write_function_id=_FeatureRW.default_write_fnid,
true_value=_BooleanV.default_true,
false_value=_BooleanV.default_false,
mask=_BooleanV.default_mask,
label=None, description=None, device_kind=None):
validator = _BooleanV(true_value=true_value, false_value=false_value, mask=mask)
rw = _FeatureRW(feature, read_function_id, write_function_id)
return _Setting(name, rw, validator, label=label, description=description, device_kind=device_kind)
#
# common strings for settings
#
_SMOOTH_SCROLL = ('smooth-scroll', _("Smooth Scrolling"),
_("High-sensitivity mode for vertical scroll with the wheel."))
_SIDE_SCROLL = ('side-scroll', _("Side Scrolling"),
_("When disabled, pushing the wheel sideways sends custom button events\n"
"instead of the standard side-scrolling events."))
_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."))
_HAND_DETECTION = ('hand-detection', _("Hand Detection"),
_("Turn on illumination when the hands hover over the keyboard."))
#
#
#
def _register_hand_detection(register=_R.keyboard_hand_detection,
true_value=b'\x00\x00\x00', false_value=b'\x00\x00\x30', mask=b'\x00\x00\xFF'):
return register_toggle(_HAND_DETECTION[0], register, true_value=true_value, false_value=false_value,
label=_HAND_DETECTION[1], description=_HAND_DETECTION[2],
device_kind=_DK.keyboard)
def _register_fn_swap(register=_R.keyboard_fn_swap, true_value=b'\x00\x01', mask=b'\x00\x01'):
return register_toggle(_FN_SWAP[0], register, true_value=true_value, mask=mask,
label=_FN_SWAP[1], description=_FN_SWAP[2],
device_kind=_DK.keyboard)
def _register_smooth_scroll(register=_R.mouse_button_flags, true_value=0x40, mask=0x40):
return register_toggle(_SMOOTH_SCROLL[0], register, true_value=true_value, mask=mask,
label=_SMOOTH_SCROLL[1], description=_SMOOTH_SCROLL[2],
device_kind=_DK.mouse)
def _register_side_scroll(register=_R.mouse_button_flags, true_value=0x02, mask=0x02):
return register_toggle(_SIDE_SCROLL[0], register, true_value=true_value, mask=mask,
label=_SIDE_SCROLL[1], description=_SIDE_SCROLL[2],
device_kind=_DK.mouse)
def _register_dpi(register=_R.mouse_dpi, choices=None):
return register_choices(_DPI[0], register, choices,
label=_DPI[1], description=_DPI[2],
device_kind=_DK.mouse)
def _feature_fn_swap():
return feature_toggle(_FN_SWAP[0], _F.FN_INVERSION,
label=_FN_SWAP[1], description=_FN_SWAP[2],
device_kind=_DK.keyboard)
#
#
#
from collections import namedtuple
_SETTINGS_LIST = namedtuple('_SETTINGS_LIST', [
'fn_swap',
'smooth_scroll',
'side_scroll',
'dpi',
'hand_detection',
'typing_illumination',
])
del namedtuple
RegisterSettings = _SETTINGS_LIST(
fn_swap=_register_fn_swap,
smooth_scroll=_register_smooth_scroll,
side_scroll=_register_side_scroll,
dpi=_register_dpi,
hand_detection=_register_hand_detection,
typing_illumination=None,
)
FeatureSettings = _SETTINGS_LIST(
fn_swap=_feature_fn_swap,
smooth_scroll=None,
side_scroll=None,
dpi=None,
hand_detection=None,
typing_illumination=None,
)
del _SETTINGS_LIST
#
#
#
def check_feature_settings(device, already_known):
"""Try to auto-detect device settings by the HID++ 2.0 features they have."""
if device.features is None:
return
if device.protocol and device.protocol < 2.0:
return
if not any(s.name == _FN_SWAP[0] for s in already_known) and _F.FN_INVERSION in device.features:
fn_swap = FeatureSettings.fn_swap()
already_known.append(fn_swap(device))

View File

@@ -0,0 +1,334 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
# Reprogrammable keys information
from __future__ import absolute_import, division, print_function, unicode_literals
from .common import NamedInts as _NamedInts
# <controls.xml awk -F\" '/<Control /{sub(/^LD_FINFO_(CTRLID_)?/, "", $2);printf("\t%s=0x%04X,\n", $2, $4)}' | sort -t= -k2
CONTROL = _NamedInts(
Volume_Up=0x0001,
Volume_Down=0x0002,
Mute=0x0003,
Play__Pause=0x0004,
Next=0x0005,
Previous=0x0006,
Stop=0x0007,
Application_Switcher=0x0008,
BURN=0x0009,
Calculator=0x000A,
CALENDAR=0x000B,
CLOSE=0x000C,
EJECT=0x000D,
Mail=0x000E,
HELP_AS_HID=0x000F,
HELP_AS_F1=0x0010,
LAUNCH_WORD_PROC=0x0011,
LAUNCH_SPREADSHEET=0x0012,
LAUNCH_PRESENTATION=0x0013,
UNDO_AS_CTRL_Z=0x0014,
UNDO_AS_HID=0x0015,
REDO_AS_CTRL_Y=0x0016,
REDO_AS_HID=0x0017,
PRINT_AS_CTRL_P=0x0018,
PRINT_AS_HID=0x0019,
SAVE_AS_CTRL_S=0x001A,
SAVE_AS_HID=0x001B,
PRESET_A=0x001C,
PRESET_B=0x001D,
PRESET_C=0x001E,
PRESET_D=0x001F,
FAVORITES=0x0020,
GADGETS=0x0021,
MY_HOME=0x0022,
GADGETS_AS_WIN_G=0x0023,
MAXIMIZE_AS_HID=0x0024,
MAXIMIZE_AS_WIN_SHIFT_M=0x0025,
MINIMIZE_AS_HID=0x0026,
MINIMIZE_AS_WIN_M=0x0027,
MEDIA_PLAYER=0x0028,
MEDIA_CENTER_LOGI=0x0029,
MEDIA_CENTER_MSFT=0x002A, # Should not be used as it is not reprogrammable under Windows
CUSTOM_MENU=0x002B,
MESSENGER=0x002C,
MY_DOCUMENTS=0x002D,
MY_MUSIC=0x002E,
WEBCAM=0x002F,
MY_PICTURES=0x0030,
MY_VIDEOS=0x0031,
MY_COMPUTER_AS_HID=0x0032,
MY_COMPUTER_AS_WIN_E=0x0033,
LAUNC_PICTURE_VIEWER=0x0035,
ONE_TOUCH_SEARCH=0x0036,
PRESET_1=0x0037,
PRESET_2=0x0038,
PRESET_3=0x0039,
PRESET_4=0x003A,
RECORD=0x003B,
INTERNET_REFRESH=0x003C,
ROTATE_RIGHT=0x003D,
SEARCH=0x003E,
SHUFFLE=0x003F,
SLEEP=0x0040,
INTERNET_STOP=0x0041,
SYNCHRONIZE=0x0042,
ZOOM=0x0043,
ZOOM_IN_AS_HID=0x0044,
ZOOM_IN_AS_CTRL_WHEEL=0x0045,
ZOOM_IN_AS_CLTR_PLUS=0x0046,
ZOOM_OUT_AS_HID=0x0047,
ZOOM_OUT_AS_CTRL_WHEEL=0x0048,
ZOOM_OUT_AS_CLTR_MINUS=0x0049,
ZOOM_RESET=0x004A,
ZOOM_FULL_SCREEN=0x004B,
PRINT_SCREEN=0x004C,
PAUSE_BREAK=0x004D,
SCROLL_LOCK=0x004E,
CONTEXTUAL_MENU=0x004F,
LEFT_CLICK=0x0050,
RIGHT_CLICK=0x0051,
MIDDLE_BUTTON=0x0052,
BACK_AS_BUTTON_4=0x0053,
BACK_AS_HID=0x0054,
BACK_AS_ALT_WIN_ARROW=0x0055,
FORWARD_AS_BUTTON_5=0x0056,
FORWARD_AS_HID=0x0057,
FORWARD_AS_ALT_WIN_ARROW=0x0058,
BUTTON_6=0x0059,
LEFT_SCROLL_AS_BUTTON_7=0x005A,
LEFT_SCROLL_AS_AC_PAN=0x005B,
RIGHT_SCROLL_AS_BUTTON_8=0x005C,
RIGHT_SCROLL_AS_AC_PAN=0x005D,
BUTTON_9=0x005E,
BUTTON_10=0x005F,
BUTTON_11=0x0060,
BUTTON_12=0x0061,
BUTTON_13=0x0062,
BUTTON_14=0x0063,
BUTTON_15=0x0064,
BUTTON_16=0x0065,
BUTTON_17=0x0066,
BUTTON_18=0x0067,
BUTTON_19=0x0068,
BUTTON_20=0x0069,
BUTTON_21=0x006A,
BUTTON_22=0x006B,
BUTTON_23=0x006C,
BUTTON_24=0x006D,
SHOW_DESKTOP=0x006E,
Lock_PC=0x006F,
FN_F1=0x0070,
FN_F2=0x0071,
FN_F3=0x0072,
FN_F4=0x0073,
FN_F5=0x0074,
FN_F6=0x0075,
FN_F7=0x0076,
FN_F8=0x0077,
FN_F9=0x0078,
FN_F10=0x0079,
FN_F11=0x007A,
FN_F12=0x007B,
FN_F13=0x007C,
FN_F14=0x007D,
FN_F15=0x007E,
FN_F16=0x007F,
FN_F17=0x0080,
FN_F18=0x0081,
FN_F19=0x0082,
IOS_HOME=0x0083,
ANDROID_HOME=0x0084,
ANDROID_MENU=0x0085,
ANDROID_SEARCH=0x0086,
ANDROID_BACK=0x0087,
HOME_COMBO=0x0088,
LOCK_COMBO=0x0089,
IOS_VIRTUAL_KEYBOARD=0x008A,
IOS_LANGUAGE_SWICH=0x008B,
MAC_EXPOSE=0x008C,
MAC_DASHBOARD=0x008D,
WIN7_SNAP_LEFT=0x008E,
WIN7_SNAP_RIGHT=0x008F,
WIN7_MINIMIZE_AS_WIN_ARROW=0x0090,
WIN7_MAXIMIZE_AS_WIN_ARROW=0x0091,
WIN7_STRETCH_UP=0x0092,
WIN7_MONITOR_SWITCH_AS_WIN_SHIFT_LEFTARROW=0x0093,
WIN7_MONITOR_SWITCH_AS_WIN_SHIFT_RIGHTARROW=0x0094,
WIN7_SHOW_PRESENTATION_MODE=0x0095,
WIN7_SHOW_MOBILITY_CENTER=0x0096,
ANALOG_HSCROLL=0x0097,
METRO_APPSWITCH=0x009F,
METRO_APPBAR=0x00A0,
METRO_CHARMS=0x00A1,
CALC_VKEYBOARD=0x00A2,
METRO_SEARCH=0x00A3,
COMBO_SLEEP=0x00A4,
METRO_SHARE=0x00A5,
METRO_SETTINGS=0x00A6,
METRO_DEVICES=0x00A7,
METRO_START_SCREEN=0x00A9,
ZOOMIN=0x00AA,
ZOOMOUT=0x00AB,
BACK_HSCROLL=0x00AC,
SHOW_DESKTOP_HPP=0x00AE,
)
CONTROL._fallback = lambda x: 'unknown:%04X' % x
# <tasks.xml awk -F\" '/<Task /{gsub(/ /, "_", $6); printf("\t%s=0x%04X,\n", $6, $4)}'
TASK = _NamedInts(
Volume_Up=0x0001,
Volume_Down=0x0002,
Mute=0x0003,
# Multimedia tasks:
Play__Pause=0x0004,
Next=0x0005,
Previous=0x0006,
Stop=0x0007,
Application_Switcher=0x0008,
BurnMediaPlayer=0x0009,
Calculator=0x000A,
Calendar=0x000B,
Close_Application=0x000C,
Eject=0x000D,
Email=0x000E,
Help=0x000F,
OffDocument=0x0010,
OffSpreadsheet=0x0011,
OffPowerpnt=0x0012,
Undo=0x0013,
Redo=0x0014,
Print=0x0015,
Save=0x0016,
SmartKeySet=0x0017,
Favorites=0x0018,
GadgetsSet=0x0019,
HomePage=0x001A,
WindowsRestore=0x001B,
WindowsMinimize=0x001C,
Music=0x001D, # also known as MediaPlayer
# Both 0x001E and 0x001F are known as MediaCenterSet
Media_Center_Logitech=0x001E,
Media_Center_Microsoft=0x001F,
UserMenu=0x0020,
Messenger=0x0021,
PersonalFolders=0x0022,
MyMusic=0x0023,
Webcam=0x0024,
PicturesFolder=0x0025,
MyVideos=0x0026,
My_Computer=0x0027,
PictureAppSet=0x0028,
Search=0x0029, # also known as AdvSmartSearch
RecordMediaPlayer=0x002A,
BrowserRefresh=0x002B,
RotateRight=0x002C,
SearchForFiles=0x002D,
MM_SHUFFLE=0x002E,
Sleep=0x002F, # also known as StandBySet
BrowserStop=0x0030,
OneTouchSync=0x0031,
ZoomSet=0x0032,
ZoomBtnInSet2=0x0033,
ZoomBtnInSet=0x0034,
ZoomBtnOutSet2=0x0035,
ZoomBtnOutSet=0x0036,
ZoomBtnResetSet=0x0037,
LeftClick=0x0038,
RightClick=0x0039,
MiddleMouseButton=0x003A,
Back=0x003B,
BackEx=0x003C,
BrowserForward=0x003D,
BrowserForwardEx=0x003E,
HorzScrollLeftSet=0x003F,
HorzScrollRightSet=0x0040,
QuickSwitch=0x0041,
BatteryStatus=0x0042,
ShowDesktop=0x0043,
WindowsLock=0x0044,
FileLauncher=0x0045,
FolderLauncher=0x0046,
GotoWebAddress=0x0047,
GenericMouseButton=0x0048,
KeystrokeAssignment=0x0049,
LaunchProgram=0x004A,
MinMaxWindow=0x004B,
VOLUMEMUTE_NoOSD=0x004C,
New=0x004D,
Copy=0x004E,
CruiseDown=0x004F,
CruiseUp=0x0050,
Cut=0x0051,
Do_Nothing=0x0052,
PageDown=0x0053,
PageUp=0x0054,
Paste=0x0055,
SearchPicture=0x0056,
Reply=0x0057,
PhotoGallerySet=0x0058,
MM_REWIND=0x0059,
MM_FASTFORWARD=0x005A,
Send=0x005B,
ControlPanel=0x005C,
UniversalScroll=0x005D,
AutoScroll=0x005E,
GenericButton=0x005F,
MM_NEXT=0x0060,
MM_PREVIOUS=0x0061,
Do_Nothing_One=0x0062, # also known as Do_Nothing
SnapLeft=0x0063,
SnapRight=0x0064,
WinMinRestore=0x0065,
WinMaxRestore=0x0066,
WinStretch=0x0067,
SwitchMonitorLeft=0x0068,
SwitchMonitorRight=0x0069,
ShowPresentation=0x006A,
ShowMobilityCenter=0x006B,
HorzScrollNoRepeatSet=0x006C,
TouchBackForwardHorzScroll=0x0077,
MetroAppSwitch=0x0078,
MetroAppBar=0x0079,
MetroCharms=0x007A,
Calculator_VKEY=0x007B, # also known as Calculator
MetroSearch=0x007C,
MetroStartScreen=0x0080,
MetroShare=0x007D,
MetroSettings=0x007E,
MetroDevices=0x007F,
MetroBackLeftHorz=0x0082,
MetroForwRightHorz=0x0083,
Win8_Back=0x0084, # also known as MetroCharms
Win8_Forward=0x0085, # also known as AppSwitchBar
)
TASK._fallback = lambda x: 'unknown:%04X' % x
KEY_FLAG = _NamedInts(
reprogrammable=0x10,
FN_sensitive=0x08,
nonstandard=0x04,
is_FN=0x02,
mse=0x01
)

View File

@@ -0,0 +1,325 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import absolute_import, division, print_function, unicode_literals
from time import time as _timestamp
from logging import getLogger, DEBUG as _DEBUG
_log = getLogger(__name__)
del getLogger
from .i18n import _
from .common import NamedInts as _NamedInts, NamedInt as _NamedInt
from . import hidpp10 as _hidpp10
from . import hidpp20 as _hidpp20
_R = _hidpp10.REGISTERS
#
#
#
ALERT = _NamedInts(NONE=0x00, NOTIFICATION=0x01, SHOW_WINDOW=0x02, ATTENTION=0x04, ALL=0xFF)
KEYS = _NamedInts(
BATTERY_LEVEL=1,
BATTERY_CHARGING=2,
BATTERY_STATUS=3,
LIGHT_LEVEL=4,
LINK_ENCRYPTED=5,
NOTIFICATION_FLAGS=6,
ERROR=7,
)
# If the battery charge is under this percentage, trigger an attention event
# (blink systray icon/notification/whatever).
_BATTERY_ATTENTION_LEVEL = 5
# If no updates have been receiver from the device for a while, ping the device
# and update it status accordinly.
# _STATUS_TIMEOUT = 5 * 60 # seconds
_LONG_SLEEP = 15 * 60 # seconds
#
#
#
def attach_to(device, changed_callback):
assert device
assert changed_callback
if device.kind is None:
device.status = ReceiverStatus(device, changed_callback)
else:
device.status = DeviceStatus(device, changed_callback)
#
#
#
class ReceiverStatus(dict):
"""The 'runtime' status of a receiver, mostly about the pairing process --
is the pairing lock open or closed, any pairing errors, etc.
"""
def __init__(self, receiver, changed_callback):
assert receiver
self._receiver = receiver
assert changed_callback
self._changed_callback = changed_callback
# self.updated = 0
self.lock_open = False
self.new_device = None
self[KEYS.ERROR] = None
def __str__(self):
count = len(self._receiver)
return (_("No paired devices.") if count == 0 else
_("1 paired device.") if count == 1 else
(str(count) + _(" paired devices.")))
__unicode__ = __str__
def changed(self, alert=ALERT.NOTIFICATION, reason=None):
# self.updated = _timestamp()
self._changed_callback(self._receiver, alert=alert, reason=reason)
# def poll(self, timestamp):
# r = self._receiver
# assert r
#
# if _log.isEnabledFor(_DEBUG):
# _log.debug("polling status of %s", r)
#
# # make sure to read some stuff that may be read later by the UI
# r.serial, r.firmware, None
#
# # get an update of the notification flags
# # self[KEYS.NOTIFICATION_FLAGS] = _hidpp10.get_notification_flags(r)
#
#
#
class DeviceStatus(dict):
"""Holds the 'runtime' status of a peripheral -- things like
active/inactive, battery charge, lux, etc. It updates them mostly by
processing incoming notification events from the device itself.
"""
def __init__(self, device, changed_callback):
assert device
self._device = device
assert changed_callback
self._changed_callback = changed_callback
# is the device active?
self._active = None
# timestamp of when this status object was last updated
self.updated = 0
def __str__(self):
def _item(name, format):
value = self.get(name)
if value is not None:
return format % value
def _items():
# TODO properly string approximative battery levels
battery_level = self.get(KEYS.BATTERY_LEVEL)
if battery_level is not None:
if isinstance(battery_level, _NamedInt):
yield _("Battery") + ': ' + _(str(battery_level))
else:
yield _("Battery") + ': ' + ('%d%%' % battery_level)
battery_status = _item(KEYS.BATTERY_STATUS, ' (%s)')
if battery_status:
yield battery_status
light_level = _item(KEYS.LIGHT_LEVEL, _("Lighting") + ': %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 set_battery_info(self, level, status, timestamp=None):
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: battery %s, %s", self._device, level, status)
if level is None:
# Some notifications may come with no battery level info, just
# charging state info, so assume the level is unchanged.
level = self.get(KEYS.BATTERY_LEVEL)
else:
assert isinstance(level, int)
# TODO: this is also executed when pressing Fn+F7 on K800.
old_level, self[KEYS.BATTERY_LEVEL] = self.get(KEYS.BATTERY_LEVEL), level
old_status, self[KEYS.BATTERY_STATUS] = self.get(KEYS.BATTERY_STATUS), status
charging = status in (_hidpp20.BATTERY_STATUS.recharging, _hidpp20.BATTERY_STATUS.slow_recharge)
old_charging, self[KEYS.BATTERY_CHARGING] = self.get(KEYS.BATTERY_CHARGING), charging
changed = old_level != level or old_status != status or old_charging != charging
alert, reason = ALERT.NONE, None
if _hidpp20.BATTERY_OK(status) and level > _BATTERY_ATTENTION_LEVEL:
self[KEYS.ERROR] = None
else:
_log.warn("%s: battery %d%%, ALERT %s", self._device, level, status)
if self.get(KEYS.ERROR) != status:
self[KEYS.ERROR] = status
# only show the notification once
alert = ALERT.NOTIFICATION | ALERT.ATTENTION
if isinstance(level, _NamedInt):
reason = 'battery: %s (%s)' % (level, status)
else:
reason = 'battery: %d%% (%s)' % (level, status)
if changed or reason:
# update the leds on the device, if any
_hidpp10.set_3leds(self._device, level, charging=charging, warning=bool(alert))
self.changed(active=True, alert=alert, reason=reason, timestamp=timestamp)
def read_battery(self, timestamp=None):
if self._active:
d = self._device
assert d
if d.protocol < 2.0:
battery = _hidpp10.get_battery(d)
else:
battery = _hidpp20.get_battery(d)
# Really unnecessary, if the device has SOLAR_DASHBOARD it should be
# broadcasting it's battery status anyway, it will just take a little while.
# However, when the device has just been detected, it will not show
# any battery status for a while (broadcasts happen every 90 seconds).
if battery is None and _hidpp20.FEATURE.SOLAR_DASHBOARD in d.features:
d.feature_request(_hidpp20.FEATURE.SOLAR_DASHBOARD, 0x00, 1, 1)
return
if battery is not None:
level, status = battery
self.set_battery_info(level, status)
elif KEYS.BATTERY_STATUS in self:
self[KEYS.BATTERY_STATUS] = None
self[KEYS.BATTERY_CHARGING] = None
self.changed()
def changed(self, active=None, alert=ALERT.NONE, reason=None, timestamp=None):
assert self._changed_callback
d = self._device
# assert d # may be invalid when processing the 'unpaired' notification
timestamp = timestamp or _timestamp()
if active is not None:
d.online = active
was_active, self._active = self._active, active
if active:
if not was_active:
# Make sure to set notification flags on the device, they
# get cleared when the device is turned off (but not when the device
# goes idle, and we can't tell the difference right now).
if d.protocol < 2.0:
self[KEYS.NOTIFICATION_FLAGS] = d.enable_notifications()
# If we've been inactive for a long time, forget anything
# about the battery.
if self.updated > 0 and timestamp - self.updated > _LONG_SLEEP:
self[KEYS.BATTERY_LEVEL] = None
self[KEYS.BATTERY_STATUS] = None
self[KEYS.BATTERY_CHARGING] = None
# Devices lose configuration when they are turned off,
# make sure they're up-to-date.
# _log.debug("%s settings %s", d, d.settings)
for s in d.settings:
s.apply()
if self.get(KEYS.BATTERY_LEVEL) is None:
self.read_battery(timestamp)
else:
if was_active:
battery = self.get(KEYS.BATTERY_LEVEL)
self.clear()
# If we had a known battery level before, assume it's not going
# to change much while the device is offline.
if battery is not None:
self[KEYS.BATTERY_LEVEL] = battery
if self.updated == 0 and active == True:
# if the device is active on the very first status notification,
# (meaning just when the program started or a new receiver was just
# detected), pop-up a notification about it
alert |= ALERT.NOTIFICATION
self.updated = timestamp
# if _log.isEnabledFor(_DEBUG):
# _log.debug("device %d changed: active=%s %s", d.number, self._active, dict(self))
self._changed_callback(d, alert, reason)
# def poll(self, timestamp):
# d = self._device
# if not d:
# _log.error("polling status of invalid device")
# return
#
# if self._active:
# if _log.isEnabledFor(_DEBUG):
# _log.debug("polling status of %s", d)
#
# # read these from the device, the UI may need them later
# d.protocol, d.serial, d.firmware, d.kind, d.name, d.settings, None
#
# # make sure we know all the features of the device
# # if d.features:
# # d.features[:]
#
# # devices may go out-of-range while still active, or the computer
# # may go to sleep and wake up without the devices available
# if timestamp - self.updated > _STATUS_TIMEOUT:
# if d.ping():
# timestamp = self.updated = _timestamp()
# else:
# self.changed(active=False, reason='out of range')
#
# # if still active, make sure we know the battery level
# if KEYS.BATTERY_LEVEL not in self:
# self.read_battery(timestamp)
#
# elif timestamp - self.updated > _STATUS_TIMEOUT:
# if d.ping():
# self.changed(active=True)
# else:
# self.updated = _timestamp()

23
lib/solaar/__init__.py Normal file
View File

@@ -0,0 +1,23 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import absolute_import, division, print_function, unicode_literals
__version__ = '0.9.2'
NAME = 'Solaar'

434
lib/solaar/cli.py Normal file
View File

@@ -0,0 +1,434 @@
#!/usr/bin/env python
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import absolute_import, division, print_function, unicode_literals
import sys
import logging
NAME = 'solaar-cli'
from solaar import __version__
#
#
#
def _fail(text):
if sys.exc_info()[0]:
logging.exception(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(dev_path=None):
from logitech_receiver import Receiver
from logitech_receiver.base import receivers
for dev_info in receivers():
if dev_path is not None and dev_path != dev_info.path:
continue
try:
r = Receiver.open(dev_info)
if r:
return r
except Exception as e:
_fail(str(e))
return r
_fail("Logitech receiver not found")
def _find_device(receiver, name, may_be_receiver=False):
if len(name) == 1:
try:
number = int(name)
except:
pass
else:
if number < 1 or number > receiver.max_devices:
_fail("%s (%s) supports device numbers 1 to %d" % (receiver.name, receiver.path, receiver.max_devices))
dev = receiver[number]
if dev is None:
_fail("no paired device with number %s" % 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 (" USB id : 046d:%s" % receiver.product_id)
print (" Serial :", receiver.serial)
for f in receiver.firmware:
print (" %-11s: %s" % (f.kind, f.version))
print (" Has", paired_count, "paired device(s) out of a maximum of", receiver.max_devices, ".")
from logitech_receiver import hidpp10
notification_flags = hidpp10.get_notification_flags(receiver)
if notification_flags is not None:
if notification_flags:
notification_names = hidpp10.NOTIFICATION_FLAG.flag_names(notification_flags)
print (" Notifications: 0x%06X = %s" % (notification_flags, ', '.join(notification_names)))
else:
print (" Notifications: (none)")
activity = receiver.read_register(hidpp10.REGISTERS.devices_activity)
if activity:
activity = [(d, ord(activity[d - 1:d])) for d in range(1, receiver.max_devices)]
activity_text = ', '.join(('%d=%d' % (d, a)) for d, a in activity if a > 0)
print (" Device activity counters:", activity_text or '(empty)')
def _print_device(dev, verbose=False):
assert dev
state = '' if dev.ping() else 'offline'
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)
print (" Wireless PID :", dev.wpid)
if dev.protocol:
print (" Protocol : HID++ %1.1f" % dev.protocol)
else:
print (" Protocol : unknown (device is offline)")
print (" Polling rate :", dev.polling_rate, "ms")
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 %s." % dev.power_switch_location)
from logitech_receiver import hidpp10, hidpp20, special_keys
if dev.online:
notification_flags = hidpp10.get_notification_flags(dev)
if notification_flags is not None:
if notification_flags:
notification_names = hidpp10.NOTIFICATION_FLAG.flag_names(notification_flags)
print (" Notifications: 0x%06X = %s." % (notification_flags, ', '.join(notification_names)))
else:
print (" Notifications: (none).")
if dev.online:
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: %-22s {%04X} %s" % (index, feature, feature, ', '.join(flags)))
if dev.online:
if dev.keys:
print (" Has %d reprogrammable keys:" % len(dev.keys))
for k in dev.keys:
flags = special_keys.KEY_FLAG.flag_names(k.flags)
print (" %2d: %-26s => %-27s %s" % (k.index, k.key, k.task, ', '.join(flags)))
if dev.online:
battery = hidpp20.get_battery(dev)
if battery is None:
battery = hidpp10.get_battery(dev)
if battery is not None:
from logitech_receiver.common import NamedInt as _NamedInt
level, status = battery
if isinstance(level, _NamedInt):
text = str(level)
else:
text = '%d%%' % level
print (" Battery: %s, %s," % (text, status))
else:
print (" Battery status unavailable.")
else:
print (" Battery status is unknown (device is offline).")
#
#
#
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_receiver import base, hidpp10, status, notifications
receiver.status = status.ReceiverStatus(receiver, lambda *args, **kwargs: None)
# check if it's necessary to set the notification flags
old_notification_flags = hidpp10.get_notification_flags(receiver) or 0
if not (old_notification_flags & hidpp10.NOTIFICATION_FLAG.wireless):
hidpp10.set_notification_flags(receiver, old_notification_flags | hidpp10.NOTIFICATION_FLAG.wireless)
class HandleWithNotificationHook(int):
def notifications_hook(self, n):
assert n
if n.devnumber == 0xFF:
notifications.process(receiver, n)
elif n.sub_id == 0x41 and n.address == 0x04:
if n.devnumber not in known_devices:
receiver.status.new_device = receiver[n.devnumber]
timeout = 20 # seconds
receiver.handle = HandleWithNotificationHook(receiver.handle)
receiver.set_lock(False, timeout=timeout)
print ("Pairing: turn your new device on (timing out in", timeout, "seconds).")
# the lock-open notification may come slightly later, wait for it a bit
from time import time as timestamp
pairing_start = timestamp()
patience = 5 # seconds
while receiver.status.lock_open or timestamp() - pairing_start < patience:
n = base.read(receiver.handle)
if n:
n = base.make_notification(*n)
if n:
receiver.handle.notifications_hook(n)
if not (old_notification_flags & hidpp10.NOTIFICATION_FLAG.wireless):
# only clear the flags if they weren't set before, otherwise a
# concurrently running Solaar app might stop working properly
hidpp10.set_notification_flags(receiver, old_notification_flags)
if receiver.status.new_device:
dev = receiver.status.new_device
print ("Paired device %d: %s [%s:%s:%s]" % (dev.number, dev.name, dev.wpid, dev.codename, dev.serial))
else:
error = receiver.status[status.KEYS.ERROR] or 'no device detected?'
_fail(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_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' [%r]" % (setting.name, value, value))
print ("%s = %s" % (setting.name, result))
#
#
#
def _parse_arguments():
from argparse import ArgumentParser
arg_parser = 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__)
arg_parser.add_argument('-D', '--hidraw', action='store', dest='hidraw_path', metavar='PATH',
help='unifying receiver to use; the first detected receiver if unspecified. Example: /dev/hidraw2')
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()
# Python 3 has an undocumented 'feature' that breaks parsing empty args
# http://bugs.python.org/issue16308
if not 'cmd' in args:
arg_parser.print_usage(sys.stderr)
sys.stderr.write('%s: error: too few arguments\n' % NAME.lower())
sys.exit(2)
if args.debug > 0:
log_level = logging.WARNING - 10 * args.debug
log_format='%(asctime)s,%(msecs)03d %(levelname)8s %(name)s: %(message)s'
logging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format, datefmt='%H:%M:%S')
else:
logging.root.addHandler(logging.NullHandler())
logging.root.setLevel(logging.ERROR)
return args
def main():
_require('pyudev', 'python-pyudev')
args = _parse_arguments()
receiver = _receiver(args.hidraw_path)
args.cmd(receiver, args)
if __name__ == '__main__':
main()

133
lib/solaar/configuration.py Normal file
View File

@@ -0,0 +1,133 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
import os as _os
import os.path as _path
from json import load as _json_load, dump as _json_save
from logging import getLogger, DEBUG as _DEBUG, INFO as _INFO
_log = getLogger(__name__)
del getLogger
_XDG_CONFIG_HOME = _os.environ.get('XDG_CONFIG_HOME') or _path.expanduser(_path.join('~', '.config'))
_file_path = _path.join(_XDG_CONFIG_HOME, 'solaar', 'config.json')
from solaar import __version__
_KEY_VERSION = '_version'
_KEY_NAME = '_name'
_configuration = {}
def _load():
if _path.isfile(_file_path):
loaded_configuration = {}
try:
with open(_file_path, 'r') as config_file:
loaded_configuration = _json_load(config_file)
except:
_log.error("failed to load from %s", _file_path)
# loaded_configuration.update(_configuration)
_configuration.clear()
_configuration.update(loaded_configuration)
if _log.isEnabledFor(_DEBUG):
_log.debug("load => %s", _configuration)
_cleanup(_configuration)
_configuration[_KEY_VERSION] = __version__
return _configuration
def save():
# don't save if the configuration hasn't been loaded
if _KEY_VERSION not in _configuration:
return
dirname = _os.path.dirname(_file_path)
if not _path.isdir(dirname):
try:
_os.makedirs(dirname)
except:
_log.error("failed to create %s", dirname)
return False
_cleanup(_configuration)
try:
with open(_file_path, 'w') as config_file:
_json_save(_configuration, config_file, skipkeys=True, indent=2, sort_keys=True)
if _log.isEnabledFor(_INFO):
_log.info("saved %s to %s", _configuration, _file_path)
return True
except:
_log.error("failed to save to %s", _file_path)
def _cleanup(d):
# remove None values from the dict
for key in list(d.keys()):
value = d.get(key)
if value is None:
del d[key]
elif isinstance(value, dict):
_cleanup(value)
def _device_key(device):
return '%s:%s' % (device.wpid, device.serial)
class _DeviceEntry(dict):
def __init__(self, *args, **kwargs):
super(_DeviceEntry, self).__init__(*args, **kwargs)
def __setitem__(self, key, value):
super(_DeviceEntry, self).__setitem__(key, value)
save()
def _device_entry(device):
if not _configuration:
_load()
device_key = _device_key(device)
c = _configuration.get(device_key) or {}
if not isinstance(c, _DeviceEntry):
c[_KEY_NAME] = device.name
c = _DeviceEntry(c)
_configuration[device_key] = c
return c
def attach_to(device):
"""Apply the last saved configuration to a device."""
if not _configuration:
_load()
persister = _device_entry(device)
for s in device.settings:
if s.persister is None:
s.persister = persister
assert s.persister == persister

90
lib/solaar/gtk.py Normal file
View File

@@ -0,0 +1,90 @@
#!/usr/bin/env python
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import absolute_import, division, print_function, unicode_literals
from solaar import __version__, NAME
import solaar.i18n as _i18n
#
#
#
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('-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,%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
logging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format, datefmt='%H:%M:%S')
else:
logging.root.addHandler(logging.NullHandler())
logging.root.setLevel(logging.ERROR)
if logging.root.isEnabledFor(logging.INFO):
logging.info("language %s (%s), translations path %s", _i18n.language, _i18n.encoding, _i18n.path)
return args
def main():
_require('pyudev', 'python-pyudev')
_require('gi.repository', 'python-gi')
_require('gi.repository.Gtk', 'gir1.2-gtk-3.0')
_parse_arguments()
# handle ^C in console
import signal
signal.signal(signal.SIGINT, signal.SIG_DFL)
try:
import solaar.ui as ui
ui.init()
import solaar.listener as listener
listener.setup_scanner(ui.status_changed, ui.error_dialog)
listener.start_all()
# main UI event loop
ui.run_loop()
listener.stop_all()
except Exception as e:
import sys
sys.exit('%s: error: %s' % (NAME.lower(), e))
if __name__ == '__main__':
main()

64
lib/solaar/i18n.py Normal file
View File

@@ -0,0 +1,64 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import absolute_import, division, print_function, unicode_literals
from solaar import NAME as _NAME
#
#
#
def _find_locale_path(lc_domain):
import os.path as _path
import sys as _sys
prefix_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..'))
src_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..', 'share'))
del _sys
from glob import glob as _glob
for location in prefix_share, src_share:
mo_files = _glob(_path.join(location, 'locale', '*', 'LC_MESSAGES', lc_domain + '.mo'))
if mo_files:
return _path.join(location, 'locale')
# del _path
import locale
locale.setlocale(locale.LC_ALL, '')
language, encoding = locale.getlocale()
del locale
_LOCALE_DOMAIN = _NAME.lower()
path = _find_locale_path(_LOCALE_DOMAIN)
import gettext as _gettext
_gettext.bindtextdomain(_LOCALE_DOMAIN, path)
_gettext.textdomain(_LOCALE_DOMAIN)
_gettext.install(_LOCALE_DOMAIN)
try:
unicode
_ = lambda x: _gettext.gettext(x).decode('UTF-8')
except:
_ = _gettext.gettext

311
lib/solaar/listener.py Normal file
View File

@@ -0,0 +1,311 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import absolute_import, division, print_function, unicode_literals
from logging import getLogger, INFO as _INFO
_log = getLogger(__name__)
del getLogger
from solaar.i18n import _
from . import configuration
from logitech_receiver import (
Receiver,
listener as _listener,
status as _status,
notifications as _notifications
)
#
#
#
from collections import namedtuple
_GHOST_DEVICE = namedtuple('_GHOST_DEVICE', ('receiver', 'number', 'name', 'kind', 'status', 'online'))
_GHOST_DEVICE.__bool__ = lambda self: False
_GHOST_DEVICE.__nonzero__ = _GHOST_DEVICE.__bool__
del namedtuple
def _ghost(device):
return _GHOST_DEVICE(
receiver=device.receiver,
number=device.number,
name=device.name,
kind=device.kind,
status=None,
online=False)
#
#
#
# how often to poll devices that haven't updated their statuses on their own
# (through notifications)
# _POLL_TICK = 5 * 60 # seconds
class ReceiverListener(_listener.EventsListener):
"""Keeps the status of a Receiver.
"""
def __init__(self, receiver, status_changed_callback):
super(ReceiverListener, self).__init__(receiver, self._notifications_handler)
# no reason to enable polling yet
# self.tick_period = _POLL_TICK
# self._last_tick = 0
assert status_changed_callback
self.status_changed_callback = status_changed_callback
_status.attach_to(receiver, self._status_changed)
def has_started(self):
if _log.isEnabledFor(_INFO):
_log.info("%s: notifications listener has started (%s)", self.receiver, self.receiver.handle)
notification_flags = self.receiver.enable_notifications()
self.receiver.status[_status.KEYS.NOTIFICATION_FLAGS] = notification_flags
self.receiver.notify_devices()
self._status_changed(self.receiver) #, _status.ALERT.NOTIFICATION)
def has_stopped(self):
r, self.receiver = self.receiver, None
assert r is not None
if _log.isEnabledFor(_INFO):
_log.info("%s: notifications listener has stopped", r)
# because udev is not notifying us about device removal,
# make sure to clean up in _all_listeners
_all_listeners.pop(r.path, None)
r.status = _("The receiver was unplugged.")
if r:
try:
r.close()
except:
_log.exception("closing receiver %s" % r.path)
self.status_changed_callback(r) #, _status.ALERT.NOTIFICATION)
# def tick(self, timestamp):
# if not self.tick_period:
# raise Exception("tick() should not be called without a tick_period: %s", self)
#
# # not necessary anymore, we're now using udev monitor to watch for receiver status
# # if self._last_tick > 0 and timestamp - self._last_tick > _POLL_TICK * 2:
# # # if we missed a couple of polls, most likely the computer went into
# # # sleep, and we have to reinitialize the receiver again
# # _log.warn("%s: possible sleep detected, closing this listener", self.receiver)
# # self.stop()
# # return
#
# self._last_tick = timestamp
#
# try:
# # 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
#
# self.receiver.status.poll(timestamp)
#
# # Iterating directly through the reciver would unnecessarily probe
# # all possible devices, even unpaired ones.
# # Checking for each device number in turn makes sure only already
# # known devices are polled.
# # This is okay because we should have already known about them all
# # long before the first poll() happents, through notifications.
# for number in range(1, 6):
# if number in self.receiver:
# dev = self.receiver[number]
# if dev and dev.status is not None:
# dev.status.poll(timestamp)
# except Exception as e:
# _log.exception("polling", e)
def _status_changed(self, device, alert=_status.ALERT.NONE, reason=None):
assert device is not None
if _log.isEnabledFor(_INFO):
if device.kind is None:
_log.info("status_changed %s: %s, %s (%X) %s", device,
'present' if bool(device) else 'removed',
device.status, alert, reason or '')
else:
_log.info("status_changed %s: %s %s, %s (%X) %s", device,
'paired' if bool(device) else 'unpaired',
'online' if device.online else 'offline',
device.status, alert, reason or '')
if device.kind is None:
assert device == self.receiver
# the status of the receiver changed
self.status_changed_callback(device, alert, reason)
return
assert device.receiver == self.receiver
if not device:
# Device was unpaired, and isn't valid anymore.
# We replace it with a ghost so that the UI has something to work
# with while cleaning up.
_log.warn("device %s was unpaired, ghosting", device)
device = _ghost(device)
self.status_changed_callback(device, alert, reason)
if not device:
# the device was just unpaired, need to update the
# status of the receiver as well
self.status_changed_callback(self.receiver)
def _notifications_handler(self, n):
assert self.receiver
# if _log.isEnabledFor(_DEBUG):
# _log.debug("%s: handling %s", self.receiver, n)
if n.devnumber == 0xFF:
# a receiver notification
_notifications.process(self.receiver, n)
return
# a device notification
assert n.devnumber > 0 and n.devnumber <= self.receiver.max_devices
already_known = n.devnumber in self.receiver
if not already_known and n.sub_id == 0x41:
dev = self.receiver.register_new_device(n.devnumber, n)
else:
dev = self.receiver[n.devnumber]
if not dev:
_log.warn("%s: received %s for invalid device %d: %r", self.receiver, n, n.devnumber, dev)
return
if not already_known:
if _log.isEnabledFor(_INFO):
_log.info("%s triggered new device %s (%s)", n, dev, dev.kind)
# If there are saved configs, bring the device's settings up-to-date.
# They will be applied when the device is marked as online.
configuration.attach_to(dev)
_status.attach_to(dev, self._status_changed)
# the receiver changed status as well
self._status_changed(self.receiver)
assert dev
assert dev.status is not None
_notifications.process(dev, 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
if _log.isEnabledFor(_INFO):
_log.info("%s: pairing detected new device", self.receiver)
self.receiver.status.new_device = dev
elif dev:
if dev.online is None:
dev.ping()
def __str__(self):
return '<ReceiverListener(%s,%s)>' % (self.receiver.path, self.receiver.handle)
__unicode__ = __str__
#
#
#
# all known receiver listeners
# listeners that stop on their own may remain here
_all_listeners = {}
def _start(device_info):
assert _status_callback
receiver = Receiver.open(device_info)
if receiver:
rl = ReceiverListener(receiver, _status_callback)
rl.start()
_all_listeners[device_info.path] = rl
return rl
_log.warn("failed to open %s", device_info)
def start_all():
# just in case this it called twice in a row...
stop_all()
if _log.isEnabledFor(_INFO):
_log.info("starting receiver listening threads")
for device_info in _base.receivers():
_process_receiver_event('add', device_info)
def stop_all():
listeners = list(_all_listeners.values())
_all_listeners.clear()
if listeners:
if _log.isEnabledFor(_INFO):
_log.info("stopping receiver listening threads %s", listeners)
for l in listeners:
l.stop()
configuration.save()
if listeners:
for l in listeners:
l.join()
# stop/start all receiver threads on suspend/resume events, if possible
from . import upower
upower.watch(start_all, stop_all)
from logitech_receiver import base as _base
_status_callback = None
_error_callback = None
def setup_scanner(status_changed_callback, error_callback):
global _status_callback, _error_callback
assert _status_callback is None, 'scanner was already set-up'
_status_callback = status_changed_callback
_error_callback = error_callback
_base.notify_on_receivers_glib(_process_receiver_event)
# receiver add/remove events will start/stop listener threads
def _process_receiver_event(action, device_info):
assert action is not None
assert device_info is not None
assert _error_callback
if _log.isEnabledFor(_INFO):
_log.info("receiver event %s %s", action, device_info)
# whatever the action, stop any previous receivers at this path
l = _all_listeners.pop(device_info.path, None)
if l is not None:
assert isinstance(l, ReceiverListener)
l.stop()
if action == 'add':
# a new receiver device was detected
try:
_start(device_info)
except OSError:
# permission error, ignore this path for now
_error_callback('permissions', device_info.path)

183
lib/solaar/ui/__init__.py Normal file
View File

@@ -0,0 +1,183 @@
# -*- python-mode -*-
# -*- coding: UTF-8 -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import absolute_import, division, print_function, unicode_literals
from logging import getLogger, DEBUG as _DEBUG, INFO as _INFO
_log = getLogger(__name__)
del getLogger
from gi.repository import GLib, Gtk
from solaar.i18n import _
#
#
#
assert Gtk.get_major_version() > 2, 'Solaar requires Gtk 3 python bindings'
GLib.threads_init()
def _init_application():
APP_ID = 'io.github.pwr.solaar'
app = Gtk.Application.new(APP_ID, 0)
# not sure this is necessary...
# app.set_property('register-session', True)
registered = app.register(None)
dbus_path = app.get_dbus_object_path() if hasattr(app, 'get_dbus_object_path') else APP_ID
if _log.isEnabledFor(_INFO):
_log.info("application %s, registered %s", dbus_path, registered)
# assert registered, "failed to register unique application %s" % app
# if there is already a running instance, bail out
if app.get_is_remote():
# pop up the window in the other instance
app.activate()
raise Exception("already running")
return app
application = _init_application()
#
#
#
def _error_dialog(reason, object):
_log.error("error: %s %s", reason, object)
if reason == 'permissions':
title = _("Permissions error")
text = _("Found a Logitech Receiver (%s), but did not have permission to open it.") % object + \
'\n\n' + \
_("If you've just installed Solaar, try removing the receiver and plugging it back in.")
elif reason == 'unpair':
title = _("Unpairing failed")
text = _("Failed to unpair %s from %s.") % (object.name, object.receiver.name) + \
'\n\n' + \
_("The receiver returned an error, with no further details.")
else:
raise Exception("ui.error_dialog: don't know how to handle (%s, %s)", reason, object)
assert title
assert text
m = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, text)
m.set_title(title)
m.run()
m.destroy()
def error_dialog(reason, object):
assert reason is not None
GLib.idle_add(_error_dialog, reason, object)
#
# A separate thread is used to read/write from the device
# so as not to block the main (GUI) thread.
#
try:
from Queue import Queue
except ImportError:
from queue import Queue
_task_queue = Queue(16)
del Queue
from threading import Thread, current_thread as _current_thread
def _process_async_queue():
t = _current_thread()
t.alive = True
while t.alive:
function, args, kwargs = _task_queue.get()
if function:
function(*args, **kwargs)
if _log.isEnabledFor(_DEBUG):
_log.debug("stopped")
_queue_processor = Thread(name='AsyncUI', target=_process_async_queue)
_queue_processor.daemon = True
_queue_processor.alive = False
_queue_processor.start()
del Thread
def async(function, *args, **kwargs):
task = (function, args, kwargs)
_task_queue.put(task)
#
#
#
from . import notify, tray, window
def init():
notify.init()
tray.init(lambda _ignore: window.destroy())
window.init()
def run_loop():
def _activate(app):
assert app == application
if app.get_windows():
window.popup()
else:
app.add_window(window._window)
def _shutdown(app):
# stop the async UI processor
_queue_processor.alive = False
async(None)
tray.destroy()
notify.uninit()
application.connect('activate', _activate)
application.connect('shutdown', _shutdown)
application.run(None)
#
#
#
from logitech_receiver.status import ALERT
def _status_changed(device, alert, reason):
assert device is not None
if _log.isEnabledFor(_DEBUG):
_log.debug("status changed: %s (%s) %s", device, alert, reason)
tray.update(device)
if alert & ALERT.ATTENTION:
tray.attention(reason)
need_popup = alert & (ALERT.SHOW_WINDOW | ALERT.ATTENTION)
window.update(device, need_popup)
if alert & ALERT.NOTIFICATION:
notify.show(device, reason)
def status_changed(device, alert=ALERT.NONE, reason=None):
GLib.idle_add(_status_changed, device, alert, reason)

Some files were not shown because too many files have changed in this diff Show More