Solaar-0.9.2/000077500000000000000000000000001217372044600127515ustar00rootroot00000000000000Solaar-0.9.2/.gitignore000066400000000000000000000002361217372044600147420ustar00rootroot00000000000000*.pyc *.pyo __pycache__/ *.log *.mo /lib/Solaar.egg-info/ /build/ /sdist/ /dist/ /deb_dist/ /MANIFEST /docs/captures/ /share/logitech_icons/ /share/locale/ Solaar-0.9.2/COPYING000066400000000000000000000432541217372044600140140ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 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 The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free 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 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 price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any 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 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this 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 themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering 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 void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot 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 may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes 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 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 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 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 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 free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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. 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 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. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice 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 Lesser General Public License instead of this License. Solaar-0.9.2/COPYRIGHT000066400000000000000000000000731217372044600142440ustar00rootroot00000000000000Copyright 2012, 2013 Daniel Pavel Solaar-0.9.2/ChangeLog000066400000000000000000000045361217372044600145330ustar00rootroot000000000000000.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. Solaar-0.9.2/MANIFEST.in000066400000000000000000000001531217372044600145060ustar00rootroot00000000000000include COPYRIGHT COPYING README.md ChangeLog recursive-include rules.d * recursive-include share/locale * Solaar-0.9.2/README000077700000000000000000000000001217372044600151032README.mdustar00rootroot00000000000000Solaar-0.9.2/README.md000066400000000000000000000067211217372044600142360ustar00rootroot00000000000000**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. Solaar-0.9.2/bin/000077500000000000000000000000001217372044600135215ustar00rootroot00000000000000Solaar-0.9.2/bin/solaar000077500000000000000000000027501217372044600147340ustar00rootroot00000000000000#!/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') # 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() Solaar-0.9.2/bin/solaar-cli000077500000000000000000000025651217372044600155050ustar00rootroot00000000000000#!/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() Solaar-0.9.2/docs/000077500000000000000000000000001217372044600137015ustar00rootroot00000000000000Solaar-0.9.2/docs/20121210110342697.pdf000066400000000000000000005320701217372044600162140ustar00rootroot00000000000000%PDF-1.4 % 4 0 obj <> stream JFIF,,C 2!=,.$2I@LKG@FEPZsbPUmVEFdemw{N`}s~|C;!!;|SFS|||||||||||||||||||||||||||||||||||||||||||||||||| " }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?𢁠[`sج נ((((((PEPEPEPEPEPc=h-((1EPEPEPEPEPEP`QZ((((( ( ( ( ( ( ( ( ( KE&)hP@(((((((((( ( ( ( ( ( ( ( ( (KEQEQEQEQEQEQEQEQE&(((((((((((((((((((((((((((((((((((((PEPEPEPEPEPEPEPEPEPEPEQEQEQEQEQEQERw(((((((((((((((((()1KEQEbLQZ(0=( Z(6ؿ:Mv`zR@ اGFPmR@)0=)h h{jf((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((+c<~!jWtC5@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@~DԬv N ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((/ß?6Jy?6J(((((((((((((((((((((((((((((((((((((((xQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEᱍ#}R9 XޓCjԠ(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((( ڵ++_?6Z(((((((((((EѻQ@88HҀEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPW cjլ F=A֠(((((((((((((((((((((((((((((((()H:(((((()I:(((((((((((((((((((((Lh(((((((((((((((((((((((((((( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (2|21C֬ a$f((((((((((((((((((((((((((((( #5Rg GQSAuɠF욊k֙ % ;NW&9e Q9a:Iݖrq* ^9'&iV*2rM?ZAEFҁ~cU^HE(*0rؽE GCU*O"F.NȵEPHQETfdjnoiQERI.'\EV[lE.J1rصEU{WX=hYa=ZM;x8RA2ʹ{ҹ-QA!EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPH-QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE,ʹ?\Rq@ EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPG??묟z̓?ѭz(((((((((((((((((((((((((((((<}5j PU}*k22.ߧZ3!ܩniu:c(OSU wJ}Ny'D<߽Uh#L,FAb;c+Ko9]<IJxJ\,"ʁqCTOLQz2F`A܎ΰ]qW)03s@)S6kvd<:ӚBzz @AA~?]I !$[ p+@9(8>d+A39bTN ^,UA4{OGZYHj:w[0(2 #oGPN8QAP+%0\`^[6L W(jm;'cAQ7P5~Ls'Gp*6d`6O|օ U؎8#l%@4PBAEPHQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEო+H?Zc2A<((((((((((((((((((((((((((((((*+y?,Ҭ) qpZ`AA L~${UcgsVh aEPfQEQEQES]8'V7>b5 &'Ty pjQ-QAQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE&G?:\Q@ E&E-QEQEQEQEQEQER1ڤ VtQE ( ( ( ( ( ( ( L□QC)恫75^o1vE-[ӮFh+rQ[&>1Š(ALF)uȠkr;iI ⦪Zs| & 7^qV(%(AEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP/NtGÏֶ˜ϟ_VQEQEQEQEQHsڀ(((((((((((((((((((((*)H.~ց۲%ܛ&:Š(AEP{e]eOCUW4kB IEYQEaEPEPEPEPIKEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEI`*J((x/DkZ@l,m [BV-"+ϵJ4&Sv:n=+u&Ȗ&S0mOT.p%qRve)Kg Ţ((((*e]0]4M&0RvHQԊQF4 2)hECdϖNZvbc>h5˲T4JIiQMFquAEPE򿊁Rl] +f5m1w7zQoAZè($ʚ2שs*+JTlW)sj6bsl :TmnR;|9g s7bőXO(jv^:}E[de*Û{eÊ;3 ;OltJ)R+oZx5:yc]eX'Ys85+Ȉ2`Ӱ*8Pg=Ey`yGF r A4B "HcH^+n*H=A^ƅQ-?v㍫Zbf͸(IecyBgiē. <{ e3ޞ*lj}#֦8Р@5n]P=b(#>s;ɹU5jHQF u(1MQA!T-ܸ/9>v ]ѕ50CT s>c4:{)R*FD;N\]ċ%RE7wr&6}yhm1 agI%rTPϹp$G*95%P QEHIdKEn,H#)}Zb!GsvojE{Y}EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEso[c +<_PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEQsiɨ {;Ee,H BOk~*/' ?,Sͮ8!@X-CI3 D\eG5GN?ZHHǞ(^D\tW,`sGh>ytzA}*ʅ@ZμᵚTyrNj-;I$}3 cր7袊@QMwXԳ@ƅ fm6?J~dq5~($ڃ%QQA)QLQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@&-QEQEQEQE(ALR wmGB(54-!bYY 0 U47TZ@_܍!U>%PCmQE ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (9{O7?B (((((((((((((((() ɥ`H=ciWr\aITu\:A]E6ERM} 7pڹ>l1Xus<UTzvq-XT/SNk, }99Z:fms`%b`Oj\6gFoLժ⯬%,VSu:Lq,.A'fC5'+`H=`k"{ U"EJ=!qd"Rp*/dby.%1*P0PZn68$ S袃-Š((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((" #dnSkKco}*P9I݅Q@(V@|I@`(QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQExC?eS!] s[o+((((((((((((((((*kl-g04Lj$<ti.9Cli!eX@5ɪNsT` }xB;FnLh_ص)Rr)$fhYE$AV4mC'r] ޠlf MxE~[4EK7AQ4e%$@k}PP"bz{۬MCP @_3ZCM(QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQES<<>FA₢rQA"dgjW7;Iky@O^ zQ@Q@!8KQ\kwhWv$#Z` TQv (B(((Jy%jEP ( ڟ X YPI!1Ulix9'4]Ϊ2 D_EP ((((((((((((((.È*s.egq!2%~sO{(uZzrRiQLW ( ( ( ( ( ( ( ( ( ( ( ( ( (9{O;|=.[ ((((((((((((((((ӮTw ER?Jt؊\zI+/꫘aAI=ZCL2YfqJ^r3jAdѩ^Cryqlc&]_PGfQ NN z3'ԫ=[O{N؁&@HtZ@r:gR`~ۭyzz{y}]O嚭+L>Vr8vq|k~Ah@tTQE ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ϋ_+BfPkK4( :£") w\}Lfz?,tQ 2;BAj1_`jA5 (0(a#֖`v"~Tr&^RJϽD~$ QE ( _*.:EBE|U%+I 1jeJ6_9?$]çjWPU YkvjY Iok;6fsAqMo0=1-V]ç$[R$PfyıEG XqrP$8ȠJnȵED."#;9eFVcAEPEP &ɷ8SRCr9fA;$ =[5<0tf=qI3PKf84*1asBC#  kd~uG:s0ȁj ns$?)REU. ={PCNq`:dVt{Wp#55| ;P\\EC4:j1{'޴&TUH Τks>"C>!uqkF?F!¸'Ң@A4Hq,v\Ϊ2F1'ӥ(fIUKx%`7ɤSa#шD5bZyʑRFOYqi\*in}.1gҠRkźAq4Ȳ eiB)8T\O&LQh6*6xU` @Ţ($((((((((((#AI]s?tTQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQERE-ۡrnXMt}wĐo$A?κ:l2KtkF}w3QKvi@{9 ~q,grX1Yr4JZUx$H84'gmYիx C?x!Wu%1p:(Y 3a٧YT;}f_,~^Oȧ=4[U]Eىܻ$+Ѫ_ '|gӑRI5NLUzd5Ct)$P(R fVI_̙ڲ_(@oE((((((4PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPHzqKE ZZ(((((((((((((((((((((((((((((@00:RU5D 5n4r̅[CRU53Vz :56SlmUiJL0^wNQ)+YGU8[IڐLr\xPI_sbX4#Ef7 ejNjWvylG:Pd'm0g+b<jp4Ļsb2I= ,H rVr Ңxr JѬ`ڑ^Bfr*~jUe s`w \,/dhN2i~?#Y5 J51=(\9uow$̪5] KAi(JeV*OqOZ~AӲcԄ9ϹB~e9W ZUWPϡ)[az+zedg6LUI*re[ 9:U:QA|MUNv($W6wlOtQOүA{R(O)Rj?OsV(ROW1z=3M}=b5r'ܨXeFWQA.n[QA!EP%K-pnZт(p,MTZ_RGZ잍 5vqV9K(921j@ҝE)[DXeIS`.1u{X!$4o{(+ϹVy7R.Vh^W!6BZć!s榢s˹Zk$PR-cZE1VMqUM씭]i`3Y$a)F$r))nJeH⢴⍄9Q@U%!5d3E\ݬʍo$wjyQ*(%ɵanRzD]E]Xl,{c2]c`xZP( ((((((((((>]@<(((((((((((((((((MO ;m5qfV?RzV6[>RY:Tk5z@T]tjqGh?@Q@Q@`ڮ2\ӗ`G5YZEshCqš$_?Vqk@#ӎ?((kr@'6)wE"Si,gQEduwr&r.GE((((M;oӂIEPEPEPEPEPEPEPEPEP\ψo3QG󮚹u_*@8ҿk^JLvDa@(((yCp <FAȠvbE( QEqʍ/JZ0H?Ju ()Cp{#v\7!c֬ZB 9#AkEQ@ݖ[w*pqڢi"!H5bP lLUpEW(@>`L<ÜPvl$)Q1E1XX✬C)4E(((((((((((((((((((((((((L-!=h(((((((((((LzZ(3P=x(((((((((((IH0 6+x9EԴP>gk\(QEQEQEQEQEQEQEQERt+i{n ۸zM-}.B֓4hg8QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEx8S(9_#T>/j(((((((((((((((((([Ns+E jW9fb&?[tcx=֑>9Y:d٬?g(>VǶh@Sdvmr2O_\U][v%˞dnyFqZZ6 ̸PutTsM$GsYĻп#PmyAJ w7VrHk0<9\tnDUy AHR"m͑Ҵ4kۛY<8$*{ ڱkzuP3RI"DuU*f-KP{R =hx|2+LIYeYi#`Ҩhڤt \X9=1ڀ: +[{ي}x5H(]K*5 )h?(I$W?zhizDJK``* pv?hאO"ȃQY*2" 9 Wzԓ+-cq\Vwg*ya֓Fӣ&`{()V 2MIEsa~~&#S3iΑѹ_\hwc܏4YےPJF^+cDb. SR 5Rk/Bx^BT#χYaV9c$pM_= ˺-RuzW51ULU9pAk6daER*23KEQEQEQEQEQEQEQEQEQEſXWQ\Ơ;yt袊@QEQE$X,qOn^=ߥԣmK!~,mpYKm#Er[=Q`6x4SwI"ƻUsyk)oxCʜ֩ l6Þ&ȧ8ݽKh$af{9*% R {bjG=ҟ|_85 xn<ի!JC^TNJA`"+whD E,qT"FQm&ndPr}jl.Y`%9;K:bYYk)ƲѸX`OwRvPz%ۏ*1us .%9mJ=Ŏl8 p)47 0)hTY!9<ʽ֡ˬ؜+JHɃ3RYizu4n(EPEPEPEPEPEPEPEPEPEP\֣s/3y}i<WS@tQE ( ( KQd;*!˕ӝH#Ӭs_jF׽p)@=Fih B20ih*c<Ө ( ( ( ( (U>() EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPW >IoKlmOԒ((((((((((((((((((((((((((((((((((((((((((((<uf ]j<٫(((((((((((((((((=r`q qW&~]r?X)QEQEQEQEQEVhe={bj7Prt҄y[Ne]w:B 2'BXHcXU' cZZ@QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEkeW72ֶMQELZRUw/n`@Ţ(QE|;Őw"շ(afjItԖOGu⑼i,QE((**qwb d ISP Y(QEQEQEQUc@[رUo7_JXnkTҮ}E$%qy)=zQ8Zz]94Nm4Scq$j*/SA TgVu^) dY7€*7,MVhfQE ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ('4Z\IrNBURƭ\( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (9gt_z"ys- ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (Zpc|}of3H?V)QEQEQHN:, O4( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ._5+U50QMQEB2CMiTq!c*Pg)9;($((>ԴQEQEQEQEQEQEQEQEQEQEQEQEQER2aKEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE*$q>p.T{f$&Š((((((((((((((((((((((((((((((((((((((((((((Oo8WG\#_Һ(((((((((((((((((¬:uWR#]MQE (2 F2AYSkwF:/_ d^ Y,đ5xm4gHF7=遏 jANLqU9렮 r`֗t˧Ws[Ճ;4G1!QOm>?IHަwV)QAQvwo' KT25i4QEQ@Q@^Ճf'jX}6ڬA~VRAZ4r ($tQE+*ZgQA[ym2m6#Tkbu AA^T:(>FA8zv1u5(AAm[ EP@QEQEQH4QEQEQEQEQEQH̨bM8C J["`giQFq@ yc4r2*-lO+V@75Jj;qjNHh&Q:((((((((((((((((((((((((((((((((((}D=gAus9?҃ZZ^]01KEQ@Q@Q@Q@Q@Q@b ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (9 5:}*+¿wPoPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP'q:*ҺDJj_-~ ki|߮) L6K/r3U,C4n>Ӊ#q+Ee@C55] );hYz֡{WH$r@p=kS얼ؒ  Ѐm̰N' w$M+íiGÑ/pGJX68>>^&<9ٮ_of JHH&28"zdxM¡&({iXrEu?R{G⡞h2]GIYCG}I.m~b_ s AFt`>Rt>Xu; q (s3N?IKjHFCZk}1}xBLˏ 5"p) 8V[(>PN0lȠ=yU+UJSAߕHl=8J؎H=xu'q*JIԯpcw#hPjQ 8b>^H (V,2!hO'8@]l`jP4k#AN%HxβwxcҶa nDP@lN0 c9?T98uSn}j X^@5;h w)fKweu/M]ӼLet@o[`N؏DSެiWbBvչ UXqo 䌎}*g,Y 岒G\#P]ro(3fXQ]y &TIV-Մ@IM+`p¡Hnx`55k4N 2]G33[kLM?Mn6=ҤKH( Pe/OsWjV8qV4aMvڌރ4s, ƛA1Wv%RVM\VMsVɋnf'QsIQz6f(VOZ{0QqIïkMH%#9F=kTeFW9iE s0@уFky@v.?r8+³3s@r ~WFz"|?k+_$XF2A3ӽVl Ƞ(QYo3wWn|:MuOƷ|߿j7cSf,V41RXipK,3&z@1G +)b(;Z)FT8!\Mt4PbhvbGB:H& xm-vJ8jMOEVX!nF3REQEQEQEQEQE&sZZ(B3KEA+LS 玠(t+X_t9V!PLӕU*v(((+@+~Jh(EP[ϔ#n@ޢ}jH.NxQEmWeۯ~j?UB Uf5<•+,cxn҃y%'w|;+0iQdLŌS[CǂrLI rZ*`.AdAASc *8<2*3`7J`k5 fXAסeH"eA8OA.Obj 6&jњ1`CQ#K47՗?g8j+fBKL͚qKb@01KAQE^76:j #A`y^# ުZ[<?Ae5rKm,SmlYe'${.*x4+(VDG?>Ԑ鶐6R3VQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@GRVzgn)$YCƀEPEPEPEPEPEPE! u }ih((Hyzʜ+zu)utSS_:Q@][¹RE-+A%(3H$ Z(((IJ0뚅laW݌zE)I+& `5#(Wh)THju(((((((((((((?2PH8SLUEh),71+!Вqlrh>N0+XpCojr9oSS*Nl(((*1x?JHJ=h.d6 ^F0EG9\H 4ݛ379jX_4T0-3;Š((((((((((((((((((((((((((,jYxv>(((KB\ӆeɭ |V (((((((((((((((((((((((((((((((((((((((((((((s3:ج 3ֶ(((((((((((((((((('_8LVw.#򼞲)ݏlm80,E(Rp5">3-(.l&OuT@=X杀ޢY$DuƪBsc֢vd⣒(2u9Elu 1\6OjYln,Olҋ4s$áAO#11=BG?kRPLgmM@Sh݊a@ou{Gك# 5-er8 QEQEQEQXkrG5È98 ( 51A5r9$7@HL.{qkV74IlEPEP"ʮt@1k TzO(栻u J1t\F_?ֺl(EPEP?`h| MnH۞> ^C%ISH0:(Q@aGT"+4.O?jh #]TGEˤ1Bm9U@QEWIS. f/҂QESd1j}kQ@Q@Q@ uY]A^~[M(B((((<zSjffAn+( ( Fӎ)YL9 'je4WN5AUfT3\8'>YP:4r:4aD?iFg֖RCZQEQEQEb((((((((((((((((((((((((((((((((((BQ@[ $n(QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQH((((((((((((((((((((((((((((? ?O +#_ ϬU@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ح,uZ.HW\@[dX1_@қQIH}ˌVYfPW>?֥KHV*KwE>U"$v9;W0/*(84ș[a#%@S:@8rvfi $B&,jCe}#4qW5SO9mOȩ.Moω0{c0/hmtrJ*O=0MQhKʙ:VS0 t2'U}=cOX3zFeS+gzWqYN`mK~L7ȇSxv a+Kw<_ΰ,%'L:wɮ]84(ǻTңݪIN%Pd˨Ժ ̠LtaY3ŐTsUmːZ\O KxZY[j(5mY1x}Hyq l?"&w8X2zKY>wk&V*>V 1uBV#1jEs/}jf6 =Gz+ҘQ]H2lؒ3m}LJǯ'\VM\cy=v[@av,,Z<8+sY<>.o,'rIax&5*E9c=$I vUF:Y>U9@yޫ[hR2,ހ |:. rё,^}?2vQgFs@&G^Ti<8 f \ypsBN#,ild*Fsi%=FbU䷕fsԁӄS| $S"kg,\, rUEg>dcsU`+x)A Qt>q KFqVȮuÎHBbVVܩi0Vh߃*IPgpzRk4BF\(7rb$摯8D-R (A?/ᚙcDnDEŰ{QAwaEP ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((('?Oj<^j((((((((((((((((('cUn)ƒ jkb~R?_ ֢8;sTD.«3&dՋh-|\c*8 }@+5m3;B#[It7Ck[qֲ;l! Rr޴nlV)Y@j]+N[y1c2 w\v*XAzL:F hH~BKb\i8=iÌeUt4P]m' )bxҬ]gsPt:-Xݹ9_B x<.riQ@Q@Q@s~#O؉$׮H^R2~d4٣oZUJWWT#"Ť)Y<+گR*Ps3dYqY3Ͻj\<7,G zPkG i1Nӧ]XK;H H#ԑo''6m2kfc9-g2V @Up-?2#TpHY[7sVpحeD;ڨjcDYUAֆ˾xcv]Rnx#XeUfrBwj6֬Q@:}HӊtVBrϩ楢yk\Ua-.űҋR_͈:(RlPa$gMi1Xw L0]cN9fcRKg ye@V(i'ԥ?xOl" mNJr:t($()`R@ KEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE1\pZ}Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@RdzIFr1@ E4Ȁd4hI)t_?IB$D[G} >- gyl q [?B[x@Q5tLup'h\^)RsQ@P}H4"i7|Tk4i |hj*)nÝ6?ʚnGlgT= PJA84϶N홠 4U3 5@uIb"Efzu2k$p4 8ߟLQmr, p#@!h^_ڶpI Sgmb!~}5K 2)<:Rni:IxUO %# $Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@><8 ֵdhĚkPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPWt9*܏ZbxbD_?}bxm?,.lʱsPeE=nҌpKӞi% 8Oʜ]Ӄ.Qss(Qt: ~T)@=M>Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ OG ʰmSmQ@ 5%1Q)Q@Q@Q@Q@Q@Q@Q@axIջ +vH ^BO0gz6?:Ԭ  9'MhPEPEPH@=ihQ@Q@Q@5:=Q@L]DkN<~wҤQEQ@!Pzih((((((((((((((((((( ұ]bg{K=8߃Ԛ[w}?bi'G1Y::O;,g1u-1u ~k3s]:3g>E#ѝbdf26~ahOK:5K3 &䍹S@,F?׏sH ܧG3h:U90ӗKQŴg3Lk~Ѭ@S:Ϗh:ajFog)Xk6'>V#Ņ:}xvmXH*iNb?:jZ[0FGM +.dĀqn8̟Bs#f6(ld 4jǑ)+bAQ@ddy4xnqf1"T $:Y1M>!Z 4G@^>[?ZEנcF>0zƜ(F:"˜oT^"B:uGE=@+}g+ U4,`uVKH a9}OZH; lS} `*iכ?-j*P( @?*Xbœ5ؤ_¶( _ْ=]>3SI:gu8?ʜ5%~¶ 6nKeϡ1uۭ6+o@V?55B5n@V3[|ځꧭ;?[PjZm4Ta'5EaXpܥ7z2G_[tQp1 ư@9i~ӫ->G\ EXW&=)Ln( MoE(uyq kf. ]h>/V\ _t)V)?[4Qp1i̴j#$]а ע-#IUӥ%  (RY^e̛B_N:}ǔfV\ E\rVSOLeSڴPgIǴ鋷Ea$+R:A1#VE+u t贸B~yr9"F) aDC:v#Qi3aօ\ NJN<ǚѢV+R{(L F9f@ec'Ǥb\@;yk_,-ȶ[E| @VK(Uٶ!m#p*+Tp( !4O"xUʓý?jt~T( :@w69R*IE4 _''ߊuxLdǣR@-PEPH@=ihQқh?ȩhż#QlP1cNU҂NJʝE7bt~Tm_ʝE&h# Z(t#PБP14$ŃFA4ė1*M@2%QH(((((((((((( cԬJ(((((((((((((((((Rӣ?ցϸ5 6Q2+\tER(((((((((((((((((((((((((((׉?jV~ Ѐgڴ3M.:((((((((((((((((((((((((((((((K̡gӊM4<#>ֵnPm,`EcUv?8EnW7 m K!¨ZO#ob1Ƕ?ƀ35ˉ4J>OMd-VL|1oʵcyJF~]Sk;`"&C#X涍9XZEQ@R2R #8#EA1O((((((()ʑ(g8=@(((B@@ EPEPE5H.r@iQEQEG (PZ܋wAVPR3R@Q'UU]drJEPE2NQK9 Ojť 1qUpu&ˮ,笧?^@ EU>e""fG= NydMa耎[@)qa6-.ƿyI$H/#UMVϹM&SGݽ)R٦q-,V,SO[H]͎w>e|y ք_NOG֟ )a#]?_z`8 3qZZB2)^nEEr#;KDvOpR] qH1s 0924袊@QEQEQEQEQEQEQEQEQEQEQEQEghCniyִk;B=r@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@^$#Pz1Q֍ż`t?gxgG9r": ((iHuL_$Cjq遯#N<. :r ڮ:Ң5$?kiR)jj1+[;*㬖Nl̀k[[k])ML"k`4xPjMLpdoƗ[͖ ڇ4nϪab"RFch&?]eԣPTs,2yA§Xwj (iH"Bۧ3R@P2IU#Hs Pڕ"bIwXлUQOAYR,=z~o:\G :Ӥ&VnzЪ@DAǠ X=j8<žI 0$u:*HQM+!=MIkm_Ğ@HWwްۮ]wA}..nǢSVJ w7sj;kdB$噹,}ʐ^F =TFpegI@9y3<2OIQ$*I# 80I\!#IEA=ʱ{_POqޠ KTm. ‡J.5 KYDSΑ*.R F@e @HFeV-Zb98ɫy@r{Z}#\ MV+yG_ ֖! {rY1MVᰒ5\@j`]L$ϕzn s !1#%T-Jr1ҟo~od(Ì{Sċy:mIŽMfG LN2), lp3T{M_\u>-\ (9}ϽK}y5K`wL 'R)צj[(;1 +;WEc vuRdmNx=9FO4JDQFIWW\MŸnIzO9M03q H?}5rXP%Tk6;YE} f9ڽ}})=iwg>y!A$8p&4=(.f\_؀jԛMߏ9rs)EZP0_݋9oҴ pUn\ fFqeW5>Y$=1W0YicEDPª^K;o)g}b lUbXSj9$'[@ЬQQ>楪^HT\$C0di.뫑"¨4$==m K+mUj;g[6I GE ow;\rW2D?}>r9 n@p3z]" j2'O3=o +;?]&;x*)"!Eڠ`b+fY!0Y$x} Ȟ[nžR KEVf2f=?m41*R,:V *\(P PM+1FOff|aU(P@cր<)cy @` *8WW2?V<ҥPeԚN*9?.eܻc ތ<с8i6:(;Ff>\WE\ & RΔ(@QEQEQEQEQEQEQEQEQEQEQEQEgB @YKWh((((((((((((((((Α/8QXG C6D5TA`-QH(((CҖ ( ( ( ( ( ( ( )K@Q@ F-QEQE҈I%A'(U_ Z((((('Ḋp' KEPEPEPEPEPL*w;v*I+qҀ3TӔ)=kNH>O+FEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPXvb?*5]cL u{ogpjzkƏ譎 t!=\ *xHTB; mu{;*|}N@|&ASI 8S֞5VdhHu5D[=iW[Ppvƀ5g-.Zd6!mHbI9ʵ1Dp3 hVY#hqJB"Co@XKs!ade@uTz\Zo˱r?:t3j1bv> !ծ[ʮz ad-żi',Bc=nB}Ωol8aZ( =@Yiq"n[N)p֋bMh.A!}k-b@Z9)o99 @ hJٸeqpzU%ZxV!wdq5y FpY F@HQ@Գҿ,Y8݌ \{n5,qkZ6ǩWܦĉ{B0= +Y>XM@j0BI朲j\}c@E.j$*D3iRMBA;z/Vgm$pzT꣍iA{>^;mh9ZVګH.rX԰8ޣWՉ I,%+cZYj턚9Uv1;`sM]L w FG8zzƹi#L~UFF:)|C֡/y1h'bIdY/.LJjגFu4C4뿶Zaj3#ǚ=Ӆ<( bE 28m3M,sjEdˣIK,HێyRgK%̲A?)(8y{d֭ vrrMI" #d9)PMJ|ɦ%Vv]*Z(;Z)T202֨NדoN1[ GsϭZ2-P`VV]qԴPK6(!⣋G,Y#f$~U~@008`acG ԔP0[ےa#'QF"bh((eV`u@ uSYCqu }-Pf$ƄiF=-R`R@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@s1bU~Z諝|L3QH(((((((((((( @_hV~ {/ʴ((((((((((((((((((7\؅Si/5Y,t' (=+>Z)iʖ:q@4R3R@OjDuC#SЃ@llP8րEsCO7d|oc3@oJէ{I>_]%QM2"RhQEQEQEQEQEQTKƱi0 zrhѯդ@`ezР(((-V8@FB542, @(R+k :(y=`fku ~d\ME% :km#<3ޖ+ڱd_~u_Q|d|PV [䲃a' ?t5KbT{ j2nW&=GZ{)Ds(PQk5٣rAygmxq M pv hMv6Zl3A}đ#>$LbT,odBF`RkmơFA$Knl -i;!=sdzͼ1B3AUZU ڀ45Mm. +fn٪ W6q߭Ɠq-?0IyqU$+hO\(c9Urs#1xVqcP%8|Ȇ>o⥴ ,ڳ%{@ڭҗ32z*Z:.+\ {7A׾c'ӣd`՟F ( k bԱd?i5arV5M' ۗCcܬ@zի{ {hL16H<@[bc[F [GUތ? 2#50-ghBK#w% &owuHFvmV9 V *$IZ$E*ZB2H' ?LS02TEm,2:js,AYbk MV;٭Lmz6=*sCDp ֥92FpޮhbvfSט46L_tʚULpmn1;e‘O\ĐD(@ROH 铊筵+إ%gvq?\.vQ< *g%g$T-*6WKw4Edr$PFX8yLF0̠@E`7'׊ܬ1umP]1fh(((((((((((((((((((((((((((((((((((( QEQEQEQEQEjE_c.}y5Fgߢ)QEQEQEQEQEQEQEQEQEQEQEQEGF(+Rg\WU((((((((((((((((( FOBMi1 Eex K j[[3 ׊`I\ ] Ulr;5tTbI)*SHR[M,=F}Mt:n1̬`%Oxu#'?j5i"d?m_p_T+ ,+ PȪ3Ҁ9ɖy<1l@bVL 8R@Cf%q؀v6Ezi$6Jj[gl5X^8^*FpkoJeFFH\qWs0*dgt$$(Bx<@ce'?tߧ p9g^F[}E]0Q1aѳҺԊ8w f_1X4d(@2h%nguI|ujݷgbc<;j3@՝cQT"C2ִh{Qgghw6  .q[PedtR]h\L.zJ) VtVVrKz,Q wR(i6C KYedHPY,(*HHcXP(QKHzZ59xZXHQP2fǶ7'koIs&lǯ8()h ( ( ( ( ( ( ( ( ( IeǞ988hN?媜cM%,wC?4d#~&ݬ&ܪ07V (EPEPEPEPEPEP$q?|EOE)4VS4c ѣ2@ʏX}V{PH1—U .QTS9%К:AEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPYZ_?5YzQ#PԔzJԢ(?Kb^n<Ҳrmq(}2m9Oy!#l~JVҞxFQϦi]DJyu4EᴽB?{Hb;7uȨ@bˏ[3QvTZq+đDIJʪ{Pͧ[$*q{v2`WotIREYB5loc@J-;Ջ_t-z4fH5QsqRlH歮o4 +3LIq򼠏pqkhՈ,#J "U%9SզPUwU$n{$Y'(ݱZlwDWZ}fwf0tō?Ր-UX_fEiny=*އ!Q3nj }6)dx>Б +ض-D"-jEp.yTS2Mk~6n+z(ZVw'Hk7\&K$*> 1$RO^(e' zr\KnZJۃjVD3.zP'xp0L tQE ( ( Yѕ@2/)ZEajf+=1lhj3ixZTEfeP:ӨT~vwuL&:a#qZm-ąvԔ+^L1T.oRkdܦFRxqFOSPNKD=Λr*qئ(Q@Q@Q@Q@Q@Q@ih0Q6c!\T}G<܏Ϛ5F|c5ri.zbQE ( ( ( ( ( ( ( ( ( ( vUYK=1B- [¿0ݞح d4yL hEPEPEPEPEPEPEPEPEPEP U&6犳E)5Aซ v;2?JH@#f$BQzz/kڣ%gځZ/f[è>b*D> j)(((((((((((((((4 j8QVk'MXԇZ`kEʚGg$ʼj.1qvx+hd_Lq1&G PǭO %Nh#&?ȢsgZI#tjOH&~<Ԝd@"tETx^?\ ׮N H?Zwy"LQɋy(?[w tǮM1u80 P‹zz\bkV(((((((((((((((((((((((((()1PEPEPEPEPEPEPEPEPEPEPEPEPEPEPXw89Ϲ԰5|cЍ4QH*&>?{UNnwTmBr)*M7S' .gѢ%?^9l,D$[Ҭz-|g=\((((((( OʭM/ArU(((((((((((((((((!ʤ5sJ92_NՒe:ޟi4 `MER(((((((((((((((((((((((((((ZΕs$խb̀m'8֓Qq>pbCۨ/ H6O|\%?L4D]E ( ( )>wc=)QM+xc: ( ( ( ( ( ( ( ( (zKh9 SQ@kb 1ӃÌZEs!5*`i oT g؏c@[\ӦjDSJ@{7ԵE5\eH4 Š((((((((((((?ⲬF}Z5K}=gW#kmR9G690:PisY?Z:xO#4/+w–-u$?lBkwA"E^0z8 z+ Џa/?Ɨz '"5E5|C h\ȢlQX(B}G|E?ɓ$ ٤qvP<@X +8 n)Gc9Ż @mRV8`{H# z D[)[+`(j Ob}:m'NPyEl5C!6Z7(#9)KRsn3+EbN3lsTM 2v@V4Eݘ54Eam]dnNxGBIQ`7ܜZں_Eܢw/siX + u]@+Rk6JqSV~}@4 6d7llXU5o4U'<4X +ouR9T [=6lXWEAN:ܵ עkX_l"\s shVA^gh"<ۏEڢDg1Cͭ1 9tEcyʌh/cL2?Z٢XDg =Z>Z`P3@~(Xa jQYRG.6M .)-e3ģע-iG)=pH@Tn։8ԂWwfJ٢_?›mdItEcX mR ql-` eukZTV8ո֣7Q[ +4LP&j{#4Ecf]HE;* 2:ע#n~Rt94բH#B2g<i{Nuw}N?06h.j2i~|1 J+;u 4hrMo”`7s~tע_BB`é,N EcC6)Ed 7dGFC1)IY+G(>n'(l~Ic[ؒDi_QoQՇYY 9+6l~_1o -oOγ?,@H#*cd_βLJmc}bMmNzE6j!؎Ɯt{b?)`N>>YSt%4k g)??(Md:hzFsp84Y۳覜4S&˲f m~}QiZe]fB=h:]h}Tr#ia/5piV#]4 0-h:_FT^/u;yY~d]׭ҬmӞK{K.Q;E-4V1VƋQEL!1STlI/)_Hε(d Bv9*Z0mwn0iEˎY?.T]K3U(ܰ%H#EIE "iTSu6ҭQ@E*bc1j950Bu"#wLL !3I"p~N_#WH48G&_3h̜JStlG!#Z4[pLL趍f9L'vE4>ߖpEmm`ccܤ'Ѡ.Ȫ:0p=j8+ynislN:mRe@Q iD9'5 93#i}qyl훭G(ZSy6檝nIn j}U[S#V=v 4\Տ E'eoX'Ո,E(=|Gl"X~; ZMzF`r¤iEOC֢3 /cv[Ө_vix%:).3Xx (Tr_X[800il uW0Ge;;Wi:($j:C1vo5VR?1kQ@; cӧ:()Y--Ǭ}kQE6c >lE6ysGoăeCEѯV7P!*swJ袐Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@FF,(iXFE0}p\]JzR N4hyAM*P?qh{."?!)vWԵEE8oΤZ(Q@Rfh`{Ө((((( [Kx^ @F8dk O0-dXG c46̨Sn8tRi6-ҝ*2;(iM?*AYqn1ޮQ@dfN=EI;s?TPaaj-jV'ѡʊ`F4EaH(knD1<6.>(3MCQH-"$R@ 0NLhOɋy'*J(1#H?"&,L%-UϮ)vs-O(SSG@DQS=&P`zQ)h(((((((((((((((((((((((((((((((((((((((((((((+ \ ?@JZVQH(KBrqIENkI[eG ~+;hԸOsUc'*JFh1>P>X=~]G? cGҀRZxt/4}ܾ彄$}EH0!0(~%.zu&㟥\̐ %( Š(((m9jɵxkPQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@qLEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPE'9(((uN6ZޮwćeOdΚq~H((((CҫZNϔlEPHQEQEQMVHPh+è>6GD?2o/ BzkR*%(2 ( Z@Q*xZ((((((((((((((((((((((((((((((((((((((((((((((((((()y-QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQERSKEWx*\[@[^O\ -}KXJ6ղIK.Pt(&S( ( ( ( ɇ?\ֵclj#PQ@!8ԵWRJv(2/??:Dzag[qWWBIOz}nc"ƤpKlrT)$4eN~aǽC~򨪧B#jÁ#&glǩ[&wJ5E|? ˿sBdo f5R3/_Ω?84piH?Fh sU>:YPgTZfŌ}违S:-N"3Zi0Qma\ˏnj,BHOMVÑ6ʠv5b?/乓 `Ѡ[\\eۓZQT?mmle33KI,-p!3U\€;uSЃໂa]Kov$1 " PZiYK4[K@*z1@#9ՙ}}o$v2d@W[xȤQEQEQE!z;yZ+BHjPԆPkOWViQ@Q@Q@Q@Q@Q@R (((Y( OFF=F)PEP)h ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( $+ ??o?SaS@n?p )@Gj]fêZ\anTOZW٧)2ҝL* &)5j8|,{0V"CN]jCT-ѹ_OwQvoEQm@,]VEJEUKYqS[&\~?Z p|)Irjm3>KU@)_Ɉ)m7KEPsQ@Ǵ?STVTQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEV`Q <ǫN7Lr`4EPT5?%Q T2~ h ЍhV~sE?AhAs[@ZF 5oz1m7R)$idsl_zT=kA(LT2:HU9@,HKXΕ5RF2[稭](p00XdQEy (*jeU.'V~_+KgB~=Ly1}xVWQԚ@Y VcwU=,Ú|qFL#(}Rk0Lۛn>ï@Sa\=j @h%(zm1H쫴Y zzSЬկ$lǖH瓞k]w){ԣ=ih(( b+&8V^6 Ķ )j6Ilcnކ<;+}`H?ʘQE ( ( 789=6AzAQviX6|qVk.8Xw-c]:~߰ŌƔY??S@wmΣ<?XD?T/DQPتkD ut>hP^ڳmp0*R걨?J($AH$V}Ć`tiZʡ+V!\ }h6VXcD;S袃&,7:nD()OK2"ewylg7ZE)v"<CRQ@9EqghEmA%iRq)UlɜF+OO}'qTM&S$֬ik99"SjһܿEUDsD&w/)599Wh6:cGncړQՔ5v85/R5T/3ne}j)7p(#t*b#TQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEVFߛykf>X)_CX|̘ ϯkoă:z?}85Zwv! ŶE_X#/B*fBzv4'E<+qJg'@QEQEQER7?JZlh^ &0U *lH;#ӊ(3mŠ(AEPEPEPEPEPEPEPEPEPEPIZ(0=(h`zR@ 0=)hR@ (>MQzʖMQKE&(Ҍ Z(0=)qEQEQEQEQEQEQEQEQEQEQEQEQEQEV/G6͸q[ULb=ҚeqJH'HU0KEQEQEQEQEUK!BuZ n.JZ̢+Nqizmg !hF4 :AQQEQ@Q@Q@Q@1G)"@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@d1X`9Vd$p`-T-14ږ4X 76OkÄ g?nJٔXzv]L°sSkZ7N]RaX ae*+ FխK-[^DT]@: Z٨ՇEea)OK 7Iq ,*jGZ>~X\ҧ})ƥ:ŻeJsM$EjYWqU kk>C} ,Ecal11|Ee.?GʑtX ZtijVi?O\}x81`E"r{`GlrA=h6ۻ5G‘n|S?[EkY'VayjųgEej˜}G8 {*,9>0jğ`M^ѻl {'ɢkY_o=B_޳ǩEdx8t}P$`5謖@ǟE-Ƥ& ^Ef ?~6zP?ƋEf8XhFԉX *+9Ppcj0ڦx^=Eբj%)u"‹Ee00GMeHq `5Q~ Z+(A|L:f_΀5( \`FRϽiY?b3_ߜf4Ee/3MM>1s+ޟi&t¤ 34Ec.Nc#>ӿo^ր5. ȹ8s/g&Ge*vu${:D8E4i3'G.Ga@rx~u4_\>+ŏ@:(hϵ5 @b}:O6?YsȤ: EO?c<֗@3qk(R0nEiФ702`+8hPO><߀{vB?U|v?3 ?Rpy?U#x.zqKmi?B{UnGސ~"4Ku$hmXL6fCQldOK]Lznl>w:id:;AقNZ;2szA!N}xcKy|8 qNt;C{]6NMC9'|fX^42Z'ċJa_ƀ/X=ղ;+zՊJYg׊c7[} BH  X'LRg@Q@Q@Q@Q@8G^^?ee'1v(*+L03u Ow OYS ʀѤY(fQEQEQE58U))QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEVTEj: Zլ6и J( >=|Iآ3C_}ť>۸:(E)&9hПu(`F袏*<\>o_ʐ }-3Q@ ڣʗh- P( L Z((((((((((((((((((((((((((((((((((>2=x&/$H? ]q%:4jJ]6kSX$PЂqkfIFRHHd_O *7zA(~65(p4R?܉GTTlD|TMhx!(NsXS&n1 ZH3ޘ̒4i ɪk @6lR\d"}f>Z)Fr1GVp _? j)#)>Li0U$ wQI/q`-ϖ#׽V,b:gb b",@=)Z_ ó)4QETn"b[EEk8IW ԴQEQERdZo{ ed tTQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEdR@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ w1I3ĭ5QEQEQEQEQEQEQEe*qA.,iiGVjlUf 'qiجy  OU-CiCRg(({#TN@患kKEGD I@QE(((((*$:<^td`TeEm7O큊AU"JZJZ Š(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((y$ܩ֖VdܼzJrJ(Q@Q@Q@Q@Q@e5;~ZqsPע(MOM>7??ʀ3|; j^1`@cV7#~:9Q\e#iUSs7 L@WŒoκ; yhx#'}gFoD_b<9)QnćeOcNݧƸRj)Sy` 4l( }hd@*wRi)O,*W9>Hl<:)86sA5ꭾRQP@VEƴ#X#2OAހ5~]LD <ӵl~ZEPEPE%h((((((((׮嶷`m#`+)c^l#wG֫34R^# ;Б4gv 2I\- p=O[<C.P=h._JU`ܹi21;tjAi<;lQ E($kӀwYe9fCuڦS]RѮq@h@Rdg [kc }JH2GZZڍ̯F*啪[,JrG$`X)QEQEQE7zg>QEQE5cBUFI=U+]Vs .KT@Q@V+Fqpi,{+d ET sɲHрȩTs4bX|=GZ$"B0UMsA|nP0}*K&Ԑz4y1ᩁ\A"șT椨x#X 1rvD!N@&u ="|銯;d z ~OE5Hu QEQEQEQEQEVV&5m1cbk'Q֛TEPQIQǼTCAxح:'M9õjPMuQ@5ܣ#hfiZ13gwF,<Huw3E+$o߭Mݥ֜Ę$z0od;\uj+ obey%NZP E-44=b-3 ÷FH%N=^cB&=@*Ֆoc;| *2}p? &ȉ >sXIgK3 g^Gl4nWv Y|+cʷ]CJ9^I z aaGfrźNz狇ˁ[Xr`Rh#!tx (`?>jL} V  +t*M#HO1h;Tu4u g$o# =Mro.tT/}gsw`J?aZd &1נ ڬI42Gsj˿DvbThPbgyn:3.G2\!l].to,zM_2Eu 3յ!45QHDZ>̢ y%@y'uEHTVa= c<<Y+ހ9k{ˋIbAJ'e# * MnYCeJ$s@>!bu8P?ԓm_E=U{M{qBZsK) 2sלr(Ru}>սcC}? 8 6Ŏ׶)t%4#o2;q`zP}˦9U1jεk5y<S]CHK\855 P=HC%aP[M#V9Jb(<~4&[}B%W&9_k/c[[#rl9 :IkZuR:3v(EPEPEPYLVgk˻Iw JԖ(¥u?'SߚҬ{?Sm#΀*Ҭ۶S-6R*2֎U1zPh۩l5K _YVLlMBwq"kF ;!';c^Rw%*y*$&2)+t30|۶:ր FUqzuQA+xHz2]*eR8")D :j{e(bSt9ɫ3[$z#HBj}FP6}jXJ0MMEsݕ T. zR@ݐ5 rc"QS9QEC)F* $zP@e Oh(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((ׅ=ˌf_kG]? Ќ" !W2 dYƀy,$a;NN7Lqr:)̅2I%DFq ,Py4fTn[LFG9o6}Gҥ("m=QZ;7&6g&6'vw:6tV$JdSnYGUtP>z{g\Wbk8b͐O\U(vօxᏢdS2+PG:ۻb*P0AKEQEQEQEQEQEZՕhJQE$t*ug{7>xJƘf((Fyu%UUޮAG( ( ("<Hj[P?01)h ( ( ( ( ( ( ( ( ( (-5S((((((((((((( quk, ugh}Ŕ…`f(/-#.vإX'ޭQ@Q@ Z(((('e`];LO2vrگQ@Q@Q@q:06LOv5j _1SMFE-Ci'i @jj@QEQEQEQEQEQEQEQEQEQEQEQEQEQEGZҮ=?^mҧ'PI'^JPٜzf5U5\?iVv v` z~5AeeNOSAZ(((((((((((fbrM>p(QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQER0ܤP5/gkfEmn@ޣ5(+]QEG0Kpt 1Vd`V=z -"Z(0CȠF'4dgjL_07'vv-+b; HCC@%kرEPHQEQEQEQEVNH4?YZt@jESdVCNJq@]hjV_@#VDʆE*Z>=α:b[Z#E7hL +hdL?|u @VoSq룏ͦ^\d 4(Ѥ9{ۃlSr.s[1d+?(dSB#4ioptФ#H_2 yIsL ԙItuf^顥˶$顤̏Z2=j]ݻ_4 *9*! dztrxAZcs@7/F?:tR*M*Ԝo_Ϊed~h]3@tlyǘȪv\y4ٶ}S ̃)D*,pmJSZnPtCZ'(| m4|ҔijE`{ 0c>tx(Ds&=w vEmКQُwOʘX#@u?WɧqxQ6̜?ʏ3O-D|q5wOԡCCp]Rmc5 'x##]QuQH((((((((lsv+3ff 4QEQE&P-2(#ꪭwrTʰ@yd쒿 ٭X${Vh((B3EQEQEQEQEQEQEQEQEQEQEV~.7tb*35>oZ*i:}NOf ׇK\0uY\db'ӿm\U6ۜZ(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((( V,J`&k7PM؃QmO)u:[梼uK1V~! cJ]\*GN*M1m2|M)iu:$i1^qTogVy,WaRkpdB/R$Xjt*[(9IFȒ)CޕZzȎV(%2 )dd\8J29KݪAP)>n(c|z;n1ME5(e`[);QAEPE>Kߊ݈55Z[njNײ (u2FnoZՓi珥4QH/t9ITgC+R|81I+Z0,CcF1߬-9H 8QV0 (:u /RdR^\F<|'$S78\`:lԐnb*xU%RJ9hԷ ` ܶ),9 *?zb;((EPEeڛZ%nW`sX&y60:cY:^j {,4K @ֆt66GU l;UQOjuex4݌qA mvJ}6Zw n  I “,8QX 44FCo88E$[ Ҁ6@QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEr'ܚ. _pG\4yti$9vKSDLZ.׭},-Cu/(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@SՀ:e=6ʩtیpvA5OI$Č,U*ƕ9T5)ðPpƟl?*U4L'CTNGu%(QEQEQEQEQMf 9PEPEPEJ((((((((((((((((((((((((((4QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEɣbMb5Lc#k_KteY$wBeX -=1OlXFx<}1Ɲ}AĖ[r+K?mBYhџЊ׆-l,2Վ+ق#lҀ75MAtUm!seG; [P=ixEgTj7v!W@n`c +u:+!Er(an+uGY (@QEQEQEQEQEQEQEQEQEQEQEQEQEb q5vikA5dl_H~(EPEPEPEPEPEPEPEPEPEPUfm`}2Ww\ƋbI˩V q@˪7Q7֡7_ngg{g Vdw Sr$6  qPaS?Lԟ G 82Tֳ6ݒ{_?Tս7F63L۾\ 8#T{}$85H(((((((k:Uvj:m?4i_ b{ ang׊@R8nr0>Wk?\\ЩDžK6?췊WjJ-As7PkI}Ktqc(FګZVv{a9VM ZrD 4x#W4IE%؟PIdpZ!F0rOSBtt{2LCP׵gXɋ S߆!d Ӵ{Iɪmy)rEWim7c+IfНy:LRm&lsPC H uV\RlNj\!.L`թ&T8c5pk9 88vIXÎ^`3j[h OtIHw0qR!F9秥Z6'ȇUQAQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQER)i!ft((((((((((((((((((((((((((((((((((((((((((((( dP# ^FJb,dPAuӢH`ih1 ( lf8ku*hPGWPr :A7q$O'KP[dYg:0((FjZR/JZDQ@cc/O8ElVFfaN~tQE d_OK4c ?jV_?V02,@:R+b_纂?J٤Y>$ra ygjC=XҀ)TqL3đ+X\+/%jЦJ\NRhՍ*(f@&cɍ1 )1KEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEbs&u ҳE!<ÃZVE0#2\u5!VГSwFpT}k;W[D*W,ǻf65.FԇOC5\M+"`W]c+Me 0΀)f 2MUԭˈ `F8( (+^0Y O58 G n`.alC?JM奶x;l `jQE((((~l.[&?szJ Ryzz{G}# Dʹ"|!uQL 1ӗd n9}k +,?SO=653zǞb *#=̆[g?yrHimE$rk"HumBU(1 {^DYusKg9X3F\Ѽ}8ma?u;ArKdRx{S,+oaΆOv>e Lbyq:9(o<09E5$l|̦ۚK>?QH(( K4{[Ve]뭮ghG=V?PEPEPEPEPEPEPEPTbL$6tr˺_,x6pc?* KC[KӌbkAs`g@ 0]:ORT~.I}(.ćZǺ})f[7*uͲRJ3c@RfEwC>S'.- H=JKcZjN%i,y,w:=٨tVk\|mNUez, >AŬ#v} (N5\trbƀ4VMcEӢ; &g?~ԏLԢĒ^ t٫ ) *+7տ?V *+=au ?΅T hB ԻB>6?~m q|#i~y?~1@hMozz^8Cit9 Ui|:BZ^wh@^[-/1?FzldwUҚ,jԢEP[ P' a+}`JEgeRtGJiչk{3 *+5h?Ү?>S@TVybSGL|fMMhQYG#4cA&IMhYEL }L4[E cY;duogY;4"u5S2 liY3e'ع`[S֗zx~uLhc4d~ι=NM[28ܿly֪c~ʇ9t9@ "gHn`^F?BeXd'I`3@`q} >m?bʰHq,sC| myH4jVG^ٶ@N6V@6: O03όƤO$E9#=䍣#Ա~?}Tt!NV(Q@Nju{pnC 2+}TSE#1"jXu>ӫ X,J1E in :~qh:֞?#V (y14EŚ~Fayğd&yQJP:(0(6O\*l#_(RP1RՆ6b? iSGI?*:4v.vN홭,AFeo4nFW)mpOLR)۝Rz1ˣ,BqE09eTm߂N ?)0=s ,DH?ִ5ȧFNc<7itEAL bWȠ U2`{KYs>MŊЏQKso/iLT$3q}(((((:r*K j9FJ.Z9DS|iZi;cVNzX&qsRT?yy SL)afp!g5NVYt 0\P38p*Y!Nj͜LqLt>D^3?NDd.@.3V-HV8j}TD 򮲹 YK5ܫ6XzS e@Ǡ3\йӦ0̣cjLFPwy$VQXo@Zy+i di#uQ@Q@Q@Q@Q@Q@%hkRГi 8ϖҳXH.cT֐٫f?~I$5FPVI7Ҁ!5$x䴄8V&,6Ir7Fۊ`F~UK_ \O,kl=PQK19HV-=?zk\\j :NR}(XE3;]ZMkMwK%W4)`S23-## uQE ( ($q@ M{8 iOC@߆e_k[åc>]MQEQEQEQEQEQEQESUmNV&~~mR[Mbu#ļ~f wZTQ2hoߌڷvnO]֝14OD_}*fITvvRwfVĭVk#MbeNEk[>0.&1mUOLUe_QQp2O'$znldT(1GJ piTEBhf1G8$ =*KXNOgfw+(=sSDHF3@Uqr)DnPdyrf8歼Ȍ$.⠹ ʹ9iؿY+0[^8ӊePӌU=:$=*2Oڂ%EQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@PuWG(mϷ&,M/+2çjGDT?@FV8iC7p/QPZAyx$ TQE13E@k4\S?5b ( ( ( ( ( ( PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPֹMVZyCY>EšWKm3n DGBKH((((( -Wc˟0gj# ­I.748qU$IUm02޵(9F9f4ʷ95@XE)4 :EQ@Q@Q@Q@#}KEE `*Z$$GXE/CM(E ׁ8+j5&0MERMOM>D@\iHO+V6Oih2T~+".I?1L G"J3q9*FVZS4Gq "&ҢqF 09ۧefeVj8isH ,}ن?![6 "RfsY%$1AOH@[N* (" #dn0k2c-y2l+}b]H3qH\ kvKmfFii[㔅c&GK$Kps@p\n;A z6=ijDS}*ߢھoBӠ=}o26P?/&(-!-psƗ<sWFI 2F+B֟> =<ֳ!;2]Aut];@E,sPvmum?-X5Yϭ["Ff4Ee&j$YN9}#4X vJ9\[gҫć*1>j inב,r+n>*ǻ!5'.uzUlY|mօ!$@0ZAn"F;nWsʦ]Vl&tL:.qRE0-iJ9䚭k M1Spϻ u48„{cӭ mu9.g )8H WUn0oU8iV[EC! ?EW6¿VBm-;9o% jYndi}SUp =YEVt%g29 AV0rNxɸXdF~VchZ-p)kw#niEs;U%?mM9_$JBH UMA 68ۓX_8ԧ/|&Lo +;Mk>(T59n6b36m8Y@ UF4XhD%ͥ 7ف:/Z.P$$95'Y2 UOk.;oSJ~py(G(X|(Aygdu=RO Kz&rϲPڤ:BYcMkKhi\5ԧfDV]"Ur1mdݷ div`BHlm2*zQ[_tCS!818E\ʱd:MlIGEC?cJ2}XbbF<>.d4M{-Rܩ7b@HS]$zUL`RG5obt~T\SAs$UbG񮶐(((((((*+ FR7je'€iU>ۜy*͔mS7| FoO5qG`2GJ? ;Q@ }Dq?k{E*GzAI%*"g5VW24֪rJ`:(EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPPqg9mMP.)63MMKYiΝl*@Aze:l?J}?ͿA1m&ܞ?)]*Nx'5~ ( ( ( ( ( ( ( E>lg=h((((((((((((((j pպ:ЀDJf~ rs )6\S Z!O* az-$(fϨƒE$K$v ) b~q qH@ڞTK1Vԇ^OAW[kd 0&nR45ݺ4𬻂[ffajM>N=jH3*}M r*735>* Վ(oSPE5Qrzm᧌0JVHQIUtd3|<ҢF")$ mg - id@:*No/O_C_݋G<TLo M{\k[ vwA{|A-Y&#=tqP*ܷʲʈ[8KMwXѝFId~5].VG o˷b݇ 9xò8=@zMR]^Ohf_;ܥcMբo2Vp:Tm~Y0.T3AnTO*F[gRiaS2@YVGQʹ'<[GN$u܅U$VxcZqoE^eq^˜ZO +k~OV5c@UFoϕ#qi-Esp4@ڥuMPO{#Xf8i{K f7+ù (&2#zUաٲռ߄P8ϽYy Lg9*Wk{p>O.ަPE#2%w5RMV3: } +]!E3zl?vV7wYp8&[,0gEɨNg峇ctA4t,ۮ\M1W  kuqr9f=X55iQTd ljPTo5hC F1XԘlcOzЬU0*+b'fYOzi OjM.5RUE)mg oDR< 9?Ґ.i?(ҳa6 x4y>bv皒 VmӸtURO)4W=zPWћL<#"@U^v6ݤ9V.U(nnb̝qzϟT[f۸;Ҁ%͋kf Ѩ㵅bp??sRX"!D,{ ѲBp+'궜}?1՚@QEQEQEQEQEQEQEQEQEQEQEQEQEQE].b Jo2ԳER +_$\ZcN5r=zT7HXe$;/֜5DxtqwN[Wff)kҶqF`u(1@ȎiZHbo|M\\k$mzq@ZۼN; ?[[?(c֐QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEeKi֌?SY bk?GT(EPEPEPEPEPEPEPE"ʐG4QEQEQEQEQEQEQEQEQEQETr~S޼/a|s`GqV(((((((((M>7;PYΙjO_V\U*9?X+ Gts_S0kB ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( nj՚}*脶zf~Sz (klҤ/1_^ aSh=NJlHc{V[xyS'^B7۪A?!`L v,2+N.L ?THizjj1NF9+8x \{nZS@=H_8S zn)H$|``` wlv avSAAh>!xG}#sO:XWpwcfo:cAGLREo OM)&z nOrq0hlq_4a]br/jrrhN;DRPAZ>b;St6lHgm;hkmbY̊;v5X'Vn=ʵ #PGm\a7HFsJg8zjck7 b4EFu@\ HgΟa_ur#\ij:dZFwtdQt s[4RWl7 P) t֍$YȬGmJ3ӐqVgEk02݁Ѣ{A{*+HFU[@?-Ę V[HI=/lE6 joIL x1֥ }A}2}S*tvYU(a%rX5fk MRlz(%4՚Y)iZÊ( wl ۃ'FSUƅnduER znfcҮ2R Z(,hjN 1?nHsZPV% yڃ=jZAK@s4$nX}oi)0c}*QEZ 8bMES:4ZPv% @A p5b*s!֝PEPq,`Xp5%mi/mYEAwg >TYs0jE5E2(8Fo, ;:a4P+}*U%m-U+_2H$*!Vc6g4+wH{8I_r#5,\&eԟb9p0:*sOLɀ G?Zg^@Vu§q15-ͬ7q0GsVZzyՏ$ HtH%*1e9T}#\!bA"~Ū3/Ӆs͠ K}*a*!:sX#OO{bCQW 7M@Z]Y&@11;(lvwX`zy9tAZV4sm&r>kl+$On~V>iIF]Ԋ*6NI7~+V=A #Q.zIFR?~;<3!آ)cIhPXprF1۵wTҊz?sv^Me9 H&5{y.t% V"BŠՂjj`rmJZ[]HF$L1,hQ4̙?*[g{fudKf*@1nvwOXFcp85Yij$g!GjjvjH!` D:;n/xC!ZOQHۙW)QEU#/Erv3Wk%5t1PSEPEPE\=+LasPCEPEPEPE\Z[o#hQP\%պLt55Rm)h((('Dc)8ԧby랴hd.nsV\Gnvp~e!Aat.?:Zyn!́~yܽ(֡1ʗiWcۑL )5qр4@QEQEWR2-EZnNm?CçY[֭fJ%V ؗFzQܵ͐26RA?ʬ'k*ʚв1+#QH(@A( ϲY0q]8!5-mvRW'qI#}qU8KgM=N!Lv.*KoᷙTyK#RzUo^B`mfoeqdJh,VCoHN'F#kWH%\+:=A "-ܨ+&ѓs}(ǰfR|Zfci:$3s[h4A{ckZr239?[/XY g9D ꞗki$0I=I((((((()M>/t}(K kRѳk*WSӮU]L\p h9G7 1u@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@W]76UbRs(%SBbcF@WMEaV`GkB õ@W)zd4NFec*k 9 Io䑝- @7͞O44)ĉrBǸi;}I RkT/Xė#o?Etk9˟'Bbj 2i0cN`jiX@f ( ( ( ( ( ( ( ( ( @1ܟ-QEQEQEQEU Sǡ:q{qA> 袊 K[gxVbx [g:RP8PM[ :'?cY_Vds``3Ϯk`e CL?µ%OT^ֵIeŶok= qfN٦Ԍ*dWڗTm4oX$c $Q흢t]-?Zѕ5<@38dT./_j]Jl!;##4g-w iЀwoSՔ|oF%}q3m0N6֫Ad>xhhuK# 7h EekMnp2I vUo Wt A٠ I6j&Ȧ3!aUy͂BUhe)H+V4"\3mCW_ȎX玵Mev8[ 8=)61+;@&>ZRX U(FۇS@V=ψ-HFN +&Y#eiQΗ uh5Vu?+uUxqw#סj- # ;_WDV Ρ@OZBK ƪC>~IyqbU}B. &PGYe1l\nsQ7x¥_t>2)A}\yW2T5L!N}]F3 j<?VZ)j^8L65 YQYy(Es#a Zȸv$9G>w9eP28ɭ}9nJ:sȹP0ҟgE8s"zgޢm.c>Z^.gP@nM-PEPX1ByqbN3[ufXPg(EPEPEPEPEPEPLKiN!sɠ N?r8̶1厕r 2?5rjMC8δk7ApiPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPHr{@4𪓅Ӑnc]'8{zkuڙ rie(oeRHs)ݬ; S^.stn â7(CΊ,EPw~t5a2_ድ.\̱" Ci}\%I&-g|1C=k7ryH5C=?,g bxTm7y*K~ud,TPx[r;pր:(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@%-QEQEQEQEQEQEQEQEQEQEQEQEV~A#Zj[܀N8@4V@AH8o(`8?S4V+xBԟǿ?EۢT?R7@5j!:_HmQX F?܃n ~Aǐ8z,4et*DT(g&w nr<]0B Y &ۥV"0 x~W:-id_(n9ʹoOWe.1tT ((((((((((((((*%Z[Q@J>KsnV/0D_ 9E>S0=: FKH I [KlV7`f O^) xGf@X1:P:줄}2QȮ[N:mI2>*:?jERҠ;)'M@[].`I SnrElwR 3#In? $h3ir^?kne?F'Tݖ*a{C~ܶ;EF̕ݞA/7u=|=d>aPM!+ɸI涴{ rH*H4($$YeXW)OT{'H\rֹ^y6S¼һ(ӬԜɅ[=a[ˋ}1@*Zbhrt=A&7K'!d5Yy2Ż/.0zӖVcP0li!gnF2sҢGYX󸑌ƻk!"=XM>_ii4 7q;MGM[8v, (xzݕe_Xx~I7H$ME K;q eN['5=PT5=1urx5~e(:32MY[h?-a@ 1R@ionCޛ;L̎v_I}Tƀ9#2 7ul*̊#4Aɮ4ƨu@ZV҉C!S.k{L]hH u>15$vW#= Z[X# ;m6PTtp)P`cej\9pU(aʚ5xEI,h zS(((( ió `UG^R\zhۭ!lc1LN$h$$*vJaflsƓŏJ]HGSZQ[b_,W՚) [ 7n|g;kA-EcQTRh0Ӄǵ- oğuzTPNium}us r=y&Ey/!sҶ)6i5pm*s<\j.E4[L#Յ'?*x5`reS]s7c"Jh`gkكԩ4w''ݢQ v晨h7dAJ) (|7iY &%nС"sʺ6u_}M( ECwlv A#əf@5QH 'l4^ 98LWEE;QE(+/^CPA`{֥!d2.U"#`x5qM.cXEt1ARTV3j9̗262=꼚Fv[O oy_njqX Ð}X:t [̀㰐Q--=9٧dc`4VPX݀/zv`?D5!,O\J4(6}d Gq֭kxR?%V=HF0)Ԙ(((((((0=1Nɟ-)˳JZU4ߦ[60|VapxUuXϹv7 vV4:Ѭiδ((((((((((((((((((((((B@Z'~~jِVmoV(r6u7nQɭZBh.M:V(.]Fhס֙'|mE#p0080\ghstޢ15EñbOV`\8"hq8Ϝ+kT 89ħ>sE 1AG?}R=Ed J`h mGV4?ۗßz6٢NdE.{d0ġOrh-E(D}sEF5;"Ey5K#(Agl E/-_S 1z`uKQ&/I|v@4ӫY g@ܜ"PZ)o.֣۱"B@S@m#)E?(^쒑O H@?dȤK+hGo@5|AwoSڟ|S4!ů>֨'~I"ng**t`:y`պ@QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQ`x*۬O!@m/JZd|aOTjZC7[1\GX<[4Ǚsx$:=+"S\bR Z@QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEf#`^b#V@:w98qZJB@@C}+_kWıhs f@Q@Q@Q@fhez?iRxTӢS U,/dڥJ};;y^5ꄪq@QY:yvT==+Z (REQE)UPinR+s,e8h7ΏxO1w<.gX, O\ / HoHet#4Z(Q@VOo0!Nܳc +QLH'#9#Ys_#-&9`;J+}?V=$v˟Ze4 ]*Ǯ=(r(EPEPE8ހ(((((bN8Sdb P-?6pzsWꆆX6庐s@W X\g~bj:}*>:HvviPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPLS;N:Tzn|2z]efb9̍X -k1/QOuleUWnz)9GEg~,/8w٢ )H#el&2kn:A(/(;DФ!W60d #(>`W$0QQcµ"*?/( GHK-Vϖ_ʗbt~T Sn?R1-S?*6A@:ڑy=OV OٿͿ A'L|V5Rnʚuql utVe%kicS@tGyG2-\r[PdF|Gy0չEakjY6SSe(:m Rؒ; ? TgN7x+׭(jq?֐k'5EbƱ (0mƶb8oT5E~?)kG{kn..l"Z>V\ b} Z#+f.9Z? ~jO#Z=ճEk=R\yAV\ oj`])Zi~ԙ(ހ~ ߎF\ QŨ4K9[٢{~Ɛi7'\ aA}֏'o5EhX#4܅j2ۮ?lE:-cuƗ`A]BQQ׭(N~Qק&,rU +^.\]̉)OPTsQ>w.lJc?]Or )S*EE[ԊrhH.MZPwI徺_KG9k鹇jQ@e_N€;dj@сnn{iciƴh ArOo%i(8h{= ƑKSҢ3m0F=%aG%o8VѢ3ERHY94ؖ=cv?ִh 9ȈmؖB8 o<~EgO~,MؚhkB`@N=)`.=2qW KX/KXI=cP1Xk)3nt&X:|66ќV N" hU(XZ7޶7.}bjO.h}gEn"7qZ[PO?ɿ3I똫SJe/tQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEV/m=[UL8rtS893U%QHzP?F"1XǗu>qɭ`eH,pY2rֵ)QU/\$҅b3ʺ_&T ,ˮ!^uvUsHQH((((((}iPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPv?Y# |75v#&#I`WРT77r%Nn{K;(1""(;V xV8vK]&IIJFN98z͔SR +;2=>=:kd2ϧ^+K$upNShRH FzVv-;%`&NO8^YI;>AuyK`f(u[nS#9dž>oIAȞ`zumF"+ʰ4[2[YQH z[nś "X ke #ہsTFknPSjY( i.@+>j__q;NP-!8ϭm3iL/8QEeד.gj'[^EgYR@FrZfS2+gY78>F8EgxzHw jgj.B_8 ]G=bsmmvjq(8<{b+Iax~'"4#4E(((((((()p}Ԅd@4"NNOЬcJ{kF d=66*O2T(tܜdgZӬ0ujPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEp8WO.1=5nivUb̔xUƬ/wcygTRš9\ec_E\:uV- @ cp^ ozbiVQoӽv5^_&gCTN#c@^2&Z: nvʏƱĄFNXchyVQz )i23=w J<8sS)g!T {P}b`-0+B (2FFGZZ(+a1Oь3sq] H@gU''UH@YA4) !x9Zg9FMjR((((((((((((((((((((((((((((((((((((((q\T/Ǭm-#2ߐp i6?QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEV?3kbP2 g@EIVt" $3UfZQ`fyL@Xz֮iuRyR$c; 1T!FqqvHdX9FM9] X`ޝE 2FXZ.c+1@?hQ@}0y)* qLuzkN@(.3ͷ ok+Z@p^mmd"xq#EdIo2B3z٥8)ej}3JG#n =2Ifޱx?.yc$O5w#t9L=giܿ\,r6sZIZk!$2ܞvFge" IQմ,!w`r:5ٹQF'9QE }dcujEq*iwU̦pByo5:+:f86D^X`#v"]0m ]%sRAu^w}] 9Zcwj 戌B?J@`hVp]LB03*OF}c@Vg0ޢA~գ,*ۻ}ܰȦ-Cip)L&ik g qڬi$< 5m6c$du7_c8\WO\Ij  Z,O`X:rd$WRes4T 2x9Kx.YXA3֝ Aى?.2-[`sb3]\2𤱜C8,mm2C#V(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ A-Pv?ZʩkͷJܨE^R)Y I=%ClB#z3M5Ɋ"^ s\;]*Kր;{aYb9Vk/@GK`%}H(asQQ]v<ffa`AsLlo6 gt4.UTk[=Ĉvh\WSW G~n>~8WkQ5/"!u1%-[PKkV% Til"zHXxqz(EPEPEPPCSS%wr)jfVv (Q@Q@Q@pS!J\(AEPEPEPEPEPEPEPEPHzZ:g:ZgЍin ?;3ZTS&8&LS27?zlVQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEKM~Z59?AVꖞ~{'QWi gY!*z{v\ܜPuLk/"9KEFPdPkw1^4Lxuܿ֯ο$E0&t+ޭ"\U{LOV`&( 9=-&)_N@QEQEQEQE888=-ÅČ}!8EPEPEPEPEPEPEPw((((((((((((((() -!dRqN(((((((((02 g*>FRJ}* `kBhKWh((((((((((((((((<8Y~ ӿbNma?STD1SEP?pYk^xްVe^65ͫĎ2CWY oߊ#Z"x,C޴mn/ʟ2@y"UM<1A>Q sDzdڐkY) =xdZ$[QH^+KCrHZEcN;ն!FIGY20e= K$I\1g1 T]2n`2=zj ie,軙@/4ZIbriwXĜgh[=$m<jGe`LjZ{s&(a52R5 Mަ?$ kv0eɊvD\[Pİđ¨z֖H$&鮬դ ȼ1 S$"B0U$N9iQEQE2HaC铡8hu'=quq-h8cSwOtZ cdh$sޝ(vE$A4V{i1R0v@4Xm HM$~5  ,CythgNZ+[#Aw [3]lƋQ\r:ĆQWiDcǪ`::+k]qٶ>eZaՇEѢ$mS8эƭ8=E EPEPEPEPEPn30t]1T5u'rmJ:Y`K <_]D,s_O58b$ Η-ppH'P-[\*2=OcGmQʩ|znaU>qn$Enp4_풘%P$A smOKXhqϵI<vO_q@ab=čЀ8ՃU"n#kE,!F:QD$({ZQE OB+Q%fx%wAW4ir.F,@JŕݪZϼb!lP]5x=ij3س}x4$(KZNQ$ d#g #x ֦}AXPvK1 õУ~f{,5W#\ku;\EKi^KZKxI%83W+<m {J* =cEdN=03Xv#Vfkq0!d!Aie1O$j=iRČOA@^#Cta}=;I-` <Xc2e=CoamlۢhH`ePByA.DZ$ʺMKEO7h Rg"̞cLT\ }h =[ⵋ˅vsexlO0nsqZ^^ZI 88>eP%4d Um'K@%R.yOjl%T}oL` ѦF.순HHmXn0r95f@ssx~e 2xy),6$cZ4P>|8SuLj+$=kb BDU_$2$O EW~0O~5Ut;E<= VvQ*袀 ( ( ( ( (3KWR5@(ր1@RyEZI;PIT0ОAkw/Gkn]YmoQL @0è=iO].7y]=o]\-V&]FD`TvZWED< e*ZdG2B*(m}޸Q@Q@Q@p@)kW{kxek7k`MER(((IT5"xtM{EPfQEToS - =MMpJ]DIi] (3 oi$JEMљq5Sa 1%טWZTR5Y"3e BA?34S3SSk9h)re>aD1Vbk=8 ڠ*9\P4(fMÏQRP YمQ@(((((((((((((((((((qkPcEO:*U$q?5j ( ( ( ( J(QHzRZJ)h)h ( ( ( ( ( ( ( ( ( ( ( (STDGAiQPH+J ( ( ( ( ( ( ( ),@4V"9IEPEPEPEPEPEPEPH*c9$K1$> o1h[_9H_scY\KEj((((((((((()s 4GAE0MhVn$~*((((((((((((((((+/98֥PB27 O}s_Vj[gy@Q@<Y5XtIMlS+SqZՑjzi$Jנ(EPEPEPEPM(CiPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPviv^Zo* rt*ZkÃy(Q"FYyK;Ui'Ӛ8z?4k{m)ei*o''02L/fq3՜>?)L>S|˷o=Vvv\[R)bp9 cMN[f!?Ϯ!#KO*s@?\%݁Y66WZlƧVo.[R+E&LV5(kyU~Mp.W\έqȏW#ku;A#tTTVwN;oϷjZ@QEQEQlGLt}*2歧J e!h#PdRz u%s IES$}  +{|fX'DVcNyHj0c(+ ybҧʈ/~%QEQES]Pr2(h sҀjH ȥ'&STrBGj}Ղ( c$3t{Ҭti֭<@y?l??l|Ҷ(/65=2au₢FS +F-{cZuiQLw3CD.1j=AAٸ5H27bhPYe-1j(QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQER7?JgӞr jU،`Ɵժ(((kdY\)vڿZq\μYu>mVߟ۬pd9`"Z7m}kPBNHҴmA3u5Gzu%-Z))h(J)hwqYdhONPYJ#/QEQEQEQEQEV%ƴ: gi^ֶV#ݬ!*}N4Vq .?jMpK"*o% >[Q޺xc{{tQH((((((nAGObܓE;_'8?JML M_SQ61g?M6hf>t;lO}RA-QH(((((P4Y6ư`# P0${WHt+B|u=˱T3L t*PlqJueW6qK2mPjRLEQEQEQEQEQEQEQEQEQES_7ҝHt(#ÜZ̹ ج,x@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@g뀶uܿδ+?[Ә &?mʭU]7>9j (1 $edh"85L d0%VdX/V%-R(((((((((((((((((((((((((t ?έǔ0(BQK|vǤE6B$#ЎnH2IykZ,pBl`I)_*ͪF#r,jl'XdC!@yVFAuV)RݦC?X W!vӚݢ^w,05-މouv. 1EjTs n3@ eCXȽh56-ٹ@F0jfo&4ټ[i b@qv5hapK2ڡF8kH siVSd.NI˧MU(:}mj|A9'5KF'2`zEWi< FL0zN ( Cq֖jn7w|tQES>֭F1ҫ^*_0asPg̓S@}bR=j$#®cqzV/VAtFQAQE*yɐGvJ]ݸu5m!hruUQJu KAUynJGR)>_9* (-Yj:L>^5-U!]jM$ (FB Vyp:PqV֫+n=qApkiR 2Hސ۝#hGkh2

hȣ dQW؉amj&f!N1ԵfA0I\Y-r2 1i#oOZxK)7 :(ɿɏ}51H4S`= k#Dh_[2t It%K9/4߁Tr[*@SL6|Ns@5uh(۞{6f'q%et4e&;~cԙ [qҢGk'KةbUԁ ~uzY4%+-f\؇m:¥TRd]LxrzK;„}?7nZȤjO]7:#""$$JZf\ܤ6 5PKm(QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQER#}( ^Yvg3KǗ>@ KIE-Q@Q@>#GXgJ8സaTucԐF]@vQJRQHQ@Q@%-Q@Q@cwj'oZجMBD[T(#S@U6_7E|Vz3¢Fğ-ϡm=hՙ(˜cRP%}+ .E}3Ag42ȤKo.IkXgf s\$q]JCOҚΑI7Oczqڌxl6>[35L1@O-I$6k2 Ki"ꨦ*FA@QEQEU,pY*b3ArPcPf15% 4wk\*ȪRmdN9Z IߗH=q֮"pPe)_dQE id0sV(ڰQEq'%P6QE ( ( ( (+\ cUoc9W,s鞕50uv]!EM4RF)h(NmgzVcs巸O)lzu!h#ë!?Xϑ8*۠6#ZQ\@Q -JJ,02NxWVn;U ։;QAQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQIK@Q@5})ԍʟSvYY&yG= iS)QEQEQEQEQEQER ;EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQIF-c2H=*/gifg}/}gOCrM_= Am}mw̮GP:¬Pn骪r7*:rB ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 6Lz*Qq.}W]ِc3WjsM^( m"g#yЍlVF5M}.?席Z?kPER(((((((((((((((((((((,Sz} t(((;^c}0cMAt/[1e2YZUɷ.wł+nEތș巷ԎieXyaO^U{-Vye^;°째E`sfc.>r펀b:[{X-"H5,GRkW"c;{RxVk;vLnrGP=PjY13= ,H#Iob.6RC$ XRON?f/dǰyETИ.qrM V %I;GAL֭g;C|jru>Kw'q#nTd~ \0ʐGXd6IèS÷B?8_Bx fѮQrm'&W]1\~kěc׊۳У\ہVPjN\W+l[VF¥O`G4|E O$|@6Q[$TwHBx\?րJīr5W#k@c2}?UP(@QEWfTZPH3:ʖȎv+ Qh81<:T6WVA#H;FdĮ;y V܎*s\iQA!EPEPEPEPEPEPEPEPEPEPEPE `sRGܐI\z>U%h= -!@.?ζ,j@kn ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (= U]cZT}̯?".#y1(2i-59]֔jOln(Vw8ϦG!EѢN(BpO˧NJ,vĪgPf9Eբ{2^oOM4/EբN#50FwlQYvi15|iEԣj祴{njQY[mƌ=v}2hRƲ@T`Ӽ\EiQYX͸iBO?쟭/ER?P.9lKXH]}X`?֘bF~!Ӭ| p/RSj? T^QU(pRhHcL?lOKMذd[@:C)Tci|aJssOǢW-3RQ#؞”kVg7UAK@Z$&?Hm#dkF]f8XnIlI*(;Xc"*Tv =PZ4Pw(ѹ%Ѣ?.֛g2\{ȹ)o01Js!3ƘkJ`fHaN7z@xF+B7k(i~GpE3퇾iw<_ZTQp2ַ L}M/N?{էE0k fKPO .\kQ5n[jZ/6& EZq}fS7?zAi?zӢ=G_P, iu Ң}}RLF)Fv?'1 Ңr['SǠRd.*(-FW#ukN.chstu4Ѡ[g&[mMj@g@??4?cw jQ@GY3/!Ԣ3b3~5ֽBH΅[~6嘜 EǓ `_#ӚѬ=~}y((((((((((((((((* &?Uꥬg.vZn T4_D= (zyz3θFZZc+'_̦QE ( ( ( ( ( ( ( (8 sϵ-PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPvf_*m/At~?H;sU䲷_2Hje*$3A6qQAiolX F[Q֡/ơnXUij )+ǥ:;_!@1ثTP-3N]:'Ur,H:}2_6XA\5hpRiċH#!DQgZk=庐 gV-3vfڳ]4Op Zи.axW5OOѠȬI3vENM2K)Udt'jQEKSơ 򌍹O^ج_;J2wutqi/3 (Z((( X=XtTjY_cByVR3|7y#A&eS?:]XaAK4^f8`*E4eŏz KVX阄c&arim|@V=).:TS#Ob3RI0`X"io3v}MxR҃HvrI4@ܸ#JAs8E|O%QsEI򦖥zF0+Fg;yh=[Iʤ!R/dQM+Jp5X$!Qb17ùkpr1@ bǝ)>c/QUЦ?|4[29fX=MV>,k )`VbI^ާ>zғsZzfrfQq R@< Z ޛQ-A*lwadis0*֝ SzK\5)݌qvjmz8D;(_ZVR((){PEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEc=i1KE&)h ( ( ( ( ( ( ( ( ( (3tO|uY((j{\5i((((((((-VHSPOz:LI[|4M-fLAђG'mXa2 JqڠQX\[E(QEQEQEQEQEQEU-\\g֮=WAPZ:bа4B5@Q@:&ÿkZrEv.[5M*OQ*iEegL>+N?k(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@u?.A`W8?/mܠ 88\h*/XyyvuFK'M:1ݝO,WԓXgqh.2FpOYך{%pUNvh IoQT%`IWAG0>խSPR[ H`4$WLɷ#mexጄvRky6֘n!f;^_hɷy@ݴg㮯gX(=Er[#WZVJ,qs(1FXw4W=oj7[{xa׷[3HҨG PsHEQEQEQEQEQER' vr3Vꥮ *܎w) gK1YY nR=k-32E|sSR/JZ m!QEkTP4@QEQEQEQEQETN&g_ QE((WM q<hGA&RѾMOPB>mMmV&^6Oặ3 ;QEbht:cBkn4vIl(EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPHzZCҀ)XȋfB8`d{]B»[+J޻ -ͺHHJ915xZU6H-wMHc[t MX\n\(;\sʌR7pkO#sN+"| GJQX:\o.#bZ:5ĉ4 k/4}R[LdG84̖(C+{y7sg;QZk ܮd9<>3c*@\P2Oz\gv_6_ghD +w֨/qr}~O=OM \ʠivH$#6]E̻9!2t{܍~u%QI,1X+XDP8@&mTet" p>sD9*Zkk!(9pO_RVʗ 3W9jQH e2F'$pۢ"?MX ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (2Ln Z@ wJҦ(@QEQEQEQEQE#0U,zYr׌DsB5$㹦Tc5=[QAEPEPEPEPEPEPU5OjV6f3@4JtNNZTQEdݜ?I}}Okh3iFшY ]cz_W=LMER(((((((((((((((((((((((((@~Av ֓Z8%)4BFQE 9^ Vǽ>@pӕAkacyWjp47E& v 'ߔd+J@gh{E*!z I9^N*VAj>e( v[)53yz,x5zx~t6C6~zQ)+un4$_ 5^ъI_kQAR3cKYz8# yb"q/"T4BxvaP1 RlQYZifI<dP_'EfE,ɲI  ֟Z P( aLB>aWYVdq98*Q^@pG7C#и]MTQu4Š(F&gƶC?6ER((((((((((((((((((((((7L-A<bեYz2Ňޅ0~B)cIhPgv9fEPFv((()(=(Z( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (34~Ӭ$6g@!<δk3AN럝iEP&?8IG}>O6vƞ}Xx ?S\pP(Q@Q@Q@ LPEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEi3!L'L\}K.|)Ѝ04k/]$'Io+{S~R}Is3&FޯCЮo 'X8P@ֶ5-fqw\v%i rB* vre8`>][PKTbh0ʐG-Ql^$J5d#sR^FUiP~nȯf6,X!GSBzʋX@eOJvX:i{I/y9MwI$# G'n3iApAwfɼx؎@r ڀ:it  z k]"m"qsת:EޭѓC1 }Ԣ$`P RLw~+@AQol5}fd1=>c V)_)c`$dWE1@aQ}bp U  GhR5tRo X5;Y֭@5yngV=ׇa985Q@:\:L1;%1=OqSRYZ2͏1q+B{jqA5&L+zi[ZH$INjPER{YeM(*)7kp4.G&)pŏAIE=-55PCww (Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@:&v_oVjܦ(@bh&űm&1#[tQH(((((((((((((((((((((( 6{W1S5](@QEQYچq@4T693ڦ((((((*`dEP}^[VLܚˊ,jڀH*;QE((((((U$4U" ^֤[[vAhCNF)IWk|۽HTcؓںl(EPEPEPEPEPEjH#(ą$ p+5x/!;V&ɮT(wϥb֦&JncS)QEQEQEQEQEIXS\ͣ0c̆H]e"RxٰbU ӾZA)>Yyjn,yR:{a") >(EPEPEP~wgY Qs6kF ( ( ( (bRIҁ}<*V6BS#F>aV`thJZ(((((((((PEQETW_7T 6~!ZP0G(DP۬M/=yЫn̿Νts4_FOty3G@/E(((((((((((((((((((((((((tz?WvS >U>sN(EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEQEQEQEQE%-PEPEPEP?gxzukzVrwll(&Ƶr=wxms ݦ(@QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEP?e}!jY`h`QE ³y&EuR+R?04kdֲpf@ۘxVt9E!wn=vVknGw>( @QEQEQEQEQEQE6爓jSֺJʟEI!,4V]QZF !ϩZ@@;ʳԦ,~Pݍu4Tb[ZS0OH((((((giL.I)T$U]񲃌3\ڛH7tZhkG%¬|b*4K NpyWRYhf{ 6WK$6+ߢ)QEQEQEQEQEU^sO"5zVf@u9\?IV鞌)W_̾a+~O9s>5n=:S~f%:#9jmڱh!Uc]OViQEQEQEQEQEQEV~jX0*OJߪ IOeb3E_ZX'$AߙM~usJ~,^c#`jQE((((()03 @zQ,mo)HCkz((((ӳD1GZ5B'@QEQERVvs٠ŲIg ޔa-M< !9n[֦nIh((((((((((((mRTs3Ä9I(zBzIؠ(  ջXIT?UMGYl9\~bDrӵݦM~FH:j:Ң)QEQEQEQEQEQEQER M-QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEOV뙪i͓eb=ާ ۟ʫxAN(EG-Q@Q@Q@Q@RZ( ( ( ( ( ( ( ( (9 ( ( (-Q@Q@Q@&y-QEQEQEǿ+z0m)?*i (K]9xڶkϟO?`QE ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (3~縇h]df) (s\j0;03 5sM{"OP:ԖĶ!C ^ D_evZЂ_:GXr)ZQ~EPEPEPEPEPEPEPQ h`O )*jl_QhJ?M>|:Ɯ΀4h@QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEVԀ:u= mXw՝DgO*8I~Ԣ)TLJ')&-BI[̗FVboj_l#xTQEQEQEQEQEQEQEQEQEQEQEQEQERR@Q@Q@Q@Q@ʳȱATt[ָK<iEQEQEQEQEQEQEcab'91n|_]6ERC0nV>#`QE ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (33#Qõhu1֍6ER(((((EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPutUYvl#?\3 Hϭ1K$O>g->89OH1#Tc4(((((((( Z( ( ( ( ( ( ( d18SO(G?_UlPEPR샕-5X3oX1ۦg:|uPD0U@T7Ski&#;zV5.gHfF!3[0RFg-Q@Q@Q@S]U(59 N*(+RZ!!q`jWkQs)7設a|_jPЩVSO~3W+R?`QE dvp;@Au+vc)zQ v@:*J(OFQU%XxčfP?n#tΡ?MOk(@az1XQ`x{kv)QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEhz$Z̶j|v_ V6ER(((((((((((((((((((((((((((((((((((((((((((((((I0y &$$PIt+£X|Z EuC ? DT(f[ ՠ&Š((((((((((((((((= -!hÄfcJ٬o.ϴ/#[4QEirn$$-_%pv]E6]PܓSmN9&o~]JҶd;T`KwE3 sȠ F9#WnۦF- #>s0* ʁڀ5MI"DHq8Ҳ,!  ׃Xkɟh%550+}ERz`EtF'/ HWJHsam$<.zn6I\V\KYʑw`5<;Kum[ila%,']} Yh9 EIнtd T(3m]r`|ѣg*̄x0/:(((((((((((((((() @'u=sbx&k>G5MQE% 9cX`y;_l|WOMWSu?UK&.1UT܀qW |c (Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@%? ]/\ Ѿ*,n fec"(f( ˭+ƹ3H˂*{/+jo:l{^sq yO5|0L : S|}vOG$vn.*Nx kxQ:Zp8!6ElZiqִ.+y1dUKdHc裯h!4+-HD"nQE5\ Ƣ}U.A|p775EI"V;ygս?K_ >v{Ԋ@v\ ?+gwv:^:}(GE yϨ,?TçakÞ)E>>V\ _j}X^`dj.'3vRMd}G[Qp1Xr?Y+ZbX9yﻧJںE°Bj٢mqp֣?Zcux?ƒXϹ#?ʶhb,Ol P>yyxR5Tc?€7h-R3M8>LP4P9-}u Eb?R9=Fu&97k + i&d]ytEaǣ׬f?֗ק3@tV0Sx߈?JtikzB:c`rnicA;#k<e6Ïg3>@b}:i%iPv[2~7票1܅zbHIEsWڛ!C mu 툞>^ @p3]5OOk51r)QEQEQEG4Te})TR_ʭTN( ( ( ( ( ((( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (9_rLџ+n|< cNS`QE 0;|I5X7G*8L~FQE((((((((((((((((((((((@?PO+N!uaX?Jl(EPEPEPEPEPEPEPEPEPEPE((((((((((((+/]V#d8w5\vqj2/hAM]<>4&kYq</Zimwq3*uǯ HܸP9>3M ZCORsV㰸&xW Vcntb@*IQ`7$g#؀uۿp°+ZOkV)QEQHFihFAȥ(((((((d1*laV0`EPEPEPEPEPEPEPEPEC=63H>Y20e=EQEQEu?ay\Xʸ1J3KkL0Yǹ+@i]3[{(nKm[ sSGu ܢŦ3q e +)l/_=1M6x8TEcfߖ|w9t˽ B\x_ROm:蓋1hN˹#I]&|Ұ Z+,/aѦc^ɏbG z+/n-ؖhMMjdzSHt돯 s'CК#VR1]Կ6Y.ޠ MѹGβCN3ni?J8޿l_γ?iF0dlu@^l`gztyEf c UЭ>8 ,c?iTN\ H;u?3N_70G} O>txxU!YӞ3@l?Р^qЪؖGS,h q(7UVKQźPhQn#iuCim9CTh Ju+A`?QhY?SGM4AZ4cߡɴ !9Ɨ.9(OѠ5k6M V~F[ UXgڕlm+FBڽLBL85/-wp8[A0NzQZ ~SMՑ#ǐ* W=Hu qjȳ dRKv0FT:ՠd *̥0 )tUͿ,?ͺY9OCy4hE1$gsN:Ÿ8+ >b**)F>A -drI+CȋQc/@[R}Wb ϷZA@TSD<ҁvfs%"1+E"P0NP C]3ХqMjlOK: el6R**aVLQz@-ʽmu4kr3hG [J]^[&*:_UQfXc<Ed.xbI]GٟOV>qnHSFt_h+fѽ*HǦAF ~ lQ@R,@N(5EdUoj"ɪ}Ң2m$ɦTkEY51BGgW9[sӢ3v|]&h M[w,N\kF.w񋈳Vh󟽴bhd~TRua-}1kEX5Rg OF?lEKmSLm\c?¶c}WqH~ :Zbv<ԿlMFi8Z(wI:]?MkE:]R{j#^1 շE K7yhŸA\v_v_7#'uQp9vrp$b2{S뢢4*}?Ҧ_ q?z 6ienëɫ4Q@Q@z[)nV3Zd3ɭ`QE ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (2#[^EkP6O'MQE(I]cjr`ڇ!z?ZOMa>6`l|KiF% |j! QQ@Q@Q@Q@Q@Q@V71@n%)8 ˻|%>Dg*SZ= HoAY " (Q@Q@Q@Q@Q@Q@ E'UFx $8M{VXAu .""6#~_MڋbI&$i&X]|]Zgmc֢)QERRP\׊aWKX>(O[ELJhiSi=$R3#n6svQՔ;KHq~Ի7 ?mC~oe}V*fmL}홈1!]^;à]y@QřlnJn}xPr/žY@~S\%ĒA'&r }3g1>(ە:P 5]滪c-651QE ( ( )8fT+=i]رEWI|v+%2I( 1tQ`&i|/.@iTXTB۟o9vr7; "Ob/OТeҟ}ƮQ@WB449T(op(QEQEQEQEQEQEQEQEw ]$gt{3ZՍzAj lI`QE ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (0k|nV&규rKdmA99 ER03 EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE#6$aعg}#^®BEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEbjMZ#9%Gkn5ڦE((((((((((((h IPEPEPEPEPEPEPEPEPEPEPi57EiVS7N!^s>ZQH(sQ,9^e:f6EqPf0'NpE;HkFcc֮iL$}ESAG3Zh* uQH(((((nEԌ;w#dy$*(ms%x̹VV}Usm)gY[I/? ; Z(Q@Q@Q@Q@Q@Q@Q@E#19%{ԖZl\3&0\PEPEPEPX3cɪ,]|_rGTbOn4 aaUVyfP&] oɎO/P>r0gR=z.8-+KH^b0*bPʖ+U$ЯP4[99ɩ-Yل,X4Kk54L@cP2.+㸩bDz}*G`Vd'odz Z3Jd+R+gPNzj{3Dɱ@tQE ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (2OlǪ7J˺>qjS)QEQEQEfPEPEPEPEPEPEPEPEPEPEPEPEP&ރ۬M7jw~cMQE(((((((((((((((((((((((((((((()oʟU.̙b#A]LFcՊEPtQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEbj fͽ@ ׭֛`QE ( ( ( ( ( ( ( ( ( ( (2azZCҖ ( ( ( ( ( ( ( ( ( (2#\#͏Vdjl(EPEPEPEPEPEPEPEPEPEPHI`dԴPEPEPEPEPEPEPEPEPEPEPEPHii6ُ5VB9o]1$kΡkrZZI1z:s:fvq"wp+gWv%ڧΟ)^ۿÚӬ[Vs"w\`VnLn]h81ōӫ H0 (]* #8#5Fͮ^Xs]h,ȌWCʾƀ:!CHds4[[| r`A@ \k`1]?Sn"նH{z晬qn$trcY PybQ@Q@Q@Q@Q@Q@! ( ( ( ( ( ( ( ( ( ( ( (3.OlZu{:{Zt(@QEQEQEQEQEQEQEQEQEQEQEQEQE2I<WdRҀ ( (0H k[tzgMQE(.1;0ֳtjcprC#g"'?uI#Օyp ?9iU$[fv8-AyAr)] vQER(\m1{yoJg9,rN+jrhLPGIDP:7jP@G.^(Q*45QH5XVfD Z5#'~BuDg'"h;h~m%MjZLsK@Q@Q@Q@Q@Q@Q@Q@Q@Q@88:uQEQEQEQEQEQEQEQEQEQEɤ-ߵ>Jyv(ʎv>> պUETRww (B(((((((((*=:(((((((((((((ɥ ( ( ( ( (0u{nsŒoW?C=Cژ ER(((((((((((((((((-ޖh9Sr;((ƛqkf'$x"Mm`QE ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ()jN@fxj,Mq!?ª?Z۸΂HW;attۗFpژ ľLA5}L0=-K};Wl s#Ҁ5d N}Zkwq*=>KWIE!jR2bc?Y5Ke+ǻp('Rlꓓ.j3[ lT0Yrd9#?ZvoDϩ94ۋ|ۍ8h2@~wqYVT|[_Yn8R[g5bXk B"IZK]ZO.9>rx 1*N5]rz-}T~p ;n ^JX$r@EҗRycIa#k. fou#kNn! q"o*0SVp>2?5\b6׭<8Q\`T]gdAU$Vɂ$IYxv5f+Be6}::svX _(Sۙ{VI,%9ɩ]#JuE:@YWOrW*y>c9]5g9?կ!x(xv.6N9UTRd▐ V)."PSإ@QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEG4ZűwBizb۲QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQERn:˟LP~D%>a_*v劉#0d'-Р EdM)>myus9j~lEPmfzkVD hQY#2w Ic?@tVbmdNn2 iYm `CNmr@'?7ԧW;ZjJ.̄Ve$ZG)i6>S04@QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQER0 4}ݩ]PEPEP%Lj!_kn.H!5MQE((((((((((p-޿:o_ΓO@fFL>)2&KEBnɚ?S q@hGThZc>zcEET:BqHڭ\6f?IՖqSNhQYYɦnZ붋Ef6fǠog#mZݶ~~#8FRN鷸!)c^.(:+l>nL;w?VCVR0GelM($ddӼ۸i@&A9>JsܐWN~<unhF#QS#9Jj1Yw祑'} k+6=I$T38P=ik-F7q=aq|-6=[O4FJh ܧ".wj@G2*{GP7&r6 5J[/Dz3ͣyn5@76nőζE0Os5ƴE_H!]Z I 4QprA!͞Z=7eUw7ץ^Ep%_{~[t$8?ukD]3Tc? /ivm |铜S&}-z8U?`o\f[S'Jco`]Ĩ=5Di7Ԯ3ߦ*13bMF]UKvrq;[BLk垪exI_W'4.t:ޜwsVԥ&Ƈ#0/)A?(:P>ρ pi,tK)_i{G} -$&Non? in#V񏳏Ni?,ßrƀ,iY_ФӲ:AqܮMGa1s)>٣^~&˺iX4a,/iۚ~3㦥UmS-!E0 ˬذț4OV?*nq̜i?qgm\[+U9[xtSEg7-kB[xT`D)|"2Lja92dvȤ:fՅj""OSThEnXLލhm`~buH8'+/ + !ؐ)0(z{Nhjk]XnFMe lxֲ@ 9N]c@f*vBM~-si}{} Cyn?g"A3I8qv( yl Vo^E1ʵhe"jɒ,ǵ)V 0~֥\ V'jE\|hsZQp2ͮH{ FnҵhdyY*`^hQEzJO9mN+V.P~Ɛ鷥?5E1ۖ9s܁צ*^he&MkiخO7=?ɭz)\ ѝ:]O?G3]9Ƶp2 ϵ5t۽@z(CC\ Sz'ZR@?备 I%ϮEiE%1ɡtKU˟ƴ m9Z= EQm&՟qVƚ5HqF5Eg FqZPh'c%؃ع(<$4v ֍%ۏW5+Vnr&Q@%`ȩ(#U>bEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEG;B~cF(sG+[cZK{sc/UTx+03֢AAO>OѢ3'd_Pr*y}A@XeQ+Zuf'z֦ER(((((((((((((((( ɬkQԗSObEXXX43|0)GԔQAwՅQ@((((((((((((((((((((((((((py5-V8%ƥŠ(AEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEs@ EPEPEPEPEPEPEPEPEPEPvL~f+S\6 nS`Sb3i*=g鷪K"չYW2eymԎƄ? ܷ$ߒI1R$p !жբ`W~th7x嚁 C]pʞ@ C(jZvC)wnj˿ +VS(!)]Jcթq;6HqTnK'8"(lH=ZS#oր>2NY~SW5!_Z>y |օ ܦ]7@rt{W.FKIylHvSi37VcW(Gݦw fbAlV^D9œ.kAyj𓴞A5gD]6.1)cfd9)9)%ڋRE ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (1@VL" V `-QH((((((((((((((*xnmF79뛍Hi=j-msP\R搶JeV(97'vQE ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (#qۡUu51 V-QEQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@?b,x?mGcη (Q@Q@Q@s ʡ$*5VݠYpB)0<>GhclbjiٱhT#* )qQE`REQEQEQEQEQEQEQEQE%-PE5qyPEPEPERREPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPHN;f@sKEQEQIqK@Q@Q@Q@Q@Q@Q@^ !mbb3@@֚d'+'ă6HFx~CZ)(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@f%TJҢ>S65wg j>ѽ{ԴP9OQEQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@2W=Sْ`z nf0ƦŠ(AEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPErcu\u?Zi (Q@Q@Q@Q@Q@Q@Q@Q@Q@TsOoL nP+8$TQEQEQEQEQEQEQEQEQEQE%Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@>!\!ԌԁY#o VG1!QLE((((((F`X5VM ދBų8f#fdQQA!2IhY q< ^ 2qAD uCj;htnEP (%21Qe9eTr:tT(@QEQEQEQEQEQHHPI ;AmnR)TowEŲv< ۮSu?j)p|AI{ld6Hvێ1KH(((((((((((JZ(((((((((>+W#׭jVg?[8e}5L)QEQEQEQEQEQE K{UaNh$TR(qr¢9Y@4@~OterxSȕ+d6>bT=jŽțvF("te*JjOjv'!ǡ12a(b)=a$'`j EuZEGт'h[u1' Syy\[;I"&ʖf op(QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEROH*X׫ui]ZBOOHj;R ݅Q@(((((((((((((((((((((((((((((((((((((((((((((((((((((((( MYsZ3Ыnuo ZzeGյL(EPEPuZY Lm:䫃uQcN1nbmeH#b/viY@QEQEQEQEQEsv:}̺Irsf$}ONƵ(p9[Τ"n95^ Q0]$H1"+B3BE @(QE ( ( ( ( ((ImY* B$.k+ 2PrOHZEO_Ӵ XbVP u' JFp20iQEQEQE&~lRm5Ol ( ( .nP$-ZP{[(m'=5F3Z~EawKt=唍nQEQEQEQEQE&Z(((((((((-PEPEPEPE'9(((  F7kJ/I7ִRLE((((((tW BUk’ hGEIW=9"X{y#L$; Be㍋m*եݛ~sʫj+eNjo(ҧme)# 3RQHV8 d`@97 ZyU5P QE(((((((((((((((((((((((((((((yw+-2C1GB;Q5HAw>\Pk槙[;snZIQA!EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPd%Q*+i9OT-۷AӟARqe(0(LPEQEQEQE%-Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@m=O=[uq} ٦ER((((((((((((((((((@KMQuQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEeAִ Q?PִccQ@QEQEQEQEQEQEQEQEQEQEQQ\iJĀ-giDhT+QAEPEPEPEPEPEPEPEPEPEPEPEPEPHH'ڃqfԏ?uޯ  'ss\9Ns)Tmx(g8QE!8LA"SQEQEQE^ IJ= PӏPi (0(`avqҦPit * F^#23A.-$QEQEQEQEEqn"`HF5!8U{k;ێŚ((((((((()cPH$PaAJo28ӜjXF dgڃofSn*ۗhe8Y{ԑPC-Yc\pjhISr$)*#czS&E֑0S@X0ʐG-T+s. NAv1R6-Yhub@ A#.X EEugEZCSr )[Ϩ#jƧ|@Ca$ӒǗց+UfkȑuuʐGqTSO+l{T1jHH#oւ$-MGY2N((((((((((((((((((((((((((((((((((U\v wa؊خz{BsMt4QH(((((((((((((((((((():u((((((#Ro_΀E4ȃi ю PUq>m>|X(z*?iK87QgER:TV$rJEP}f:?#M݋, hT?sǚ}liMB:# >:O\Vh4U'?vBr9ǩ`/Yj3ơa>G}y6|<NhT>ӨgluT..*s~j |Ƃ?y y8pż/'4$dqbo|À)6pF\Ffב,3>nI>*?>/违,rq}((Q@ B{YP?(FmUaf`:AMrH_ZDa`@.̰@NR7h.$Vɩ+:h S%PK}c$cYWت"Ʒ11|g4T\ H%@`vEPjU<$'4)5Ө2 ( BýR(=L, N2c݃Ls*ziPOM *6j)-2T`.ͺR13`8{X*ȷG?iph)ԋ)VF.N47єg ެPcҢ$lx5fחgYdkBKxP 銍,^ւR 6EK)ŸJFDZhF'>2wJGuҝ-#}h^ӟ\Rxn(kdf̛8e*#'r+VtZ[z.EI,Lw6*z(3DW[#f Zv ֭5XaI9-ܞ[%gV֠E^NmȒydsfIPð5#HQKf/ҕQ3$^W dP$C ?RPM )Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@! /a{vib2}sx#;0⺪RYj[WgVͪ\iii'HL] J+)w|ɃTM^pIX\{ƁNOrhk#֚d@p]GY#Bl{a`#/6[v1D!MD8ϝ? IpR%Q_hz)F?Bco=$ǧF^dOSQSsUFn:I(GTh>h4hږC^cd|?L41@tȶ(7QֲDfy~jO=:b}Q`jˏ6Om%aMY пg䵂5 FT۲784lCgo&7O1R$R}Dcv h`lyp!=sZ>DAyihPap?ui$}XNԢ2ƩrN0Ͻ*wLHl~aZtPXlzoc\+ZmJxlhgj($]j/qN3jh:jԢ2Dv5֭nQ49jڎl6A۰\:ע3>ۨӹA 6!(E3::`SL`>kۖ(P:Z[j94˹tf!HsWD{yo=1@ EPEPEPEPEPEPEPEPI(A((((((((( yymGOK~J/.ܚnXs-ʱK$bK$nz5 Dl1TVR, Q[[-m$ Mrdsv$_H$.m7 $CWY"ΈSZnfsj֤  [Gfr9%v(M]'X r>#Tm[k0O[7sjEud @` `1&uIEwbP)hQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@sZV˶1Ȭk˖T胊|WREԖj]NEʚTOuuJ0AVI V}xYb]r{UhCɸvMA+')L r' {r[F$44mʀRWȭRIDN5,bX7CUO&>NTuܱ)@?vGj4@Iփ)(c ( A(¯Rz d)Y`@2 }j(v)H6wTFsAn*ZBC<\ ''p t#($gn3#iRKkn=&Os#4O31>P?k>dGB~DT@QA.MŠ*3*D}JTS!M:zԊw(#gk<ȝF*8`dVh|ܣ H_yQ(3֣0DzƿIEMIxAϖKE(QEEqP~đ ")P;X(QLg OzEPEP]B?2ձy;ez[_:`JKES9((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((e Z(((((((((((((((((((((((((((((((((((((((((((((((((((܈@PFOZUcj *wyJI䚛RA-Š(AEPEPEPEPEPEPEPEPEPEPEPEPTPĎUγs%G/5 lj)TPe(1)"I=)-`|5R?RAApWv(jlQE4ȃ u6BDlW82B )zVҭjF֕RNU{&êՊlOB(".- ޥyS/N* (led[4sFԤe(QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEV}{"օg dl~Iy_"u8WnBMler޴.YOJBG.^І\@'i@U#AaE.HB\H$օ2HU".WpINPz騋A)}(AL@SN+>t)45ʓۛxU.XWM P\L\KEQ@Q@Q@Q@ eb~QW9=P;Q@((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((()#.C1sz}(QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEۈ [EG> (Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@m譌dN((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((C?(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((endstream endobj 5 0 obj <> stream 0.24000 0 0 0.24000 0 0 cm q 2480 0 0 1748 0 0 cm /Im1 Do Q endstream endobj 6 0 obj 60 endobj 7 0 obj <> >> endobj 8 0 obj <> endobj 3 0 obj <> endobj 1 0 obj <> endobj 2 0 obj <> endobj %RRR010createpdf*001.00.200**********1000 xref 0 9 0000000000 65535 f 0000176802 00000 n 0000176862 00000 n 0000176742 00000 n 0000000015 00000 n 0000176424 00000 n 0000176535 00000 n 0000176553 00000 n 0000176618 00000 n trailer <> startxref 176951 %%EOF Solaar-0.9.2/docs/debian.md000066400000000000000000000003771217372044600154540ustar00rootroot00000000000000# 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/ ./ Solaar-0.9.2/docs/devices.md000066400000000000000000000161431217372044600156520ustar00rootroot00000000000000# 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 Solaar-0.9.2/docs/devices/000077500000000000000000000000001217372044600153235ustar00rootroot00000000000000Solaar-0.9.2/docs/devices/k360.txt000066400000000000000000000076351217372044600165620ustar00rootroot00000000000000Receiver 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 Solaar-0.9.2/docs/devices/k800.txt000066400000000000000000000047461217372044600165610ustar00rootroot00000000000000# 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. Solaar-0.9.2/docs/devices/m345.txt000066400000000000000000000031771217372044600165640ustar00rootroot00000000000000Receiver 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, Solaar-0.9.2/docs/devices/m510.txt000066400000000000000000000023771217372044600165570ustar00rootroot00000000000000# 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' Solaar-0.9.2/docs/devices/m515.txt000066400000000000000000000027461217372044600165640ustar00rootroot000000000000001: 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, Solaar-0.9.2/docs/devices/m525.txt000066400000000000000000000001511217372044600165510ustar00rootroot00000000000000No non-error messages received for GET_REG and GET_REG_LONG. Perhaps because this is a HID++ 2.0 device? Solaar-0.9.2/docs/devices/m705.txt000066400000000000000000000032321217372044600165540ustar00rootroot00000000000000registers: # 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' Solaar-0.9.2/docs/devices/mk700.txt000066400000000000000000000017471217372044600167330ustar00rootroot00000000000000# 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' Solaar-0.9.2/docs/devices/performance-mx.txt000066400000000000000000000027461217372044600210200ustar00rootroot00000000000000# 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' Solaar-0.9.2/docs/i18n.md000066400000000000000000000023041217372044600150010ustar00rootroot00000000000000# 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 `; it will create/update the file `./po/.po`. 3. Edit `./po/.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= ./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. Solaar-0.9.2/docs/icons_names.txt000066400000000000000000000040121217372044600167350ustar00rootroot00000000000000# 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) Solaar-0.9.2/docs/installation.md000066400000000000000000000046561217372044600167370ustar00rootroot00000000000000# 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=""` 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 plugdev`. If these users were not assigned to the group before, they must re-login for the changes to take effect. Solaar-0.9.2/docs/logitech/000077500000000000000000000000001217372044600154775ustar00rootroot00000000000000Solaar-0.9.2/docs/logitech/battery-level.txt000066400000000000000000000021341217372044600210170ustar00rootroot00000000000000The 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 Solaar-0.9.2/docs/logitech/hid10.txt000066400000000000000000000064621217372044600171550ustar00rootroot00000000000000 *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) Solaar-0.9.2/docs/usb-ids.txt000066400000000000000000000005041217372044600160070ustar00rootroot00000000000000Unifying 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 Solaar-0.9.2/docs/usb.ids.txt000066400000000000000000000237101217372044600160140ustar00rootroot00000000000000# # List of USB ID's # # Maintained by Stephen J. Gowdy # 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 Solaar-0.9.2/lib/000077500000000000000000000000001217372044600135175ustar00rootroot00000000000000Solaar-0.9.2/lib/hidapi/000077500000000000000000000000001217372044600147555ustar00rootroot00000000000000Solaar-0.9.2/lib/hidapi/__init__.py000066400000000000000000000021551217372044600170710ustar00rootroot00000000000000# -*- 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.""" from __future__ import absolute_import, division, print_function, unicode_literals __version__ = '0.9' from hidapi.udev import ( enumerate, open, close, open_path, monitor_glib, read, write, get_manufacturer, get_product, get_serial, ) Solaar-0.9.2/lib/hidapi/hidconsole.py000066400000000000000000000157601217372044600174670ustar00rootroot00000000000000# -*- 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 import hidapi as _hid # # # try: read_packet = raw_input 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 is_string(data): s = marker + ' ' + data else: hexs = strhex(data) s = '%s (% 8.3f) [%s %s %s %s] %s' % (marker, t, hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:], repr(data)) 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 _error(text, scroll=False): _print('!!', text, scroll) def _continuous_read(handle, timeout=2000): while True: try: reply = _hid.read(handle, 128, timeout) except OSError as e: _error("Read failed, aborting: " + str(e), True) break assert reply is not None if reply: _print('>>', reply, True) def _validate_input(line, hidpp=False): try: data = unhexlify(line.encode('ascii')) except Exception as e: _error("Invalid input: " + str(e)) return None if hidpp: if len(data) < 4: _error("Invalid HID++ request: need at least 4 bytes") return None if data[:1] not in b'\x10\x11': _error("Invalid HID++ request: first byte must be 0x10 or 0x11") return None if data[1:2] not in b'\xFF\x01\x02\x03\x04\x05\x06': _error("Invalid HID++ request: second byte must be 0xFF or one of 0x01..0x06") return None if data[:1] == b'\x10': if len(data) > 7: _error("Invalid HID++ request: maximum length of a 0x10 request is 7 bytes") return None while len(data) < 7: data = (data + b'\x00' * 7)[:7] elif data[:1] == b'\x11': if len(data) > 20: _error("Invalid HID++ request: maximum length of a 0x11 request is 20 bytes") return None while len(data) < 20: data = (data + b'\x00' * 20)[:20] return data def _open(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 (default ~/.hidconsole-history)") arg_parser.add_argument('--hidpp', action='store_true', help="ensure input data is a valid HID++ request") arg_parser.add_argument('device', nargs='?', help="linux device to connect to (/dev/hidrawX); " "may be omitted if --hidpp is given, in which case it looks for the first Logitech receiver") return arg_parser.parse_args() def main(): args = _parse_arguments() handle = _open(args) 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 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) if __name__ == '__main__': main() Solaar-0.9.2/lib/hidapi/udev.py000066400000000000000000000252711217372044600163010ustar00rootroot00000000000000# -*- 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 implemented by signal11 (https://github.com/signal11/hidapi), and requires ``pyudev``. 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, Monitor as _Monitor, Device as _Device native_implementation = 'udev' # 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 # # exposed API # docstrings mostly copied from hidapi.h # def init(): """This function is a no-op, and exists only to match the native hidapi implementation. :returns: ``True``. """ return True def exit(): """This function is a no-op, and exists only to match the native hidapi implementation. :returns: ``True``. """ return True 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 vendor_id, product_id, and/or interface_number. :returns: a list of matching ``DeviceInfo`` tuples. """ for dev in _Context().list_devices(subsystem='hidraw'): 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): """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``. """ for device in enumerate(vendor_id, product_id): if serial is None or serial == device.serial: return open_path(device.path) 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``. """ assert device_path assert device_path.startswith('/dev/hidraw') return _os.open(device_path, _os.O_RDWR | _os.O_SYNC) def close(device_handle): """Close a HID device. :param device_handle: a device handle returned by open() or open_path(). """ assert device_handle _os.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). """ 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): """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. """ 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'' _DEVICE_STRINGS = { 0: 'manufacturer', 1: 'product', 2: 'serial', } 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 get_indexed_string(device_handle, 0) 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 get_indexed_string(device_handle, 1) 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 = get_indexed_string(device_handle, 2) 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. """ 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, _ignore, _ignore = hid_dev['HID_ID'].split(':') if bus == '0003': # USB usb_dev = dev.find_parent('usb', 'usb_device') assert usb_dev key = _DEVICE_STRINGS[index] attrs = usb_dev.attributes if key in attrs: return attrs[key] elif bus == '0005': # BLUETOOTH # TODO pass Solaar-0.9.2/lib/logitech_receiver/000077500000000000000000000000001217372044600172015ustar00rootroot00000000000000Solaar-0.9.2/lib/logitech_receiver/__init__.py000066400000000000000000000033541217372044600213170ustar00rootroot00000000000000# -*- 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 Solaar-0.9.2/lib/logitech_receiver/base.py000066400000000000000000000401731217372044600204720ustar00rootroot00000000000000# -*- 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) Solaar-0.9.2/lib/logitech_receiver/base_usb.py000066400000000000000000000043061217372044600213410ustar00rootroot00000000000000# -*- 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, ) Solaar-0.9.2/lib/logitech_receiver/common.py000066400000000000000000000175121217372044600210510ustar00rootroot00000000000000# -*- 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 Solaar-0.9.2/lib/logitech_receiver/descriptors.py000066400000000000000000000214161217372044600221200ustar00rootroot00000000000000# -*- 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(), ], ) Solaar-0.9.2/lib/logitech_receiver/hidpp10.py000066400000000000000000000242321217372044600210230ustar00rootroot00000000000000# -*- 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 Solaar-0.9.2/lib/logitech_receiver/hidpp20.py000066400000000000000000000266111217372044600210270ustar00rootroot00000000000000# -*- 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 } Solaar-0.9.2/lib/logitech_receiver/i18n.py000066400000000000000000000031141217372044600203310ustar00rootroot00000000000000# -*- 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"), ) Solaar-0.9.2/lib/logitech_receiver/listener.py000066400000000000000000000145761217372044600214150ustar00rootroot00000000000000# -*- 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__ Solaar-0.9.2/lib/logitech_receiver/notifications.py000066400000000000000000000214621217372044600224310ustar00rootroot00000000000000# -*- 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 <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") # 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) Solaar-0.9.2/lib/logitech_receiver/receiver.py000066400000000000000000000405461217372044600213700ustar00rootroot00000000000000# -*- 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 '' % (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) Solaar-0.9.2/lib/logitech_receiver/settings.py000066400000000000000000000252461217372044600214240ustar00rootroot00000000000000# -*- 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 '' % (self._rw.kind, self._validator.kind, self._device.codename, self.name, self._value) return '' % (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) Solaar-0.9.2/lib/logitech_receiver/settings_templates.py000066400000000000000000000137031217372044600234750ustar00rootroot00000000000000# -*- 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)) Solaar-0.9.2/lib/logitech_receiver/special_keys.py000066400000000000000000000174641217372044600222420ustar00rootroot00000000000000# -*- 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 # _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() Solaar-0.9.2/lib/solaar/000077500000000000000000000000001217372044600150005ustar00rootroot00000000000000Solaar-0.9.2/lib/solaar/__init__.py000066400000000000000000000016431217372044600171150ustar00rootroot00000000000000# -*- 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' Solaar-0.9.2/lib/solaar/cli.py000066400000000000000000000340711217372044600161260ustar00rootroot00000000000000#!/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() Solaar-0.9.2/lib/solaar/configuration.py000066400000000000000000000065641217372044600202340ustar00rootroot00000000000000# -*- 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 Solaar-0.9.2/lib/solaar/gtk.py000066400000000000000000000051701217372044600161420ustar00rootroot00000000000000#!/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() Solaar-0.9.2/lib/solaar/i18n.py000066400000000000000000000034601217372044600161340ustar00rootroot00000000000000# -*- 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 Solaar-0.9.2/lib/solaar/listener.py000066400000000000000000000226061217372044600172050ustar00rootroot00000000000000# -*- 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 '' % (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) Solaar-0.9.2/lib/solaar/ui/000077500000000000000000000000001217372044600154155ustar00rootroot00000000000000Solaar-0.9.2/lib/solaar/ui/__init__.py000066400000000000000000000111321217372044600175240ustar00rootroot00000000000000# -*- 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) Solaar-0.9.2/lib/solaar/ui/about.py000066400000000000000000000052001217372044600170760ustar00rootroot00000000000000# -*- 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 gi.repository import Gtk from solaar import __version__, NAME from solaar.i18n import _ # # # _dialog = None def _create(): about = Gtk.AboutDialog() about.set_program_name(NAME) about.set_version(__version__) about.set_comments(_("Shows status of devices connected\nthrough wireless Logitech receivers.")) about.set_logo_icon_name(NAME.lower()) about.set_copyright('© 2012-2013 Daniel Pavel') about.set_license_type(Gtk.License.GPL_2_0) about.set_authors(('Daniel Pavel http://github.com/pwr',)) try: about.add_credit_section(_("GUI design"), ('Julien Gascard', 'Daniel Pavel')) about.add_credit_section(_("Testing"), ( 'Douglas Wagner', 'Julien Gascard', 'Peter Wu http://www.lekensteyn.nl/logitech-unifying.html', )) about.add_credit_section(_("Logitech documentation"), ( 'Julien Danjou http://julien.danjou.info/blog/2012/logitech-unifying-upower', 'Nestor Lopez Casado http://drive.google.com/folderview?id=0BxbRzx7vEV7eWmgwazJ3NUFfQ28', )) except TypeError: # gtk3 < ~3.6.4 has incorrect gi bindings import logging logging.exception("failed to fully create the about dialog") except: # the Gtk3 version may be too old, and the function does not exist import logging logging.exception("failed to fully create the about dialog") about.set_translator_credits('\n'.join(( 'Adrian Piotrowicz (polski)', 'Daniel Pavel (română)', ))) about.set_website('http://pwr.github.io/Solaar/') about.set_website_label(NAME) about.connect('response', lambda x, y: x.hide()) def _hide(dialog, event): dialog.hide() return True about.connect('delete-event', _hide) return about def show_window(trigger=None): global _dialog if _dialog is None: _dialog = _create() _dialog.present() Solaar-0.9.2/lib/solaar/ui/action.py000066400000000000000000000060311217372044600172440ustar00rootroot00000000000000# -*- 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 gi.repository import Gtk, Gdk # from logging import getLogger # _log = getLogger(__name__) # del getLogger from solaar.i18n import _ # # # def make(name, label, function, stock_id=None, *args): action = Gtk.Action(name, label, label, None) action.set_icon_name(name) if stock_id is not None: action.set_stock_id(stock_id) if function: action.connect('activate', function, *args) return action def make_toggle(name, label, function, stock_id=None, *args): action = Gtk.ToggleAction(name, label, label, None) action.set_icon_name(name) if stock_id is not None: action.set_stock_id(stock_id) action.connect('activate', function, *args) return action # # # # def _toggle_notifications(action): # if action.get_active(): # notify.init('Solaar') # else: # notify.uninit() # action.set_sensitive(notify.available) # toggle_notifications = make_toggle('notifications', 'Notifications', _toggle_notifications) from .about import show_window as _show_about_window from solaar import NAME about = make('help-about', _("About") + ' ' + NAME, _show_about_window, stock_id=Gtk.STOCK_ABOUT) # # # from . import pair_window def pair(window, receiver): assert receiver assert receiver.kind is None pair_dialog = pair_window.create(receiver) pair_dialog.set_transient_for(window) pair_dialog.set_destroy_with_parent(True) pair_dialog.set_modal(True) pair_dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG) pair_dialog.set_position(Gtk.WindowPosition.CENTER) pair_dialog.present() from ..ui import error_dialog def unpair(window, device): assert device assert device.kind is not None qdialog = Gtk.MessageDialog(window, 0, Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE, _("Unpair") + ' ' + device.name + ' ?') qdialog.set_icon_name('remove') qdialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) qdialog.add_button(_("Unpair"), Gtk.ResponseType.ACCEPT) choice = qdialog.run() qdialog.destroy() if choice == Gtk.ResponseType.ACCEPT: receiver = device.receiver assert receiver device_number = device.number try: del receiver[device_number] except: # _log.exception("unpairing %s", device) error_dialog('unpair', device) Solaar-0.9.2/lib/solaar/ui/config_panel.py000066400000000000000000000124251217372044600204170ustar00rootroot00000000000000# -*- 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 gi.repository import Gtk, GLib from solaar.i18n import _ from solaar.ui import async as _ui_async from logitech_receiver.settings import KIND as _SETTING_KIND # # # def _read_async(setting, force_read, sbox, device_is_online): def _do_read(s, force, sb, online): v = s.read(not force) GLib.idle_add(_update_setting_item, sb, v, online, priority=99) _ui_async(_do_read, setting, force_read, sbox, device_is_online) def _write_async(setting, value, sbox): _ignore, failed, spinner, control = sbox.get_children() control.set_sensitive(False) failed.set_visible(False) spinner.set_visible(True) spinner.start() def _do_write(s, v, sb): v = setting.write(v) GLib.idle_add(_update_setting_item, sb, v, True, priority=99) _ui_async(_do_write, setting, value, sbox) # # # def _create_toggle_control(setting): def _switch_notify(switch, _ignore, s): if switch.get_sensitive(): _write_async(s, switch.get_active() == True, switch.get_parent()) c = Gtk.Switch() c.connect('notify::active', _switch_notify, setting) return c def _create_choice_control(setting): def _combo_notify(cbbox, s): if cbbox.get_sensitive(): _write_async(s, cbbox.get_active_id(), cbbox.get_parent()) c = Gtk.ComboBoxText() for entry in setting.choices: c.append(str(entry), str(entry)) c.connect('changed', _combo_notify, setting) return c # def _create_slider_control(setting): # def _slider_notify(slider, s): # if slider.get_sensitive(): # _apply_queue.put(('write', s, slider.get_value(), slider.get_parent())) # # c = Gtk.Scale(setting.choices) # c.connect('value-changed', _slider_notify, setting) # # return c # # # def _create_sbox(s): sbox = Gtk.HBox(homogeneous=False, spacing=6) sbox.pack_start(Gtk.Label(s.label), False, False, 0) spinner = Gtk.Spinner() spinner.set_tooltip_text(_("Working") + '...') failed = Gtk.Image.new_from_icon_name('dialog-warning', Gtk.IconSize.SMALL_TOOLBAR) failed.set_tooltip_text(_("Read/write operation failed.")) if s.kind == _SETTING_KIND.toggle: control = _create_toggle_control(s) elif s.kind == _SETTING_KIND.choice: control = _create_choice_control(s) # elif s.kind == _SETTING_KIND.range: # control = _create_slider_control(s) else: raise NotImplemented control.set_sensitive(False) # the first read will enable it sbox.pack_end(control, False, False, 0) sbox.pack_end(spinner, False, False, 0) sbox.pack_end(failed, False, False, 0) if s.description: sbox.set_tooltip_text(s.description) sbox.show_all() spinner.start() # the first read will stop it failed.set_visible(False) return sbox def _update_setting_item(sbox, value, is_online=True): _ignore, failed, spinner, control = sbox.get_children() spinner.set_visible(False) spinner.stop() # print ("update", control, "with new value", value) if value is None: control.set_sensitive(False) failed.set_visible(is_online) return failed.set_visible(False) if isinstance(control, Gtk.Switch): control.set_active(value) elif isinstance(control, Gtk.ComboBoxText): control.set_active_id(str(value)) # elif isinstance(control, Gtk.Scale): # control.set_value(int(value)) else: raise NotImplemented control.set_sensitive(True) # # # # config panel _box = None _items = {} def create(): global _box assert _box is None _box = Gtk.VBox(homogeneous=False, spacing=8) _box._last_device = None return _box def update(device, is_online=None): assert _box is not None assert device device_id = (device.receiver.path, device.number) if is_online is None: is_online = bool(device.online) # if the device changed since last update, clear the box first if device_id != _box._last_device: _box.set_visible(False) _box._last_device = device_id # hide controls belonging to other devices for k, sbox in _items.items(): sbox = _items[k] sbox.set_visible(k[0:2] == device_id) for s in device.settings: k = (device_id[0], device_id[1], s.name) if k in _items: sbox = _items[k] else: sbox = _items[k] = _create_sbox(s) _box.pack_start(sbox, False, False, 0) _read_async(s, False, sbox, is_online) _box.set_visible(True) def clean(device): """Remove the controls for a given device serial. Needed after the device has been unpaired. """ assert _box is not None device_id = (device.receiver.path, device.number) for k in list(_items.keys()): if k[0:2] == device_id: _box.remove(_items[k]) del _items[k] def destroy(): global _box _box = None _items.clear() Solaar-0.9.2/lib/solaar/ui/icons.py000066400000000000000000000157441217372044600171150ustar00rootroot00000000000000# -*- 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 gi.repository import Gtk # # # _LARGE_SIZE = 64 Gtk.IconSize.LARGE = Gtk.icon_size_register('large', _LARGE_SIZE, _LARGE_SIZE) # Gtk.IconSize.XLARGE = Gtk.icon_size_register('x-large', _LARGE_SIZE * 2, _LARGE_SIZE * 2) # print ("menu", int(Gtk.IconSize.MENU), Gtk.icon_size_lookup(Gtk.IconSize.MENU)) # print ("small toolbar", int(Gtk.IconSize.SMALL_TOOLBAR), Gtk.icon_size_lookup(Gtk.IconSize.SMALL_TOOLBAR)) # print ("button", int(Gtk.IconSize.BUTTON), Gtk.icon_size_lookup(Gtk.IconSize.BUTTON)) # print ("large toolbar", int(Gtk.IconSize.LARGE_TOOLBAR), Gtk.icon_size_lookup(Gtk.IconSize.LARGE_TOOLBAR)) # print ("dnd", int(Gtk.IconSize.DND), Gtk.icon_size_lookup(Gtk.IconSize.DND)) # print ("dialog", int(Gtk.IconSize.DIALOG), Gtk.icon_size_lookup(Gtk.IconSize.DIALOG)) TRAY_INIT = 'solaar-init' TRAY_OKAY = 'solaar' TRAY_ATTENTION = 'solaar-attention' def _look_for_application_icons(): import os.path as _path from os import environ as _environ import sys as _sys if _log.isEnabledFor(_DEBUG): _log.debug("sys.path[0] = %s", _sys.path[0]) prefix_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..')) src_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..', 'share')) local_share = _environ.get('XDG_DATA_HOME', _path.expanduser(_path.join('~', '.local', 'share'))) data_dirs = _environ.get('XDG_DATA_DIRS', '/usr/local/share:/usr/share') del _sys share_solaar = [prefix_share] + list(_path.join(x, 'solaar') for x in [src_share, local_share] + data_dirs.split(':')) for location in share_solaar: location = _path.join(location, 'icons') if _log.isEnabledFor(_DEBUG): _log.debug("looking for icons in %s", location) if _path.exists(_path.join(location, TRAY_ATTENTION + '.svg')): yield location del _environ # del _path _default_theme = Gtk.IconTheme.get_default() for p in _look_for_application_icons(): _default_theme.prepend_search_path(p) if _log.isEnabledFor(_DEBUG): _log.debug("icon theme paths: %s", _default_theme.get_search_path()) # # # _has_gpm_icons = _default_theme.has_icon('gpm-battery-020-charging') _has_oxygen_icons = _default_theme.has_icon('battery-charging-caution') and \ _default_theme.has_icon('battery-charging-040') _has_gnome_icons = _default_theme.has_icon('battery-caution-charging') and \ _default_theme.has_icon('battery-full-charged') _has_elementary_icons = _default_theme.has_icon('battery-020-charging') if _log.isEnabledFor(_DEBUG): _log.debug("detected icon sets: gpm %s, oxygen %s, gnome %s, elementary %s", _has_gpm_icons, _has_oxygen_icons, _has_gnome_icons, _has_elementary_icons) if (not _has_gpm_icons and not _has_oxygen_icons and not _has_gnome_icons and not _has_elementary_icons): _log.warning("failed to detect a known icon set") # # # def battery(level=None, charging=False): icon_name = _battery_icon_name(level, charging) if not _default_theme.has_icon(icon_name): _log.warning("icon %s not found in current theme", icon_name); # elif _log.isEnabledFor(_DEBUG): # _log.debug("battery icon for %s:%s = %s", level, charging, icon_name) return icon_name def _battery_icon_name(level, charging): if level is None or level < 0: return 'gpm-battery-missing' \ if _has_gpm_icons and _default_theme.has_icon('gpm-battery-missing') \ else 'battery-missing' level_approx = 20 * ((level + 10) // 20) if _has_gpm_icons: if level == 100 and charging: return 'gpm-battery-charged' return 'gpm-battery-%03d%s' % (level_approx, '-charging' if charging else '') if _has_oxygen_icons: if level_approx == 100 and charging: return 'battery-charging' level_name = ('low', 'caution', '040', '060', '080', '100')[level_approx // 20] return 'battery%s-%s' % ('-charging' if charging else '', level_name) if _has_elementary_icons: if level == 100 and charging: return 'battery-charged' return 'battery-%03d%s' % (level_approx, '-charging' if charging else '') if _has_gnome_icons: if level == 100 and charging: return 'battery-full-charged' if level_approx == 0 and charging: return 'battery-caution-charging' level_name = ('empty', 'caution', 'low', 'good', 'good', 'full')[level_approx // 20] return 'battery-%s%s' % (level_name, '-charging' if charging else '') if level == 100 and charging: return 'battery-charged' # fallback... most likely will fail return 'battery-%03d%s' % (level_approx, '-charging' if charging else '') # # # def lux(level=None): if level is None or level < 0: return 'light_unknown' return 'light_%03d' % (20 * ((level + 50) // 100)) # # # _ICON_SETS = {} def device_icon_set(name='_', kind=None): icon_set = _ICON_SETS.get(name) if icon_set is None: icon_set = Gtk.IconSet.new() _ICON_SETS[name] = icon_set # names of possible icons, in reverse order of likelihood # the theme will hopefully pick up the most appropiate names = ['preferences-desktop-peripherals'] if kind: if str(kind) == 'numpad': names += ('input-keyboard', 'input-dialpad') elif str(kind) == 'touchpad': names += ('input-mouse', 'input-tablet') elif str(kind) == 'trackball': names += ('input-mouse',) names += ('input-' + str(kind),) # names += (name.replace(' ', '-'),) source = Gtk.IconSource.new() for n in names: source.set_icon_name(n) icon_set.add_source(source) icon_set.names = names return icon_set def device_icon_file(name, kind=None, size=_LARGE_SIZE): icon_set = device_icon_set(name, kind) assert icon_set for n in reversed(icon_set.names): if _default_theme.has_icon(n): return _default_theme.lookup_icon(n, size, 0).get_filename() def device_icon_name(name, kind=None): icon_set = device_icon_set(name, kind) assert icon_set for n in reversed(icon_set.names): if _default_theme.has_icon(n): return n def icon_file(name, size=_LARGE_SIZE): if _default_theme.has_icon(name): theme_icon = _default_theme.lookup_icon(name, size, 0) file_name = theme_icon.get_filename() # if _log.isEnabledFor(_DEBUG): # _log.debug("icon %s(%d) => %s", name, size, file_name) return file_name _log.warn("icon %s(%d) not found in current theme", name, size) Solaar-0.9.2/lib/solaar/ui/notify.py000066400000000000000000000077431217372044600173120ustar00rootroot00000000000000# -*- 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. # Optional desktop notifications. from __future__ import absolute_import, division, print_function, unicode_literals from solaar.i18n import _ # # # try: # this import is allowed to fail, in which case the entire feature is unavailable from gi.repository import Notify from logging import getLogger, INFO as _INFO _log = getLogger(__name__) del getLogger from solaar import NAME from . import icons as _icons # assumed to be working since the import succeeded available = True # cache references to shown notifications here, so if another status comes # while its notification is still visible we don't create another one _notifications = {} def init(): """Init the notifications system.""" global available if available: if not Notify.is_initted(): if _log.isEnabledFor(_INFO): _log.info("starting desktop notifications") try: return Notify.init(NAME) except: _log.exception("initializing desktop notifications") available = False return available and Notify.is_initted() def uninit(): if available and Notify.is_initted(): if _log.isEnabledFor(_INFO): _log.info("stopping desktop notifications") _notifications.clear() Notify.uninit() # def toggle(action): # if action.get_active(): # init() # else: # uninit() # action.set_sensitive(available) # return action.get_active() def alert(reason, icon=None): assert reason if available and Notify.is_initted(): n = _notifications.get(NAME) if n is None: n = _notifications[NAME] = Notify.Notification() # we need to use the filename here because the notifications daemon # is an external application that does not know about our icon sets icon_file = _icons.icon_file(NAME.lower()) if icon is None \ else _icons.icon_file(icon) n.update(NAME, reason, icon_file) n.set_urgency(Notify.Urgency.NORMAL) try: # if _log.isEnabledFor(_DEBUG): # _log.debug("showing %s", n) n.show() except Exception: _log.exception("showing %s", n) def show(dev, reason=None, icon=None): """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() message = reason or (_("unpaired") if dev.status is None else (str(dev.status) or (_("connected") if dev.status else _("offline")))) # we need to use the filename here because the notifications daemon # is an external application that does not know about our icon sets icon_file = _icons.device_icon_file(dev.name, dev.kind) if icon is None \ else _icons.icon_file(icon) n.update(summary, message, icon_file) urgency = Notify.Urgency.LOW if dev.status else Notify.Urgency.NORMAL n.set_urgency(urgency) try: # if _log.isEnabledFor(_DEBUG): # _log.debug("showing %s", n) n.show() except Exception: _log.exception("showing %s", n) except ImportError: available = False init = lambda: False uninit = lambda: None # toggle = lambda action: False alert = lambda reason: None show = lambda dev, reason=None: None Solaar-0.9.2/lib/solaar/ui/pair_window.py000066400000000000000000000147541217372044600203240ustar00rootroot00000000000000# -*- 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 gi.repository import Gtk, GLib from logging import getLogger, DEBUG as _DEBUG _log = getLogger(__name__) del getLogger from solaar.i18n import _ from . import icons as _icons from logitech_receiver.status import KEYS as _K # # # _PAIRING_TIMEOUT = 30 # seconds _STATUS_CHECK = 500 # milliseconds def _create_page(assistant, kind, header=None, icon_name=None, text=None): p = Gtk.VBox(False, 8) assistant.append_page(p) assistant.set_page_type(p, kind) if header: item = Gtk.HBox(False, 16) p.pack_start(item, False, True, 0) label = Gtk.Label(header) label.set_alignment(0, 0) label.set_line_wrap(True) item.pack_start(label, True, True, 0) if icon_name: icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG) icon.set_alignment(1, 0) item.pack_start(icon, False, False, 0) if text: label = Gtk.Label(text) label.set_alignment(0, 0) label.set_line_wrap(True) p.pack_start(label, False, False, 0) p.show_all() return p def _check_lock_state(assistant, receiver, count=2): if not assistant.is_drawable(): if _log.isEnabledFor(_DEBUG): _log.debug("assistant %s destroyed, bailing out", assistant) return False if receiver.status.get(_K.ERROR): # receiver.status.new_device = _fake_device(receiver) _pairing_failed(assistant, receiver, receiver.status.pop(_K.ERROR)) return False if receiver.status.new_device: device, receiver.status.new_device = receiver.status.new_device, None _pairing_succeeded(assistant, receiver, device) return False if not receiver.status.lock_open: if count > 0: # the actual device notification may arrive after the lock was paired, # so have a little patience GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver, count - 1) else: _pairing_failed(assistant, receiver, 'failed to open pairing lock') return False return True def _prepare(assistant, page, receiver): index = assistant.get_current_page() if _log.isEnabledFor(_DEBUG): _log.debug("prepare %s %d %s", assistant, index, page) if index == 0: if receiver.set_lock(False, timeout=_PAIRING_TIMEOUT): assert receiver.status.new_device is None assert receiver.status.get(_K.ERROR) is None spinner = page.get_children()[-1] spinner.start() GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver) assistant.set_page_complete(page, True) else: GLib.idle_add(_pairing_failed, assistant, receiver, 'the pairing lock did not open') else: assistant.remove_page(0) def _finish(assistant, receiver): if _log.isEnabledFor(_DEBUG): _log.debug("finish %s", assistant) assistant.destroy() receiver.status.new_device = None if receiver.status.lock_open: receiver.set_lock() else: receiver.status[_K.ERROR] = None def _pairing_failed(assistant, receiver, error): if _log.isEnabledFor(_DEBUG): _log.debug("%s fail: %s", receiver, error) assistant.commit() header = _("Pairing failed") + ': ' + _(str(error)) + '.' if 'timeout' in str(error): text = _("Make sure your device is within range, and has a decent battery charge.") elif str(error) == 'device not supported': text = _("A new device was detected, but it is not compatible with this receiver.") elif 'many' in str(error): text = _("The receiver only supports %d paired device(s).") else: text = _("No further details are available about the error.") _create_page(assistant, Gtk.AssistantPageType.SUMMARY, header, 'dialog-error', text) assistant.next_page() assistant.commit() def _pairing_succeeded(assistant, receiver, device): assert device if _log.isEnabledFor(_DEBUG): _log.debug("%s success: %s", receiver, device) page = _create_page(assistant, Gtk.AssistantPageType.SUMMARY) header = Gtk.Label(_("Found a new device") + ':') header.set_alignment(0.5, 0) page.pack_start(header, False, False, 0) device_icon = Gtk.Image() icon_set = _icons.device_icon_set(device.name, device.kind) device_icon.set_from_icon_set(icon_set, Gtk.IconSize.LARGE) device_icon.set_alignment(0.5, 1) page.pack_start(device_icon, True, True, 0) device_label = Gtk.Label() device_label.set_markup('%s' % device.name) device_label.set_alignment(0.5, 0) page.pack_start(device_label, True, True, 0) hbox = Gtk.HBox(False, 8) hbox.pack_start(Gtk.Label(' '), False, False, 0) hbox.set_property('expand', False) hbox.set_property('halign', Gtk.Align.CENTER) page.pack_start(hbox, False, False, 0) def _check_encrypted(dev): if assistant.is_drawable(): if device.status.get(_K.LINK_ENCRYPTED) == False: hbox.pack_start(Gtk.Image.new_from_icon_name('security-low', Gtk.IconSize.MENU), False, False, 0) hbox.pack_start(Gtk.Label(_("The wireless link is not encrypted") + '!'), False, False, 0) hbox.show_all() else: return True GLib.timeout_add(_STATUS_CHECK, _check_encrypted, device) page.show_all() assistant.next_page() assistant.commit() def create(receiver): assert receiver is not None assert receiver.kind is None assistant = Gtk.Assistant() assistant.set_title(receiver.name + ': ' + _("pair new device")) assistant.set_icon_name('list-add') assistant.set_size_request(400, 240) assistant.set_resizable(False) assistant.set_role('pair-device') page_intro = _create_page(assistant, Gtk.AssistantPageType.PROGRESS, _("Turn on the device you want to pair."), 'preferences-desktop-peripherals', _("If the device is already turned on,\nturn if off and on again.")) spinner = Gtk.Spinner() spinner.set_visible(True) page_intro.pack_end(spinner, True, True, 24) assistant.connect('prepare', _prepare, receiver) assistant.connect('cancel', _finish, receiver) assistant.connect('close', _finish, receiver) return assistant Solaar-0.9.2/lib/solaar/ui/tray.py000066400000000000000000000327671217372044600167650ustar00rootroot00000000000000# -*- 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 time import time as _timestamp from gi.repository import Gtk, GLib from gi.repository.Gdk import ScrollDirection from solaar import NAME from solaar.i18n import _ from logitech_receiver.status import KEYS as _K from . import icons as _icons from .window import popup as _window_popup, toggle as _window_toggle # # constants # _TRAY_ICON_SIZE = 32 # pixels _MENU_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR _RECEIVER_SEPARATOR = ('~', None, None, None) # # # def _create_menu(quit_handler): menu = Gtk.Menu() # per-device menu entries will be generated as-needed no_receiver = Gtk.MenuItem.new_with_label(_("No Logitech receiver found")) no_receiver.set_sensitive(False) menu.append(no_receiver) menu.append(Gtk.SeparatorMenuItem.new()) from .action import about, make menu.append(about.create_menu_item()) menu.append(make('application-exit', _("Quit"), quit_handler, stock_id=Gtk.STOCK_QUIT).create_menu_item()) del about, make menu.show_all() return menu try: # raise ImportError from gi.repository import AppIndicator3 if _log.isEnabledFor(_INFO): _log.info("using AppIndicator3") _last_scroll = 0 def _scroll(ind, _ignore, direction): if direction != ScrollDirection.UP and direction != ScrollDirection.DOWN: # ignore all other directions return if len(_devices_info) < 4: # don't bother with scrolling when there's only one receiver # with only one device (3 = [receiver, device, separator]) return # scroll events come way too fast (at least 5-6 at once) # so take a little break between them global _last_scroll now = _timestamp() if now - _last_scroll < 0.33: # seconds return _last_scroll = now # if _log.isEnabledFor(_DEBUG): # _log.debug("scroll direction %s", direction) global _picked_device candidate = None if _picked_device is None: for info in _devices_info: # pick first peripheral found if info[1] is not None: candidate = info break else: found = False for info in _devices_info: if not info[1]: # only conside peripherals continue # compare peripherals if info[0:2] == _picked_device[0:2]: if direction == ScrollDirection.UP and candidate: # select previous device break found = True else: if found: candidate = info if direction == ScrollDirection.DOWN: break # if direction is up, but no candidate found before _picked, # let it run through all candidates, will get stuck with the last one else: if direction == ScrollDirection.DOWN: # only use the first one, in case no candidates are after _picked if candidate is None: candidate = info else: candidate = info # if the last _picked_device is gone, clear it # the candidate will be either the first or last one remaining, # depending on the scroll direction if not found: _picked_device = None _picked_device = candidate or _picked_device if _log.isEnabledFor(_DEBUG): _log.debug("scroll: picked %s", _picked_device) _update_tray_icon() def _create(menu): theme_paths = Gtk.IconTheme.get_default().get_search_path() ind = AppIndicator3.Indicator.new_with_path( 'indicator-solaar', _icons.TRAY_INIT, AppIndicator3.IndicatorCategory.HARDWARE, ':'.join(theme_paths)) ind.set_title(NAME) ind.set_status(AppIndicator3.IndicatorStatus.ACTIVE) ind.set_attention_icon_full(_icons.TRAY_ATTENTION, '') # ind.set_label(NAME, NAME) ind.set_menu(menu) ind.connect('scroll-event', _scroll) return ind def _destroy(indicator): indicator.set_status(AppIndicator3.IndicatorStatus.PASSIVE) def _update_tray_icon(): if _picked_device: _ignore, _ignore, name, device_status = _picked_device battery_level = device_status.get(_K.BATTERY_LEVEL) battery_charging = device_status.get(_K.BATTERY_CHARGING) tray_icon_name = _icons.battery(battery_level, battery_charging) description = '%s: %s' % (name, device_status) else: # there may be a receiver, but no peripherals tray_icon_name = _icons.TRAY_OKAY if _devices_info else _icons.TRAY_INIT tooltip_lines = _generate_tooltip_lines() description = '\n'.join(tooltip_lines).rstrip('\n') # icon_file = _icons.icon_file(icon_name, _TRAY_ICON_SIZE) _icon.set_icon_full(tray_icon_name, description) def _update_menu_icon(image_widget, icon_name): image_widget.set_from_icon_name(icon_name, _MENU_ICON_SIZE) # icon_file = _icons.icon_file(icon_name, _MENU_ICON_SIZE) # image_widget.set_from_file(icon_file) # image_widget.set_pixel_size(_TRAY_ICON_SIZE) def attention(reason=None): if _icon.get_status != AppIndicator3.IndicatorStatus.ATTENTION: _icon.set_attention_icon_full(_icons.TRAY_ATTENTION, reason or '') _icon.set_status(AppIndicator3.IndicatorStatus.ATTENTION) GLib.timeout_add(10 * 1000, _icon.set_status, AppIndicator3.IndicatorStatus.ACTIVE) except ImportError: if _log.isEnabledFor(_INFO): _log.info("using StatusIcon") def _create(menu): icon = Gtk.StatusIcon.new_from_icon_name(_icons.TRAY_INIT) icon.set_name(NAME) icon.set_title(NAME) icon.set_tooltip_text(NAME) icon.connect('activate', _window_toggle) icon.connect('popup_menu', lambda icon, button, time: menu.popup(None, None, icon.position_menu, icon, button, time)) return icon def _destroy(icon): icon.set_visible(False) def _update_tray_icon(): tooltip_lines = _generate_tooltip_lines() tooltip = '\n'.join(tooltip_lines).rstrip('\n') _icon.set_tooltip_markup(tooltip) if _picked_device: _ignore, _ignore, name, device_status = _picked_device battery_level = device_status.get(_K.BATTERY_LEVEL) battery_charging = device_status.get(_K.BATTERY_CHARGING) tray_icon_name = _icons.battery(battery_level, battery_charging) else: # there may be a receiver, but no peripherals tray_icon_name = _icons.TRAY_OKAY if _devices_info else _icons.TRAY_ATTENTION _icon.set_from_icon_name(tray_icon_name) def _update_menu_icon(image_widget, icon_name): image_widget.set_from_icon_name(icon_name, _MENU_ICON_SIZE) _icon_before_attention = None def _blink(count): global _icon_before_attention if count % 2: _icon.set_from_icon_name(_icons.TRAY_ATTENTION) else: _icon.set_from_icon_name(_icon_before_attention) if count > 0: GLib.timeout_add(1000, _blink, count - 1) def attention(reason=None): global _icon_before_attention if _icon_before_attention is None: _icon_before_attention = _icon.get_icon_name() GLib.idle_add(_blink, 9) # # # def _generate_tooltip_lines(): if not _devices_info: yield '%s: ' % NAME + _("no receiver") return yield '%s' % NAME yield '' for _ignore, number, name, status in _devices_info: if number is None: # receiver continue p = str(status) if p: # does it have any properties to print? yield '%s' % name if status: yield '\t%s' % p else: yield '\t%s (' % p + _("offline") + ')' else: if status: yield '%s (' % name + _("no status") + ')' else: yield '%s (' % name + _("offline") + ')' yield '' def _pick_device_with_lowest_battery(): if not _devices_info: return None picked = None picked_level = 1000 for info in _devices_info: if info[1] is None: # is receiver/separator continue level = info[-1].get(_K.BATTERY_LEVEL) # print ("checking %s -> %s", info, level) if level is not None and picked_level > level: picked = info picked_level = level or 0 if _log.isEnabledFor(_DEBUG): _log.debug("picked device with lowest battery: %s", picked) return picked # # # def _add_device(device): assert device assert device.receiver receiver_path = device.receiver.path assert receiver_path index = None for idx, (path, _ignore, _ignore, _ignore) in enumerate(_devices_info): if path == receiver_path: # the first entry matching the receiver serial should be for the receiver itself index = idx + 1 break assert index is not None # proper ordering (according to device.number) for a receiver's devices while True: path, number, _ignore, _ignore = _devices_info[index] if path == _RECEIVER_SEPARATOR[0]: break assert path == receiver_path assert number != device.number if number > device.number: break index = index + 1 new_device_info = (receiver_path, device.number, device.name, device.status) assert len(new_device_info) == len(_RECEIVER_SEPARATOR) _devices_info.insert(index, new_device_info) # label_prefix = b'\xE2\x94\x84 '.decode('utf-8') label_prefix = ' ' new_menu_item = Gtk.ImageMenuItem.new_with_label(label_prefix + device.name) new_menu_item.set_image(Gtk.Image()) new_menu_item.show_all() new_menu_item.connect('activate', _window_popup, receiver_path, device.number) _menu.insert(new_menu_item, index) return index def _remove_device(index): assert index is not None menu_items = _menu.get_children() _menu.remove(menu_items[index]) removed_device = _devices_info.pop(index) global _picked_device if _picked_device and _picked_device[0:2] == removed_device[0:2]: # the current pick was unpaired _picked_device = None def _add_receiver(receiver): index = len(_devices_info) new_receiver_info = (receiver.path, None, receiver.name, None) assert len(new_receiver_info) == len(_RECEIVER_SEPARATOR) _devices_info.append(new_receiver_info) new_menu_item = Gtk.ImageMenuItem.new_with_label(receiver.name) _menu.insert(new_menu_item, index) icon_set = _icons.device_icon_set(receiver.name) new_menu_item.set_image(Gtk.Image().new_from_icon_set(icon_set, _MENU_ICON_SIZE)) new_menu_item.show_all() new_menu_item.connect('activate', _window_popup, receiver.path) _devices_info.append(_RECEIVER_SEPARATOR) separator = Gtk.SeparatorMenuItem.new() separator.set_visible(True) _menu.insert(separator, index + 1) return 0 def _remove_receiver(receiver): index = 0 found = False # remove all entries in devices_info that match this receiver while index < len(_devices_info): path, _ignore, _ignore, _ignore = _devices_info[index] if path == receiver.path: found = True _remove_device(index) elif found and path == _RECEIVER_SEPARATOR[0]: # the separator after this receiver _remove_device(index) break else: index += 1 def _update_menu_item(index, device): assert device assert device.status is not None menu_items = _menu.get_children() menu_item = menu_items[index] level = device.status.get(_K.BATTERY_LEVEL) charging = device.status.get(_K.BATTERY_CHARGING) icon_name = _icons.battery(level, charging) image_widget = menu_item.get_image() image_widget.set_sensitive(bool(device.online)) _update_menu_icon(image_widget, icon_name) # # # # for which device to show the battery info in systray, if more than one # it's actually an entry in _devices_info _picked_device = None # cached list of devices and some of their properties # contains tuples of (receiver path, device number, name, status) _devices_info = [] _menu = None _icon = None def init(_quit_handler): global _menu, _icon assert _menu is None _menu = _create_menu(_quit_handler) assert _icon is None _icon = _create(_menu) def destroy(): global _icon, _menu, _devices_info assert _icon is not None i, _icon = _icon, None _destroy(i) i = None _icon = None _menu = None _devices_info = None def update(device=None): if _icon is None: return if device is not None: if device.kind is None: # receiver is_alive = bool(device) receiver_path = device.path if is_alive: index = None for idx, (path, _ignore, _ignore, _ignore) in enumerate(_devices_info): if path == receiver_path: index = idx break if index is None: _add_receiver(device) else: _remove_receiver(device) else: # peripheral is_paired = bool(device) receiver_path = device.receiver.path index = None for idx, (path, number, _ignore, _ignore) in enumerate(_devices_info): if path == receiver_path and number == device.number: index = idx if is_paired: if index is None: index = _add_device(device) _update_menu_item(index, device) else: # was just unpaired if index: _remove_device(index) menu_items = _menu.get_children() no_receivers_index = len(_devices_info) menu_items[no_receivers_index].set_visible(not _devices_info) menu_items[no_receivers_index + 1].set_visible(not _devices_info) global _picked_device if not _picked_device and device is not None and device.kind is not None: # if it's just a receiver update, it's unlikely the picked device would change _picked_device = _pick_device_with_lowest_battery() _update_tray_icon() Solaar-0.9.2/lib/solaar/ui/window.py000066400000000000000000000615351217372044600173100ustar00rootroot00000000000000# -*- 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 gi.repository import Gtk, Gdk, GLib from gi.repository.GObject import TYPE_PYOBJECT from solaar import NAME from solaar.i18n import _ # from solaar import __version__ as VERSION from solaar.ui import async as _ui_async from logitech_receiver import hidpp10 as _hidpp10 from logitech_receiver.common import NamedInts as _NamedInts, NamedInt as _NamedInt from logitech_receiver.status import KEYS as _K from . import config_panel as _config_panel from . import action as _action, icons as _icons from .about import show_window as _show_about_window # # constants # _SMALL_BUTTON_ICON_SIZE = Gtk.IconSize.MENU _NORMAL_BUTTON_ICON_SIZE = Gtk.IconSize.BUTTON _TREE_ICON_SIZE = Gtk.IconSize.BUTTON _INFO_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR _DEVICE_ICON_SIZE = Gtk.IconSize.DND # tree model columns _COLUMN = _NamedInts(PATH=0, NUMBER=1, ACTIVE=2, NAME=3, ICON=4, STATUS_TEXT=5, STATUS_ICON=6, DEVICE=7) _COLUMN_TYPES = (str, int, bool, str, str, str, str, TYPE_PYOBJECT) _TREE_SEPATATOR = (None, 0, False, None, None, None, None, None) assert len(_TREE_SEPATATOR) == len(_COLUMN_TYPES) assert len(_COLUMN_TYPES) == len(_COLUMN) _TOOLTIP_LINK_SECURE = _("The wireless link between this device and its receiver is encrypted.") _TOOLTIP_LINK_INSECURE = _("The wireless link between this device and its receiver is not encrypted.\n" "\n" "For pointing devices (mice, trackballs, trackpads), this is a minor security issue.\n" "\n" "It is, however, a major security issue for text-input devices (keyboards, numpads),\n" "because typed text can be sniffed inconspicuously by 3rd parties within range.") _UNIFYING_RECEIVER_TEXT = ( _("No device paired") + '.\n\n' + _("Up to %d devices can be paired to this receiver") + '.', '%d ' + _("paired devices") + '\n\n' + _("Up to %d devices can be paired to this receiver") + '.', ) _NANO_RECEIVER_TEXT = ( _("No device paired") + '.\n\n ', ' \n\n' + _("Only one device can be paired to this receiver") + '.', ) # # create UI layout # Gtk.Window.set_default_icon_name(NAME.lower()) Gtk.Window.set_default_icon_from_file(_icons.icon_file(NAME.lower())) def _new_button(label, icon_name=None, icon_size=_NORMAL_BUTTON_ICON_SIZE, tooltip=None, toggle=False, clicked=None): if toggle: b = Gtk.ToggleButton() else: b = Gtk.Button(label) if label else Gtk.Button() if icon_name: image = Gtk.Image.new_from_icon_name(icon_name, icon_size) b.set_image(image) if tooltip: b.set_tooltip_text(tooltip) if not label and icon_size < _NORMAL_BUTTON_ICON_SIZE: b.set_relief(Gtk.ReliefStyle.NONE) b.set_focus_on_click(False) if clicked is not None: b.connect('clicked', clicked) return b def _create_receiver_panel(): p = Gtk.Box.new(Gtk.Orientation.VERTICAL, 4) p._count = Gtk.Label() p._count.set_padding(24, 0) p._count.set_alignment(0, 0.5) p.pack_start(p._count, True, True, 0) p._scanning = Gtk.Label(_("Scanning") + '...') p._spinner = Gtk.Spinner() bp = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 8) bp.pack_start(Gtk.Label(' '), True, True, 0) bp.pack_start(p._scanning, False, False, 0) bp.pack_end(p._spinner, False, False, 0) p.pack_end(bp, False, False, 0) return p def _create_device_panel(): p = Gtk.Box.new(Gtk.Orientation.VERTICAL, 4) def _status_line(label_text): b = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 8) b.set_size_request(10, 28) b._label = Gtk.Label(label_text) b._label.set_alignment(0, 0.5) b._label.set_size_request(170, 10) b.pack_start(b._label, False, False, 0) b._icon = Gtk.Image() b.pack_start(b._icon, False, False, 0) b._text = Gtk.Label() b._text.set_alignment(0, 0.5) b.pack_start(b._text, True, True, 0) return b p._battery = _status_line(_("Battery")) p.pack_start(p._battery, False, False, 0) p._secure = _status_line(_("Wireless Link")) p._secure._icon.set_from_icon_name('dialog-warning', _INFO_ICON_SIZE) p.pack_start(p._secure, False, False, 0) p._lux = _status_line(_("Lighting")) p.pack_start(p._lux, False, False, 0) p._config = _config_panel.create() p.pack_end(p._config, False, False, 4) return p def _create_details_panel(): p = Gtk.Frame() p.set_shadow_type(Gtk.ShadowType.NONE) p.set_size_request(240, 0) p.set_state_flags(Gtk.StateFlags.ACTIVE, True) p._text = Gtk.Label() p._text.set_padding(6, 4) p._text.set_alignment(0, 0) p._text.set_selectable(True) p.add(p._text) return p def _create_buttons_box(): bb = Gtk.ButtonBox(Gtk.Orientation.HORIZONTAL) bb.set_layout(Gtk.ButtonBoxStyle.END) bb._details = _new_button(None, 'dialog-information', _SMALL_BUTTON_ICON_SIZE, tooltip=_("Show Technical Details"), toggle=True, clicked=_update_details) bb.add(bb._details) bb.set_child_secondary(bb._details, True) bb.set_child_non_homogeneous(bb._details, True) def _pair_new_device(trigger): assert _find_selected_device_id() is not None receiver = _find_selected_device() assert receiver is not None assert bool(receiver) assert receiver.kind is None _action.pair(_window, receiver) bb._pair = _new_button(_("Pair new device"), 'list-add', clicked=_pair_new_device) bb.add(bb._pair) def _unpair_current_device(trigger): assert _find_selected_device_id() is not None device = _find_selected_device() assert device is not None assert bool(device) assert device.kind is not None _action.unpair(_window, device) bb._unpair = _new_button(_("Unpair"), 'edit-delete', clicked=_unpair_current_device) bb.add(bb._unpair) return bb def _create_empty_panel(): p = Gtk.Label() p.set_markup('' + _("Select a device") + '') p.set_sensitive(False) return p def _create_info_panel(): p = Gtk.Box.new(Gtk.Orientation.VERTICAL, 4) p._title = Gtk.Label(' ') p._title.set_alignment(0, 0.5) p._icon = Gtk.Image() b1 = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 4) b1.pack_start(p._title, True, True, 0) b1.pack_start(p._icon, False, False, 0) p.pack_start(b1, False, False, 0) p.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), False, False, 0) # spacer p._receiver = _create_receiver_panel() p.pack_start(p._receiver, True, True, 0) p._device = _create_device_panel() p.pack_start(p._device, True, True, 0) p.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), False, False, 0) # spacer p._buttons = _create_buttons_box() p.pack_end(p._buttons, False, False, 0) return p def _create_tree(model): tree = Gtk.TreeView() tree.set_size_request(240, 0) tree.set_headers_visible(False) tree.set_show_expanders(False) tree.set_level_indentation(20) # tree.set_fixed_height_mode(True) tree.set_enable_tree_lines(True) tree.set_reorderable(False) tree.set_enable_search(False) tree.set_model(model) def _is_separator(model, item, _ignore=None): return model.get_value(item, _COLUMN.PATH) is None tree.set_row_separator_func(_is_separator, None) icon_cell_renderer = Gtk.CellRendererPixbuf() icon_cell_renderer.set_property('stock-size', _TREE_ICON_SIZE) icon_column = Gtk.TreeViewColumn('Icon', icon_cell_renderer) icon_column.add_attribute(icon_cell_renderer, 'sensitive', _COLUMN.ACTIVE) icon_column.add_attribute(icon_cell_renderer, 'icon-name', _COLUMN.ICON) icon_column.set_fixed_width(1) tree.append_column(icon_column) name_cell_renderer = Gtk.CellRendererText() name_column = Gtk.TreeViewColumn('device name', name_cell_renderer) name_column.add_attribute(name_cell_renderer, 'sensitive', _COLUMN.ACTIVE) name_column.add_attribute(name_cell_renderer, 'text', _COLUMN.NAME) name_column.set_expand(True) tree.append_column(name_column) tree.set_expander_column(name_column) status_cell_renderer = Gtk.CellRendererText() status_cell_renderer.set_property('scale', 0.85) status_cell_renderer.set_property('xalign', 1) status_column = Gtk.TreeViewColumn('status text', status_cell_renderer) status_column.add_attribute(status_cell_renderer, 'sensitive', _COLUMN.ACTIVE) status_column.add_attribute(status_cell_renderer, 'text', _COLUMN.STATUS_TEXT) status_column.set_expand(True) tree.append_column(status_column) battery_cell_renderer = Gtk.CellRendererPixbuf() battery_cell_renderer.set_property('stock-size', _TREE_ICON_SIZE) battery_column = Gtk.TreeViewColumn('status icon', battery_cell_renderer) battery_column.add_attribute(battery_cell_renderer, 'sensitive', _COLUMN.ACTIVE) battery_column.add_attribute(battery_cell_renderer, 'icon-name', _COLUMN.STATUS_ICON) battery_column.set_fixed_width(1) tree.append_column(battery_column) return tree def _create_window_layout(): assert _tree is not None assert _details is not None assert _info is not None assert _empty is not None assert _tree.get_selection().get_mode() == Gtk.SelectionMode.SINGLE _tree.get_selection().connect('changed', _device_selected) tree_panel = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) tree_panel.set_homogeneous(False) tree_panel.pack_start(_tree, True, True, 0) tree_panel.pack_start(_details, False, False, 0) panel = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 16) panel.pack_start(tree_panel, False, False, 0) panel.pack_start(_info, True, True, 0) panel.pack_start(_empty, True, True, 0) about_button = _new_button(_("About") + ' ' + NAME, 'help-about', icon_size=_SMALL_BUTTON_ICON_SIZE, clicked=_show_about_window) bottom_buttons_box = Gtk.ButtonBox(Gtk.Orientation.HORIZONTAL) bottom_buttons_box.set_layout(Gtk.ButtonBoxStyle.START) bottom_buttons_box.add(about_button) # solaar_version = Gtk.Label() # solaar_version.set_markup('' + NAME + ' v' + VERSION + '') # bottom_buttons_box.add(solaar_version) # bottom_buttons_box.set_child_secondary(solaar_version, True) vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 8) vbox.set_border_width(8) vbox.pack_start(panel, True, True, 0) vbox.pack_end(bottom_buttons_box, False, False, 0) vbox.show_all() _details.set_visible(False) _info.set_visible(False) return vbox def _create(): window = Gtk.Window() window.set_title(NAME) window.set_role('status-window') # window.set_type_hint(Gdk.WindowTypeHint.UTILITY) # window.set_skip_taskbar_hint(True) # window.set_skip_pager_hint(True) window.set_keep_above(True) window.connect('delete-event', _hide) vbox = _create_window_layout() window.add(vbox) geometry = Gdk.Geometry() geometry.min_width = 600 geometry.min_height = 320 geometry.max_width = 800 geometry.max_height = 600 window.set_geometry_hints(vbox, geometry, Gdk.WindowHints.MIN_SIZE | Gdk.WindowHints.MAX_SIZE) window.set_position(Gtk.WindowPosition.CENTER) return window # # window updates # def _find_selected_device(): selection = _tree.get_selection() model, item = selection.get_selected() return model.get_value(item, _COLUMN.DEVICE) if item else None def _find_selected_device_id(): selection = _tree.get_selection() model, item = selection.get_selected() if item: return _model.get_value(item, _COLUMN.PATH), _model.get_value(item, _COLUMN.NUMBER) # triggered by changing selection in the tree def _device_selected(selection): model, item = selection.get_selected() device = model.get_value(item, _COLUMN.DEVICE) if item else None # if _log.isEnabledFor(_DEBUG): # _log.debug("window tree selected device %s", device) if device: _update_info_panel(device, full=True) else: # When removing a receiver, one of its children may get automatically selected # before the tree had time to remove them as well. # Rather than chase around for another device to select, just clear the selection. _tree.get_selection().unselect_all() _update_info_panel(None, full=True) def _receiver_row(receiver_path, receiver=None): assert receiver_path item = _model.get_iter_first() while item: # first row matching the path must be the receiver one if _model.get_value(item, _COLUMN.PATH) == receiver_path: return item item = _model.iter_next(item) if not item and receiver: icon_name = _icons.device_icon_name(receiver.name) status_text = None status_icon = None row_data = (receiver_path, 0, True, receiver.name, icon_name, status_text, status_icon, receiver) assert len(row_data) == len(_TREE_SEPATATOR) # if _log.isEnabledFor(_DEBUG): # _log.debug("new receiver row %s", row_data) item = _model.append(None, row_data) if _TREE_SEPATATOR: _model.append(None, _TREE_SEPATATOR) return item or None def _device_row(receiver_path, device_number, device=None): assert receiver_path assert device_number is not None receiver_row = _receiver_row(receiver_path, None if device is None else device.receiver) item = _model.iter_children(receiver_row) new_child_index = 0 while item: assert _model.get_value(item, _COLUMN.PATH) == receiver_path item_number = _model.get_value(item, _COLUMN.NUMBER) if item_number == device_number: return item if item_number > device_number: item = None break new_child_index += 1 item = _model.iter_next(item) if not item and device: icon_name = _icons.device_icon_name(device.name, device.kind) status_text = None status_icon = None row_data = (receiver_path, device_number, bool(device.online), device.codename, icon_name, status_text, status_icon, device) assert len(row_data) == len(_TREE_SEPATATOR) # if _log.isEnabledFor(_DEBUG): # _log.debug("new device row %s at index %d", row_data, new_child_index) item = _model.insert(receiver_row, new_child_index, row_data) return item or None # # # def select(receiver_path, device_number=None): assert _window assert receiver_path is not None if device_number is None: item = _receiver_row(receiver_path) else: item = _device_row(receiver_path, device_number) if item: selection = _tree.get_selection() selection.select_iter(item) else: _log.warn("select(%s, %s) failed to find an item", receiver_path, device_number) def _hide(w, _ignore=None): assert w == _window # some window managers move the window to 0,0 after hide() # so try to remember the last position position = _window.get_position() _window.hide() _window.move(*position) return True def popup(trigger=None, receiver_path=None, device_id=None): if receiver_path: select(receiver_path, device_id) _window.present() return True def toggle(trigger=None): if _window.get_visible(): _hide(_window) else: _window.present() # # # def _update_details(button): assert button visible = button.get_active() if visible: # _details._text.set_markup('reading...') def _details_items(device, read_all=False): # If read_all is False, only return stuff that is ~100% already # cached, and involves no HID++ calls. if device.kind is None: yield (_("Path"), device.path) # 046d is the Logitech vendor id yield (_("USB id"), '046d:' + device.product_id) if read_all: yield (_("Serial"), device.serial) else: yield (_("Serial"), '...') else: # yield ('Codename', device.codename) yield (_("Index"), device.number) yield (_("Wireless PID"), device.wpid) hid_version = device.protocol yield (_("Protocol"), 'HID++ %1.1f' % hid_version if hid_version else 'unknown') if read_all and device.polling_rate: yield (_("Polling rate"), '%d ms (%dHz)' % (device.polling_rate, 1000 // device.polling_rate)) if read_all or not device.online: yield (_("Serial"), device.serial) else: yield (_("Serial"), '...') if read_all: for fw in list(device.firmware): yield (' ' + str(fw.kind), (fw.name + ' ' + fw.version).strip()) elif device.kind is None or device.online: yield (' %s' % _("Firmware"), '...') flag_bits = device.status.get(_K.NOTIFICATION_FLAGS) if flag_bits is not None: flag_names = ('(%s)' % _("none"),) if flag_bits == 0 else _hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits) yield (_("Notifications"), ('\n%15s' % ' ').join(flag_names)) def _set_details(text): _details._text.set_markup(text) def _make_text(items): text = '\n'.join('%-13s: %s' % i for i in items) return '' + text + '' def _read_slow(device): items = _details_items(selected_device, True) text = _make_text(items) if device == _details._current_device: GLib.idle_add(_set_details, text) selected_device = _find_selected_device() assert selected_device _details._current_device = selected_device read_all = not (selected_device.kind is None or selected_device.online) items = _details_items(selected_device, read_all) _set_details(_make_text(items)) if read_all: _details._current_device = None else: _ui_async(_read_slow, selected_device) _details.set_visible(visible) def _update_receiver_panel(receiver, panel, buttons, full=False): assert receiver devices_count = len(receiver) if receiver.max_devices > 1: if devices_count == 0: panel._count.set_markup(_UNIFYING_RECEIVER_TEXT[0] % receiver.max_devices) else: panel._count.set_markup(_UNIFYING_RECEIVER_TEXT[1] % (devices_count, receiver.max_devices)) else: if devices_count == 0: panel._count.set_markup(_NANO_RECEIVER_TEXT[0]) else: panel._count.set_markup(_NANO_RECEIVER_TEXT[1]) is_pairing = receiver.status.lock_open if is_pairing: panel._scanning.set_visible(True) if not panel._spinner.get_visible(): panel._spinner.start() panel._spinner.set_visible(True) else: panel._scanning.set_visible(False) if panel._spinner.get_visible(): panel._spinner.stop() panel._spinner.set_visible(False) panel.set_visible(True) # b._insecure.set_visible(False) buttons._unpair.set_visible(False) may_pair = receiver.may_unpair and not is_pairing if may_pair and devices_count >= receiver.max_devices: online_devices = tuple(n for n in range(1, receiver.max_devices) if n in receiver and receiver[n].online) may_pair &= len(online_devices) < receiver.max_devices buttons._pair.set_sensitive(may_pair) buttons._pair.set_visible(True) def _update_device_panel(device, panel, buttons, full=False): assert device is_online = bool(device.online) panel.set_sensitive(is_online) battery_level = device.status.get(_K.BATTERY_LEVEL) if battery_level is None: icon_name = _icons.battery() panel._battery._icon.set_sensitive(False) panel._battery._icon.set_from_icon_name(icon_name, _INFO_ICON_SIZE) panel._battery._text.set_sensitive(True) panel._battery._text.set_markup('%s' % _("unknown")) else: charging = device.status.get(_K.BATTERY_CHARGING) icon_name = _icons.battery(battery_level, charging) panel._battery._icon.set_from_icon_name(icon_name, _INFO_ICON_SIZE) panel._battery._icon.set_sensitive(True) if isinstance(battery_level, _NamedInt): text = str(battery_level) else: text = '%d%%' % battery_level if is_online: if charging: text += ' (%s)' % _("charging") else: text += ' (%s)' % _("last known") panel._battery._text.set_sensitive(is_online) panel._battery._text.set_markup(text) if is_online: not_secure = device.status.get(_K.LINK_ENCRYPTED) == False if not_secure: panel._secure._text.set_text(_("not encrypted")) panel._secure._icon.set_from_icon_name('security-low', _INFO_ICON_SIZE) panel._secure.set_tooltip_text(_TOOLTIP_LINK_INSECURE) else: panel._secure._text.set_text(_("encrypted")) panel._secure._icon.set_from_icon_name('security-high', _INFO_ICON_SIZE) panel._secure.set_tooltip_text(_TOOLTIP_LINK_SECURE) panel._secure._icon.set_visible(True) else: panel._secure._text.set_markup('%s' % _("offline")) panel._secure._icon.set_visible(False) panel._secure.set_tooltip_text('') if is_online: light_level = device.status.get(_K.LIGHT_LEVEL) if light_level is None: panel._lux.set_visible(False) else: panel._lux._icon.set_from_icon_name(_icons.lux(light_level), _INFO_ICON_SIZE) panel._lux._text.set_text('%d %s' % (light_level, _("lux"))) panel._lux.set_visible(True) else: panel._lux.set_visible(False) buttons._pair.set_visible(False) buttons._unpair.set_sensitive(device.receiver.may_unpair) buttons._unpair.set_visible(True) panel.set_visible(True) if full: _config_panel.update(device, is_online) def _update_info_panel(device, full=False): if device is None: # no selected device, show the 'empty' panel _details.set_visible(False) _info.set_visible(False) _empty.set_visible(True) return # a receiver must be valid # a device must be paired assert device _info._title.set_markup('%s' % device.name) icon_name = _icons.device_icon_name(device.name, device.kind) _info._icon.set_from_icon_name(icon_name, _DEVICE_ICON_SIZE) if device.kind is None: _info._device.set_visible(False) _info._icon.set_sensitive(True) _info._title.set_sensitive(True) _update_receiver_panel(device, _info._receiver, _info._buttons, full) else: _info._receiver.set_visible(False) is_online = bool(device.online) _info._icon.set_sensitive(is_online) _info._title.set_sensitive(is_online) _update_device_panel(device, _info._device, _info._buttons, full) _empty.set_visible(False) _info.set_visible(True) if full: _update_details(_info._buttons._details) # # window layout: # +--------------------------------+ # | tree | receiver | empty | # | | or device | | # |------------| status | | # | details | | | # |--------------------------------| # | (about) | # +--------------------------------| # either the status or empty panel is visible at any point # the details panel can be toggle on/off _model = None _tree = None _details = None _info = None _empty = None _window = None def init(): global _model, _tree, _details, _info, _empty, _window _model = Gtk.TreeStore(*_COLUMN_TYPES) _tree = _create_tree(_model) _details = _create_details_panel() _info = _create_info_panel() _empty = _create_empty_panel() _window = _create() def destroy(): global _model, _tree, _details, _info, _empty, _window w, _window = _window, None w.destroy() w = None _config_panel.destroy() _empty = None _info = None _details = None _tree = None _model = None def update(device, need_popup=False): if _window is None: return assert device is not None if need_popup: popup() selected_device_id = _find_selected_device_id() if device.kind is None: # receiver is_alive = bool(device) item = _receiver_row(device.path, device if is_alive else None) assert item if is_alive and item: was_pairing = bool(_model.get_value(item, _COLUMN.STATUS_ICON)) is_pairing = bool(device.status.lock_open) _model.set_value(item, _COLUMN.STATUS_ICON, 'network-wireless' if is_pairing else None) if selected_device_id == (device.path, 0): full_update = need_popup or was_pairing != is_pairing _update_info_panel(device, full=full_update) elif item: if _TREE_SEPATATOR: separator = _model.iter_next(item) _model.remove(separator) _model.remove(item) else: # peripheral is_paired = bool(device) assert device.receiver assert device.number is not None and device.number > 0, "invalid device number" + str(device.number) item = _device_row(device.receiver.path, device.number, device if is_paired else None) if is_paired and item: was_online = _model.get_value(item, _COLUMN.ACTIVE) is_online = bool(device.online) _model.set_value(item, _COLUMN.ACTIVE, is_online) battery_level = device.status.get(_K.BATTERY_LEVEL) if battery_level is None: _model.set_value(item, _COLUMN.STATUS_TEXT, None) _model.set_value(item, _COLUMN.STATUS_ICON, None) else: if isinstance(battery_level, _NamedInt): status_text = str(battery_level) else: status_text = '%d%%' % battery_level _model.set_value(item, _COLUMN.STATUS_TEXT, status_text) charging = device.status.get(_K.BATTERY_CHARGING) icon_name = _icons.battery(battery_level, charging) _model.set_value(item, _COLUMN.STATUS_ICON, icon_name) if selected_device_id is None or need_popup: select(device.receiver.path, device.number) elif selected_device_id == (device.receiver.path, device.number): full_update = need_popup or was_online != is_online _update_info_panel(device, full=full_update) elif item: _model.remove(item) _config_panel.clean(device) # make sure all rows are visible _tree.expand_all() Solaar-0.9.2/lib/solaar/upower.py000066400000000000000000000046431217372044600167020ustar00rootroot00000000000000# -*- 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 # # As suggested here: http://stackoverflow.com/a/13548984 # _suspend_callback = None def _suspend(): if _suspend_callback: if _log.isEnabledFor(_INFO): _log.info("received suspend event from UPower") _suspend_callback() _resume_callback = None def _resume(): if _resume_callback: if _log.isEnabledFor(_INFO): _log.info("received resume event from UPower") _resume_callback() def watch(on_resume_callback, on_suspend_callback): """Register callback for suspend/resume events. They are called only if the system DBus is running, and the UPower daemon is available.""" global _resume_callback, _suspend_callback _suspend_callback = on_suspend_callback _resume_callback = on_resume_callback try: import dbus _UPOWER_BUS = 'org.freedesktop.UPower' _UPOWER_INTERFACE = 'org.freedesktop.UPower' # integration into the main GLib loop from dbus.mainloop.glib import DBusGMainLoop DBusGMainLoop(set_as_default=True) bus = dbus.SystemBus() assert bus bus.add_signal_receiver(_suspend, signal_name='Sleeping', dbus_interface=_UPOWER_INTERFACE, bus_name=_UPOWER_BUS) bus.add_signal_receiver(_resume, signal_name='Resuming', dbus_interface=_UPOWER_INTERFACE, bus_name=_UPOWER_BUS) if _log.isEnabledFor(_INFO): _log.info("connected to system dbus, watching for suspend/resume events") except: # Either: # - the dbus library is not available # - the system dbus is not running _log.warn("failed to register suspend/resume callbacks") pass Solaar-0.9.2/packaging/000077500000000000000000000000001217372044600146755ustar00rootroot00000000000000Solaar-0.9.2/packaging/README000066400000000000000000000005201217372044600155520ustar00rootroot00000000000000The debian/ folder is maintained by me and used for all releases for Debian and Ubuntu. The rest of the files serve as samples/guidelines for packaging Solaar. They may be out-of-date or simply wrong, and are not necessarily used to build the official packages. See README.md for links to official packages for various distributions. Solaar-0.9.2/packaging/arch/000077500000000000000000000000001217372044600156125ustar00rootroot00000000000000Solaar-0.9.2/packaging/arch/PKGBUILD000066400000000000000000000015371217372044600167440ustar00rootroot00000000000000# Maintainer: Arnaud Taffanel pkgname=solaar pkgver=0.8.8.1 pkgrel=1 pkgdesc="Device manager for Logitech's Unifying receiver peripherals" url="http://pwr.github.com/Solaar/" license=('GPL2') groups=() arch=('any') depends=('python' 'python-pyudev' 'python-gobject' 'pygtk') makedepends=('python') provides=('solaar') conflicts=('solaar') options=(!emptydirs) install=solaar.install source=("https://github.com/pwr/Solaar/archive/${pkgver}.tar.gz" 'solaar.install') md5sums=('2fee5353702b32e6958a51c2e603178f' '2416fcb58a4c24da5bbb94a9207799b4') package() { cd "$srcdir/Solaar-${pkgver}/" python3 setup.py install --root="$pkgdir/" --optimize=1 install -D -m0644 rules.d/99-logitech-unifying-receiver.rules \ "$pkgdir/etc/udev/rules.d/99-logitech-unifying-receiver.rules" } # vim:set ts=2 sw=2 et: Solaar-0.9.2/packaging/arch/solaar.install000066400000000000000000000003471217372044600204670ustar00rootroot00000000000000pre_install() { if ! getent group plugdev >/dev/null; then groupadd --system plugdev fi } post_install() { udevadm control --reload-rules echo "To be able to use this application, user must be in the plugdev group." } Solaar-0.9.2/packaging/build_deb.sh000077500000000000000000000076161217372044600171570ustar00rootroot00000000000000#!/bin/sh set -e if test "$DEBSIGN_KEYID"; then # only build a source package, and sign it DPKG_BUILPACKAGE_OPTS="-sa -S -k$DEBSIGN_KEYID" BUILDER_ROLE='Uploaders' test "$DEBEMAIL" test "$DEBFULLNAME" DEBCHANGE_OPTIONS="$@" else # build an unsigned binary package DPKG_BUILPACKAGE_OPTS="-b -us -uc" BUILDER_ROLE='Changed-By' export DEBFULLNAME="$(/usr/bin/getent passwd "$USER" | \ /usr/bin/cut --delimiter=: --fields=5 | /usr/bin/cut --delimiter=, --fields=1)" export DEBEMAIL="${EMAIL:-$USER@$(/bin/hostname --long)}" DEBCHANGE_OPTIONS="--fromdirname" fi export DEBMAIL="$DEBEMAIL" export DEBCHANGE_VENDOR=${DEBCHANGE_VENDOR:-$(/usr/bin/dpkg-vendor --query vendor | /usr/bin/tr 'A-Z' 'a-z')} test "$DEBCHANGE_VENDOR" DISTRIBUTION=${DISTRIBUTION:-UNRELEASED} test "$DISTRIBUTION" cd "$(dirname "$0")/.." DEBIAN_FILES="$PWD/packaging/debian" DEBIAN_FILES_VENDOR="$PWD/packaging/$DEBCHANGE_VENDOR" DIST_DIR="$PWD/dist" # # Build a python sdist package, then unpack and create .orig and source dir. # P_NAME="$(python2.7 setup.py --name)" P_VERSION="$(python2.7 setup.py --version)" SDIST_FILE="$DIST_DIR/$P_NAME-$P_VERSION.tar.gz" ORIG_FILE="$DIST_DIR/${P_NAME}_${P_VERSION}.orig.tar.gz" BUILD_DIR="$DIST_DIR/$P_NAME-$P_VERSION" if test -d "$BUILD_DIR"; then echo "*** $BUILD_DIR already exists, is it a leftover from previous builds? Aborting." exit 1 fi export TMPDIR="$(/bin/mktemp --directory --tmpdir debbuild-$P_NAME-$P_VERSION-$USER-XXXXXX)" ./tools/po-compile.sh python2.7 setup.py sdist --formats=gztar --quiet /bin/tar --extract --gunzip --file "$SDIST_FILE" --directory "$DIST_DIR" test -d "$BUILD_DIR" # If the orig file already exists for this version, check that no source # changes occured. if test -r "$ORIG_FILE"; then ORIG_SOURCES="$TMPDIR/$P_NAME-$P_VERSION" DIFF_OUTPUT="$TMPDIR/orig-diff-$P_VERSION" /bin/tar --extract --gunzip --file "$ORIG_FILE" --directory "$TMPDIR" /usr/bin/diff --recursive --minimal --unified \ "$ORIG_SOURCES" "$BUILD_DIR" >"$DIFF_OUTPUT" || true # either way, the sdist archive is no longer useful /bin/rm --force "$SDIST_FILE" if test -s "$DIFF_OUTPUT"; then /bin/rm --force --recursive "$BUILD_DIR" echo '*** Current sbuild differs from existing .orig archive. Aborting.' cat "$DIFF_OUTPUT" exit 1 fi unset ORIG_SOURCES DIFF_OUTPUT else /bin/mv "$SDIST_FILE" "$ORIG_FILE" fi unset P_NAME P_VERSION SDIST_FILE ORIG_FILE # # preparing to build the package # cd "$BUILD_DIR" /bin/cp --archive --target-directory=. "$DEBIAN_FILES" /bin/sed --in-place --file=- debian/control <<-CONTROL /^Maintainer:/ a\ $BUILDER_ROLE: $DEBFULLNAME <$DEBEMAIL> CONTROL /usr/bin/debchange \ --vendor "$DEBCHANGE_VENDOR" \ --distribution "$DISTRIBUTION" \ --force-save-on-release \ --auto-nmu \ $DEBCHANGE_OPTIONS if test "$DEBCHANGE_VENDOR" = debian; then # if this is the main (Debian) build, update the source changelog /bin/cp --archive --no-target-directory debian/changelog "$DEBIAN_FILES"/changelog elif test -d "$DEBIAN_FILES_VENDOR"; then # else copy any additional files /bin/cp --archive --target-directory=debian/ "$DEBIAN_FILES_VENDOR"/* || true fi # install vendor-specific substvars files, if any /usr/bin/find debian/ -type f -name "substvars.*.$DEBCHANGE_VENDOR" |\ while read subst_source; do subst_target="${subst_source%.$DEBCHANGE_VENDOR}" /bin/mv --force "$subst_source" "$subst_target" done # remove the templates, they are not relevant to the debian source package /bin/rm --force debian/substvars.*.* # apply custom substvars and clean-up debian/ cat debian/substvars.* | /bin/grep '^[-A-Za-z]*=' | /usr/bin/tr '=' ' ' |\ while read variable value; do /bin/sed --in-place --expression="s/\${solaar:$variable}/$value/" debian/control done /bin/rm --force debian/substvars.* /usr/bin/debuild \ --lintian --tgz-check \ --preserve-envvar=DISPLAY \ $DPKG_BUILPACKAGE_OPTS \ --lintian-opts --profile "$DEBCHANGE_VENDOR" /bin/rm --force --recursive "$BUILD_DIR" Solaar-0.9.2/packaging/debian/000077500000000000000000000000001217372044600161175ustar00rootroot00000000000000Solaar-0.9.2/packaging/debian/changelog000066400000000000000000000005121217372044600177670ustar00rootroot00000000000000solaar (0.9.1-1) unstable; urgency=low * Release 0.9.1. -- Daniel Pavel Sat, 13 Jul 2013 12:03:09 +0200 solaar (0.9.0-1) unstable; urgency=low * Release 0.9.0. * Initial Debian release (closes: #715172) -- Daniel Pavel Tue, 09 Jul 2013 14:50:08 +0200 Solaar-0.9.2/packaging/debian/compat000066400000000000000000000000021217372044600173150ustar00rootroot000000000000009 Solaar-0.9.2/packaging/debian/control000066400000000000000000000030051217372044600175200ustar00rootroot00000000000000Source: solaar Section: misc Priority: optional Maintainer: Daniel Pavel Build-Depends: debhelper (>= 8) Build-Depends-Indep: python, po-debconf X-Python-Version: >= 2.7 X-Python3-Version: >= 3.2 Standards-Version: 3.9.4 Homepage: http://pwr.github.io/Solaar Vcs-Git: git://github.com/pwr/Solaar.git Vcs-browser: http://github.com/pwr/Solaar Package: solaar Architecture: all Depends: ${misc:Depends}, ${debconf:Depends}, udev (>= 175), passwd | adduser, ${python:Depends}, python-pyudev (>= 0.13), python-gi (>= 3.2), gir1.2-gtk-3.0 (>= 3.4), ${solaar:Desktop-Icon-Theme} Recommends: gir1.2-notify-0.7, consolekit (>= 0.4.3) | systemd (>= 44), python-dbus (>= 1.1.0), upower Suggests: gir1.2-appindicator3-0.1, solaar-gnome3 (= ${source:Version}) Description: Logitech Unifying Receiver peripherals manager for Linux Solaar is a Linux device manager for Logitech's Unifying Receiver peripherals. It is able to pair/unpair devices to the receiver, and for some devices read battery status. Package: solaar-gnome3 Architecture: all Section: gnome Depends: ${misc:Depends}, solaar (= ${source:Version}), gir1.2-appindicator3-0.1, gnome-shell (>= 3.4) | unity (>= 5.10), ${solaar:Gnome-Icon-Theme} Enhances: solaar Description: gnome-shell/Unity integration for Solaar Solaar is a Linux device manager for Logitech's Unifying Receiver peripherals. It is able to pair/unpair devices to the receiver, and for some devices read battery status. . This metapackage ensures integration with gnome-shell/Unity. Solaar-0.9.2/packaging/debian/copyright000066400000000000000000000051071217372044600200550ustar00rootroot00000000000000Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0 Upstream-Name: Solaar Upstream-Contact: Daniel Pavel Upstream-Source: http://github.com/pwr/Solaar Files: * Copyright: Copyright 2012-2013 Daniel Pavel License: GPL-2 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation. . This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. . You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. . On Debian systems, the complete text of the GNU General Public License, version 2, can be found in /usr/share/common-licenses/GPL-2. Files: share/icons/solaar*.svg Copyright: Copyright 2012-2013 Daniel Pavel License: LGPL This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. . This library 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 Lesser General Public License for more details. . You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Files: share/icons/light_*.png Copyright: Oxygen Icons License: LGPL Comment: These files were copied from the Oxygen icon theme (weather-*). This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. . This library 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 Lesser General Public License for more details. . You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . Solaar-0.9.2/packaging/debian/po/000077500000000000000000000000001217372044600165355ustar00rootroot00000000000000Solaar-0.9.2/packaging/debian/po/POTFILES.in000066400000000000000000000000531217372044600203100ustar00rootroot00000000000000[type: gettext/rfc822deb] solaar.templates Solaar-0.9.2/packaging/debian/po/en.po000066400000000000000000000053261217372044600175050ustar00rootroot00000000000000# English translations for solaar package. # Copyright (C) 2013 Daniel Pavel # This file is distributed under the same license as the solaar package. # Automatically generated, 2013. # msgid "" msgstr "" "Project-Id-Version: solaar\n" "Report-Msgid-Bugs-To: daniel.pavel@gmail.com\n" "POT-Creation-Date: 2013-06-17 16:01+0200\n" "PO-Revision-Date: 2013-06-17 16:01+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: en\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. Type: boolean #. Description #: ../solaar.templates:1001 msgid "Use plugdev group?" msgstr "Use plugdev group?" #. Type: boolean #. Description #: ../solaar.templates:1001 msgid "" "By default, the Logitech receiver devices are only accessible by the root " "user." msgstr "" "By default, the Logitech receiver devices are only accessible by the root " "user." #. Type: boolean #. Description #: ../solaar.templates:1001 msgid "" "To allow access to regular users (through solaar), the needed ACLs can be " "applied, either by the ConsoleKit or systemd daemon, to the current seat " "(logged-in user). Right now, ${SEAT_DAEMON_STATUS} daemon is running." msgstr "" "To allow access to regular users (through solaar), the needed ACLs can be " "applied, either by the ConsoleKit or systemd daemon, to the current seat " "(logged-in user). Right now, ${SEAT_DAEMON_STATUS} daemon is running." #. Type: boolean #. Description #: ../solaar.templates:1001 msgid "" "If neither of these daemons is installed on your system, or you want to make " "the receiver accessible to ssh logged-in through ssh, members of the " "'plugdev' system group can be given access to the receiver devices." msgstr "" "If neither of these daemons is installed on your system, or you want to make " "the receiver accessible to ssh logged-in through ssh, members of the " "'plugdev' system group can be given access to the receiver devices." #. Type: boolean #. Description #: ../solaar.templates:1001 msgid "" "If you do use the 'plugdev' group, don't forget to make sure all your " "desktop users are members of the plugdev system group. You can add new " "members to the group by running, as root:\n" " gpasswd --add plugdev\n" "For the group membership to take effect, the affected users have to log-out " "and log-in again." msgstr "" "If you do use the 'plugdev' group, don't forget to make sure all your " "desktop users are members of the plugdev system group. You can add new " "members to the group by running, as root:\n" " gpasswd --add plugdev\n" "For the group membership to take effect, the affected users have to log-out " "and log-in again." Solaar-0.9.2/packaging/debian/po/templates.pot000066400000000000000000000035321217372044600212620ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: solaar\n" "Report-Msgid-Bugs-To: solaar@packages.debian.org\n" "POT-Creation-Date: 2013-06-17 16:01+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #. Type: boolean #. Description #: ../solaar.templates:1001 msgid "Use plugdev group?" msgstr "" #. Type: boolean #. Description #: ../solaar.templates:1001 msgid "" "By default, the Logitech receiver devices are only accessible by the root " "user." msgstr "" #. Type: boolean #. Description #: ../solaar.templates:1001 msgid "" "To allow access to regular users (through solaar), the needed ACLs can be " "applied, either by the ConsoleKit or systemd daemon, to the current seat " "(logged-in user). Right now, ${SEAT_DAEMON_STATUS} daemon is running." msgstr "" #. Type: boolean #. Description #: ../solaar.templates:1001 msgid "" "If neither of these daemons is installed on your system, or you want to make " "the receiver accessible to ssh logged-in through ssh, members of the " "'plugdev' system group can be given access to the receiver devices." msgstr "" #. Type: boolean #. Description #: ../solaar.templates:1001 msgid "" "If you do use the 'plugdev' group, don't forget to make sure all your " "desktop users are members of the plugdev system group. You can add new " "members to the group by running, as root:\n" " gpasswd --add plugdev\n" "For the group membership to take effect, the affected users have to log-out " "and log-in again." msgstr "" Solaar-0.9.2/packaging/debian/rules000077500000000000000000000005541217372044600172030ustar00rootroot00000000000000#!/usr/bin/make -f # -*- makefile -*- # Uncomment this to turn on verbose mode. #export DH_VERBOSE=1 #export DH_OPTIONS=-v PREFIX = /usr %: # Adding the required helpers dh $@ --with=python2 override_dh_auto_install: dh_auto_install -- --prefix=$(PREFIX) --install-lib=$(PREFIX)/share/solaar/lib override_dh_python2: dh_python2 $(PREFIX)/share/solaar/lib Solaar-0.9.2/packaging/debian/solaar.config000077500000000000000000000025011217372044600205700ustar00rootroot00000000000000#!/bin/sh set -e . /usr/share/debconf/confmodule db_version 2.0 db_capb backup consolekit_running() { test -e /var/run/ConsoleKit/database } systemd_running() { test -e /sys/fs/cgroup/systemd } plugdev_group_exists() { getent group plugdev >/dev/null } # During normal installation/upgrade, try to avoid bothering the user # if the current settings are sane. if test -z "$DEBCONF_RECONFIGURE"; then consolekit_running && exit 0 systemd_running && exit 0 if db_get solaar/use_plugdev_group; then test "$RET" = true && plugdev_group_exists && exit 0 else # If the group already exists, just use it. plugdev_group_exists && db_set solaar/use_plugdev_group true && exit 0 fi fi # If the package hasn't been configured yet, and no seat daemon is running, # change the default. if ! db_get solaar/use_plugdev_group; then if ! consolekit_running && ! systemd_running; then plugdev_group_exists && db_set solaar/use_plugdev_group true fi fi # update the question template if consolekit_running; then db_subst solaar/use_plugdev_group SEAT_DAEMON_STATUS "the ConsoleKit" elif systemd_running; then db_subst solaar/use_plugdev_group SEAT_DAEMON_STATUS "the systemd" else db_subst solaar/use_plugdev_group SEAT_DAEMON_STATUS "NEITHER" fi # ask the question db_input high solaar/use_plugdev_group || true db_go || true exit 0 Solaar-0.9.2/packaging/debian/solaar.install000066400000000000000000000002451217372044600207710ustar00rootroot00000000000000usr/bin/ usr/share/solaar/ usr/share/locale/ usr/share/icons/hicolor/scalable/apps/ usr/share/applications/ usr/share/applications/solaar.desktop etc/xdg/autostart/ Solaar-0.9.2/packaging/debian/solaar.postinst000066400000000000000000000004211217372044600212020ustar00rootroot00000000000000#!/bin/sh set -e . /usr/share/debconf/confmodule db_get solaar/use_plugdev_group if test "$RET" = true; then # make sure the group exists if required if ! getent group plugdev >/dev/null; then groupadd --system plugdev || addgroup --system plugdev fi fi #DEBHELPER# Solaar-0.9.2/packaging/debian/solaar.templates000066400000000000000000000016611217372044600213240ustar00rootroot00000000000000Template: solaar/use_plugdev_group Type: boolean Default: false _Description: Use plugdev group? By default, the Logitech receiver devices are only accessible by the root user. . To allow access to regular users (through solaar), the needed ACLs can be applied, either by the ConsoleKit or systemd daemon, to the current seat (logged-in user). Right now, ${SEAT_DAEMON_STATUS} daemon is running. . If neither of these daemons is installed on your system, or you want to make the receiver accessible to ssh logged-in through ssh, members of the 'plugdev' system group can be given access to the receiver devices. . If you do use the 'plugdev' group, don't forget to make sure all your desktop users are members of the plugdev system group. You can add new members to the group by running, as root: gpasswd --add plugdev For the group membership to take effect, the affected users have to log-out and log-in again. Solaar-0.9.2/packaging/debian/solaar.udev000066400000000000000000000024211217372044600202640ustar00rootroot00000000000000# This rule was added by Solaar. # # Allows non-root users to have raw access the Logitech Unifying USB Receiver # device. For development purposes, allowing users to write to the receiver is # potentially dangerous (e.g. perform firmware updates). ACTION != "add", GOTO="solaar_end" SUBSYSTEM != "hidraw", GOTO="solaar_end" # official Unifying receivers ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c52b", GOTO="solaar_apply" ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c532", GOTO="solaar_apply" # Nano receiver, "Unifying ready" ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c52f", GOTO="solaar_apply" # classic Nano receiver -- VX Nano mouse ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c526", GOTO="solaar_apply" GOTO="solaar_end" # # # LABEL="solaar_apply" # don't apply to the paired peripherals, just the receivers DRIVERS=="logitech-djdevice", GOTO="solaar_end" # if the package configuration does not want the plugdev group, don't change the # group and access mode PROGRAM="/usr/bin/debconf-show solaar", RESULT=="*use_plugdev_group: false*", GOTO="solaar_no_plugdev" MODE="0660", GROUP="plugdev" LABEL="solaar_no_plugdev" # tags for systemd/consolekit, they will apply the right ACLs for seated users TAG+="uaccess", TAG+="udev-acl" # # # LABEL="solaar_end" # vim: ft=udevrules Solaar-0.9.2/packaging/debian/source/000077500000000000000000000000001217372044600174175ustar00rootroot00000000000000Solaar-0.9.2/packaging/debian/source/format000066400000000000000000000000141217372044600206250ustar00rootroot000000000000003.0 (quilt) Solaar-0.9.2/packaging/debian/substvars.theme000066400000000000000000000003071217372044600211770ustar00rootroot00000000000000# distro package containing some required icons Desktop-Icon-Theme=gnome-icon-theme | oxygen-icon-theme # dependency for solaar-gnome3 (gnome-shell/unity specific) Gnome-Icon-Theme=gnome-icon-theme Solaar-0.9.2/packaging/debian/substvars.theme.ubuntu000066400000000000000000000001551217372044600225210ustar00rootroot00000000000000Desktop-Icon-Theme=gnome-icon-theme-full | oxygen-icon-theme-complete Gnome-Icon-Theme=gnome-icon-theme-full Solaar-0.9.2/packaging/debian/watch000066400000000000000000000005231217372044600171500ustar00rootroot00000000000000version=3 # GitHubRedir generator # download url: # http://githubredir.debian.net/github/pwr/Solaar/${tag_name}.tar.gz http://githubredir.debian.net/github/pwr/Solaar/ (.*).tar.gz # official GitHub page # download url: # https://github.com/pwr/Solaar/archive/${tag_name}.tar.gz #https://github.com/pwr/Solaar/tags .*/(\d.*)\.tar\.gz Solaar-0.9.2/packaging/dput.cf000066400000000000000000000007011217372044600161610ustar00rootroot00000000000000[DEFAULT] progress_indicator = 2 allow_unsigned_uploads = 0 login = anonymous [solaar-ppa] fqdn = ppa.launchpad.net incoming = ~daniel.pavel/solaar/ubuntu/ method = ftp allowed_distributions = precise [solaar-snapshots-ppa] fqdn = ppa.launchpad.net incoming = ~daniel.pavel/solaar-snapshots/ubuntu/ method = ftp allowed_distributions = precise [mentors] fqdn = mentors.debian.net incoming = /upload method = http allowed_distributions = unstable Solaar-0.9.2/packaging/gentoo/000077500000000000000000000000001217372044600161705ustar00rootroot00000000000000Solaar-0.9.2/packaging/gentoo/solaar-0.8.8.1.ebuild000066400000000000000000000020571217372044600214530ustar00rootroot00000000000000# Copyright 1999-2012 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 # $Header: $ EAPI=5 PYTHON_COMPAT=( python{2_7,3_2} ) inherit distutils-r1 udev user linux-info gnome2-utils DESCRIPTION="Solaar is a Linux device manager for Logitech's Unifying Receiver peripherals" HOMEPAGE="http://pwr.github.io/Solaar/" SRC_URI="https://github.com/pwr/Solaar/archive/${PV}.tar.gz" LICENSE="GPL-2" SLOT="0" KEYWORDS="~amd64" IUSE="doc" RDEPEND="${PYTHON_DEPS} dev-python/pyudev dev-python/pygobject[${PYTHON_USEDEP}]" MY_P="Solaar-${PV}" S="${WORKDIR}/${MY_P}" DOCS=( README.md COPYING COPYRIGHT ChangeLog ) pkg_setup() { enewgroup plugdev CONFIG_CHECK="HID_LOGITECH_DJ" linux-info_pkg_setup } src_install() { distutils-r1_src_install udev_dorules rules.d/*.rules if use doc; then dodoc -r docs/* fi } pkg_postinst() { gnome2_icon_cache_update elog "To be able to use this application, the user must be on the plugdev group." } pkg_preinst() { gnome2_icon_savelist; } pkg_postrm() { gnome2_icon_cache_update; } Solaar-0.9.2/packaging/upload_launchpad.sh000077500000000000000000000007201217372044600205360ustar00rootroot00000000000000#!/bin/sh set -e export DEBCHANGE_VENDOR=ubuntu export DISTRIBUTION=precise export DEBFULLNAME='Daniel Pavel' export DEBEMAIL='daniel.pavel+launchpad@gmail.com' export DEBSIGN_KEYID=07D8904B Z="$(readlink -f "$(dirname "$0")")" "$Z"/build_deb.sh --rebuild "$@" cd "$Z/../dist" CHANGES_FILE=$(/bin/ls --format=single-column --sort=time solaar_*_source.changes | /usr/bin/head --lines=1) /usr/bin/dput --config="$Z/dput.cf" solaar-snapshots-ppa "$CHANGES_FILE" Solaar-0.9.2/packaging/upload_mentors.sh000077500000000000000000000007011217372044600202650ustar00rootroot00000000000000#!/bin/sh set -e export DEBCHANGE_VENDOR=debian export DISTRIBUTION=unstable export DEBFULLNAME='Daniel Pavel' export DEBEMAIL='daniel.pavel+debian@gmail.com' export DEBSIGN_KEYID=0B34B1A7 Z="$(readlink -f "$(dirname "$0")")" "$Z"/build_deb.sh --release "$@" cd "$Z/../dist" CHANGES_FILE=$(/bin/ls --format=single-column --sort=time solaar_*_source.changes | /usr/bin/head --lines=1) /usr/bin/dput --config="$Z/dput.cf" mentors "$CHANGES_FILE" Solaar-0.9.2/po/000077500000000000000000000000001217372044600133675ustar00rootroot00000000000000Solaar-0.9.2/po/README000066400000000000000000000001141217372044600142430ustar00rootroot00000000000000See docs/i18n.md for instructions about creating or updating a translation. Solaar-0.9.2/po/pl.po000066400000000000000000000316371217372044600143540ustar00rootroot00000000000000# Polish translations for solaar package. # Copyright (C) 2013 THE solaar'S COPYRIGHT HOLDER # This file is distributed under the same license as the solaar package. # Automatically generated, 2013. # msgid "" msgstr "Project-Id-Version: solaar 0.9.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-07-23 22:38+0200\n" "PO-Revision-Date: 2013-07-23 10:41+0100\n" "Last-Translator: Adrian Piotrowicz \n" "Language-Team: none\n" "Language: pl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n" "%100<10 || n%100>=20) ? 1 : 2);\n" "X-Generator: Poedit 1.5.4\n" #: lib/logitech_receiver/i18n.py:38 msgid "critical" msgstr "krytyczny" #: lib/logitech_receiver/i18n.py:38 msgid "empty" msgstr "pusty" #: lib/logitech_receiver/i18n.py:38 lib/logitech_receiver/i18n.py:41 msgid "full" msgstr "pełny" #: lib/logitech_receiver/i18n.py:38 msgid "good" msgstr "dobry" #: lib/logitech_receiver/i18n.py:38 msgid "low" msgstr "niski" #: lib/logitech_receiver/i18n.py:41 msgid "almost full" msgstr "prawie pełny" #: lib/logitech_receiver/i18n.py:41 msgid "discharging" msgstr "rozładowywanie" #: lib/logitech_receiver/i18n.py:41 msgid "recharging" msgstr "ładowanie" #: lib/logitech_receiver/i18n.py:42 msgid "invalid battery" msgstr "nieprawidłowa bateria" #: lib/logitech_receiver/i18n.py:42 msgid "slow recharge" msgstr "wolne ładowanie" #: lib/logitech_receiver/i18n.py:42 msgid "thermal error" msgstr "błąd temperatury" #: lib/logitech_receiver/i18n.py:45 msgid "device not supported" msgstr "urządzenie nie obsługiwane" #: lib/logitech_receiver/i18n.py:45 msgid "device timeout" msgstr "upłynął limit czasu dla urządzenia" #: lib/logitech_receiver/i18n.py:45 msgid "sequence timeout" msgstr "upłynął limit czasu dla sekwencji" #: lib/logitech_receiver/i18n.py:45 msgid "too many devices" msgstr "za dużo urządzeń" #: lib/logitech_receiver/i18n.py:48 msgid "Bootloader" msgstr "Bootloader" #: lib/logitech_receiver/i18n.py:48 lib/solaar/ui/window.py:538 msgid "Firmware" msgstr "Firmware" #: lib/logitech_receiver/i18n.py:48 msgid "Hardware" msgstr "Hardware" #: lib/logitech_receiver/i18n.py:48 msgid "Other" msgstr "Inne" #: lib/logitech_receiver/notifications.py:67 msgid "closed" msgstr "zamknięte" #: lib/logitech_receiver/notifications.py:67 msgid "open" msgstr "otwarte" #: lib/logitech_receiver/notifications.py:67 msgid "pairing lock is " msgstr "blokada parowania jest" #: lib/logitech_receiver/notifications.py:192 msgid "powered on" msgstr "włączone" #: lib/logitech_receiver/receiver.py:107 lib/solaar/ui/window.py:625 msgid "unknown" msgstr "nieznane" #: lib/logitech_receiver/settings_templates.py:77 msgid "Smooth Scrolling" msgstr "Płynne przewijanie" #: lib/logitech_receiver/settings_templates.py:78 msgid "High-sensitivity mode for vertical scroll with the wheel." msgstr "Tryb wysokiej rozdzielczości dla przewijania pionowego\n" "przy użyciu kółka myszy." #: lib/logitech_receiver/settings_templates.py:79 msgid "Side Scrolling" msgstr "Przewijanie na boki" #: lib/logitech_receiver/settings_templates.py:80 msgid "When disabled, pushing the wheel sideways sends custom button " "events\n" "instead of the standard side-scrolling events." msgstr "Gdy jest wyłączone to przechylanie kółka myszy wysyła " "niestandardowe\n" "zdarzenia przycisków zamiast standardowych do przewijania na boki." #: lib/logitech_receiver/settings_templates.py:82 msgid "Sensitivity (DPI)" msgstr "Czułość (DPI)" #: lib/logitech_receiver/settings_templates.py:83 msgid "Swap Fx function" msgstr "Funkcja Swap Fx" #: lib/logitech_receiver/settings_templates.py:84 msgid "When set, the F1..F12 keys will activate their special function,\n" "and you must hold the FN key to activate their standard function." msgstr "Gdy włączona to po wciśnięciu klawiszy F1..F12 aktywowana zostanie\n" "ich funkcja pomocnicza. Aby aktywować standardową funkcję należy\n" "przytrzymać klawisz FN." #: lib/logitech_receiver/settings_templates.py:87 msgid "When unset, the F1..F12 keys will activate their standard function,\n" "and you must hold the FN key to activate their special function." msgstr "Gdy wyłączona to po wciśnięciu klawiszy F1..F12 aktywowana zostanie\n" "ich standardowa funkcja. Aby aktywować funkcję pomocniczą należy\n" "przytrzymać klawisz FN." #: lib/logitech_receiver/settings_templates.py:89 msgid "Hand Detection" msgstr "Wykrywanie dłoni" #: lib/logitech_receiver/settings_templates.py:90 msgid "Turn on illumination when the hands hover over the keyboard." msgstr "Włącz podświetlenie gdy dłoń znajdzie się nad klawiaturą." #: lib/logitech_receiver/status.py:98 msgid "No paired devices." msgstr "Brak sparowanych urządzeń." #: lib/logitech_receiver/status.py:99 msgid "1 paired device." msgstr "1 sparowane urządzenie." #: lib/logitech_receiver/status.py:100 msgid " paired devices." msgstr "sparowane(-ych) urządzeń." #: lib/logitech_receiver/status.py:153 lib/logitech_receiver/status.py:155 #: lib/solaar/ui/window.py:146 msgid "Battery" msgstr "Bateria" #: lib/logitech_receiver/status.py:161 lib/solaar/ui/window.py:153 msgid "Lighting" msgstr "Podświetlenie" #: lib/logitech_receiver/status.py:161 lib/solaar/ui/window.py:666 msgid "lux" msgstr "lux" #: lib/solaar/listener.py:95 msgid "The receiver was unplugged." msgstr "Odbiornik został odłączony." #: lib/solaar/ui/__init__.py:69 msgid "Permissions error" msgstr "Błąd uprawnień" #: lib/solaar/ui/__init__.py:70 #, fuzzy, python-format msgid "Found a Logitech Receiver (%s), but did not have permission to open " "it." msgstr "Znaleziono odbiornik Logitech (%s), ale nie ma uprawnień do " "otworzenia go." #: lib/solaar/ui/__init__.py:72 msgid "If you've just installed Solaar, try removing the receiver and " "plugging it back in." msgstr "Jeżeli właśnie zainstalowałeś Solaar spróbuj odłączyć nadajnik i " "podłączyć go ponownie." #: lib/solaar/ui/__init__.py:74 msgid "Unpairing failed" msgstr "Usunięcie parowania nie powiodło się" #: lib/solaar/ui/__init__.py:75 #, python-format msgid "Failed to unpair %s from %s." msgstr "Nie powidło się usunięcie parowania %s z %s." #: lib/solaar/ui/__init__.py:77 msgid "The receiver returned an error, with no further details." msgstr "Odbiornik zwrócił błąd bez dodatkowych informacji." #: lib/solaar/ui/about.py:39 msgid "Shows status of devices connected\n" "through wireless Logitech receivers." msgstr "Wyświetla status urządzeń podłączonych\n" "przez bezprzewodowe odbiorniki Logitech." #: lib/solaar/ui/about.py:48 msgid "GUI design" msgstr "Projekt GUI" #: lib/solaar/ui/about.py:49 msgid "Testing" msgstr "Testy" #: lib/solaar/ui/about.py:54 msgid "Logitech documentation" msgstr "Dokumentacja Logitech" #: lib/solaar/ui/action.py:68 lib/solaar/ui/window.py:319 msgid "About" msgstr "O" #: lib/solaar/ui/action.py:95 lib/solaar/ui/action.py:98 #: lib/solaar/ui/window.py:206 msgid "Unpair" msgstr "Usuń parowanie" #: lib/solaar/ui/config_panel.py:97 msgid "Working" msgstr "Pracuję" #: lib/solaar/ui/config_panel.py:100 msgid "Read/write operation failed." msgstr "Operacja odczytu/zapisu nie powiodła się." #: lib/solaar/ui/notify.py:115 msgid "unpaired" msgstr "nie sparowany" #: lib/solaar/ui/notify.py:116 msgid "connected" msgstr "podłączony" #: lib/solaar/ui/notify.py:116 lib/solaar/ui/tray.py:285 #: lib/solaar/ui/tray.py:290 lib/solaar/ui/window.py:656 msgid "offline" msgstr "wyłączony" #: lib/solaar/ui/pair_window.py:133 msgid "Pairing failed" msgstr "Parowanie nie powiodło się" #: lib/solaar/ui/pair_window.py:135 msgid "Make sure your device is within range, and has a decent battery " "charge." msgstr "Upewnij się, że urządzenie jest w zasięgu i ma naładowane baterie." #: lib/solaar/ui/pair_window.py:137 msgid "A new device was detected, but it is not compatible with this " "receiver." msgstr "Wykryto nowe urządzenie, jednak nie jest ono kompatybilne z tym " "odbiornikiem." #: lib/solaar/ui/pair_window.py:139 #, python-format msgid "The receiver only supports %d paired device(s)." msgstr "Maksymalna ilość urządzeń do sparowania z tym odbiornikiem: %d" #: lib/solaar/ui/pair_window.py:141 msgid "No further details are available about the error." msgstr "Brak dodatkowych informacji na temat błędu." #: lib/solaar/ui/pair_window.py:155 msgid "Found a new device" msgstr "Wykryto nowe urządzenie" #: lib/solaar/ui/pair_window.py:180 msgid "The wireless link is not encrypted" msgstr "Połączenie nie jest szyfrowane" #: lib/solaar/ui/pair_window.py:197 msgid "pair new device" msgstr "sparuj nowe urządzenie" #: lib/solaar/ui/pair_window.py:205 msgid "Turn on the device you want to pair." msgstr "Włącz urządzenie które chcesz sparować." #: lib/solaar/ui/pair_window.py:206 msgid "If the device is already turned on,\n" "turn if off and on again." msgstr "Jeżeli urządzenie jest już włączone,\n" "wyłącz je i włącz ponownie." #: lib/solaar/ui/tray.py:55 msgid "No Logitech receiver found" msgstr "Nie znaleziono odbiornika Logitech" #: lib/solaar/ui/tray.py:62 msgid "Quit" msgstr "Wyjdź" #: lib/solaar/ui/tray.py:269 msgid "no receiver" msgstr "brak odbiornika" #: lib/solaar/ui/tray.py:288 msgid "no status" msgstr "brak statusu" #: lib/solaar/ui/window.py:58 msgid "The wireless link between this device and its receiver is encrypted." msgstr "Połączenie bezprzewodowe pomiędzy tym urządzeniem i odbiornikiem " "jest szyfrowane." #: lib/solaar/ui/window.py:59 msgid "The wireless link between this device and its receiver is not " "encrypted.\n" "\n" "For pointing devices (mice, trackballs, trackpads), this is a minor " "security issue.\n" "\n" "It is, however, a major security issue for text-input devices " "(keyboards, numpads),\n" "because typed text can be sniffed inconspicuously by 3rd parties " "within range." msgstr "Połączenie pomiędzy tym urządzeniem i odbiornikiem nie jest " "szyfrowane.\n" "\n" "Dla urządzeń wskazujących (myszki, trackballe, trackpady) nie " "stanowi to zagrożenia\n" "bezpieczeństwa.\n" "\n" "Jest to jednak duże zagrożenie dla urządzeń służących do " "wprowadzania tekstu\n" "(klawiatury, klawiatury numeryczne), gdyż wpisywany tekst może być " "podsłuchany\n" "przez kogoś będącego w zasięgu." #: lib/solaar/ui/window.py:67 lib/solaar/ui/window.py:71 msgid "No device paired" msgstr "Brak sparowanych urządzeń." #: lib/solaar/ui/window.py:67 lib/solaar/ui/window.py:68 #, python-format msgid "Up to %d devices can be paired to this receiver" msgstr "Maksymalna ilość urządzeń do sparowania z tym odbiornikiem: %d" #: lib/solaar/ui/window.py:68 msgid "paired devices" msgstr "sparowane urządzenia" #: lib/solaar/ui/window.py:72 msgid "Only one device can be paired to this receiver" msgstr "Tylko jedno urządzenie może być sparowane z tym odbiornikiem" #: lib/solaar/ui/window.py:113 msgid "Scanning" msgstr "Wyszukiwanie" #: lib/solaar/ui/window.py:149 msgid "Wireless Link" msgstr "Połączenie bezprzewodowe" #: lib/solaar/ui/window.py:182 msgid "Show Technical Details" msgstr "Wyświetl szczegóły techniczne" #: lib/solaar/ui/window.py:195 msgid "Pair new device" msgstr "Sparuj nowe urządzenie" #: lib/solaar/ui/window.py:214 msgid "Select a device" msgstr "Wybierz urządzenie" #: lib/solaar/ui/window.py:511 msgid "Path" msgstr "Ścieżka" #: lib/solaar/ui/window.py:513 msgid "USB id" msgstr "USB id" #: lib/solaar/ui/window.py:516 lib/solaar/ui/window.py:518 #: lib/solaar/ui/window.py:530 lib/solaar/ui/window.py:532 msgid "Serial" msgstr "Serial" #: lib/solaar/ui/window.py:522 msgid "Index" msgstr "Index" #: lib/solaar/ui/window.py:523 msgid "Wireless PID" msgstr "Wireless PID" #: lib/solaar/ui/window.py:525 msgid "Protocol" msgstr "Protokół" #: lib/solaar/ui/window.py:527 msgid "Polling rate" msgstr "Częstotliwość próbkowania" #: lib/solaar/ui/window.py:542 msgid "none" msgstr "brak" #: lib/solaar/ui/window.py:543 msgid "Notifications" msgstr "Powiadomienia" #: lib/solaar/ui/window.py:638 msgid "charging" msgstr "ładowanie" #: lib/solaar/ui/window.py:640 msgid "last known" msgstr "ostatni znany" #: lib/solaar/ui/window.py:647 msgid "not encrypted" msgstr "nie szyfrowane" #: lib/solaar/ui/window.py:651 msgid "encrypted" msgstr "szyfrowane" Solaar-0.9.2/po/ro.po000066400000000000000000000275341217372044600143620ustar00rootroot00000000000000# Romanian translations for solaar package. # Copyright (C) 2013 THE solaar'S COPYRIGHT HOLDER # This file is distributed under the same license as the solaar package. # Automatically generated, 2013. # msgid "" msgstr "Project-Id-Version: solaar 0.9.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-07-23 22:35+0200\n" "PO-Revision-Date: 2013-07-17 20:27+0100\n" "Last-Translator: Daniel Pavel \n" "Language-Team: none\n" "Language: ro\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n" "%100 < 20)) ? 1 : 2;\n" "X-Generator: Poedit 1.5.4\n" #: lib/logitech_receiver/i18n.py:38 msgid "critical" msgstr "aproape descărcată" #: lib/logitech_receiver/i18n.py:38 msgid "empty" msgstr "descărcată" #: lib/logitech_receiver/i18n.py:38 lib/logitech_receiver/i18n.py:41 msgid "full" msgstr "plină" #: lib/logitech_receiver/i18n.py:38 msgid "good" msgstr "bună" #: lib/logitech_receiver/i18n.py:38 msgid "low" msgstr "joasă" #: lib/logitech_receiver/i18n.py:41 msgid "almost full" msgstr "aproape plină" #: lib/logitech_receiver/i18n.py:41 msgid "discharging" msgstr "în descarcare" #: lib/logitech_receiver/i18n.py:41 msgid "recharging" msgstr "re-încărcare" #: lib/logitech_receiver/i18n.py:42 msgid "invalid battery" msgstr "baterie necorespunzătoare" #: lib/logitech_receiver/i18n.py:42 msgid "slow recharge" msgstr "încarcare inceată" #: lib/logitech_receiver/i18n.py:42 msgid "thermal error" msgstr "eroare termică" #: lib/logitech_receiver/i18n.py:45 msgid "device not supported" msgstr "periferic incompatibil" #: lib/logitech_receiver/i18n.py:45 msgid "device timeout" msgstr "" #: lib/logitech_receiver/i18n.py:45 msgid "sequence timeout" msgstr "" #: lib/logitech_receiver/i18n.py:45 msgid "too many devices" msgstr "prea multe periferice" #: lib/logitech_receiver/i18n.py:48 msgid "Bootloader" msgstr "" #: lib/logitech_receiver/i18n.py:48 lib/solaar/ui/window.py:538 msgid "Firmware" msgstr "" #: lib/logitech_receiver/i18n.py:48 msgid "Hardware" msgstr "" #: lib/logitech_receiver/i18n.py:48 msgid "Other" msgstr "" #: lib/logitech_receiver/notifications.py:67 msgid "closed" msgstr "închis" #: lib/logitech_receiver/notifications.py:67 msgid "open" msgstr "deschis" #: lib/logitech_receiver/notifications.py:67 msgid "pairing lock is " msgstr "lacătul de contectare este " #: lib/logitech_receiver/notifications.py:192 msgid "powered on" msgstr "a pornit" #: lib/logitech_receiver/receiver.py:107 lib/solaar/ui/window.py:625 msgid "unknown" msgstr "necunoscută" #: lib/logitech_receiver/settings_templates.py:77 msgid "Smooth Scrolling" msgstr "Derulare fină" #: lib/logitech_receiver/settings_templates.py:78 msgid "High-sensitivity mode for vertical scroll with the wheel." msgstr "Senzitivitate crescută la derularea verticală cu rotița." #: lib/logitech_receiver/settings_templates.py:79 msgid "Side Scrolling" msgstr "Derulare orizontală" #: lib/logitech_receiver/settings_templates.py:80 msgid "When disabled, pushing the wheel sideways sends custom button " "events\n" "instead of the standard side-scrolling events." msgstr "" #: lib/logitech_receiver/settings_templates.py:82 msgid "Sensitivity (DPI)" msgstr "Sentivitivate (PPI)" #: lib/logitech_receiver/settings_templates.py:83 msgid "Swap Fx function" msgstr "Inversează funcțiile Fx" #: lib/logitech_receiver/settings_templates.py:84 msgid "When set, the F1..F12 keys will activate their special function,\n" "and you must hold the FN key to activate their standard function." msgstr "Când este activ, tastele F1..F12 vor opera funcțiile speciale,\n" "și trebuie să țineți apăsată tasta FN pentru a folosi funcțiile lor " "standard." #: lib/logitech_receiver/settings_templates.py:87 msgid "When unset, the F1..F12 keys will activate their standard function,\n" "and you must hold the FN key to activate their special function." msgstr "Când nu este activ, tastele F1..F12 vor opera functiile standard,\n" "și trebuie să țineți apăsată tasta FN pentru a folosi funcțiile lor " "speciale." #: lib/logitech_receiver/settings_templates.py:89 msgid "Hand Detection" msgstr "" #: lib/logitech_receiver/settings_templates.py:90 msgid "Turn on illumination when the hands hover over the keyboard." msgstr "" #: lib/logitech_receiver/status.py:98 msgid "No paired devices." msgstr "Nici un periferic contectat." #: lib/logitech_receiver/status.py:99 msgid "1 paired device." msgstr "Un periferic contectat." #: lib/logitech_receiver/status.py:100 msgid " paired devices." msgstr " periferice contectate." #: lib/logitech_receiver/status.py:153 lib/logitech_receiver/status.py:155 #: lib/solaar/ui/window.py:146 msgid "Battery" msgstr "Baterie" #: lib/logitech_receiver/status.py:161 lib/solaar/ui/window.py:153 msgid "Lighting" msgstr "Lumină" #: lib/logitech_receiver/status.py:161 lib/solaar/ui/window.py:666 msgid "lux" msgstr "lucși" #: lib/solaar/listener.py:95 msgid "The receiver was unplugged." msgstr "Receptor deconectat." #: lib/solaar/ui/__init__.py:69 msgid "Permissions error" msgstr "Eroare de permisiuni" #: lib/solaar/ui/__init__.py:70 #, python-format msgid "Found a Logitech Receiver (%s), but did not have permission to open " "it." msgstr "Receptor Logitech detectat (%s), dar nu am permisiunea să-l deschid." #: lib/solaar/ui/__init__.py:72 msgid "If you've just installed Solaar, try removing the receiver and " "plugging it back in." msgstr "Dacă tocmai ați instalat Solaar, scoateți receptorul și re-" "introduceți-l." #: lib/solaar/ui/__init__.py:74 msgid "Unpairing failed" msgstr "Deconectare eșuată" #: lib/solaar/ui/__init__.py:75 #, python-format msgid "Failed to unpair %s from %s." msgstr "Deconectarea %s de la %s a eșuat." #: lib/solaar/ui/__init__.py:77 msgid "The receiver returned an error, with no further details." msgstr "Receptorul a semnalat o eroare, fără alte detalii." #: lib/solaar/ui/about.py:39 msgid "Shows status of devices connected\n" "through wireless Logitech receivers." msgstr "Afișează starea perifericelor conectate\n" "printr-un receptor Logitech fără fir." #: lib/solaar/ui/about.py:48 msgid "GUI design" msgstr "Interfață grafica" #: lib/solaar/ui/about.py:49 msgid "Testing" msgstr "Testare" #: lib/solaar/ui/about.py:54 msgid "Logitech documentation" msgstr "Documentație Logitech" #: lib/solaar/ui/action.py:68 lib/solaar/ui/window.py:319 msgid "About" msgstr "Despre" #: lib/solaar/ui/action.py:95 lib/solaar/ui/action.py:98 #: lib/solaar/ui/window.py:206 msgid "Unpair" msgstr "Deconectează" #: lib/solaar/ui/config_panel.py:97 msgid "Working" msgstr "Prelucrez" #: lib/solaar/ui/config_panel.py:100 msgid "Read/write operation failed." msgstr "Operațiunea a eșuat." #: lib/solaar/ui/notify.py:115 msgid "unpaired" msgstr "deconectat(ă)" #: lib/solaar/ui/notify.py:116 msgid "connected" msgstr "conectat(ă)" #: lib/solaar/ui/notify.py:116 lib/solaar/ui/tray.py:285 #: lib/solaar/ui/tray.py:290 lib/solaar/ui/window.py:656 msgid "offline" msgstr "inactivă" #: lib/solaar/ui/pair_window.py:133 msgid "Pairing failed" msgstr "Conectare eșuată" #: lib/solaar/ui/pair_window.py:135 msgid "Make sure your device is within range, and has a decent battery " "charge." msgstr "Asigurați-vă că dispozitivul este în apropiere, iar bateria este " "încarcată." #: lib/solaar/ui/pair_window.py:137 msgid "A new device was detected, but it is not compatible with this " "receiver." msgstr "A fost detectat un nou periferic, dar nu este compatibil cu acest " "receptor." #: lib/solaar/ui/pair_window.py:139 #, python-format msgid "The receiver only supports %d paired device(s)." msgstr "Receptorul suportă maxim %d periferic(e) contectate." #: lib/solaar/ui/pair_window.py:141 msgid "No further details are available about the error." msgstr "Alte detalii despre eroare nu sunt disponibile." #: lib/solaar/ui/pair_window.py:155 msgid "Found a new device" msgstr "Periferic nou detectat" #: lib/solaar/ui/pair_window.py:180 msgid "The wireless link is not encrypted" msgstr "Legătura fără fir nu este criptată" #: lib/solaar/ui/pair_window.py:197 msgid "pair new device" msgstr "conectează periferic nou" #: lib/solaar/ui/pair_window.py:205 msgid "Turn on the device you want to pair." msgstr "Porniți dispozitivul pe care doriți să-l conectați." #: lib/solaar/ui/pair_window.py:206 msgid "If the device is already turned on,\n" "turn if off and on again." msgstr "Dacă dispozitivul este deja pornit,\n" "opriți-l și porniți-l din nou." #: lib/solaar/ui/tray.py:55 msgid "No Logitech receiver found" msgstr "Nu am găsit nici un receptor Logitech" #: lib/solaar/ui/tray.py:62 msgid "Quit" msgstr "Ieșire" #: lib/solaar/ui/tray.py:269 msgid "no receiver" msgstr "nici un receptor" #: lib/solaar/ui/tray.py:288 msgid "no status" msgstr "stare necunoscută" #: lib/solaar/ui/window.py:58 msgid "The wireless link between this device and its receiver is encrypted." msgstr "Legătura fără fir este criptată." #: lib/solaar/ui/window.py:59 msgid "The wireless link between this device and its receiver is not " "encrypted.\n" "\n" "For pointing devices (mice, trackballs, trackpads), this is a minor " "security issue.\n" "\n" "It is, however, a major security issue for text-input devices " "(keyboards, numpads),\n" "because typed text can be sniffed inconspicuously by 3rd parties " "within range." msgstr "Legătura fără fir nu este criptată." #: lib/solaar/ui/window.py:67 lib/solaar/ui/window.py:71 msgid "No device paired" msgstr "Nici un periferic conectat" #: lib/solaar/ui/window.py:67 lib/solaar/ui/window.py:68 #, python-format msgid "Up to %d devices can be paired to this receiver" msgstr "Acest receptor suportă maxim %d periferice conectate" #: lib/solaar/ui/window.py:68 msgid "paired devices" msgstr "periferice conectate" #: lib/solaar/ui/window.py:72 msgid "Only one device can be paired to this receiver" msgstr "Acest receptor suportă un singur periferic conectat" #: lib/solaar/ui/window.py:113 msgid "Scanning" msgstr "Caut" #: lib/solaar/ui/window.py:149 msgid "Wireless Link" msgstr "Legatură fără fir" #: lib/solaar/ui/window.py:182 msgid "Show Technical Details" msgstr "Detalii tehnice" #: lib/solaar/ui/window.py:195 msgid "Pair new device" msgstr "Conectează periferic" #: lib/solaar/ui/window.py:214 msgid "Select a device" msgstr "Selectați un dispozitiv" #: lib/solaar/ui/window.py:511 msgid "Path" msgstr "Cale" #: lib/solaar/ui/window.py:513 msgid "USB id" msgstr "USB" #: lib/solaar/ui/window.py:516 lib/solaar/ui/window.py:518 #: lib/solaar/ui/window.py:530 lib/solaar/ui/window.py:532 msgid "Serial" msgstr "Serial" #: lib/solaar/ui/window.py:522 msgid "Index" msgstr "Index" #: lib/solaar/ui/window.py:523 msgid "Wireless PID" msgstr "Cod WPID" #: lib/solaar/ui/window.py:525 msgid "Protocol" msgstr "Protocol" #: lib/solaar/ui/window.py:527 msgid "Polling rate" msgstr "Rată acces" #: lib/solaar/ui/window.py:542 msgid "none" msgstr "nici una" #: lib/solaar/ui/window.py:543 msgid "Notifications" msgstr "Notificări" #: lib/solaar/ui/window.py:638 msgid "charging" msgstr "se încarcă" #: lib/solaar/ui/window.py:640 msgid "last known" msgstr "ultima valoare" #: lib/solaar/ui/window.py:647 msgid "not encrypted" msgstr "ne-criptată" #: lib/solaar/ui/window.py:651 msgid "encrypted" msgstr "criptată" Solaar-0.9.2/po/solaar.pot000066400000000000000000000225711217372044600154030ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "Project-Id-Version: solaar 0.9.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-07-23 22:38+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: lib/logitech_receiver/i18n.py:38 msgid "critical" msgstr "" #: lib/logitech_receiver/i18n.py:38 msgid "empty" msgstr "" #: lib/logitech_receiver/i18n.py:38 lib/logitech_receiver/i18n.py:41 msgid "full" msgstr "" #: lib/logitech_receiver/i18n.py:38 msgid "good" msgstr "" #: lib/logitech_receiver/i18n.py:38 msgid "low" msgstr "" #: lib/logitech_receiver/i18n.py:41 msgid "almost full" msgstr "" #: lib/logitech_receiver/i18n.py:41 msgid "discharging" msgstr "" #: lib/logitech_receiver/i18n.py:41 msgid "recharging" msgstr "" #: lib/logitech_receiver/i18n.py:42 msgid "invalid battery" msgstr "" #: lib/logitech_receiver/i18n.py:42 msgid "slow recharge" msgstr "" #: lib/logitech_receiver/i18n.py:42 msgid "thermal error" msgstr "" #: lib/logitech_receiver/i18n.py:45 msgid "device not supported" msgstr "" #: lib/logitech_receiver/i18n.py:45 msgid "device timeout" msgstr "" #: lib/logitech_receiver/i18n.py:45 msgid "sequence timeout" msgstr "" #: lib/logitech_receiver/i18n.py:45 msgid "too many devices" msgstr "" #: lib/logitech_receiver/i18n.py:48 msgid "Bootloader" msgstr "" #: lib/logitech_receiver/i18n.py:48 lib/solaar/ui/window.py:538 msgid "Firmware" msgstr "" #: lib/logitech_receiver/i18n.py:48 msgid "Hardware" msgstr "" #: lib/logitech_receiver/i18n.py:48 msgid "Other" msgstr "" #: lib/logitech_receiver/notifications.py:67 msgid "closed" msgstr "" #: lib/logitech_receiver/notifications.py:67 msgid "open" msgstr "" #: lib/logitech_receiver/notifications.py:67 msgid "pairing lock is " msgstr "" #: lib/logitech_receiver/notifications.py:192 msgid "powered on" msgstr "" #: lib/logitech_receiver/receiver.py:107 lib/solaar/ui/window.py:625 msgid "unknown" msgstr "" #: lib/logitech_receiver/settings_templates.py:77 msgid "Smooth Scrolling" msgstr "" #: lib/logitech_receiver/settings_templates.py:78 msgid "High-sensitivity mode for vertical scroll with the wheel." msgstr "" #: lib/logitech_receiver/settings_templates.py:79 msgid "Side Scrolling" msgstr "" #: lib/logitech_receiver/settings_templates.py:80 msgid "When disabled, pushing the wheel sideways sends custom button " "events\n" "instead of the standard side-scrolling events." msgstr "" #: lib/logitech_receiver/settings_templates.py:82 msgid "Sensitivity (DPI)" msgstr "" #: lib/logitech_receiver/settings_templates.py:83 msgid "Swap Fx function" msgstr "" #: lib/logitech_receiver/settings_templates.py:84 msgid "When set, the F1..F12 keys will activate their special function,\n" "and you must hold the FN key to activate their standard function." msgstr "" #: lib/logitech_receiver/settings_templates.py:87 msgid "When unset, the F1..F12 keys will activate their standard function,\n" "and you must hold the FN key to activate their special function." msgstr "" #: lib/logitech_receiver/settings_templates.py:89 msgid "Hand Detection" msgstr "" #: lib/logitech_receiver/settings_templates.py:90 msgid "Turn on illumination when the hands hover over the keyboard." msgstr "" #: lib/logitech_receiver/status.py:98 msgid "No paired devices." msgstr "" #: lib/logitech_receiver/status.py:99 msgid "1 paired device." msgstr "" #: lib/logitech_receiver/status.py:100 msgid " paired devices." msgstr "" #: lib/logitech_receiver/status.py:153 lib/logitech_receiver/status.py:155 #: lib/solaar/ui/window.py:146 msgid "Battery" msgstr "" #: lib/logitech_receiver/status.py:161 lib/solaar/ui/window.py:153 msgid "Lighting" msgstr "" #: lib/logitech_receiver/status.py:161 lib/solaar/ui/window.py:666 msgid "lux" msgstr "" #: lib/solaar/listener.py:95 msgid "The receiver was unplugged." msgstr "" #: lib/solaar/ui/__init__.py:69 msgid "Permissions error" msgstr "" #: lib/solaar/ui/__init__.py:70 #, python-format msgid "Found a Logitech Receiver (%s), but did not have permission to open " "it." msgstr "" #: lib/solaar/ui/__init__.py:72 msgid "If you've just installed Solaar, try removing the receiver and " "plugging it back in." msgstr "" #: lib/solaar/ui/__init__.py:74 msgid "Unpairing failed" msgstr "" #: lib/solaar/ui/__init__.py:75 #, python-format msgid "Failed to unpair %s from %s." msgstr "" #: lib/solaar/ui/__init__.py:77 msgid "The receiver returned an error, with no further details." msgstr "" #: lib/solaar/ui/about.py:39 msgid "Shows status of devices connected\n" "through wireless Logitech receivers." msgstr "" #: lib/solaar/ui/about.py:48 msgid "GUI design" msgstr "" #: lib/solaar/ui/about.py:49 msgid "Testing" msgstr "" #: lib/solaar/ui/about.py:54 msgid "Logitech documentation" msgstr "" #: lib/solaar/ui/action.py:68 lib/solaar/ui/window.py:319 msgid "About" msgstr "" #: lib/solaar/ui/action.py:95 lib/solaar/ui/action.py:98 #: lib/solaar/ui/window.py:206 msgid "Unpair" msgstr "" #: lib/solaar/ui/config_panel.py:97 msgid "Working" msgstr "" #: lib/solaar/ui/config_panel.py:100 msgid "Read/write operation failed." msgstr "" #: lib/solaar/ui/notify.py:115 msgid "unpaired" msgstr "" #: lib/solaar/ui/notify.py:116 msgid "connected" msgstr "" #: lib/solaar/ui/notify.py:116 lib/solaar/ui/tray.py:285 #: lib/solaar/ui/tray.py:290 lib/solaar/ui/window.py:656 msgid "offline" msgstr "" #: lib/solaar/ui/pair_window.py:133 msgid "Pairing failed" msgstr "" #: lib/solaar/ui/pair_window.py:135 msgid "Make sure your device is within range, and has a decent battery " "charge." msgstr "" #: lib/solaar/ui/pair_window.py:137 msgid "A new device was detected, but it is not compatible with this " "receiver." msgstr "" #: lib/solaar/ui/pair_window.py:139 #, python-format msgid "The receiver only supports %d paired device(s)." msgstr "" #: lib/solaar/ui/pair_window.py:141 msgid "No further details are available about the error." msgstr "" #: lib/solaar/ui/pair_window.py:155 msgid "Found a new device" msgstr "" #: lib/solaar/ui/pair_window.py:180 msgid "The wireless link is not encrypted" msgstr "" #: lib/solaar/ui/pair_window.py:197 msgid "pair new device" msgstr "" #: lib/solaar/ui/pair_window.py:205 msgid "Turn on the device you want to pair." msgstr "" #: lib/solaar/ui/pair_window.py:206 msgid "If the device is already turned on,\n" "turn if off and on again." msgstr "" #: lib/solaar/ui/tray.py:55 msgid "No Logitech receiver found" msgstr "" #: lib/solaar/ui/tray.py:62 msgid "Quit" msgstr "" #: lib/solaar/ui/tray.py:269 msgid "no receiver" msgstr "" #: lib/solaar/ui/tray.py:288 msgid "no status" msgstr "" #: lib/solaar/ui/window.py:58 msgid "The wireless link between this device and its receiver is encrypted." msgstr "" #: lib/solaar/ui/window.py:59 msgid "The wireless link between this device and its receiver is not " "encrypted.\n" "\n" "For pointing devices (mice, trackballs, trackpads), this is a minor " "security issue.\n" "\n" "It is, however, a major security issue for text-input devices " "(keyboards, numpads),\n" "because typed text can be sniffed inconspicuously by 3rd parties " "within range." msgstr "" #: lib/solaar/ui/window.py:67 lib/solaar/ui/window.py:71 msgid "No device paired" msgstr "" #: lib/solaar/ui/window.py:67 lib/solaar/ui/window.py:68 #, python-format msgid "Up to %d devices can be paired to this receiver" msgstr "" #: lib/solaar/ui/window.py:68 msgid "paired devices" msgstr "" #: lib/solaar/ui/window.py:72 msgid "Only one device can be paired to this receiver" msgstr "" #: lib/solaar/ui/window.py:113 msgid "Scanning" msgstr "" #: lib/solaar/ui/window.py:149 msgid "Wireless Link" msgstr "" #: lib/solaar/ui/window.py:182 msgid "Show Technical Details" msgstr "" #: lib/solaar/ui/window.py:195 msgid "Pair new device" msgstr "" #: lib/solaar/ui/window.py:214 msgid "Select a device" msgstr "" #: lib/solaar/ui/window.py:511 msgid "Path" msgstr "" #: lib/solaar/ui/window.py:513 msgid "USB id" msgstr "" #: lib/solaar/ui/window.py:516 lib/solaar/ui/window.py:518 #: lib/solaar/ui/window.py:530 lib/solaar/ui/window.py:532 msgid "Serial" msgstr "" #: lib/solaar/ui/window.py:522 msgid "Index" msgstr "" #: lib/solaar/ui/window.py:523 msgid "Wireless PID" msgstr "" #: lib/solaar/ui/window.py:525 msgid "Protocol" msgstr "" #: lib/solaar/ui/window.py:527 msgid "Polling rate" msgstr "" #: lib/solaar/ui/window.py:542 msgid "none" msgstr "" #: lib/solaar/ui/window.py:543 msgid "Notifications" msgstr "" #: lib/solaar/ui/window.py:638 msgid "charging" msgstr "" #: lib/solaar/ui/window.py:640 msgid "last known" msgstr "" #: lib/solaar/ui/window.py:647 msgid "not encrypted" msgstr "" #: lib/solaar/ui/window.py:651 msgid "encrypted" msgstr "" Solaar-0.9.2/rules.d/000077500000000000000000000000001217372044600143255ustar00rootroot00000000000000Solaar-0.9.2/rules.d/42-logitech-unify-permissions.rules000066400000000000000000000022261217372044600231250ustar00rootroot00000000000000# This rule was added by Solaar. # # Allows non-root users to have raw access the Logitech Unifying USB Receiver # device. For development purposes, allowing users to write to the receiver is # potentially dangerous (e.g. perform firmware updates). ACTION != "add", GOTO="solaar_end" SUBSYSTEM != "hidraw", GOTO="solaar_end" # official Unifying receivers ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c52b", GOTO="solaar_apply" ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c532", GOTO="solaar_apply" # Nano receiver, "Unifying Ready" ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c52f", GOTO="solaar_apply" # clasic Nano receiver -- VX Nano mouse ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c526", GOTO="solaar_apply" GOTO="solaar_end" LABEL="solaar_apply" # don't apply to the paired peripherals, just the receivers DRIVERS=="logitech-djdevice", GOTO="solaar_end" # Allow any seated user to access the receiver. # uaccess: modern ACL-enabled udev # udev-acl: for Ubuntu 12.10 and older TAG+="uaccess", TAG+="udev-acl" # Grant members of the "plugdev" group access to receiver (useful for SSH users) #MODE="0660", GROUP="plugdev" LABEL="solaar_end" # vim: ft=udevrules Solaar-0.9.2/rules.d/install.sh000077500000000000000000000026231217372044600163350ustar00rootroot00000000000000#!/bin/sh set -e Z=$(readlink -f "$0") RULES_D=/etc/udev/rules.d if ! test -d "$RULES_D"; then echo "$RULES_D not found; is udev installed?" exit 1 fi RULE=42-logitech-unify-permissions.rules if test -n "$1"; then SOURCE="$1" else SOURCE="$(dirname "$Z")/$RULE" REALUSER="${SUDO_USER-$USER}" if [ -z "$REALUSER" -o "$REALUSER" = "root" ]; then : # ignore unknown and root user else GROUP=plugdev TEMP_RULE="$(mktemp --tmpdir "ltudev.XXXXXXXX")" sed -e "/^#MODE.*\"plugdev\"/s/^#//" "$SOURCE" > "$TEMP_RULE" if ! id -G -n "$REALUSER" | grep -q -F plugdev; then GROUP="$(id -g -n "$REALUSER")" if getent group plugdev >/dev/null; then printf "User '%s' does not belong to the 'plugdev' group, " "$REALUSER" else printf "Group 'plugdev' does not exist, " fi echo "will use group '$GROUP' in the udev rule." sed -i -e "s/\"plugdev\"/\"$GROUP\"/" "$TEMP_RULE" fi SOURCE="$TEMP_RULE" fi fi if test "$(id -u)" != "0"; then echo "Switching to root to install the udev rule." test -x /usr/bin/pkexec && exec /usr/bin/pkexec "$Z" "$SOURCE" test -x /usr/bin/sudo && exec /usr/bin/sudo -- "$Z" "$SOURCE" test -x /bin/su && exec /bin/su -c "\"$Z\" \"$SOURCE\"" echo "Could not switch to root: none of pkexec, sudo or su were found?" exit 1 fi echo "Installing $RULE." install -m 644 "$SOURCE" "$RULES_D/$RULE" echo "Done. Now remove the Unfiying Receiver and plug it in again." Solaar-0.9.2/setup.py000077500000000000000000000046331217372044600144740ustar00rootroot00000000000000#!/usr/bin/env python from glob import glob as _glob from distutils.core import setup autostart_path = '/etc/xdg/autostart' import sys backup_path_0 = sys.path[0] sys.path[0] = backup_path_0 + '/lib' from solaar import NAME, __version__ sys.path[0] = backup_path_0 if 'install' in sys.argv: # naively guess where the autostart .desktop file should be installed if '--prefix' in sys.argv or '--home' in sys.argv: autostart_path = 'etc/xdg/autostart' elif '--user' in sys.argv: from os import environ from os import path xdg_config_home = environ.get('XDG_CONFIG_HOME', path.expanduser(path.join('~', '.config'))) autostart_path = path.join(xdg_config_home, 'autostart') del environ, path, xdg_config_home del sys, backup_path_0 def _data_files(): from os.path import dirname as _dirname yield 'share/solaar/icons', _glob('share/solaar/icons/solaar*.svg') yield 'share/solaar/icons', _glob('share/solaar/icons/light_*.png') yield 'share/icons/hicolor/scalable/apps', ['share/solaar/icons/solaar.svg'] for mo in _glob('share/locale/*/LC_MESSAGES/solaar.mo'): yield _dirname(mo), [mo] yield 'share/applications', ['share/applications/solaar.desktop'] yield autostart_path, ['share/applications/solaar.desktop'] del _dirname setup(name=NAME.lower(), version=__version__, description='Linux devices manager for the Logitech Unifying Receiver.', long_description=''' Solaar is a Linux device manager for Logitech's Unifying Receiver peripherals. It is able to pair/unpair devices to the receiver, and for some devices read battery status. '''.strip(), author='Daniel Pavel', author_email='daniel.pavel@gmail.com', license='GPLv2', url='http://pwr.github.io/Solaar/', classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: X11 Applications :: GTK', 'Environment :: Console', 'Intended Audience :: End Users/Desktop', 'License :: DFSG approved', 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', 'Natural Language :: English', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.2', 'Operating System :: POSIX :: Linux', 'Topic :: Utilities', ], platforms=['linux'], requires=['pyudev (>= 0.13)', 'gi.repository.GObject (>= 2.0)', 'gi.repository.Gtk (>= 3.0)'], package_dir={'': 'lib'}, packages=['hidapi', 'logitech_receiver', 'solaar', 'solaar.ui'], data_files=list(_data_files()), scripts=_glob('bin/*'), ) Solaar-0.9.2/share/000077500000000000000000000000001217372044600140535ustar00rootroot00000000000000Solaar-0.9.2/share/README000066400000000000000000000002231217372044600147300ustar00rootroot00000000000000Solaar application icon drawn by me, roughly guided by Logitech's Unifying logo. All other images are from the GNOME and Oxygen free icon themes. Solaar-0.9.2/share/applications/000077500000000000000000000000001217372044600165415ustar00rootroot00000000000000Solaar-0.9.2/share/applications/solaar.desktop000066400000000000000000000004351217372044600214170ustar00rootroot00000000000000[Desktop Entry] Name=Solaar Comment=Logitech Unifying Receiver peripherals manager Exec=solaar Icon=solaar StartupNotify=true Terminal=false Type=Application Keywords=logitech;unifying;receiver;mouse;keyboard; Categories=Utility;GTK; #Categories=Utility;GTK;Settings;HardwareSettings; Solaar-0.9.2/share/solaar-master.svg000066400000000000000000000134571217372044600173600ustar00rootroot00000000000000 Solaar master template image/svg+xml Solaar master template Daniel Pavel 2013-06-25 solaar-master Solaar-0.9.2/share/solaar/000077500000000000000000000000001217372044600153345ustar00rootroot00000000000000Solaar-0.9.2/share/solaar/icons/000077500000000000000000000000001217372044600164475ustar00rootroot00000000000000Solaar-0.9.2/share/solaar/icons/light_000.png000066400000000000000000000033261217372044600206470ustar00rootroot00000000000000PNG  IHDR szzsRGB pHYs^tIME  2ڐbKGDVIDATxV[lTU>f<5SZ 4j SkOE1$Jcb@/BODK"?h-ŶvNOGD$ƄӜ9k}ΥasG.ٳmmm=]]]mF OLL?{9_u] 6~ d6ϫ}~~.e2wv-?ɓơC>(1U*r IrjFaM6W`ݔJ4P,f#Gkdy ,{DRIcv?_طoJF{ݺu*B,k)+B˲< BL?:;/T[[K?aGDkk+m߾]FNP(Z4<FTWWo0UfjJ󼸸#屦F*Yr*jnnz[wޛrgs-o*SQЖm'B {aZr%kӻG<l6+ŧ_!HqY[JE3;GLMQ\i̐Pn˂H,njb^Bssstmy-y&aihhHmY}V> b$ x,FQ( P7v!%|0H&S7Refao.] 333!lk+V,5Eٲ}= -&"a BG.Ҋe =z{UUT}*I7o7hMZD#Գ.3g~kWR aH0*aQI[ K8T9:9DC4.dH$ jujkYNНd G>Q>zb<.r4s܅Eo5p˪jϞef*\ Ϯ=,|;yٖ,/:y~1 'i@T'p\v((PUx GRl@9R1*(%ױ-)'NH0SH@mA 3ʆ38(JW,َ3'|v `#+*Z_gz1ڸ);bFNRjcRl2#ʣ2%JX%-.2UUK2-؎L|ٜnکS*|GknjzVZTzh]Юrދ줦H*0<' DzJu~w]?_mF8:$"4H!QJeV|4Q:wR0*ԈId[ Aαt>|t\f" 86]W*kaXAj!2.| V*N*1Ӽ`F7mP$@UH6(br*b"injwS?#H`(O=| (myZOI¯[?D:b#,M),CɪnD:~U.Wp8D@Tӹ\@tmgaĖ.ɚ*B]H8$hA$4MI^CP\Ǧ/W_pGӅёJl[LL]- A-RnEѬZ-+TDP u Ėk?;vz Y"+}0`:V"T"/|YruMkHBQT:W(EC3ԍ(^cm|$^I%b5 ^GFCTͦ *;{ԏa|s@_\=;_'I#|@޻/1&*5xPK?[} :xίVk0 T d_ѐ #MP\eEN~߾ӧOHQð(ФI s֚ 'NMǿ?W * Su?а|>ڂ- u:w I6u"]zMh:cö"ӶX3$B]zd2KCQKŕ\ 0f(DR L*bzuB6rVm6н"=7F7 2GR!UMZFmo*p1*ؐ 8!1b}>d wD]$IRٰ rg;HUgTBdd7=adȠ<|@ =lz7lMGzo£Y èZ,2T0===_-HtuDc^ s=d.yVifP*u92w n~6i6ܭIENDB`Solaar-0.9.2/share/solaar/icons/light_040.png000066400000000000000000000030521217372044600206470ustar00rootroot00000000000000PNG  IHDR szzsRGB pHYs^tIMEObKGDIDATxU;lUo_ ,~") EP$"=]Z(P"cp&$ Ikgvy  μwc?Fp!իeܰ1uks~Easc-˕JbV 8 -,,UNKFtM0u2 IR4 oAxTlN8ۈ YzqsV98sKw|ze[v:$ݨV' \"7t]g@$EQ;X N?ę3g,4GJ܉׀\#o}`ůr( O,;e9+FiZ(EaIIèZ.N':k:@8NirrrarЯd# P%IBOLMM+FuԼm  As1XjYv#Ǖ8Xc"d'IZGdhp 5c.'I\E#TW4 Ȥ `r!)IK 8a,"rMX^/mZxZ}qbi8E-"A@Џ@1{)^m`ٶYG ,%皖,=_֛;;}߰, |L0!,S%*I0bU.ߩ}l$ݕgVG v% D$VqSɓY8 PVRQV@1P_JEexR:O$rf-݅ b7|_aQh2ZAi3JTB%O]T A@Ep7a8 ,X[ EHISw,#GMW&!Ӈ>kV/X1MR!\S%bv 1)f`ZDi`N|H"͝ |Ks*1t0!fl4$! LiS乪X>0BsNIRs0t]LyoVTqLhJJ$TJv5ww!K$C FNW=ܽ{w {L7o066bAXɊb(b Y;8#29d/ IGt,Tw+]͑#/僃#-h[[wwwj׼i#IODyX1<–Nl^ד!7ܳs,//`ܴ_GxE7tM'",:4^0M&R|txH[``Lwhx/\ZZNp_ס: N4aVYFAݠ^!GEnH ȶpшyA"X^03ВΞ=+h?c?/`! IENDB`Solaar-0.9.2/share/solaar/icons/light_060.png000066400000000000000000000046121217372044600206540ustar00rootroot00000000000000PNG  IHDR szzsRGB pHYs^tIME  8ٱdbKGD IDATxWIWTUw:[b{6q$N$rp B$$'$rJ"'E$D` qό=3=ޣc"ClOz߯]ՄyjEvzs?{Q>rlPT-sle0C66fK~/슉})osuSz[$SxSFfzC =M0.c10y{JHL_$/ dfxBRF[(WhބE`1ӾL3g= \el_&3v.;k1-.FMnu20[g. bK8ҬbѬwqiYČnWwoc/N!p̔`TQDF晅Ɂ|;yG$=jGY;`lT?EeV+gxsJ;"su';ʹLc6zǁރ9M z`oYcCϛ\ GW_# v^yr q?$޹ǘ8]99=g t$Bi1 oq*;#&d竼3CVfNה*׏>h[F EFqj#s`IJ4u ' c]ړ? &-):/xxy{^g]m6mۿ躙mZkf6I))}imdP0,!H&V Hh"!>z~G9=ME Ua3SӏMO8kl0yבfB4v1vN 8I8<<BjF bpQ,a \ 0ΡI8A@zt^*/fw޸pIVX_o8cT>iP@H[0#:(eҍް@ BJ?Q%Xp/4 wNU~au``pq[*sq9Ղ-rgA6Uq))`xėL|=4Ω |TAj֠RuX1x1Z5xj  ~_>|M:^j z'޺pB\񿇿 \@xIENDB`Solaar-0.9.2/share/solaar/icons/light_080.png000066400000000000000000000035661217372044600206650ustar00rootroot00000000000000PNG  IHDR szzsRGB pHYs7]7]F]tIME /(ښubKGDIDATxKԩ{tC7`)`G9r(E^dUv^F*(da !'p/]]sΗ+,"HYSU_ PA'J1*B$1ϒ-iyP' 2s'K?zA6J}М'٤ׅ /G/%3 kC9u(׉CnEfoka|;.$s{pC_m=ȸw?Qlb$] OCVA^S?!oP9KCm33騾Ͱ*kRK=AOM$ {=$ԹYqD/x:0u {`sKn?̴I 0Z@ s$?>5sNP9=O6\!>ˁaF[sdoo{0?v@+  @N*[֝{Ac6֒Eq# e! lsX~VYA³A@97)6jΕKG ?C2Lfln@~]R h{# VD g I 0 Vɩ1"u 䋞o9!zU KZ.az`@@V킜*S.Awmθcm:2x,0}x#bJ4N2 +$`67`Lh0v 7QDib̽[ᥧR8EٲKoUh2toV{{Y &a!#/u{6^9ض&vq`Nu&"Г^;lTAT6K:˩n> + ViU!hZU2 Ƿ gv' cޔJ=vXSfsR `CꂴbE</gBNyF" lr`.]k Eo񰪸7%vb@G?~*獓$Ɇ 4o6gQXsmP7 Іpy>6# [ j~soDJ]W:]°:TJ911 s)e^eY38yXKY{*udq8!랇FQnۼRYۉc6%xJ Уrmc̞x\\Aٰ Ұ-ȟQu_hYDf[d_~dJS VN9,Z>/B\7 JV2 Gp蒥/BrW | PC.> D΅3']vQq0=iLJM[Z+=Pf<@EX8>.zB`%QP {-ƽ ]eT~.܆$3 +?Q+E*}Ȣv]1m%_kR4DR4nC/:h,2X}+cwyEga?HhYiK uf Veo`ײWoIENDB`Solaar-0.9.2/share/solaar/icons/light_100.png000066400000000000000000000045101217372044600206440ustar00rootroot00000000000000PNG  IHDR szzsRGBbKGD pHYs B(xtIME(GWIDATxڵoU73vpP@<4CJiFJ}?Rї--5jҤJTr) % s|g_gImbhtI?hZhk~s2O1?x l){Dp]3@CJ}b "0ե3F a֞?* T{ s CHԃ/ cCG) O,⼏qUd2rb_з2A+I7XX [ ÐGKDtY)MB(!mҹY?$ǦTND FD+ r}2ZjVFlcWFD*H!F)cX>~k`=|1l?^<~ rH~mg,Z![g8CQfzznB>3OX }Q_AvF-}>_ w 8)bۀ !{ox"Iuu;AJyj l/qۥx{'ãLA;%H 6X AB\1>̗9̤5/ |xMxL9_eˌc)1ńqW>mfQ-Arrr 4@ ƪw#:9{dz$Wkdcm&`Do!a)<0ZI;U&0`npARM6`A :TU {8L6r^5ǪWd"db(t : P$@8 !2)'!| f7xDי%wQKAE`,7XGH @N@"l&dAB6+ (Njxs:< n4ۗkGQ3"(m7&7l6`.L"XӹT}XYctkGQa'^ %_W$ܨ@*e-$Y+}TxdfArYæ}q;!ƾ9GgE83}[cK%3$(]֯5 Sdzk҆p CU{:6t&thUd߹z--65q RN6_ypΕՒ,\z 6J k$izxdUv_ۼ‘Vs5>φS_nQ~8~ ~Ez Eؾm]Nŭ䔛S/B )azGFn郼H*X}JUEe|_a*Έ€=Y"<9i<6X@'YqFO_i/Dh8њX~0!>>F P.(8-թ,`TjݸzwI W1]]aI7ǻͧ{m7D)ADpXD=wRv+./k*z&N+7Dt2wcgG/NWDn 2| /r8`ρ܂$vF^ (Wb# ;<GSzqt2맽tGplwNg3!?3/^]k ?*,o亏TIENDB`Solaar-0.9.2/share/solaar/icons/light_unknown.png000066400000000000000000000021241217372044600220420ustar00rootroot00000000000000PNG  IHDR DsRGB pHYs^tIME  /ZPLTE  """&&&(((???@@@HHHLLLPPPQQQRRRTTTXXX[[[\\\^^^fffhhhkkklllmmmnnnpppqqqvvvxxx{{{|||}}}yݷ#tRNS /67DHNVXZZ]]]]^^^^^_mrG/"ybKGDgIDATJakhS9Yjd%mʄ@4#;c@|mig}ynHΐ0Ⱦ09ag Solaar attention image/svg+xml Solaar attention Daniel Pavel 2013-06-25 solaar-attention Solaar-0.9.2/share/solaar/icons/solaar-init.svg000066400000000000000000000064011217372044600214130ustar00rootroot00000000000000 Solaar init image/svg+xml Solaar init Daniel Pavel 2013-06-25 solaar-init Solaar-0.9.2/share/solaar/icons/solaar.svg000066400000000000000000000063621217372044600204600ustar00rootroot00000000000000 Solaar image/svg+xml Solaar Daniel Pavel 2013-06-25 solaar Solaar-0.9.2/tools/000077500000000000000000000000001217372044600141115ustar00rootroot00000000000000Solaar-0.9.2/tools/build_gh_pages.sh000077500000000000000000000077341217372044600174170ustar00rootroot00000000000000#!/bin/sh set -e BUILD="$(/bin/mktemp --directory --tmpdir solaar-gh-pages-XXXXXX)" cd "$(/usr/bin/dirname "$0")/.." SELF="$PWD" SITE="$(/bin/readlink --canonicalize "$SELF/../gh-pages")" # # # add_md() { local SOURCE="$SELF/$1" local TARGET="$BUILD/${2:-$(/usr/bin/basename "$1")}" LAYOUT=default TITLE=$(/bin/grep --max-count=1 '^# ' "$SOURCE" | /usr/bin/cut --characters=3-) if test -n "$TITLE"; then local TITLE="Solaar - $TITLE" LAYOUT=page else local TITLE=Solaar fi /bin/mkdir --parents "$(dirname "$TARGET")" /bin/cat >"$TARGET" <<-FRONTMATTER --- layout: $LAYOUT title: $TITLE --- FRONTMATTER /bin/cat "$SOURCE" >>"$TARGET" } fix_times() { local SOURCE="$SELF/$1" local TARGET="$SITE/$2" local f if test -d "$SOURCE"; then for f in "$SOURCE"/*; do f=$(/usr/bin/basename "$f") fix_times "$1/$f" "$2/$f" done fi /usr/bin/touch --reference="$SOURCE" "$TARGET" } # # # /bin/cp --archive --update "$SELF/jekyll"/* "$BUILD/" # convert the svg logo to png for the web site favicon /usr/bin/convert.im6 "$SELF/share/solaar/icons/solaar.svg" -transparent white \ -resize 32x32 "$BUILD/images/favicon.png" # optimize the converted pngs command -V optipng && optipng -preserve -quiet -o 7 "$BUILD/images"/*.png #command -V pngcrush && pngcrush -d "$BUILD/images" -oldtimestamp -q "$BUILD/images"/*.png add_md docs/devices.md add_md docs/installation.md add_md README.md index.md # fix local links to the proper .html files /bin/sed --in-place --expression='s#\[docs/\([a-z]*\)\.md\]#[\1]#g' "$BUILD/index.md" /bin/sed --in-place --expression='s#(docs/\([a-z]*\)\.md)#(\1.html)#g' "$BUILD/index.md" /bin/sed --in-place --expression='s#(COPYING)#({{ site.repository }}/blob/master/COPYING)#g' "$BUILD/index.md" # remove empty lines, to minimze html sizes for l in "$BUILD/_layouts"/*.html; do /bin/sed --expression='/^$/d' "$l" | /usr/bin/tr --delete '\t' >"$l=" /bin/mv "$l=" "$l" done # create packages/ sub-directory /bin/mkdir --parents "$SITE/../packages" "$SITE/packages/" /bin/cp --archive --update --target-directory="$SITE/../packages/" "$SELF/dist/debian"/solaar_* || true /bin/cp --archive --update --target-directory="$SITE/../packages/" "$SELF/dist/debian"/solaar-gnome3_* || true if test -x /usr/bin/dpkg-scanpackages; then cd "$SITE/../packages/" /bin/rm --force *.build /usr/bin/dpkg-scanpackages --multiversion . > Packages /usr/bin/dpkg-scansources . > Sources add_md docs/debian.md cd - fi # check for the latest released version, and update the jekyll configuration if test -x /usr/bin/uscan; then TAG=$(/usr/bin/uscan --no-conf --report-status --check-dirname-regex packaging ./packaging/ \ | /bin/grep 'Newest version' \ | /bin/grep --only-matching --word-regexp '[0-9.]*' | /usr/bin/head --lines=1) if test -n "$TAG"; then /bin/sed --in-place --expression='s#^version: .*$#'"version: $TAG#" "$SELF/jekyll/_config.yml" /bin/sed --in-place --expression='s#/archive/[0-9.]*\.tar\.gz$#'"/archive/$TAG.tar.gz#" "$SELF/jekyll/_config.yml" fi fi # Jekyll nukes the .git folder in the target # so move it out of the way while building. GIT_BACKUP="$(/bin/mktemp --dry-run --directory --tmpdir="$SITE/.." git-backup-XXXXXX)" /bin/mv --no-target-directory "$SITE/.git" "$GIT_BACKUP" jekyll --kramdown "$BUILD" "$SITE" /bin/mv --no-target-directory "$GIT_BACKUP" "$SITE/.git" /bin/cp --archive --link "$SITE/../packages/" "$SITE/" # fix some html formatting for p in "$SITE"/*.html; do /bin/sed --in-place --expression='s#^[ ]*##g' "$p" /bin/sed --in-place --file=- "$p" <<-SED bstart :eop /<\/p>/ brepl { N; beop; } :repl { s#

  • \n

    \(.*\)

    #
  • \1#g; t; } :start /
  • / N /
  • \n

    / {N;beop;} :end SED done # set timestmap of the created files to match the sources fix_times README.md index.html fix_times docs/devices.md devices.html fix_times docs/installation.md installation.html fix_times docs/debian.md debian.html fix_times jekyll/images images fix_times share/solaar/icons/solaar.svg images/favicon.png fix_times jekyll/style style Solaar-0.9.2/tools/clean.sh000077500000000000000000000002701217372044600155310ustar00rootroot00000000000000#!/bin/sh cd "$(dirname "$0")/.." find . -type f -name '*.py[co]' -delete find . -type d -name '__pycache__' -delete /bin/rm --force po/*~ /bin/rm --force --recursive share/locale/ Solaar-0.9.2/tools/hidconsole000077500000000000000000000010031217372044600161600ustar00rootroot00000000000000#!/usr/bin/env python # -*- python-mode -*- """Takes care of starting the main function.""" from __future__ import absolute_import def init_paths(): """Make the app work in the source tree.""" import sys import os.path as _path src_lib = _path.normpath(_path.join(_path.realpath(sys.path[0]), '..', 'lib')) init_py = _path.join(src_lib, 'hidapi', '__init__.py') if _path.exists(init_py): sys.path[0] = src_lib if __name__ == '__main__': init_paths() from hidapi import hidconsole hidconsole.main() Solaar-0.9.2/tools/monitor.py000066400000000000000000000010571217372044600161550ustar00rootroot00000000000000# # # from __future__ import absolute_import, division, print_function, unicode_literals import sys sys.path += (sys.path[0] + '/../lib',) import hidapi from logitech.unifying_receiver.base import DEVICE_UNIFYING_RECEIVER from logitech.unifying_receiver.base import DEVICE_UNIFYING_RECEIVER_2 from logitech.unifying_receiver.base import DEVICE_NANO_RECEIVER def print_event(action, device): print ("~~~~ device [%s] %s" % (action, device)) hidapi.monitor(print_event, DEVICE_UNIFYING_RECEIVER, DEVICE_UNIFYING_RECEIVER_2, DEVICE_NANO_RECEIVER ) Solaar-0.9.2/tools/po-compile.sh000077500000000000000000000005611217372044600165160ustar00rootroot00000000000000#!/bin/sh set -e cd "$(readlink -f "$(dirname "$0")/..")" find "$PWD/po" -type f -name '*.po' | \ while read po_file; do language="$(basename "$po_file")" language="${language%.po}" target="$PWD/share/locale/$language/LC_MESSAGES/solaar.mo" /bin/mkdir --parents "$(dirname "$target")" /usr/bin/msgfmt \ --check \ --output-file="$target" \ "$po_file" done Solaar-0.9.2/tools/po-update.sh000077500000000000000000000030711217372044600163470ustar00rootroot00000000000000#!/bin/sh if test -z "$1"; then echo "Use: $0 " exit 2 fi LL_CC="$1" shift set -e cd "$(readlink -f "$(dirname "$0")/..")" VERSION=$(python setup.py --version) DOMAIN=$(python setup.py --name) SOURCE_FILES=$(/bin/mktemp --tmpdir $DOMAIN-po-update-XXXXXX) find "lib" -name '*.py' >"$SOURCE_FILES" POT_DIR="$PWD/po" test -d "$POT_DIR" POT_FILE="$POT_DIR/$DOMAIN.pot" /usr/bin/xgettext \ --package-name "$DOMAIN" \ --package-version "$VERSION" \ --default-domain="$L_NAME" \ --language=Python --from-code=UTF-8 --files-from="$SOURCE_FILES" \ --no-escape --indent --add-location --sort-by-file \ --add-comments=I18N \ --output="$POT_FILE" /bin/sed --in-place --expression="s/charset=CHARSET/charset=UTF-8/" "$POT_FILE" PO_FILE="$POT_DIR/$LL_CC.po" test -r "$PO_FILE" || /usr/bin/msginit \ --no-translator --locale="$LL_CC" \ --input="$POT_FILE" \ --output-file="$PO_FILE" unfmt() { local SOURCE="/usr/share/locale/$LL_CC/LC_MESSAGES/$1.mo" if [ ! -f $SOURCE ] then local SOURCE="/usr/share/locale-langpack/$LL_CC/LC_MESSAGES/$1.mo" fi local TARGET="$(mktemp --tmpdir $1-$LL_CC-XXXXXX.po)" /usr/bin/msgunfmt \ --no-escape --indent \ --output-file="$TARGET" \ "$SOURCE" echo "$TARGET" } /usr/bin/msgmerge \ --update --no-fuzzy-matching \ --no-escape --indent --add-location --sort-by-file \ --lang="$LL_CC" \ --compendium="$(unfmt gtk30)" \ --compendium="$(unfmt gtk30-properties)" \ "$PO_FILE" "$POT_FILE" # /bin/sed --in-place --expression="s/Language: \\\\n/Language: $L_NAME\\\\n/" "$PO_FILE" echo "Language file is $PO_FILE" Solaar-0.9.2/tools/scan-registers.sh000077500000000000000000000024571217372044600174110ustar00rootroot00000000000000#!/bin/sh if test -z "$1"; then echo "Use: $0 []" exit 2 fi HC="$(dirname "$(readlink -f "$0")")/hidconsole" if test "$1" = "FF" -o "$1" = "ff"; then DEVNUMBER=FF else DEVNUMBER=0$1 fi HIDRAW=$2 do_req() { "$HC" --hidpp $HIDRAW | grep -v "\[1. ${DEVNUMBER} 8F.. ..0[12]" | grep -B 1 "^>> " } req00="$(mktemp --tmpdir req00-XXXXXX)" echo "10 ${DEVNUMBER} 8100 000000" | do_req >"$req00" oldflags=$(grep -Po "^>> \([0-9. ]*\) \[10 ${DEVNUMBER} 8100 \K[0-9a-f]{6}(?=\])" "$req00") if [ -n "$oldflags" ]; then echo "# Old notification flags: $oldflags" cat >"$req00-flags" <<-_CHECK_NOTIFICATIONS 10 ${DEVNUMBER} 8000 ffffff 10 ${DEVNUMBER} 8100 000000 10 ${DEVNUMBER} 8000 ${oldflags} _CHECK_NOTIFICATIONS # set all possible flags, read the new value, then restore the old value # this will show all supported notification flags by this device cat "$req00-flags" | do_req | grep "^>>.* ${DEVNUMBER} 8100 " else echo "# Warning: hidconsole API got changed - unrecognized output" cat "$req00" fi rm --force "$req00" "$req00-flags" & # read all short registers, skipping 00 for n in $(seq 1 255); do printf "10 ${DEVNUMBER} 81%02x 000000\n" $n done | do_req # read all long registers for n in $(seq 0 255); do printf "10 ${DEVNUMBER} 83%02x 000000\n" $n done | do_req