pax_global_header00006660000000000000000000000064125764661020014523gustar00rootroot0000000000000052 comment=a7067334f93f2dde86eaade55723ead1b1ce4e8c wader-0.5.13/000077500000000000000000000000001257646610200127135ustar00rootroot00000000000000wader-0.5.13/CHANGELOG000066400000000000000000000203531257646610200141300ustar00rootroot00000000000000======================================= Wader-*.*.* Overview of changes since Wader-0.3.6 ======================================= Releases are usually made in sync with stable bcm releases. List of changes: * See SVN log for details. ======================================= Wader-0.3.6 Overview of changes since Wader-0.3.5.2 ======================================= This is a new minor stable release of Wader. List of changes: * wader-core * DNS update mechanisms have been harmonized, see #154. On Ubuntu hardy, wader-core does not depend on resolvconf anymore as it causes conflicts with the stock NetworkManager. * New device supported: Novatel MC990D * Start and restart scripts merged into one: wader-core-ctl * wader-gtk * Fix PUK/PUK2 handling, see #148 * Make sure profiles work with GConf-DBus, see #144 * wader-doc * Documentation migrated to Sphinx and revised * wader-doc is no longer built by default, thus saving us from maintaining many, buggy, dependencies. ===================================== Wader-0.3.5.2 Overview of changes since Wader-0.3.5 ===================================== This is a new nano stable release of Wader. wader-0.3.5.1 was quickly replaced by wader-0.3.5.2 as it didn't built on hardy. List of changes: * wader-core * Some HSO bugs introduced in 0.3.{4,5} have been fixed. This is what you get when you don't test a release with every supported device. * Do not execute set_network_type if network_type is None * wader._version allows nano releases * wader-core depends on resolvconf as its not included by default on Hardy. * wader-gtk * Handle NoKeyringDaemonError exceptions, not finished. #149 ===================================== Wader-0.3.5 Overview of changes since Wader-0.3.4 ===================================== This is a new minor stable release of Wader. List of changes: * wader-core * DBusDevicePlugin merged into DevicePlugin * Separate better the ModemManager and Wader exceptions * get_radio_status return value fixed * Some fixes for hotplugging events * Huawei: * get_driver_name fixed for Nozomi * use HuaweiE620's cmd_dict rather than Huawei's * Handle ^MODE: 0,2 in Huawei * Handle error in ^CURC=1 command * Do not use os.system but subprocess.call instead * Set a registering lock to avoid multiple attempts of registration * Add the U1900 band * org.freedesktop.ModemManager.Modem.Simple interface implemented (and untested) * Device creation time has been reduced * Fix "undefined reference to ser" while probing ports * Unused stuff removed * wader-gtk * Many profiles bugs fixed * Only ask for profile when self.profile is None * Add Ctrl+Q accelerator to log window * Stop throbber if device is not present * Remove standard gtk symbols from translation ===================================== Wader-0.3.4 Overview of changes since Wader-0.3.3 ===================================== This is a new minor stable release of Wader. List of changes: * wader-core * WVDIAL: Use either the 'Connected' string or the DNS info to ack that we are connected. * Get rid of python-axiom * wader-gtk * Handle gracefully errors in SetBand and SetConnectionMode * Use translated strings for mode changes ===================================== Wader-0.3.3 Overview of changes since Wader-0.3.2 ===================================== This is a new minor stable release of Wader. List of changes: * wader-core * Fix initialization routine so it bails with cards that insist on replying SimBusy to +CPBR=? commands. Next time is necessary will be requested again and will succeed. * Use PDP context in HSO dialer * Only enable/disable radio if necessary * Some wvdial fixes make it more robust * CLeanup CGMM response if echo was enabled * Some ZTE love * Fix dialup with Option GTM378, it must use the hso dialer. * Better responsiveness in network registration state machine * Many, many bug fixes. * wader-gtk * only enable PIN/PUK if what user entered is meaningful * new wader icon * make sure bytes are resetted ===================================== Wader-0.3.2 Overview of changes since Wader-0.3.1 ===================================== This is a new minor stable release of Wader. List of changes: * wader-core * NetworkManager compatibility has been temporally disabled, our three target distros ship with different snapshots of NM/MM and they don't include some later patches required for compatibility. See [0] for more info. This has the downside of firefox not realizing that we are connected and it will insist on that we are disconnected. A simple workaround is to check the "Work offline" checkbox in "File". * New device added (untested): ZTE MF632 * NMPPPDDialer now listens to PropertyChanged signals * Make sure dialers are unexported upon unsuccessful connection attempt. * wader-core now depends on ozerocdoff. * Many, many bug fixes. * wader-gtk * Problems with unsigned ints and some DNS have been fixed. * Get rid of GtkSpinButton warning. * Handle gracefully unsucessful connection attempts [1]. * Do not allow Set{Band,ConnectionMode} if we are connected. [0] http://trac.warp.es/wader/ticket/133 [1] http://trac.warp.es/wader/ticket/132 ===================================== Wader-0.3.1 Overview of changes since Wader-0.3.0 ===================================== This is a new minor stable release of Wader. List of changes: * wader-core * Really restart the core upon upgrade (Debian/Ubuntu only) * Plugins are only included once and at /usr/share/wader/plugins. An upgrade routine has been added to ensure a smooth transition. * wader-core now mimics the udis that ModemManager uses to export devices. * wader-core enables/disables radio too now (+CFUN=0,1) * Use o.fd.MM.Modem rather than o.fd.ModemManager for properties * OS detection has been improved by not relying on lsb_release * wader-core runs on python2.6 * wader-core now depends on python-messaging. A joint project between Warp (Wader) & OpenShine (MobileManager) to create a solid SMS encoding/decoding library. * New models supported: Huawei E169, Huawei E180 * o.fd.DBus.Properties.GetAll implemented * wader-gtk * The UI has been gettex'd and there are Spanish and French translations * The SMS/Contacts UI has been polished and some interesting new features have been added: * Support for multipart SMS (only sending for now) * Support for categories (Inbox, Drafts and Sent for now) * Support for storing SMS and sending it from SIM * Support for searching contacts * Support for searching SMS (not enabled unless pygtk >= 2.14.0 because of a bug on set_visible_func) * A throbber is shown for every potentially long (IO) operations * Log window now updates the log every second * .desktop added for supported RPM systems * Copyright changed to 'Wader contributors' * Many, many bugs fixed * doc * User tutorial added ===================================== Wader-0.3.0 Overview of changes since Wader-0.2.X ===================================== This is a new major stable release of Wader. List of changes: * wader-core * Wader is the first project that implements the ModemManager API apart from MM itself. It implements the whole MM API except for the new Ericsson's MBM devices (introduced late in the MM development cycle, will support them as soon as we get our hands in one of them). For more info check out[0]. * Wader ditches the session bus and works exclusively on the system bus. * Dialup support for system with NM 0.6.X, NM0.7 and NM0.7.X. * Multiple active devices. * Wader doesn't depends on hsolink any more. * New devices supported: Option Icon 401, Huawei EM730V, Huawei K3520, ZTE K3520 and ZTE K3565. * Initial support for OSX. No dialup, no UI, only DBus functionality. * wader-gtk * Wader now uses gconf to store all its config settings. * Initial support for SMS/Contacts. The UI is not that great, it will be improved in next release. [0] http://trac.warp.es/wader/wiki/WhatsModemManager wader-0.5.13/CONTRIBUTING000066400000000000000000000051631257646610200145520ustar00rootroot00000000000000=========================== Guidelines for contributing =========================== Coding guidelines ================= Like any well behaved python project out there, we are `PEP-8`_ compliant. All the gory details are in the link, a quick way to find out whether your code is complaint or not is via lints, such as `pylint`_ or `pep8`_. .. _PEP-8: http://www.python.org/dev/peps/pep-0008/ .. _pylint: http://pypi.python.org/pypi/pylint .. _pep8: http://pypi.python.org/pypi/pep8 Documentation guidelines ======================== All the documentation must be written in `rst`_. From the API documentation to developer documentation, everything must be written in this lightweight format. .. _rst: http://docutils.sourceforge.net/rst.html Project guidelines ================== * Testing: We have strived for a good coverage in the core, and we want not only to keep it this way, but we want even better coverage. So every testable feature to be added to the core must be comprehensively tested. The `twisted tool`_ that we use for testing has a `coverage`_ switch that is very handy to back up this bold statements. .. _twisted tool: http://twistedmatrix.com/trac/browser/trunk/twisted/trial .. _coverage: http://twistedmatrix.com/trac/browser/trunk/twisted/scripts/trial.py#L140 * Databases: As we had some bad experiences with ORMs in the past, and our models are not that complex, we chose to create our own ORM layer. No, we are not suffering from NIH syndrome, it is a very simple piece of code. Anyway all the DB related code is contained in `wader.common.provider` and tested in `wader.test.test_provider`. The schema and all the ORM related methods must be there. Client code should never deal with cursors, connections, etc. * Portability: A great deal of effort has been invested on making this code base as portable as possible (runs on OSX and Linux). As such, you can not add any coupling in the code to any OS. All the troublesome methods are abstracted out via the `wader.common.oal` module. Similarly, there is a backend system that contains all the modules required to deal with a given environment. For example, if NetworkManager is present we will use its profile system, while if we operate standalone, a custom one will be used. Most of this abstractions are shown in `wader.common.interfaces`. * Translating: No strings should be marked for translation in the core, all the translated strings must be UI ones. The core and the UI will communicate via exceptions that the core will raise and the UI will catch. It is here where a localized dialog can be shown to the user. wader-0.5.13/LICENSE000066400000000000000000000353561257646610200137340ustar00rootroot00000000000000 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. wader-0.5.13/Makefile000066400000000000000000000021021257646610200143460ustar00rootroot00000000000000SHELL = /bin/bash VERSION := $(shell python -c 'from wader.common.consts import APP_VERSION; print APP_VERSION') SOURCES := $(shell rpmbuild --eval '%{_topdir}' 2>/dev/null)/SOURCES WV := wader-$(VERSION) all: @echo Usage: make deb \[TARGET=ubuntu-lucid\] \| rpm rpm: @if [ ! -d $(SOURCES) ] ;\ then\ echo 'SOURCES does not exist, are you running on a non RPM based system?';\ exit 1;\ fi touch resources/extra/networks.py tar -jcvf $(SOURCES)/$(WV).tar.bz2 --exclude=.git --transform="s/^\./$(WV)/" . rpmbuild -ba resources/rpm/wader.spec deb: @if [ ! -d /var/lib/dpkg ] ;\ then\ echo 'Debian package directory does not exist, are you running on a non Debian based system?';\ exit 1;\ fi touch resources/extra/networks.py @if [ -d packaging/debian/$(TARGET)/debian ] ;\ then\ PKGSOURCE=$(TARGET);\ else\ PKGSOURCE=generic;\ fi;\ tar -C packaging/debian/$$PKGSOURCE -cf - debian | tar -xf - @if ! head -1 debian/changelog | grep -q $(VERSION) ;\ then\ echo Changelog and package version are different;\ exit 1;\ fi dpkg-buildpackage -rfakeroot wader-0.5.13/README000066400000000000000000000046461257646610200136050ustar00rootroot00000000000000Wader is a 3G daemon accessible via DBus, written in Python and released under the GPLv2. Wader runs on Linux and OSX. Wader's target audience are developers. If you would like your application to interact with a UMTS device, such as a mobile data card or a mobile phone, then stop searching! Features: * Built upon modern technologies such as dbus and udev * Service invoked via DBus * A single process handles n devices * Dialup via NM 0.8/0.9 or Wvdial/NDIS dialup on systems with an older NM * Extensible AT Engine * Pluggable support for devices: Adding support for a new device is usually a matter of copying an skeleton, changing the device IDs and dropping the plugin in /usr/share/wader-core/plugins * A python shell to interact with the device in runtime History Wader is a fork of the core of Vodafone Mobile Connect Card driver for Linux[0] Some of its parts have been completely rewritten and the most juicy bits have been exported over DBus to allow other applications of the Linux desktop to use Wader. Wader is the first project (apart from ModemManager itself) that implements ModemManager's API[1]. This means that NetworkManager 0.8 / 0.9 will be able to use wader-core to perform operations on devices. Supported devices See the SUPPORTED_DEVICES file to see which devices we currently know how to handle well Project Source Repository (patches most welcome): https://github.com/andrewbird/wader This software should work (in theory) with any device that follows the relevant GSM and 3G specs. Nonetheless, every device is different and it may not work in an untested device. Try it at your own risk. If you speak Python and feel adventurous you could get involved by supporting a new device/distro. LICENSE Wader is distributed under the GPLv2. See the LICENSE file for the gory details. FAQ 0 .- Wader fails horribly with my OS Wader has been tested on the following distros: - Ubuntu 10.04 .. 12.04 - Mint 11 - Fedora 15 You can find instructions of how to add a new OS/Distro in the doc. 1 .- Wader fails horribly with my device Chances are that your device is a cousin of one of our supported devices. Adding support for a new device is relatively easy (as long as it behaves), you can find instructions of how to add a new device in the doc. [0] https://forge.vodafonebetavine.net/projects/vodafonemobilec/ [1] http://trac.warp.es/wader/wiki/WhatsModemManager wader-0.5.13/SUPPORTED_DEVICES000066400000000000000000000021521257646610200153650ustar00rootroot00000000000000============================= Well supported devices =========================== (we have physical samples and test regularily) Huawei E620 E660 E660A E870 EM730V B970 E160 E160B E172 E173 E220 E270 E272 E510 E1750 E1820 E3735 K2540 K3520 K3565 K3565rev2 K3715 K3765 K3770 K3771 K3806 K4505 K4510 K4511 K4605 ZTE MF637U K3520-Z K3565-Z K3570-Z K3571-Z K3765-Z K3770-Z K3772-Z K3805-Z K3806-Z K4505-Z K4510-Z Novatel U740 XU870 MC950 X950D MiFi2352 Option Colt Quad Fuji FujiLite Etna GtFusion K3760 E3730 Ericsson (and vendor variants) F3307 F3507g F3607gw Qualcomm (and vendor variants) Gobi2000 ========================= Less well supported devices ========================= (we don't have physical samples, either user contributed plugins or looks very similar to a device we have and we have a user report of it working) Huawei E169 E1550 E1692 E180 EM770 Option GI0335 Icon225 Icon401 GTM378 GTM380 GtMax36 Novatel EU740 EU870D MC990D S720 ZTE and ONDA MF180 MF181 MF190 MF620 MF626 MF628 MF632 MF636 MF651 MF668 MSA405HS MT503HS Ericsson MD300 SierraWireless 850 875 MC8755 LongCheer (4GSystems XS Stick P10+) wader-0.5.13/bin/000077500000000000000000000000001257646610200134635ustar00rootroot00000000000000wader-0.5.13/bin/wader-core-ctl000077500000000000000000000035611257646610200162260ustar00rootroot00000000000000#!/usr/bin/env python import os from optparse import OptionParser import sys from time import sleep from wader.common.consts import (APP_VERSION, DATA_DIR, PID_PATH) sys.path.insert(0, DATA_DIR) from twisted.python.release import sh def stop_core(): if os.path.exists(PID_PATH): sh("kill -9 `cat %s`" % PID_PATH) parser = OptionParser() parser.add_option("-v", "--version", action="store_true", dest="show_version", default=False, help="Show version and exit") parser.add_option("-r", "--restart", action="store_true", dest="should_restart", default=False, help="Restart wader-core") parser.add_option("-s", "--start", action="store_true", dest="should_start", default=False, help="Start wader-core (only to be called by D-Bus service)") parser.add_option("-t", "--stop", action="store_true", dest="should_stop", default=False, help="Stop wader-core") options, args = parser.parse_args() if not (options.show_version or options.should_restart or options.should_start or options.should_stop): print(parser.format_help().strip()) sys.exit(0) if options.show_version: print "%s: version %s" % (os.path.basename(sys.argv[0]), APP_VERSION) sys.exit(0) if options.should_stop: stop_core() if options.should_restart: try: stop_core() except: print "Failed to stop core cleanly, may still be running" sleep(1) sh("dbus-send --system --dest=org.freedesktop.ModemManager " "/org/freedesktop/ModemManager " "org.freedesktop.ModemManager.EnumerateDevices") elif options.should_start: from twisted.scripts.twistd import run from sys import argv argv[1:] = [ '--python=%s' % os.path.join(DATA_DIR, 'core-tap.py'), '--pidfile=%s' % PID_PATH, '--reactor=glib2' ] run() wader-0.5.13/core-tap.py000066400000000000000000000023521257646610200150010ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """tap file for Wader""" # Make sure the very first thing we do is to set the glib loop as default import dbus from dbus.mainloop.glib import DBusGMainLoop gloop = DBusGMainLoop(set_as_default=True) import locale # i10n stuff locale.setlocale(locale.LC_ALL, '') import sys sys.path.insert(0, '/usr/share/wader-core') from core.startup import get_wader_application application = get_wader_application() wader-0.5.13/core/000077500000000000000000000000001257646610200136435ustar00rootroot00000000000000wader-0.5.13/core/__init__.py000066400000000000000000000015461257646610200157620ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Wader's core""" wader-0.5.13/core/backends/000077500000000000000000000000001257646610200154155ustar00rootroot00000000000000wader-0.5.13/core/backends/__init__.py000066400000000000000000000023451257646610200175320ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011 Vodafone España, S.A. # Copyright (C) 2010 Warp Networks, S.L. # Author: Andrew Bird # # 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 wader.common.backends import BACKEND_LIST from core.backends.nm import nm_backend from core.backends.plain import plain_backend __backend = None def get_backend(): global __backend if __backend is not None: return __backend for name in BACKEND_LIST: backend = globals()[name] if backend.should_be_used(): __backend = backend return __backend wader-0.5.13/core/backends/nm.py000066400000000000000000000245031257646610200164050ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2011 Vodafone España, S.A. # Copyright (C) 2008-2010 Warp Networks, S.L. # Author: Pablo Martí # # 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 dbus from twisted.internet.defer import Deferred from twisted.python import log from zope.interface import implements from wader.common.backends.nm import NetworkManagerBackend as \ _NetworkManagerBackend from wader.common.consts import (WADER_PROFILES_SERVICE, WADER_PROFILES_INTFACE, WADER_PROFILES_OBJPATH, MDM_INTFACE, MM_IP_METHOD_PPP) from wader.common.interfaces import IBackend from core.dialer import Dialer NM_SERVICE = 'org.freedesktop.NetworkManager' NM_OBJPATH = '/org/freedesktop/NetworkManager' NM_INTFACE = 'org.freedesktop.NetworkManager' NM_DEVICE = '%s.Device' % NM_INTFACE NM08_GSM_INTFACE = '%s.Gsm' % NM_DEVICE NM08_USER_SETTINGS = 'org.freedesktop.NetworkManagerUserSettings' NM09_MODEM_INTFACE = '%s.Modem' % NM_DEVICE NM08_STATE = { 'UNKNOWN': 0, 'ASLEEP': 1, 'CONNECTING': 2, 'CONNECTED': 3, 'DISCONNECTED': 4, } NM09_STATE = { 'UNKNOWN': 0, 'ASLEEP': 10, 'DISCONNECTED': 20, 'DISCONNECTING': 30, 'CONNECTING': 40, 'CONNECTED_LOCAL': 50, 'CONNECTED_SITE': 60, 'CONNECTED_GLOBAL': 70, 'IPCONFIG': 100 } NM08_DEVICE_STATE_UNKNOWN = 0 NM08_DEVICE_STATE_UNMANAGED = 1 NM08_DEVICE_STATE_UNAVAILABLE = 2 NM08_DEVICE_STATE_DISCONNECTED = 3 NM08_DEVICE_STATE_PREPARE = 4 NM08_DEVICE_STATE_CONFIG = 5 NM08_DEVICE_STATE_NEED_AUTH = 6 NM08_DEVICE_STATE_IP_CONFIG = 7 NM08_DEVICE_STATE_ACTIVATED = 8 NM08_DEVICE_STATE_FAILED = 9 NM08_DEVICE_STATE = { 'UNKNOWN': NM08_DEVICE_STATE_UNKNOWN, 'UNMANAGED': NM08_DEVICE_STATE_UNMANAGED, 'UNAVAILABLE': NM08_DEVICE_STATE_UNAVAILABLE, 'DISCONNECTED': NM08_DEVICE_STATE_DISCONNECTED, 'PREPARE': NM08_DEVICE_STATE_PREPARE, 'CONFIG': NM08_DEVICE_STATE_CONFIG, 'NEED_AUTH': NM08_DEVICE_STATE_NEED_AUTH, 'IP_CONFIG': NM08_DEVICE_STATE_IP_CONFIG, 'ACTIVATED': NM08_DEVICE_STATE_ACTIVATED, 'FAILED': NM08_DEVICE_STATE_FAILED } NM09_DEVICE_STATE_UNKNOWN = 0 NM09_DEVICE_STATE_UNMANAGED = 10 NM09_DEVICE_STATE_UNAVAILABLE = 20 NM09_DEVICE_STATE_DISCONNECTED = 30 NM09_DEVICE_STATE_PREPARE = 40 NM09_DEVICE_STATE_CONFIG = 50 NM09_DEVICE_STATE_NEED_AUTH = 60 NM09_DEVICE_STATE_IP_CONFIG = 70 NM09_DEVICE_STATE_IP_CHECK = 80 NM09_DEVICE_STATE_SECONDARIES = 90 NM09_DEVICE_STATE_ACTIVATED = 100 NM09_DEVICE_STATE = { 'UNKNOWN': NM09_DEVICE_STATE_UNKNOWN, 'UNMANAGED': NM09_DEVICE_STATE_UNMANAGED, 'UNAVAILABLE': NM09_DEVICE_STATE_UNAVAILABLE, 'DISCONNECTED': NM09_DEVICE_STATE_DISCONNECTED, 'PREPARE': NM09_DEVICE_STATE_PREPARE, 'CONFIG': NM09_DEVICE_STATE_CONFIG, 'NEED_AUTH': NM09_DEVICE_STATE_NEED_AUTH, 'IP_CONFIG': NM09_DEVICE_STATE_IP_CONFIG, 'IP_CHECK': NM09_DEVICE_STATE_IP_CHECK, 'SECONDARIES': NM09_DEVICE_STATE_SECONDARIES, 'ACTIVATED': NM09_DEVICE_STATE_ACTIVATED } class NMDialer(Dialer): """I wrap NetworkManager's dialer""" def __init__(self, device, opath, **kwds): super(NMDialer, self).__init__(device, opath, **kwds) self.int = None self.conn_obj = None self.iface = self._get_stats_iface() self.state = self.NM_DISCONNECTED self.nm_opath = None self.connect_deferred = None self.disconnect_deferred = None self.sm = None def _cleanup(self): # enable +CREG notifications afterwards self.device.sconn.set_netreg_notification(1) self.sm.remove() self.sm = None def _get_device_opath(self): """ Returns the object path to use in the connection / signal """ obj = self.bus.get_object(NM_SERVICE, NM_OBJPATH) interface = dbus.Interface(obj, NM_INTFACE) for opath in interface.GetDevices(): dev = self.bus.get_object(NM_SERVICE, opath) udi = dev.Get('org.freedesktop.NetworkManager.Device', 'Udi') if self.device.opath == udi: return opath def _get_stats_iface(self): if self.device.get_property(MDM_INTFACE, 'IpMethod') == MM_IP_METHOD_PPP: iface = 'ppp0' # XXX: shouldn't hardcode to first PPP instance else: iface = self.device.get_property(MDM_INTFACE, 'Device') return iface def _on_properties_changed(self, changed): if 'State' not in changed: return if changed['State'] == self.NM_CONNECTED and \ self.state == self.NM_DISCONNECTED: # emit the connected signal and send back the opath # if the deferred is present self.state = self.NM_CONNECTED self.Connected() self.connect_deferred.callback(self.opath) return if changed['State'] == self.NM_DISCONNECTED: if self.state == self.NM_CONNECTED: self.Disconnected() if self.disconnect_deferred is not None: # could happen if we are connected and a NM_DISCONNECTED # signal arrives without having explicitly disconnected self.disconnect_deferred.callback(self.conn_obj) if self.state == self.NM_DISCONNECTED: # Occurs if the connection attempt failed self.Disconnected() if self.connect_deferred is not None: msg = 'Network Manager failed to connect' self.connect_deferred.errback(RuntimeError(msg)) self.state = self.NM_DISCONNECTED self._cleanup() def _setup_signals(self): self.sm = self.bus.add_signal_receiver(self._on_properties_changed, "PropertiesChanged", path=self._get_device_opath(), dbus_interface=self.NM_MODEM_INT) def configure(self, config): self._setup_signals() # get the profile object and obtains its uuid # get ProfileManager and translate the uuid to a NM object path profiles = self.bus.get_object(WADER_PROFILES_SERVICE, WADER_PROFILES_OBJPATH) # get the object path of the profile being used self.nm_opath = profiles.GetNMObjectPath(str(config.uuid), dbus_interface=WADER_PROFILES_INTFACE) # Disable +CREG notifications, otherwise NMDialer won't work return self.device.sconn.set_netreg_notification(0) def connect(self): raise NotImplementedError("Implement in subclass") def stop(self): self._cleanup() return self.disconnect() def disconnect(self): self.disconnect_deferred = Deferred() self.int.DeactivateConnection(self.conn_obj) # the deferred will be callbacked as soon as we get a # connectivity status change return self.disconnect_deferred class NM08Dialer(NMDialer): def __init__(self, device, opath, **kwds): self.NM_CONNECTED = NM08_DEVICE_STATE_ACTIVATED self.NM_DISCONNECTED = NM08_DEVICE_STATE_DISCONNECTED self.NM_MODEM_INT = NM08_GSM_INTFACE super(NM08Dialer, self).__init__(device, opath, **kwds) def connect(self): self.connect_deferred = Deferred() obj = self.bus.get_object(NM_SERVICE, NM_OBJPATH) self.int = dbus.Interface(obj, NM_INTFACE) # NM 0.8 - applet (USER) # # # # args = (NM08_USER_SETTINGS, self.nm_opath, self._get_device_opath(), '/') log.msg("Connecting with:\n%s\n%s\n%s\n%s" % args) try: self.conn_obj = self.int.ActivateConnection(*args) # the deferred will be callbacked as soon as we get a # connectivity status change return self.connect_deferred except dbus.DBusException, e: log.err(e) self._cleanup() class NM09Dialer(NMDialer): def __init__(self, device, opath, **kwds): self.NM_CONNECTED = NM09_DEVICE_STATE_ACTIVATED self.NM_DISCONNECTED = NM09_DEVICE_STATE_DISCONNECTED self.NM_MODEM_INT = NM09_MODEM_INTFACE super(NM09Dialer, self).__init__(device, opath, **kwds) def connect(self): self.connect_deferred = Deferred() obj = self.bus.get_object(NM_SERVICE, NM_OBJPATH) self.int = dbus.Interface(obj, NM_INTFACE) # NM 0.9 # # # args = (self.nm_opath, self._get_device_opath(), '/') log.msg("Connecting with:\n%s\n%s\n%s" % args) try: self.conn_obj = self.int.ActivateConnection(*args) # the deferred will be callbacked as soon as we get a # connectivity status change return self.connect_deferred except dbus.DBusException, e: log.err(e) self._cleanup() class NetworkManagerBackend(_NetworkManagerBackend): implements(IBackend) def get_dialer_klass(self, device): if self._get_version() == '08': return NM08Dialer elif self._get_version() == '084': return NM08Dialer else: return NM09Dialer nm_backend = NetworkManagerBackend() wader-0.5.13/core/backends/plain.py000066400000000000000000000445351257646610200171050ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 with_statement import errno from cStringIO import StringIO import os import tempfile import re import shutil from signal import SIGTERM, SIGKILL from string import Template from twisted.internet import reactor, defer, protocol, error, task from twisted.python import log, procutils from zope.interface import implements from wader.common.backends.plain import PlainBackend as _PlainBackend from wader.common.consts import (APP_NAME, FALLBACK_DNS, MDM_INTFACE, MM_ALLOWED_AUTH_NONE, MM_ALLOWED_AUTH_PAP, MM_ALLOWED_AUTH_CHAP, MM_MODEM_STATE_REGISTERED, MM_MODEM_STATE_CONNECTING, MM_MODEM_STATE_DISCONNECTING, MM_MODEM_STATE_CONNECTED) from wader.common.interfaces import IBackend from wader.common.utils import save_file, is_bogus_ip from core.dialer import Dialer from core.oal import get_os_object def proc_running(pid): try: pid = int(pid) except (TypeError, ValueError): return None if pid <= 1: return False # No killing of process group members or all of init's # children try: os.kill(pid, 0) except OSError, err: if err.errno == errno.ESRCH: return False elif err.errno == errno.EPERM: return pid else: return None # Unknown error else: return pid def signal_process(name, pid, signal): pid = proc_running(pid) if not pid: log.msg('wvdial: "%s" process (%s) already exited' % (name, str(pid))) return False log.msg('wvdial: "%s" process (%s) will be sent %s' % (name, str(pid), signal)) try: os.kill(pid, SIGKILL) except OSError, err: if err.errno == errno.ESRCH: log.msg('wvdial: "%s" process (%s) not found' % (name, str(pid))) elif err.errno == errno.EPERM: log.msg('wvdial: "%s" process (%s) permission denied' % (name, str(pid))) else: log.msg('wvdial: "%s" process exit "%s"' % (name, str(err))) return True # signal was sent def validate_dns(dynamic, use_static, static): if use_static: valid_dns = [addr for addr in static if addr] else: # If static DNS is not set, then we should use the DNS returned by the # network, but let's check if they're valid DNS IPs valid_dns = [addr for addr in dynamic if not is_bogus_ip(addr)] if len(valid_dns): return True, valid_dns # The DNS assigned by the network is invalid or missing, or the static # addresses are missing, so notify the user and fallback to Google etc return False, FALLBACK_DNS WVDIAL_PPPD_OPTIONS = os.path.join('/etc', 'ppp', 'peers', 'wvdial') WVDIAL_RETRIES = 3 WVTEMPLATE = """ [Dialer Defaults] Phone = $phone Username = $username Password = $password Stupid Mode = 1 Dial Command = ATDT New PPPD = yes Check Def Route = on Dial Attempts = 3 Dial Timeout = 30 Auto Reconnect = off Auto DNS = on [Dialer connect] Modem = $serialport Baud = 460800 Init1 = AT Init2 = AT Init3 = ATQ0 V1 E0 S0=0 &C1 &D2 Init4 = AT+CGDCONT=$context,"IP","$apn" ISDN = 0 Modem Type = Analog Modem """ CONNECTED_REGEXP = re.compile('Connected\.\.\.') PPPD_PID_REGEXP = re.compile('Pid of pppd: (?P\d+)') PPPD_IFACE_REGEXP = re.compile('Using interface (?Pppp\d+)') MAX_ATTEMPTS_REGEXP = re.compile('Maximum Attempts Exceeded') PPPD_DIED_REGEXP = re.compile('The PPP daemon has died') DNS_REGEXP = re.compile(r""" DNS\saddress \s # beginning of the string (?P # group named ip (25[0-5]| # integer range 250-255 OR 2[0-4][0-9]| # integer range 200-249 OR [01]?[0-9][0-9]?) # any number < 200 \. # matches '.' (25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) # repeat \. # matches '.' (25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) # repeat \. # matches '.' (25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) # repeat ) # end of group \b # end of the string """, re.VERBOSE) DEFAULT_TEMPLATE = """ ipparam wader debug noauth name wvdial noipdefault nomagic ipcp-accept-local ipcp-accept-remote nomp noccp nopredictor1 novj novjccomp nobsdcomp""" PAP_TEMPLATE = DEFAULT_TEMPLATE + """ refuse-chap refuse-mschap refuse-mschap-v2 refuse-eap """ CHAP_TEMPLATE = DEFAULT_TEMPLATE + """ refuse-pap """ def get_wvdial_conf_file(conf, context, serial_port): """ Returns the path of the generated wvdial.conf :param conf: `DialerConf` instance :param serial_port: The port to use :rtype: str """ text = _generate_wvdial_conf(conf, context, serial_port) dirpath = tempfile.mkdtemp('', APP_NAME, '/tmp') path = tempfile.mkstemp('wvdial.conf', APP_NAME, dirpath, True)[1] save_file(path, text) return path def _generate_wvdial_conf(conf, context, sport): """ Generates a specially crafted wvdial.conf with `conf` and `sport` :param conf: `DialerConf` instance :param sport: The port to use :rtype: str """ user = conf.username if conf.username else '*' passwd = conf.password if conf.password else '*' theapn = conf.apn # build template data = StringIO(WVTEMPLATE) template = Template(data.getvalue()) data.close() # construct number number = '*99***%d#' % context # return template props = dict(serialport=sport, username=user, password=passwd, context=context, apn=theapn, phone=number) return template.substitute(props) class WVDialDialer(Dialer): """Dialer for WvDial""" binary = 'wvdial' def __init__(self, device, opath, **kwds): super(WVDialDialer, self).__init__(device, opath, **kwds) try: self.bin_path = procutils.which(self.binary)[0] except IndexError: self.bin_path = '/usr/bin/wvdial' self.backup_path = "" self.conf = None self.conf_path = "" self.dirty = False self.proto = None self.iconn = None self.iface = 'ppp0' self.should_stop = False self.attempting_connect = False def Connected(self): self.device.set_status(MM_MODEM_STATE_CONNECTED) self.attempting_connect = False super(WVDialDialer, self).Connected() def Disconnected(self): if self.device.status >= MM_MODEM_STATE_REGISTERED: self.device.set_status(MM_MODEM_STATE_REGISTERED) self.attempting_connect = False super(WVDialDialer, self).Disconnected() def configure(self, config): self.dirty = True def get_context_id(ign): conn_id = self.device.sconn.state_dict.get('conn_id') try: context = int(conn_id) except ValueError: raise Exception('WVDialDialer context id is "%s"' % str(conn_id)) return context d = self.device.sconn.set_apn(config.apn) d.addCallback(get_context_id) d.addCallback(lambda context: self._generate_config(config, context)) return d def connect(self): if self.should_stop: self.should_stop = False return self.device.set_status(MM_MODEM_STATE_CONNECTING) self.attempting_connect = True self.proto = WVDialProtocol(self) args = [self.binary, '-C', self.conf_path, 'connect'] self.iconn = reactor.spawnProcess(self.proto, args[0], args, env=None) return self.proto.deferred def stop(self): self.should_stop = True self.attempting_connect = False return self.disconnect() def disconnect(self): if self.proto is None: return defer.succeed(self.opath) self.device.set_status(MM_MODEM_STATE_DISCONNECTING) msg = 'WVdial failed to connect' def get_pppd_pid(): pid_file = "/var/run/%s.pid" % self.iface if not os.path.exists(pid_file): return False pid = None with open(pid_file) as f: pid = f.read() return pid def cleanup_pppd(): if self.attempting_connect: self.proto.deferred.errback(RuntimeError(msg)) pppd_pid = get_pppd_pid() if not signal_process('pppd', pppd_pid, SIGTERM): # process was already gone d = defer.succeed(self._cleanup()) else: d = task.deferLater(reactor, 5, signal_process, 'pppd', pppd_pid, SIGKILL) d.addCallback(lambda _: self._cleanup()) return d # tell wvdial to quit try: self.proto.transport.signalProcess('TERM') except error.ProcessExitedAlready: log.msg("wvdial: wvdial exited") # just be damn sure that we're killing everything wvdial_pid = proc_running(self.proto.pid) if not wvdial_pid: # process was already gone d = cleanup_pppd() else: d = task.deferLater(reactor, 5, signal_process, 'wvdial', wvdial_pid, SIGKILL) d.addCallback(lambda _: cleanup_pppd()) d.addCallback(lambda _: self.opath) return d def _generate_config(self, conf, context): # backup wvdial configuration self.backup_path = self._backup_conf() self.conf = conf # generate auth configuration self._generate_wvdial_ppp_options() # generate wvdial.conf from template port = self.device.ports.dport self.conf_path = get_wvdial_conf_file(self.conf, context, port.path) def _cleanup(self, ignored=None): """cleanup our traces""" if not self.dirty: return try: path = os.path.dirname(self.conf_path) os.unlink(self.conf_path) os.rmdir(path) except (IOError, OSError): pass self._restore_conf() def _generate_wvdial_ppp_options(self): if not self.conf.refuse_chap: wvdial_ppp_options = CHAP_TEMPLATE elif not self.conf.refuse_pap: wvdial_ppp_options = PAP_TEMPLATE else: # this could be a NOOP, but the user might have modified # the stock /etc/ppp/peers/wvdial file, so the safest option # is to overwrite with our known good options. wvdial_ppp_options = DEFAULT_TEMPLATE # There are some patched pppd implementations # Most systems offer 'replacedefaultroute', but not Fedora osobj = get_os_object() if hasattr(osobj, 'get_additional_wvdial_ppp_options'): wvdial_ppp_options += osobj.get_additional_wvdial_ppp_options() save_file(WVDIAL_PPPD_OPTIONS, wvdial_ppp_options) def _backup_conf(self): path = tempfile.mkstemp('wvdial', APP_NAME)[1] try: shutil.copy(WVDIAL_PPPD_OPTIONS, path) return path except IOError: return None def _restore_conf(self): if self.backup_path: shutil.copy(self.backup_path, WVDIAL_PPPD_OPTIONS) os.unlink(self.backup_path) self.backup_path = None def _set_iface(self, iface): self.iface = iface class WVDialProtocol(protocol.ProcessProtocol): """ProcessProtocol for wvdial""" def __init__(self, dialer): self.dialer = dialer self.__connected = False self.pid = None self.retries = 0 self.deferred = defer.Deferred() self.dns = [] def connectionMade(self): self.transport.closeStdin() def outReceived(self, data): log.msg("wvdial: sysout %s" % data) def errReceived(self, data): """wvdial has this bad habit of using stderr for debug""" log.msg("wvdial: %r" % data) self._parse_output(data) def outConnectionLost(self): log.msg('wvdial: wvdial closed its stdout!') def errConnectionLost(self): log.msg('wvdial: wvdial closed its stderr.') def processEnded(self, status_object): log.msg('wvdial: quitting') if not self.__connected: self.dialer.disconnect() self._set_disconnected(force=True) def processExited(self, status): log.msg('wvdial: wvdial processExited') def _set_connected(self): if self.__connected: return valid, dns = validate_dns(self.dns, self.dialer.conf.staticdns, [self.dialer.conf.dns1, self.dialer.conf.dns2]) if not valid: if self.dialer.conf.staticdns: self.dialer.InvalidDNS([]) else: self.dialer.InvalidDNS(self.dns) osobj = get_os_object() osobj.add_dns_info(dns, self.dialer.iface) self.__connected = True self.dialer.Connected() self.deferred.callback(self.dialer.opath) def _set_disconnected(self, force=False): if not self.__connected and not force: return osobj = get_os_object() osobj.delete_dns_info(self.dialer.iface) self.__connected = False self.dialer.Disconnected() def _extract_iface(self, data): match = PPPD_IFACE_REGEXP.search(data) if match: self.dialer._set_iface(match.group('iface')) log.msg("wvdial: dialer interface %s" % self.dialer.iface) def _extract_dns_strings(self, data): for match in re.finditer(DNS_REGEXP, data): self.dns.append(match.group('ip')) def _extract_connected(self, data): if CONNECTED_REGEXP.search(data): self._set_connected() def _extract_disconnected(self, data): # more than three attempts max_attempts = MAX_ATTEMPTS_REGEXP.search(data) # pppd died pppd_died = PPPD_DIED_REGEXP.search(data) if max_attempts or pppd_died: self._set_disconnected() def _extract_tries(self, data): # force wvdial to stop after three attempts if self.retries >= WVDIAL_RETRIES: self.dialer.disconnect() self._set_disconnected() return # extract pppd pid match = PPPD_PID_REGEXP.search(data) if match: self.pid = int(match.group('pid')) self.retries += 1 log.msg("wvdial: dialer tries %d" % self.retries) def _parse_output(self, data): self._extract_iface(data) self._extract_dns_strings(data) if not self.__connected: self._extract_connected(data) else: self._extract_disconnected(data) self._extract_tries(data) class HSODialer(Dialer): """Dialer for HSO type devices""" # Note: The interface is called HSO for historical reasons but actually # it can be used by devices other than Option's e.g. ZTE's Icera def __init__(self, device, opath, **kwds): super(HSODialer, self).__init__(device, opath, **kwds) self.iface = self.device.get_property(MDM_INTFACE, 'Device') self.conf = None def configure(self, config): self.conf = config if not config.refuse_chap: auth = MM_ALLOWED_AUTH_CHAP elif not config.refuse_pap: auth = MM_ALLOWED_AUTH_PAP else: auth = MM_ALLOWED_AUTH_NONE d = self.device.sconn.set_apn(config.apn) d.addCallback(lambda _: self.device.sconn.hso_authenticate( config.username, config.password, auth)) return d def connect(self): # start the connection d = self.device.sconn.hso_connect() # now get the IP4Config and set up device and routes d.addCallback(lambda _: self.device.sconn.get_ip4_config()) d.addCallback(self._get_ip4_config_cb) d.addCallback(lambda _: self.Connected()) d.addCallback(lambda _: self.opath) return d def _get_ip4_config_cb(self, (ip, dns1, dns2, dns3)): valid, dns = validate_dns([dns1, dns2, dns3], self.conf.staticdns, [self.conf.dns1, self.conf.dns2]) if not valid: if self.conf.staticdns: self.InvalidDNS([]) else: self.InvalidDNS(self.dns) osobj = get_os_object() d = osobj.configure_iface(self.iface, ip, 'up') d.addCallback(lambda _: osobj.add_default_route(self.iface)) d.addCallback(lambda _: osobj.add_dns_info(dns, self.iface)) return d def disconnect(self): d = self.device.sconn.disconnect_from_internet() osobj = get_os_object() osobj.delete_default_route(self.iface) osobj.delete_dns_info(None, self.iface) osobj.configure_iface(self.iface, '', 'down') d.addCallback(lambda _: self.Disconnected()) return d def stop(self): # set internal flag in device for disconnection self.device.sconn.state_dict['should_stop'] = True return self.disconnect() class PlainBackend(_PlainBackend): """Plain backend""" implements(IBackend) def get_dialer_klass(self, device): if device.dialer in ['hso']: return HSODialer return WVDialDialer plain_backend = PlainBackend() wader-0.5.13/core/command.py000066400000000000000000000251051257646610200156360ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """AT Commands related classes and help functions""" import re from twisted.internet import defer from wader.common.aterrors import ERROR_REGEXP OK_REGEXP = re.compile("\r\n(?POK)\r\n") def build_cmd_dict(extract=OK_REGEXP, end=OK_REGEXP, error=ERROR_REGEXP): """Returns a dictionary ready to be used in `CMD_DICT`""" for regexp in [extract, end, error]: if isinstance(regexp, basestring): regexp = re.compile(regexp) if hasattr(regexp, 'search'): pass else: raise ValueError("Don't know what to do with %r" % regexp) return dict(extract=extract, end=end, error=error) def get_cmd_dict_copy(): """ Returns a copy of the `CMD_DICT` dictionary Use this instead of importing it directly as you may forget to copy() it """ return CMD_DICT.copy() CMD_DICT = { 'add_contact': build_cmd_dict(), 'cancel_ussd': build_cmd_dict(), 'change_pin': build_cmd_dict(), 'check_pin': build_cmd_dict(re.compile(r""" \r\n \+CPIN:\s (?P READY | SIM\sPIN2? | SIM\sPUK2? ) \r\n""", re.X)), 'delete_contact': build_cmd_dict(), 'delete_sms': build_cmd_dict(), 'disable_echo': build_cmd_dict(), 'enable_echo': build_cmd_dict(), 'enable_radio': build_cmd_dict(), 'enable_pin': build_cmd_dict(), 'find_contacts': build_cmd_dict(re.compile(r""" \r\n \+CPBF:\s (?P\d+), "(?P[+0-9a-fA-F]+)", (?P\d+), \"(?P.*)\" """, re.X)), 'get_apns': build_cmd_dict(re.compile(r""" \r\n \+CGDCONT:\s (?P\d+), "[A-Za-z0-9]*", "(?P.*)", "(?P.*)", \d,\d""", re.X)), 'get_apn_range': build_cmd_dict(re.compile(r""" \r\n \+CGDCONT:\s*\((?P\d+)-(?P\d+)\), (?P(?:"IP")|(?:"00490050"))(?P.*) """, re.X)), 'get_charsets': build_cmd_dict(re.compile('"(?P.*?)",?')), 'get_contact': build_cmd_dict(re.compile(r""" \r\n \+CPBR:\s(?P\d+), "(?P[+0-9a-fA-F]+)", (?P\d+), "(?P.*)" (?P,.*)? \r\n""", re.X)), 'list_contacts': build_cmd_dict( end=re.compile('(\r\n)?\r\n(OK)\r\n'), extract=re.compile(r""" \r\n \+CPBR:\s(?P\d+), "(?P[+0-9a-fA-F]+)", (?P\d+), "(?P.*)" """, re.X)), 'get_card_version': build_cmd_dict(re.compile( '\r\n(\+C?GMR:)?(?P.*)\r\n\r\nOK\r\n')), 'get_card_model': build_cmd_dict(re.compile( '\r\n(?P.*)\r\n\r\nOK\r\n')), 'get_charset': build_cmd_dict(re.compile( '\r\n\+CSCS:\s"(?P.*)"\r\n')), 'get_esn': build_cmd_dict(re.compile('\r\n\+ESN:\s"(?P.*)"\r\n')), 'get_manufacturer_name': build_cmd_dict(re.compile( '\r\n(?P.*)\r\n\r\nOK\r\n')), 'get_imei': build_cmd_dict(re.compile("\r\n(?P\d+)\r\n")), 'get_imsi': build_cmd_dict(re.compile('\r\n(?P\d+)\r\n')), 'get_netreg_status': build_cmd_dict(re.compile(r""" \r\n \+CREG:\s (?P\d),(?P\d+) (,[0-9a-fA-F]*,[0-9a-fA-F]*)? \r\n """, re.X)), 'get_network_info': build_cmd_dict(re.compile(r""" \r\n \+COPS:\s+ (\d,\d, # or followed by num,num,str,num "(?P[^"]*)", (?P\d) |(?P\d) ) # end of group \r\n""", re.X)), 'get_network_names': build_cmd_dict(re.compile(r""" \( (?P\d+), "(?P[^"]*)", "(?P[^"]*)", "(?P\d+)", (?P\d) \),?""", re.X), end=re.compile('\r\n\r\nOK\r\n')), 'get_signal_quality': build_cmd_dict(re.compile(r""" \r\n \+CSQ:\s(?P\d+),(?P\d+) \r\n""", re.X)), 'get_sms_format': build_cmd_dict( re.compile('\r\n\+CMGF:\s(?P\d)\r\n')), 'get_phonebook_size': build_cmd_dict(re.compile(r""" \r\n \+CPBR:\s \(\d\-(?P\d+)\)(?:,\d+)* \r\n""", re.X)), 'get_pin_status': build_cmd_dict( re.compile('\r\n\+CLCK:\s(?P\d)\r\n')), 'get_radio_status': build_cmd_dict( re.compile("\r\n\+CFUN:\s?(?P\d)\r\n")), 'get_roaming_ids': build_cmd_dict(re.compile(r""" \r\n \+CPOL:\s(?P\d+), (?P\d), "(?P\d+)" (?P,.*)? """, re.X)), 'list_sms': build_cmd_dict(re.compile(r""" \r\n \+CMGL:\s (?P\d+), (?P\d),,\d+ \r\n(?P\w+)""", re.X)), 'get_sms': build_cmd_dict(re.compile(r""" \r\n \+CMGR:\s (?P\d),, \d+\r\n (?P\w+) \r\n""", re.X)), 'get_smsc': build_cmd_dict(re.compile( '\r\n\+CSCA:\s"(?P.*)",\d+\r\n')), 'register_with_netid': build_cmd_dict(), 'reset_settings': build_cmd_dict(), 'save_sms': build_cmd_dict(re.compile('\r\n\+CMGW:\s(?P\d+)\r\n')), 'send_at': build_cmd_dict(), 'send_sms': build_cmd_dict(re.compile( '\r\n\+CMGS:\s(?P\d+)\r\n')), 'send_sms_from_storage': build_cmd_dict(re.compile( '\r\n\+CMSS:\s(?P\d+)\r\n')), 'send_pin': build_cmd_dict(), 'send_puk': build_cmd_dict(), 'send_ussd': build_cmd_dict(re.compile(""" \r\n\+CUSD:\s (?P\d) (?:,"(?P.*)")? (?:,(?P\d+))? \r\n""", re.X), re.compile('\r\n\+CUSD:\s\d(?:,".*")?(?:,\d+)?\r\n')), 'set_apn': build_cmd_dict(), 'set_charset': build_cmd_dict(), 'set_error_level': build_cmd_dict(), 'set_netreg_notification': build_cmd_dict(), 'set_network_info_format': build_cmd_dict(), 'set_sms_indication': build_cmd_dict(), 'set_sms_format': build_cmd_dict(), 'set_smsc': build_cmd_dict(), # r'CRSM:\s(?P\d+),(?P\d+)(?:,"?(?P[0-9A-Fa-f]*)"?)?' 'sim_access_restricted': build_cmd_dict(re.compile(r""" \r\n \+CRSM:\s (?P\d+), (?P\d+) (?:,"? (?P[0-9A-Fa-f]*) "?)? \r\n""", re.X)), } class ATCmd(object): """I encapsulate all the data related to an AT command""" def __init__(self, cmd, name=None, eol='\r\n', nolog=tuple()): self.cmd = cmd self.name = name self.eol = eol self.nolog = nolog # Some commands like sending a sms require an special handling this # is because we have to wait till we receive a prompt like '\r\n> ' # if splitcmd is set, the second part will be send 0.1 seconds later self.splitcmd = None # command's deferred self.deferred = defer.Deferred() self.timeout = 15 # default timeout self.call_id = None # DelayedCall reference def __repr__(self): args = (self.name, self.get_cmd(), self.timeout) return "" % args def get_cmd(self): """Returns the raw AT command plus EOL""" cmd = self.cmd + self.eol return str(cmd) wader-0.5.13/core/contact.py000066400000000000000000000114431257646610200156530ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Contact related classes and utilities""" from zope.interface import implements from wader.common.encoding import to_u from wader.common.interfaces import IContact class Contact(object): """I am a Contact on Wader""" implements(IContact) def __init__(self, name, number, index=None): super(Contact, self).__init__() self.name = to_u(name) self.number = to_u(number) self.index = index def __repr__(self): if not self.index: return '' % (self.name, self.number) args = (self.name, self.number, self.index) return '' % args __str__ = __repr__ def __eq__(self, c): if self.index and c.index: return self.index == c.index return self.name == c.name and self.number == c.number def __ne__(self, c): return not self.__eq__(c) def to_csv(self): """See :meth:`wader.common.interfaces.IContact.to_csv`""" name = '"%s"' % self.name number = '"%s"' % self.number return [name, number] class ContactStore(object): """ I am a contact store A central point to perform operations on the different contact backends (see :class:`~wader.common.interfaces.IContactProvider`) """ def __init__(self): super(ContactStore, self).__init__() self._providers = [] def add_provider(self, provider): """Adds ``provider`` to the list of registered providers""" self._providers.append(provider) def remove_provider(self, provider): """Removes ``provider`` to the list of registered providers""" self._providers.remove(provider) def close(self): """Frees resources""" while self._providers: provider = self._providers.pop() provider.close() def _call_method(self, name, *args): """ Executes method ``name`` using ``args`` in all the registered providers """ ret = [] for prov in self._providers: result = getattr(prov, name)(*args) if isinstance(result, list): ret.extend(getattr(prov, name)(*args)) else: ret.append(result) return ret def add_contact(self, data): """:meth:`~wader.common.interfaces.IContactProvider.add_contact`""" return self._call_method('add_contact', data)[0] def edit_contact(self, data): """:meth:`~wader.common.interfaces.IContactProvider.edit_contact`""" return self._call_method('edit_contact', data)[0] def find_contacts_by_name(self, name): """ :meth:`~wader.common.interfaces.IContactProvider.find_contacts_by_name` """ return self._call_method('find_contacts_by_name', name) def find_contacts_by_number(self, number): """ see `IContactProvider.find_contacts_by_number` """ # first try a full match, if succeeds return result # otherwise try to remove 3 chars and if succeeds return result # i.e. match '723123112' instead of '+44723123112' (UK, ES) # otherwise try to remove 4 chars and if succeeds return result # i.e. match '821372121' instead of '+353821372121' (IE) # otherwise we failed for n in [0, 3, 4]: # finding out if a generator returns None is a bit cumbersome # so we just consume the generator and create a list match = list(self._find_contacts_by_number(number[n:])) if match: return match return [] def _find_contacts_by_number(self, number): return self._call_method('find_contacts_by_number', number) def list_contacts(self): """:meth:`~wader.common.interfaces.IContactProvider.list_contacts`""" return self._call_method('list_contacts') def remove_contact(self, contact): """:meth:`~wader.common.interfaces.IContactProvider.remove_contact`""" self._call_method('remove_contact', contact) wader-0.5.13/core/daemon.py000066400000000000000000000172641257646610200154720ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Daemons for Wader""" from gobject import timeout_add_seconds, source_remove from time import time from twisted.python import log import wader.common.aterrors as E from wader.common.consts import (MM_MODEM_STATE_UNKNOWN, MM_MODEM_STATE_CONNECTED) import wader.common.signals as S SIG_SMS_NOTIFY_ONLINE_FREQ = 5 SIG_SMS_NOTIFY_ONLINE_POLL = 6 # interval = FREQ * POLL SIG_REG_INFO_FREQ = 120 SIG_REG_INFO_POLL = 5 SIG_RSSI_FREQ = 15 class WaderDaemon(object): """ I represent a Daemon in Wader A Daemon is an entity that performs a repetitive action, like polling signal quality from the data card. A Daemon will emit DBus signals as if the device itself had emitted them. """ def __init__(self, frequency, device): super(WaderDaemon, self).__init__() self.frequency = frequency self.device = device self.task_id = None def __repr__(self): return self.__class__.__name__ def start(self): """Starts the Daemon""" log.msg("daemon %s started..." % self) if self.task_id is None: args = (self, 'function', self.frequency) log.msg("executing %s.%s every %d seconds" % args) # call the first directly self.function() # setup the poll self.task_id = timeout_add_seconds(self.frequency, self.function) def stop(self): """Stops the Daemon""" if self.task_id is not None: source_remove(self.task_id) self.task_id = None log.msg("daemon %s stopped..." % self) def function(self): """ Function that will be called periodically It *always* needs to return True, otherwise it will be executed just once """ raise NotImplementedError() class SignalQualityDaemon(WaderDaemon): """I emit SIG_RSSI UnsolicitedNotifications""" def function(self): """Executes `get_signal_quality` periodically""" d = self.device.sconn.get_signal_quality() d.addCallback(self.device.sconn.emit_rssi) return True class NetworkRegistrationDaemon(WaderDaemon): """I monitor several network registration parameters""" def function(self): # when our next invocation will occur self.expiry = time() + self.frequency def is_registered_cb((status, number, name)): return status in [1, 5] and number and name def is_registered_eb(failure): failure.trap(E.SerialSendFailed) # Fake registration to stop polling return 1 def schedule_if_necessary(registered): def poll(): d = self.device.sconn.get_netreg_info() d.addCallbacks(is_registered_cb, is_registered_eb) d.addCallback(schedule_if_necessary) return False # once only if not registered and \ time() < (self.expiry - (SIG_REG_INFO_POLL * 2)): timeout_add_seconds(SIG_REG_INFO_POLL, poll) d = self.device.sconn.get_netreg_info() d.addCallbacks(is_registered_cb, is_registered_eb) d.addCallback(schedule_if_necessary) return True class SmsNotifyOnlineDaemon(WaderDaemon): """I monitor SMS appearing without notification""" def __init__(self, frequency, device): super(SmsNotifyOnlineDaemon, self).__init__(frequency, device) self.last_status = MM_MODEM_STATE_UNKNOWN self.last_poll = 1 def _set_smslist(self, l): self.smslist = l def _cmp_smslist(self, polled): newsms = [sms for sms in polled if not sms in self.smslist] if len(newsms): for sms in newsms: index = sms.to_dict()['index'] log.msg("SmsNotifyOnlineDaemon new SMS appeared %d" % index) self.device.sconn.mal.on_sms_notification(index) self.smslist = polled def function(self): if self.device.status is MM_MODEM_STATE_CONNECTED: if self.device.status != self.last_status: # we just got connected self.last_poll = 0 d = self.device.sconn.mal.list_sms_raw() d.addCallback(self._set_smslist) else: self.last_poll += 1 if (self.last_poll % SIG_SMS_NOTIFY_ONLINE_POLL) == 0: d = self.device.sconn.mal.list_sms_raw() d.addCallback(self._cmp_smslist) self.last_status = self.device.status return True class WaderDaemonCollection(object): """ I am a collection of Daemons I provide some methods to manage the collection. """ def __init__(self): self.daemons = {} self.running = False def append_daemon(self, name, daemon): """Adds ``daemon`` to the collection identified by ``name``""" self.daemons[name] = daemon def has_daemon(self, name): """Returns True if daemon ``name`` exists""" return name in self.daemons def remove_daemon(self, name): """Removes daemon with ``name``""" del self.daemons[name] def start_daemons(self, arg=None): """Starts all daemons""" for daemon in self.daemons.values(): daemon.start() self.running = True def stop_daemon(self, name): """Stops daemon identified by ``name``""" try: self.daemons[name].stop() except KeyError: raise def stop_daemons(self): """Stops all daemons""" for daemon in self.daemons.values(): daemon.stop() self.running = False def build_daemon_collection(device): """Returns a :class:`WaderServiceCollection` customized for ``device``""" collection = WaderDaemonCollection() # check capabilities if not device.ports.has_two() or \ S.SIG_RSSI not in device.custom.device_capabilities: # The device doesn't send unsolicited notifications about RSSI changes, # or it has only one port which means it will never be able to send us # unsolicited notifications, so we'll have to fake them. interval = SIG_RSSI_FREQ else: # Ensure we at least update every two minutes interval = 120 daemon = SignalQualityDaemon(interval, device) collection.append_daemon(S.SIG_RSSI, daemon) if S.SIG_SMS_NOTIFY_ONLINE not in device.custom.device_capabilities: # device doesn't send unsolicited notifications whilst # connected, so we'll have to poll daemon = SmsNotifyOnlineDaemon(SIG_SMS_NOTIFY_ONLINE_FREQ, device) collection.append_daemon(S.SIG_SMS_NOTIFY_ONLINE, daemon) # daemons to be used regardless of ports or capabilities daemon = NetworkRegistrationDaemon(SIG_REG_INFO_FREQ, device) collection.append_daemon(S.SIG_REG_INFO, daemon) return collection wader-0.5.13/core/dialer.py000066400000000000000000000353411257646610200154630ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Dialer module abstracts the differences between dialers on different OSes""" import dbus from dbus.service import Object, BusName, method, signal from zope.interface import implements from twisted.python import log from twisted.internet import reactor, task from twisted.internet import defer from wader.common._dbus import DBusExporterHelper from wader.common.aterrors import CallIndexError import wader.common.consts as consts from wader.common.interfaces import IDialer from wader.common.utils import convert_int_to_ip CONFIG_DELAY = RECONNECTION_DELAY = 3 SECRETS_TIMEOUT = 3 class DialerConf(object): """I contain all the necessary information to connect to Internet""" uuid = "" apn = None context = None username = None password = None pin = None connection = None band = None network_type = None autoconnect = False staticdns = False dns1 = None dns2 = None refuse_pap = True refuse_chap = True def __init__(self): super(DialerConf, self).__init__() self.opath = None def __repr__(self): msg = '' args = (self.apn, self.username, self.password) return msg % args def __str__(self): return self.__repr__() @staticmethod def get_profile_secrets(profile): resp = profile.GetSecrets('gsm', ['password'], False, timeout=SECRETS_TIMEOUT) if not resp: # if we don't get secrets without asking, lets try asking resp = profile.GetSecrets('gsm', ['password'], True, timeout=SECRETS_TIMEOUT) return resp['gsm']['passwd'] @classmethod def from_dict(cls, settings): """Returns a new `:class:DialerConf` out of ``settings``""" ret = cls() # connection ret.uuid = settings['connection']['uuid'] ret.autoconnect = settings['connection'].get('autoconnect', False) # gsm ret.apn = settings['gsm']['apn'] ret.username = settings['gsm'].get('username', '') ret.password = settings['gsm'].get('password') ret.band = settings['gsm'].get('band') ret.network_type = settings['gsm'].get('network-type') # ipv4 might not be present if 'ipv4' in settings: ret.staticdns = settings['ipv4'].get('ignore-auto-dns', False) if settings['ipv4'].get('dns'): dns1 = settings['ipv4']['dns'][0] ret.dns1 = convert_int_to_ip(dns1) if len(settings['ipv4']['dns']) > 1: dns2 = settings['ipv4']['dns'][1] ret.dns2 = convert_int_to_ip(dns2) # ppp might not be present if 'ppp' in settings: # get authentication options ret.refuse_pap = settings['ppp'].get('refuse-pap', True) ret.refuse_chap = settings['ppp'].get('refuse-chap', True) return ret @classmethod def from_dbus_path(cls, opath): """Returns a new `:class:DialerConf` out of ``opath``""" profile = dbus.SystemBus().get_object(consts.WADER_PROFILES_SERVICE, opath) ret = DialerConf.from_dict(profile.GetSettings()) ret.opath = opath # get the secrets try: ret.password = DialerConf.get_profile_secrets(profile) except Exception, e: log.err("Error fetching profile password, " "setting password to ''. Reason: %s" % e) ret.password = '' return ret class Dialer(Object): """ Base dialer class Override me for new OSes """ implements(IDialer) config = None protocol = None def __init__(self, device, opath, ctrl=None): self.bus = dbus.SystemBus() name = BusName(consts.WADER_DIALUP_SERVICE, bus=self.bus) super(Dialer, self).__init__(bus_name=name, object_path=opath) self.device = device self.opath = opath self.ctrl = ctrl def close(self, path=None): # remove from DBus bus try: self.remove_from_connection() except LookupError: # it's safe to ignore this exception pass return path def configure(self, config): """ Configures ``self.device`` with ``config`` This method should perform any necessary actions to connect to Internet like generating configuration files, modifying any necessary files, etc. :param config: `DialerConf` instance """ def connect(self): """Connects to Internet""" def stop(self): """Stops a hung connection attempt""" def disconnect(self): """Disconnects from Internet""" @signal(dbus_interface=consts.WADER_DIALUP_INTFACE, signature='') def Connected(self): log.msg("emitting Connected signal") @signal(dbus_interface=consts.WADER_DIALUP_INTFACE, signature='') def Disconnected(self): log.msg("emitting Disconnected signal") @signal(dbus_interface=consts.WADER_DIALUP_INTFACE, signature='as') def InvalidDNS(self, dns): log.msg("emitting InvalidDNS(%s)" % dns) class DialerManager(Object, DBusExporterHelper): """ I am responsible of all dial up operations I provide a uniform API to make data calls using different dialers on heterogeneous operating systems. """ def __init__(self, ctrl): self.bus = dbus.SystemBus() name = BusName(consts.WADER_DIALUP_SERVICE, bus=self.bus) super(DialerManager, self).__init__(bus_name=name, object_path=consts.WADER_DIALUP_OBJECT) self._client_count = -1 # dict with the stablished connections, key is the object path of # the connection and the value is the dialer being used. self.connections = {} # dict with the ongoing connection attempts, key is the device # path and the value is the used dialer. The rationale of using # the device path and not the connection opath is that the latter # is returned when the connection is stablished, while the former # is available from the first moment. It has the downer of only # being able to stop one connection attempt per device. self.connection_attempts = {} # dict with the cached connections, key is the device path and the # value is the used configuration. This is used to save the state # of previous connections interrupted by a MMS connection. self.connection_state = {} self.ctrl = ctrl self._connect_to_signals() def _device_removed_cb(self, opath): """Executed when a device goes away""" if opath in self.connections: log.msg("Device %s removed! deleting dialer instance" % opath) try: self.deactivate_connection(opath) except KeyError: pass def _connect_to_signals(self): self.bus.add_signal_receiver(self._device_removed_cb, "DeviceRemoved", consts.WADER_INTFACE) def get_dialer(self, dev_opath, opath, plain=False): """ Returns an instance of the dialer that will be used to connect :param dev_opath: DBus object path of the device to use :param opath: DBus object path of the dialer """ from core.backends import get_backend, plain_backend device = self.ctrl.hm.clients[dev_opath] if plain: dialer_klass = plain_backend.get_dialer_klass(device) else: dialer_klass = get_backend().get_dialer_klass(device) return dialer_klass(device, opath, ctrl=self.ctrl) def get_next_opath(self): """Returns the next free object path""" self._client_count += 1 return consts.WADER_DIALUP_BASE % self._client_count def activate_connection(self, profile_opath, device_opath): """ Start a connection with device ``device_opath`` using ``profile_opath`` """ conf = DialerConf.from_dbus_path(profile_opath) # build dialer dialer = self.get_dialer(device_opath, self.get_next_opath()) return self.do_activate_connection(conf, dialer) def do_activate_connection(self, conf, dialer): device_opath = dialer.device.opath self.connection_attempts[device_opath] = dialer, conf device = self.ctrl.hm.clients[device_opath] def get_conn_id(ign): conn_id = device.sconn.state_dict.get('conn_id') if conn_id is None: raise CallIndexError("conn_id is None") conf.context = conn_id return conn_id def connect_cb(conn_opath): # transfer the dialer from connection_attempts to connections dict self.connections[conn_opath] = dialer, conf if device_opath in self.connection_attempts: self.connection_attempts.pop(device_opath) # announce that a new connection is active self.ConnectionChanged(conn_opath, True) return conn_opath d = dialer.configure(conf) d.addCallback(get_conn_id) d.addCallback(lambda ign: dialer.connect()) d.addCallback(connect_cb) return d def activate_mms_connection(self, settings, device_opath): """ Starts a MMS connection with device ``device_opath`` using ``settings`` """ if device_opath in self.connection_state: # this should never happen log.err("activate_mms_connection: internal error, " "device_opath is already stored") # XXX: What exception should be used here? return defer.fail() # handle the case where a connection is already stablished for conn_opath, (dialer, conf) in self.connections.items(): if dialer.device.opath == device_opath: self.connection_state[device_opath] = dialer, conf d = dialer.disconnect() d.addCallback(dialer.close) break else: # handle the case where a connection attempt is going on if device_opath in self.connection_attempts: dialer, conf = self.connection_attempts[device_opath] self.connection_state[device_opath] = dialer, conf d = dialer.disconnect() d.addCallback(dialer.close) else: # if there was no connection/conn attempt, there's nothing to # handle d = defer.succeed(True) def prepare_connection_and_activate(_): conf = DialerConf.from_dict(settings) # we want the plain dialer, pass True dialer = self.get_dialer(device_opath, self.get_next_opath(), True) return task.deferLater(reactor, RECONNECTION_DELAY, self.do_activate_connection, conf, dialer) d.addCallback(prepare_connection_and_activate) return d def deactivate_connection(self, conn_opath): """Stops connection of device ``device_opath``""" if conn_opath not in self.connections: raise KeyError("Dialup %s not handled" % conn_opath) dialer, _ = self.connections.pop(conn_opath) def on_disconnect(opath): self.ConnectionChanged(conn_opath, False) return dialer.close(opath) d = dialer.disconnect() d.addCallback(on_disconnect) device_opath = dialer.device.opath if device_opath not in self.connection_state: return d # there was a connection going on before, restore it dialer, conf = self.connection_state.pop(device_opath) return task.deferLater(reactor, RECONNECTION_DELAY, self.do_activate_connection, conf, dialer) def stop_connection(self, device_opath): """Stops connection attempt of device ``device_opath``""" dialer, _ = self.connection_attempts.pop(device_opath) d = dialer.stop() d.addCallback(dialer.close) return d @method(consts.WADER_DIALUP_INTFACE, in_signature='oo', out_signature='o', async_callbacks=('async_cb', 'async_eb')) def ActivateConnection(self, profile_path, device_opath, async_cb, async_eb): """See :meth:`DialerManager.activate_connection`""" d = self.activate_connection(profile_path, device_opath) return self.add_callbacks(d, async_cb, async_eb) @method(consts.WADER_DIALUP_INTFACE, in_signature='a{sv}o', out_signature='o', async_callbacks=('async_cb', 'async_eb')) def ActivateMmsConnection(self, settings, device_opath, async_cb, async_eb): """See :meth:`DialerManager.activate_mms_connection`""" d = self.activate_mms_connection(settings, device_opath) return self.add_callbacks(d, async_cb, async_eb) @method(consts.WADER_DIALUP_INTFACE, in_signature='o', out_signature='', async_callbacks=('async_cb', 'async_eb')) def DeactivateConnection(self, device_opath, async_cb, async_eb): """See :meth:`DialerManager.deactivate_connection`""" d = self.deactivate_connection(device_opath) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(consts.WADER_DIALUP_INTFACE, in_signature='o', out_signature='', async_callbacks=('async_cb', 'async_eb')) def StopConnection(self, device_opath, async_cb, async_eb): """See :meth:`DialerManager.stop_connection`""" d = self.stop_connection(device_opath) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @signal(consts.WADER_DIALUP_INTFACE, signature='ob') def ConnectionChanged(self, conn_opath, active): log.msg("ConnectionChanged(%s, %s)" % (conn_opath, active)) wader-0.5.13/core/exported.py000066400000000000000000001054431257646610200160560ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008 Warp Networks S.L. # Author: Pablo Martí # # 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. """ I export :class:`~wader.common.middleware.WCDMAWrapper` methods over DBus """ import dbus from dbus.service import Object, BusName, method, signal from twisted.internet import defer from twisted.python import log from wader.common.consts import (SMS_INTFACE, CTS_INTFACE, NET_INTFACE, CRD_INTFACE, MDM_INTFACE, WADER_SERVICE, HSO_INTFACE, SPL_INTFACE, USD_INTFACE, MMS_INTFACE) from wader.common.sms import Message from core.mms import mms_to_dbus_data, dbus_data_to_mms from core.contact import Contact from wader.common._dbus import DBusExporterHelper from wader.common.utils import (convert_ip_to_int, convert_network_mode_to_access_technology) # welcome to the multiple inheritance madness! # python-dbus currently lacks an "export_as" keyword for use cases like # us. Where we have a main object with dozens of methods that we want to # export over several interfaces under repeated names, such as: # org.freedesktop.ModemManager.Contacts.List # org.freedesktop.ModemManager.SMS.List # currently python-dbus requires you to create a new class and it will find # the appropiated implementation through the MRO. But this leads to MH madness # What we can do thou is rely on composition instead of MH for this one def to_a(_list, signature='u'): """ Returns a :class:`dbus.Array` out of `_list` :param signature: The dbus signature of the array """ return dbus.Array(sorted(_list), signature=signature) class ModemExporter(Object, DBusExporterHelper): """I export the org.freedesktop.ModemManager.Modem interface""" def __init__(self, device): name = BusName(WADER_SERVICE, bus=dbus.SystemBus()) super(ModemExporter, self).__init__(bus_name=name, object_path=device.opath) self.device = device self.sconn = device.sconn @method(MDM_INTFACE, in_signature='s', out_signature='', async_callbacks=('async_cb', 'async_eb')) def Connect(self, number, async_cb, async_eb): """ Dials in the given number :param number: number to dial """ # XXX: Use the passed number instead :/ num = "%s***%d#" % (str(number[:-1]), self.sconn.state_dict['conn_id']) d = self.sconn.connect_to_internet(dict(number=num)) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(MDM_INTFACE, in_signature='', out_signature='', async_callbacks=('async_cb', 'async_eb')) def Disconnect(self, async_cb, async_eb): """Disconnects modem""" d = self.sconn.disconnect_from_internet() return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(MDM_INTFACE, in_signature='b', out_signature='', async_callbacks=('async_cb', 'async_eb')) def Enable(self, enable, async_cb, async_eb): """ Performs some initial setup in the device :param enable: whether device should be enabled or disabled :type enable: bool """ d = self.sconn.enable_device(enable) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(dbus.PROPERTIES_IFACE, in_signature='ss', out_signature='v') def Get(self, interface, _property): """See org.freedesktop.DBus.Properties documentation""" try: return self.device.get_property(interface, _property) except KeyError: args = (interface, _property) raise ValueError("Unknown interface %s or property %s" % args) @method(dbus.PROPERTIES_IFACE, in_signature='s', out_signature='a{sv}') def GetAll(self, interface_name): """See org.freedesktop.DBus.Properties documentation""" if interface_name in self.device.props: return self.device.props[interface_name] @method(MDM_INTFACE, in_signature='', out_signature='(sss)', async_callbacks=('async_cb', 'async_eb')) def GetInfo(self, async_cb, async_eb): """ Returns the manufacturer, modem model and firmware version :rtype: tuple """ d = self.sconn.get_hardware_info() return self.add_callbacks(d, async_cb, async_eb) @method(MDM_INTFACE, in_signature='', out_signature='(uu)', async_callbacks=('async_cb', 'async_eb')) def GetStats(self, async_cb, async_eb): """ Returns the current rx_bytes, tx_bytes of the network interface :rtype: tuple """ def sanitise(response): if response is None: return (0, 0) else: return response[:2] d = defer.succeed(self.sconn.get_stats()) d.addCallback(sanitise) return self.add_callbacks(d, async_cb, async_eb) @method(MDM_INTFACE, in_signature='', out_signature='(uuuu)', async_callbacks=('async_cb', 'async_eb')) def GetIP4Config(self, async_cb, async_eb): """ Requests the IP4 configuration from the device :rtype: tuple """ d = self.sconn.get_ip4_config() d.addCallback(lambda reply: map(convert_ip_to_int, reply)) return self.add_callbacks(d, async_cb, async_eb) @method(MDM_INTFACE, in_signature='', out_signature='', async_callbacks=('async_cb', 'async_eb')) def FactoryReset(self, async_cb, async_eb): """Reset the modem to as close to factory state as possible""" d = self.sconn.reset_settings() return self.add_callbacks_and_swallow(d, async_cb, async_eb) @signal(dbus_interface=MDM_INTFACE, signature='o') def DeviceEnabled(self, opath): log.msg("emitting DeviceEnabled('%s')" % opath) @signal(dbus_interface=MDM_INTFACE, signature='(uuuu)') def DialStats(self, (rx_bytes, tx_bytes, rx_rate, tx_rate)): pass @signal(dbus_interface=MDM_INTFACE, signature='uuu') def StateChanged(self, old, new, reason): log.msg("emitting StateChanged(%d, %d, %d)" % (old, new, reason)) @signal(dbus_interface=dbus.PROPERTIES_IFACE, signature='sa{sv}') def MmPropertiesChanged(self, iface, properties): log.msg("emitting MmPropertiesChanged: %s %s" % (iface, properties)) class SimpleExporter(ModemExporter): """I export the org.freedesktop.ModemManager.Modem.Simple interface""" @method(SPL_INTFACE, in_signature='a{sv}', out_signature='', async_callbacks=('async_cb', 'async_eb')) def Connect(self, settings, async_cb, async_eb): """ Connects with the given settings :type settings: dict :param settings: documented in ModemManager spec """ d = self.sconn.connect_simple(settings) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(SPL_INTFACE, in_signature='', out_signature='a{sv}', async_callbacks=('async_cb', 'async_eb')) def GetStatus(self, async_cb, async_eb): """ Get the modem status :rtype: dict """ def get_simple_status_cb(status): # by default it is converted to Int32 for name in ['signal_quality', 'band', 'network_mode']: if name in status: status[name] = dbus.UInt32(status[name]) return status d = self.sconn.get_simple_status() d.addCallback(get_simple_status_cb) return self.add_callbacks(d, async_cb, async_eb) class CardExporter(SimpleExporter): """I export the org.freedesktop.ModemManager.Modem.Gsm.Card methods""" @method(CRD_INTFACE, in_signature='ss', out_signature='', async_callbacks=('async_cb', 'async_eb')) def ChangePin(self, oldpin, newpin, async_cb, async_eb): """ Changes PIN from ``oldpin`` to ``newpin`` :param oldpin: The old PIN :param newpin: The new PIN """ d = self.sconn.change_pin(oldpin, newpin) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(CRD_INTFACE, in_signature='', out_signature='s', async_callbacks=('async_cb', 'async_eb')) def Check(self, async_cb, async_eb): """ Returns the SIM authentication state :raise ``SimPinRequired``: If PIN is required :raise ``SimPukRequired``: If PUK is required :raise ``SimPuk2Required``: If PUK2 is required """ d = self.sconn.check_pin() return self.add_callbacks(d, async_cb, async_eb) @method(CRD_INTFACE, in_signature='b', out_signature='', async_callbacks=('async_cb', 'async_eb')) def EnableEcho(self, enable, async_cb, async_eb): """ Enables or disables echo Enabling echo will leave your connection unusable as this application assumes that it will be disabled :param enable: Whether echo should be disabled or not """ if enable: d = self.sconn.enable_echo() else: d = self.sconn.disable_echo() return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(CRD_INTFACE, in_signature='sb', out_signature='', async_callbacks=('async_cb', 'async_eb')) def EnablePin(self, pin, enable, async_cb, async_eb): """ Enables or disables PIN authentication :param pin: The PIN to use :param enable: Whether PIN auth should be enabled or disabled """ d = self.sconn.enable_pin(pin, enable) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(CRD_INTFACE, in_signature='', out_signature='s', async_callbacks=('async_cb', 'async_eb')) def GetCharset(self, async_cb, async_eb): """Returns active charset""" d = self.sconn.get_charset() return self.add_callbacks(d, async_cb, async_eb) @method(CRD_INTFACE, in_signature='', out_signature='as', async_callbacks=('async_cb', 'async_eb')) def GetCharsets(self, async_cb, async_eb): """Returns the available charsets in SIM""" d = self.sconn.get_charsets() return self.add_callbacks(d, async_cb, async_eb) @method(CRD_INTFACE, in_signature='', out_signature='s', async_callbacks=('async_cb', 'async_eb')) def GetImei(self, async_cb, async_eb): """Returns the IMEI""" d = self.sconn.get_imei() return self.add_callbacks(d, async_cb, async_eb) @method(CRD_INTFACE, in_signature='', out_signature='s', async_callbacks=('async_cb', 'async_eb')) def GetImsi(self, async_cb, async_eb): """Returns the IMSI""" d = self.sconn.get_imsi() return self.add_callbacks(d, async_cb, async_eb) @method(CRD_INTFACE, in_signature='', out_signature='s', async_callbacks=('async_cb', 'async_eb')) def GetOperatorId(self, async_cb, async_eb): """ Returns the ID of the network operator that issued the SIM card, formatted as a 5 or 6-digit MCC/MNC code (ex "310410"). """ d = self.sconn.get_operator_id() return self.add_callbacks(d, async_cb, async_eb) @method(CRD_INTFACE, in_signature='', out_signature='s', async_callbacks=('async_cb', 'async_eb')) def GetSpn(self, async_cb, async_eb): """Returns the SPN (Service Provider Name).""" d = self.sconn.get_spn() return self.add_callbacks(d, async_cb, async_eb) @method(CRD_INTFACE, in_signature='s', out_signature='s', async_callbacks=('async_cb', 'async_eb')) def SendATString(self, at_str, async_cb, async_eb): """ Sends an arbitrary AT command :param at_str: The AT command to be sent """ d = self.sconn.send_at(at_str) return self.add_callbacks(d, async_cb, async_eb) @method(CRD_INTFACE, in_signature='s', out_signature='', async_callbacks=('async_cb', 'async_eb')) def SendPin(self, pin, async_cb, async_eb): """ Sends ``pin`` to authenticate with SIM :param pin: The PIN to authenticate with """ d = self.sconn.send_pin(pin) # check_initted_device will check if a Enable call was # interrupted because of PINNeededError and will continue # if auth is successful d.addCallback(self.sconn._check_initted_device) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(CRD_INTFACE, in_signature='ss', out_signature='', async_callbacks=('async_cb', 'async_eb')) def SendPuk(self, puk, pin, async_cb, async_eb): """ Sends ``puk`` and ``pin`` to authenticate with SIM :param puk: The PUK to authenticate with :param pin: The PIN to authenticate with """ d = self.sconn.send_puk(puk, pin) # check_initted_device will check if a Enable call was # interrupted because of PUKNeededError and will continue # if auth is successful d.addCallback(self.sconn._check_initted_device) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(CRD_INTFACE, in_signature='s', out_signature='', async_callbacks=('async_cb', 'async_eb')) def SetCharset(self, charset, async_cb, async_eb): """ Sets the SIM charset to ``charset`` :param charset: The character set to use """ d = self.sconn.set_charset(charset.encode('utf8')) return self.add_callbacks_and_swallow(d, async_cb, async_eb) class ContactsExporter(CardExporter): """ I export the org.freedesktop.ModemManager.Modem.Gsm.Contacts interface """ @method(CTS_INTFACE, in_signature='ss', out_signature='u', async_callbacks=('async_cb', 'async_eb')) def Add(self, name, number, async_cb, async_eb): """ Adds a contact and returns the index :param name: The contact name :param number: The contact number :rtype: int """ d = self.sconn.add_contact(Contact(name, number)) return self.add_callbacks(d, async_cb, async_eb) @method(CTS_INTFACE, in_signature='u', out_signature='', async_callbacks=('async_cb', 'async_eb')) def Delete(self, index, async_cb, async_eb): """ Deletes the contact at ``index`` :param index: The index of the contact to be deleted """ d = self.sconn.delete_contact(index) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(CTS_INTFACE, in_signature='uss', out_signature='u', async_callbacks=('async_cb', 'async_eb')) def Edit(self, index, name, number, async_cb, async_eb): """ Edits the contact at ``index`` :param name: The new name of the contact to be edited :param number: The new number of the contact to be edited :param index: The index of the contact to be edited """ d = self.sconn.add_contact(Contact(name, number, index=index)) return self.add_callbacks(d, async_cb, async_eb) @method(CTS_INTFACE, in_signature='s', out_signature='a(uss)', async_callbacks=('async_cb', 'async_eb')) def FindByName(self, pattern, async_cb, async_eb): """ Returns list of contacts whose name match ``pattern`` :param pattern: The pattern to match contacts against :rtype: list """ d = self.sconn.find_contacts(pattern) d.addCallback(lambda contacts: [(c.index, c.name, c.number) for c in contacts]) return self.add_callbacks(d, async_cb, async_eb) @method(CTS_INTFACE, in_signature='s', out_signature='a(uss)', async_callbacks=('async_cb', 'async_eb')) def FindByNumber(self, number, async_cb, async_eb): """ Returns list of contacts whose number match ``number`` :param number: The number to match contacts against :rtype: list """ d = self.sconn.list_contacts() d.addCallback(lambda contacts: [(c.index, c.name, c.number) for c in contacts if c.number.endswith(number)]) return self.add_callbacks(d, async_cb, async_eb) @method(CTS_INTFACE, in_signature='u', out_signature='(uss)', async_callbacks=('async_cb', 'async_eb')) def Get(self, index, async_cb, async_eb): """ Returns the contact at ``index`` :param index: The index of the contact to get :rtype: tuple """ d = self.sconn.get_contact(index) d.addCallback(lambda c: (c.index, c.name, c.number)) return self.add_callbacks(d, async_cb, async_eb) @method(CTS_INTFACE, in_signature='', out_signature='u', async_callbacks=('async_cb', 'async_eb')) def GetCount(self, async_cb, async_eb): """Returns the number of contacts in the SIM""" d = self.sconn.list_contacts() d.addCallback(len) return self.add_callbacks(d, async_cb, async_eb) @method(CTS_INTFACE, in_signature='', out_signature='i', async_callbacks=('async_cb', 'async_eb')) def GetPhonebookSize(self, async_cb, async_eb): """Returns the phonebook size""" d = self.sconn.get_phonebook_size() return self.add_callbacks(d, async_cb, async_eb) @method(CTS_INTFACE, in_signature='', out_signature='a(uss)', async_callbacks=('async_cb', 'async_eb')) def List(self, async_cb, async_eb): """ Returns all the contacts in the SIM :rtype: list of tuples """ d = self.sconn.list_contacts() d.addCallback(lambda contacts: [(c.index, c.name, c.number) for c in contacts]) return self.add_callbacks(d, async_cb, async_eb) class NetworkExporter(ContactsExporter): """I export the org.freedesktop.ModemManager.Modem.Gsm.Network interface""" @method(NET_INTFACE, in_signature='', out_signature='a(us)', async_callbacks=('async_cb', 'async_eb')) def GetApns(self, async_cb, async_eb): """Returns all the APNs stored in the system""" d = self.sconn.get_apns() return self.add_callbacks(d, async_cb, async_eb) @method(NET_INTFACE, in_signature='', out_signature='u', async_callbacks=('async_cb', 'async_eb')) def GetBand(self, async_cb, async_eb): """Returns the currently used band""" d = self.sconn.get_band() return self.add_callbacks(d, async_cb, async_eb) @method(NET_INTFACE, in_signature='', out_signature='u', async_callbacks=('async_cb', 'async_eb')) def GetNetworkMode(self, async_cb, async_eb): """Returns the network mode""" d = self.sconn.get_network_mode() return self.add_callbacks(d, async_cb, async_eb) @method(NET_INTFACE, in_signature='', out_signature='(uss)', async_callbacks=('async_cb', 'async_eb')) def GetRegistrationInfo(self, async_cb, async_eb): """ Returns the network registration status and operator :rtype: tuple """ d = self.sconn.get_netreg_info() return self.add_callbacks(d, async_cb, async_eb) @method(NET_INTFACE, in_signature='', out_signature='as', async_callbacks=('async_cb', 'async_eb')) def GetRoamingIDs(self, async_cb, async_eb): """Returns all the roaming IDs stored in the SIM""" d = self.sconn.get_roaming_ids() d.addCallback(lambda objs: [obj.netid for obj in objs]) return self.add_callbacks(d, async_cb, async_eb) @method(NET_INTFACE, in_signature='', out_signature='u', async_callbacks=('async_cb', 'async_eb')) def GetSignalQuality(self, async_cb, async_eb): """Returns the signal quality""" d = self.sconn.get_signal_quality() return self.add_callbacks(d, async_cb, async_eb) @method(NET_INTFACE, in_signature='', out_signature='aa{ss}', async_callbacks=('async_cb', 'async_eb')) def Scan(self, async_cb, async_eb): """Returns the basic information of the networks around""" d = self.sconn.get_network_names() def process_netnames(netobjs): response = [] for n in netobjs: # status should be an int, but it appeared in the # ModemManager spec first as a string and in order # to not break existing software (it seems that # nm-applet in OpenSuSe uses it) we decided not to # change it for now. net = {'status': str(n.stat), 'operator-long': n.long_name, 'operator-short': n.short_name, 'operator-num': n.netid} if n.rat: # some devices won't provide this info net['access-tech'] = str(n.rat) response.append(net) return response d.addCallback(process_netnames) return self.add_callbacks(d, async_cb, async_eb) @method(NET_INTFACE, in_signature='u', out_signature='', async_callbacks=('async_cb', 'async_eb')) def SetAllowedMode(self, mode, async_cb, async_eb): """ Set the access technologies a device is allowed to use when connecting :param mode: The allowed mode. Device may not support all modes. :type mode: int """ d = self.sconn.set_allowed_mode(mode) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(NET_INTFACE, in_signature='s', out_signature='', async_callbacks=('async_cb', 'async_eb')) def SetApn(self, apn, async_cb, async_eb): """ Sets the APN to ``apn`` :param apn: The APN to use """ d = self.sconn.set_apn(apn) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(NET_INTFACE, in_signature='u', out_signature='', async_callbacks=('async_cb', 'async_eb')) def SetBand(self, band, async_cb, async_eb): """ Sets the band to ``band`` :param band: The band to use :type band: int """ d = self.sconn.set_band(band) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(NET_INTFACE, in_signature='u', out_signature='', async_callbacks=('async_cb', 'async_eb')) def SetNetworkMode(self, mode, async_cb, async_eb): """ Sets the network mode to ``mode`` :param mode: The network mode to use :type mode: int """ d = self.sconn.set_network_mode(mode) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(NET_INTFACE, in_signature='b', out_signature='', async_callbacks=('async_cb', 'async_eb')) def SetRegistrationNotification(self, active, async_cb, async_eb): """ Sets the network registration notifications :param active: Enable registration notifications :type active: bool """ d = self.sconn.set_netreg_notification(int(active)) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(NET_INTFACE, in_signature='uu', out_signature='', async_callbacks=('async_cb', 'async_eb')) def SetInfoFormat(self, mode, _format, async_cb, async_eb): """ Sets the network info format :param mode: The network mode :type mode: int :param _format: The network format :type _format: int """ d = self.sconn.set_network_info_format(mode, _format) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(NET_INTFACE, in_signature='s', out_signature='', async_callbacks=('async_cb', 'async_eb')) def Register(self, netid, async_cb, async_eb): """ Registers with ``netid`` If netid is an empty string it will try to register with the home network or the first provider around whose MNC matches with one of the response of +CPOL? :param netid: The network id to register with :type netid: str """ d = self.sconn.register_with_netid(netid) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @signal(dbus_interface=NET_INTFACE, signature='uss') def RegistrationInfo(self, status, operator_code, operator_name): args = (status, operator_code, operator_name) log.msg("emitting RegistrationInfo(%d, '%s', '%s')" % args) @signal(dbus_interface=NET_INTFACE, signature='u') def NetworkMode(self, mode): log.msg("emitting NetworkMode(%d)" % mode) # we will update AccessTechnology from here tech = convert_network_mode_to_access_technology(mode) self.device.set_property(NET_INTFACE, 'AccessTechnology', tech) @signal(dbus_interface=NET_INTFACE, signature='u') def CregReceived(self, status): log.msg("emitting CregReceived(%d)" % status) @signal(dbus_interface=NET_INTFACE, signature='u') def SignalQuality(self, rssi): log.msg("emitting SignalQuality(%d)" % rssi) class MmsExporter(NetworkExporter): """I export the org.freedesktop.ModemManager.Modem.Gsm.Mms interface""" @method(MMS_INTFACE, in_signature='ua{sv}', out_signature='', async_callbacks=('async_cb', 'async_eb')) def Acknowledge(self, index, extra_info, async_cb, async_eb): """ Acknowledge reception of MMS identified by ``index`` :param index: The MMS index :param extra_info: Dict with MMSC url, port, etc. """ d = self.sconn.acknowledge_mms(index, extra_info) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(MMS_INTFACE, in_signature='', out_signature='a(ua{sv})', async_callbacks=('async_cb', 'async_eb')) def Available(self, async_cb, async_eb): """Lists all the available m-notification.ind""" d = self.sconn.list_available_mms() return self.add_callbacks(d, async_cb, async_eb) @method(MMS_INTFACE, in_signature='ua{sv}', out_signature='(a{sv}aa{sv})', async_callbacks=('async_cb', 'async_eb')) def Download(self, index, extra_info, async_cb, async_eb): """ Retrieves the MMS identified by ``index`` :param index: The MMS index :param extra_info: Dict with MMSC url, port, etc. """ d = self.sconn.download_mms(index, extra_info) d.addCallback(mms_to_dbus_data) return self.add_callbacks(d, async_cb, async_eb) @method(MMS_INTFACE, in_signature='a{sv}aa{sv}a{sv}', out_signature='s', async_callbacks=('async_cb', 'async_eb')) def Send(self, headers, data_parts, extra_info, async_cb, async_eb): """ Sends a MMS and returns the Message-Id :param headers: MMS headers :param data_parts: data parts of the MMS :param extra_info: Dict with MMSC url, port, etc. """ mms = dbus_data_to_mms(headers, data_parts) d = self.sconn.send_mms(mms, extra_info) d.addCallback(lambda mms: mms.headers['Message-Id']) return self.add_callbacks(d, async_cb, async_eb) @signal(dbus_interface=MMS_INTFACE, signature='s') def Delivered(self, message_id): log.msg('Emitting Delivered(%s)' % message_id) @signal(dbus_interface=MMS_INTFACE, signature='ua{sv}') def MMSReceived(self, index, headers): log.msg('Emitting MMSReceived(%d, %s)' % (index, headers)) class SmsExporter(MmsExporter): """I export the org.freedesktop.ModemManager.Modem.Gsm.SMS interface""" @method(SMS_INTFACE, in_signature='u', out_signature='', async_callbacks=('async_cb', 'async_eb')) def Delete(self, index, async_cb, async_eb): """ Deletes the SMS at ``index`` :param index: The SMS index """ d = self.sconn.delete_sms(index) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(SMS_INTFACE, in_signature='u', out_signature='a{sv}', async_callbacks=('async_cb', 'async_eb')) def Get(self, index, async_cb, async_eb): """ Returns the SMS stored at ``index`` :param index: The SMS index """ d = self.sconn.get_sms(index) d.addCallback(lambda sms: sms.to_dict()) return self.add_callbacks(d, async_cb, async_eb) @method(SMS_INTFACE, in_signature='', out_signature='s', async_callbacks=('async_cb', 'async_eb')) def GetSmsc(self, async_cb, async_eb): """Returns the SMSC number stored in the SIM""" d = self.sconn.get_smsc() return self.add_callbacks(d, async_cb, async_eb) @method(SMS_INTFACE, in_signature='', out_signature='u', async_callbacks=('async_cb', 'async_eb')) def GetFormat(self, async_cb, async_eb): """Returns 1 if SMS format is text and 0 if SMS format is PDU""" d = self.sconn.get_sms_format() return self.add_callbacks(d, async_cb, async_eb) @method(SMS_INTFACE, in_signature='', out_signature='aa{sv}', async_callbacks=('async_cb', 'async_eb')) def List(self, async_cb, async_eb): """Returns all the SMS stored in SIM""" d = self.sconn.list_sms() return self.add_callbacks(d, async_cb, async_eb) @method(SMS_INTFACE, in_signature='a{sv}', out_signature='au', async_callbacks=('async_cb', 'async_eb')) def Save(self, sms, async_cb, async_eb): """ Save a SMS ``sms`` and returns the index :param sms: dictionary with the settings to use :rtype: int """ d = self.sconn.save_sms(Message.from_dict(sms)) return self.add_callbacks(d, async_cb, async_eb) @method(SMS_INTFACE, in_signature='a{sv}', out_signature='au', async_callbacks=('async_cb', 'async_eb')) def Send(self, sms, async_cb, async_eb): """ Sends SMS ``sms`` :param sms: dictionary with the settings to use :rtype: list """ d = self.sconn.send_sms(Message.from_dict(sms)) return self.add_callbacks(d, async_cb, async_eb) @method(SMS_INTFACE, in_signature='u', out_signature='au', async_callbacks=('async_cb', 'async_eb')) def SendFromStorage(self, index, async_cb, async_eb): """ Sends the SMS stored at ``index`` and returns the new index :param index: The index of the stored SMS to be sent :rtype: int """ d = self.sconn.send_sms_from_storage(index) return self.add_callbacks(d, async_cb, async_eb) @method(SMS_INTFACE, in_signature='u', out_signature='', async_callbacks=('async_cb', 'async_eb')) def SetFormat(self, _format, async_cb, async_eb): """Sets the SMS format""" if _format not in [0, 1]: async_eb(ValueError("Invalid SMS format %s" % repr(_format))) d = self.sconn.set_sms_format(_format) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(SMS_INTFACE, in_signature='uuuuu', out_signature='', async_callbacks=('async_cb', 'async_eb')) def SetIndication(self, mode, mt, bm, ds, bfr, async_cb, async_eb): """Sets the SMS indication""" d = self.sconn.set_sms_indication(mode, mt, bm, ds, bfr) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(SMS_INTFACE, in_signature='s', out_signature='', async_callbacks=('async_cb', 'async_eb')) def SetSmsc(self, smsc, async_cb, async_eb): """ Sets the SMSC to ``smsc`` :param smsc: The SMSC to use """ d = self.sconn.set_smsc(smsc) return self.add_callbacks_and_swallow(d, async_cb, async_eb) @signal(dbus_interface=SMS_INTFACE, signature='ub') def SmsReceived(self, index, complete): log.msg('Emitting SmsReceived(%d, %s)' % (index, complete)) @signal(dbus_interface=SMS_INTFACE, signature='ub') def Completed(self, index, completed): log.msg('emitting Complete(%d)' % index) @signal(dbus_interface=SMS_INTFACE, signature='u') def Delivered(self, reference): log.msg('emitting Delivered(%d)' % reference) class UssdExporter(SmsExporter): """I export the org.freedesktop.ModemManager.Modem.Gsm.Ussd interface""" @method(USD_INTFACE, in_signature='', out_signature='', async_callbacks=('async_cb', 'async_eb')) def Cancel(self, async_cb, async_eb): """Cancels an ongoing USSD session""" d = self.sconn.cancel_ussd() return self.add_callbacks_and_swallow(d, async_cb, async_eb) @method(USD_INTFACE, in_signature='s', out_signature='s', async_callbacks=('async_cb', 'async_eb')) def Initiate(self, command, async_cb, async_eb): """Sends the USSD command ``command``""" self.device.set_property(USD_INTFACE, 'State', 'active') d = self.sconn.send_ussd(command) return self.add_callbacks(d, async_cb, async_eb) @method(USD_INTFACE, in_signature='s', out_signature='s', async_callbacks=('async_cb', 'async_eb')) def Respond(self, reply, async_cb, async_eb): """Sends ``reply`` to the network""" self.device.set_property(USD_INTFACE, 'State', 'active') d = self.sconn.send_ussd(reply) return self.add_callbacks(d, async_cb, async_eb) @signal(dbus_interface=USD_INTFACE, signature='s') def NotificationReceived(self, message): log.msg("emitting NotificationReceived(%s)" % message) @signal(dbus_interface=USD_INTFACE, signature='s') def RequestReceived(self, message): log.msg("emitting RequestReceived(%s)" % message) class WCDMAExporter(UssdExporter): """I export the org.freedesktop.ModemManager.Modem* interface""" def __str__(self): return self.device.__remote_name__ __repr__ = __str__ class HSOExporter(WCDMAExporter): """I export the org.freedesktop.ModemManager.Modem.Gsm.Hso interface""" @method(HSO_INTFACE, in_signature='ss', out_signature='', async_callbacks=('async_cb', 'async_eb')) def Authenticate(self, user, passwd, async_cb, async_eb): """ Authenticate using ``user`` and ``passwd`` :param user: The username to be used in authentication :param passwd: The password to be used in authentication """ d = self.sconn.hso_authenticate(user, passwd) return self.add_callbacks_and_swallow(d, async_cb, async_eb) wader-0.5.13/core/hardware/000077500000000000000000000000001257646610200154405ustar00rootroot00000000000000wader-0.5.13/core/hardware/__init__.py000066400000000000000000000016361257646610200175570ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """ The hardware module contains device family-specific code and utils """ wader-0.5.13/core/hardware/base.py000066400000000000000000000176631257646610200167410ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 classes for the hardware module""" from __future__ import with_statement from contextlib import closing import re import serial from time import sleep from twisted.internet.threads import deferToThread from twisted.python import log from core.command import get_cmd_dict_copy from wader.common.consts import MDM_INTFACE from core.middleware import WCDMAWrapper from core.plugin import PluginManager from core.statem.auth import AuthStateMachine from core.statem.simple import SimpleStateMachine from core.statem.networkreg import NetworkRegistrationStateMachine import wader.common.exceptions as ex class WCDMACustomizer(object): """ I contain all the custom classes and metadata that a WCDMA device needs :cvar wrapper_klass: Wrapper for the device :cvar exporter_klass: DBus Exporter for the device :cvar async_regexp: regexp to parse asynchronous notifications emited by the device. :cvar allowed_dict: Dictionary with the allowed modes :cvar band_dict: Dictionary with the supported bands :cvar conn_dict: Dictionary with the supported network modes :cvar cmd_dict: Dictionary with commands info :cvar device_capabilities: List with the unsolicited notifications that this device supports :cvar auth_klass: Class that will handle the authentication for this device :cvar netr_klass: Class that will handle the network registration for this device """ from core.exported import WCDMAExporter wrapper_klass = WCDMAWrapper exporter_klass = WCDMAExporter async_regexp = None allowed_dict = {} band_dict = {} conn_dict = {} cmd_dict = get_cmd_dict_copy() device_capabilities = [] signal_translations = {} auth_klass = AuthStateMachine simp_klass = SimpleStateMachine netr_klass = NetworkRegistrationStateMachine def build_band_dict(family_dict, supported_list): """Returns a new dict with just the supported bands of the family""" band_dict = {} for band in supported_list: band_dict[band] = family_dict[band] return band_dict def check_auth_state(plugin): codes = { 'READY': '', 'SIM PIN': 'sim-pin', 'SIM PIN2': 'sim-pin2', 'SIM PUK': 'sim-puk', 'SIM PUK2': 'sim-puk2', 'PH-NETSUB PIN': 'ph-netsub-pin', 'PH-NETSUB PUK': 'ph-netsub-puk', 'PH-SIM PIN': 'ph-sim-pin', 'PH-NET PUK': 'ph-net-puk', 'PH-NET PIN': 'ph-net-pin', 'PH-SP PIN': 'ph-sp-pin', 'PH-SP PUK': 'ph-sp-puk', 'PH-CORP PIN': 'ph-corp-pin', 'PH-CORP PUK': 'ph-corp-puk', 'PH-FSIM PIN': 'ph-fsim-pin', 'PH-FSIM PUK': 'ph-fsim-puk', } def do_check(port): with closing(serial.Serial(port.path, timeout=.5)) as ser: # some devices need to be enabled before pin can be checked if plugin.quirks.get('needs_enable_before_pin_check', False): ser.write('AT+CFUN=1\r\n') lines = ser.readlines() for i in range(15): # some devices/SIMs are slow to be available ser.flushOutput() ser.flushInput() ser.write('AT+CPIN?\r\n') lines = ser.readlines() for line in lines: line = line.replace('\r', '').replace('\n', '') m = re.search('\+CPIN:\s*(?P\w+[\w -]*\w+)', line) if m is not None: code = m.group('code') if code in codes: plugin.set_property(MDM_INTFACE, 'UnlockRequired', codes[code]) plugin.set_authtime(0) return plugin log.msg("check_auth_state: +CPIN? returned '%s'" % lines) sleep(1) # can do this as we are in a separate thread log.msg("check_auth_state: +CPIN? no match lines = %s" % str(lines)) return plugin port = plugin.ports.get_application_port() return deferToThread(do_check, port) def raw_identify_device(port): """Returns the model of the device present at `port`""" BAD_REPLIES = ['AT+CGMM', 'OK', ''] # as the readlines method blocks, this is executed in a parallel thread # with deferToThread with closing(serial.Serial(port, timeout=.5)) as ser: ser.write('ATZ E0 V1 X4 &C1\r\n') ser.readlines() ser.flushOutput() ser.flushInput() ser.write('AT+CGMM\r\n') lines = ser.readlines() # clean up \r\n as pairs or singles and avoid unsolicited notifications resp = [r for r in [l.strip('\r\n') for l in lines if not l.startswith(('^', '_'))] if r not in BAD_REPLIES] if resp: log.msg("AT+CGMM response: %s" % resp) return resp[0] raise ValueError("Reply from modem %s was meaningless: %s" % (port, lines)) def identify_device(plugin): """Returns a :class:`~core.plugin.DevicePlugin` out of `plugin`""" if not plugin.mapping: # only identify devices that require it return check_auth_state(plugin) def identify_device_cb(model): # plugin to return ret = None if model in plugin.mapping: ret = plugin.mapping[model]() elif plugin.__remote_name__ != model: # so we basically have a device identified by vendor & product id # but we know nothing of this model try: ret = PluginManager.get_plugin_by_remote_name(model) except ex.UnknownPluginNameError: plugin.name = model if ret is not None: # we found another plugin during the process ret.patch(plugin) return check_auth_state(ret) # return the original plugin, most of the time this should work return check_auth_state(plugin) port = plugin.ports.get_application_port() d = deferToThread(raw_identify_device, port.path) d.addCallback(identify_device_cb) return d def probe_port(port): """ Check whether ``port`` exists and works :rtype: bool """ with closing(serial.Serial(port, timeout=.01)) as ser: try: ser.write('AT+CGMR\r\n') # Huawei E620 with driver option registers three serial # ports and the middle one wont raise any exception while # opening it even thou its a dummy port. return ser.readlines() != [] except (serial.SerialException, OSError), e: log.err(e, "Error identifying device in port %s" % port) return False def probe_ports(ports): """ Obtains the data and control ports out of ``ports`` :rtype: tuple """ dport = cport = None for port in ports: if probe_port(port): if dport is None: # data port tends to the be the first one dport = port elif cport is None: # control port the next working one cport = port if dport and cport: break return dport, cport wader-0.5.13/core/hardware/ericsson.py000066400000000000000000000636221257646610200176500ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2009-2011 Vodafone España, S.A. # Author: Andrew Bird # # 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. """Common stuff for all Ericsson's cards""" import re from twisted.internet import defer, reactor from twisted.internet.task import deferLater from twisted.python import log import wader.common.aterrors as E from wader.common import consts from wader.common.encoding import (pack_ucs2_bytes, from_u, check_if_ucs2, from_ucs2) from wader.common.utils import revert_dict import wader.common.signals as S from core.command import get_cmd_dict_copy, build_cmd_dict, ATCmd from core.contact import Contact from core.hardware.base import WCDMACustomizer from core.middleware import WCDMAWrapper from core.plugin import DevicePlugin from core.sim import SIMBaseClass CREG_MAX_RETRIES = 6 CREG_RETRY_TIMEOUT = 4 HSO_MAX_RETRIES = 10 HSO_RETRY_TIMEOUT = 1 ERICSSON_BAND_DICT = { consts.MM_NETWORK_BAND_UNKNOWN: None, consts.MM_NETWORK_BAND_ANY: None, } ERICSSON_ALLOWED_DICT = { consts.MM_ALLOWED_MODE_ANY: 1, consts.MM_ALLOWED_MODE_2G_ONLY: 5, consts.MM_ALLOWED_MODE_3G_ONLY: 6, } ERICSSON_CONN_DICT = { consts.MM_NETWORK_MODE_ANY: 1, consts.MM_NETWORK_MODE_2G_ONLY: 5, consts.MM_NETWORK_MODE_3G_ONLY: 6, } ERINFO_2G_GPRS, ERINFO_2G_EGPRS = 1, 2 ERINFO_3G_UMTS, ERINFO_3G_HSDPA = 1, 2 ENAP_DISCONNECTED, ENAP_CONNECTED, ENAP_CONNECTING = 0, 1, 2 ERICSSON_CMD_DICT = get_cmd_dict_copy() # +CGDCONT: (1-10),("00490050"),,,(0-1),(0-1) ERICSSON_CMD_DICT['get_apn_range'] = build_cmd_dict(re.compile(r""" \r\n \+CGDCONT:\s*\((?P\d+)-(?P\d+)\), \((?P(?:"IP")|(?:"00490050"))\)(?P.*) """, re.VERBOSE)) ERICSSON_CMD_DICT['get_card_model'] = build_cmd_dict('\s*(?P\S*)\r\n') # +CIND: 5,5,0,0,1,0,1,0,1,1,0,0 ERICSSON_CMD_DICT['get_signal_quality'] = build_cmd_dict( '\s*\+CIND:\s+[0-9]*,(?P[0-9]*),.*') ERICSSON_CMD_DICT['get_network_info'] = build_cmd_dict(re.compile(r""" \r\n \+COPS:\s+ ( (?P\d) | \d,\d, # or followed by num,num,str,num "(?P[^"]*)", (?P\d) ) # end of group \s*\r\n """, re.VERBOSE)) # +CPBR: (1-200),80,14,20,80,128 ERICSSON_CMD_DICT['get_phonebook_size'] = build_cmd_dict(re.compile(r""" \r\n \+CPBR:\s \(\d\-(?P\d+)\) (?P,.*)? \r\n """, re.VERBOSE)) # *ENAP: 1 # *ENAP:2,"" ERICSSON_CMD_DICT['get_enap'] = build_cmd_dict('\*ENAP:\s*(?P\d+)') # *E2IPCFG: (,)(?:,(,)){0,3} # D5540 (f3607gw) # *E2IPCFG: (1,"10.49.116.194")(2,"10.49.116.195") # (3,"10.203.65.70")(3,"88.82.13.61") # *E2IPCFG: (1,"00310030002E00340039002E003100310036002E003100390034") # (2,"00310030002E00340039002E003100310036002E003100390035") # (3,"00310030002E003200300033002E00360035002E00370030") # (3,"00380038002E00380032002E00310033002E00360031") ERICSSON_CMD_DICT['get_ip4_config'] = build_cmd_dict( re.compile(r"""\*E2IPCFG:\s* (?:\((?P\d+), "(?P(?:\d+\.\d+\.\d+\.\d+)|(?:[\dA-Fa-f]+))"\)) (?:\((?P\d+), "(?P(?:\d+\.\d+\.\d+\.\d+)|(?:[\dA-Fa-f]+))"\))? (?:\((?P\d+), "(?P(?:\d+\.\d+\.\d+\.\d+)|(?:[\dA-Fa-f]+))"\))? (?:\((?P\d+), "(?P(?:\d+\.\d+\.\d+\.\d+)|(?:[\dA-Fa-f]+))"\))? """, re.VERBOSE)) # Map to SimBusy for early contact accesses E.ERROR_DICT['+CME ERROR: 264'] = E.SimBusy def ericsson_new_indication(args, device): indication = args.split(',') try: itype = int(indication[0]) value = int(indication[1]) except (ValueError, TypeError, IndexError): return None if itype is 2: # Signal quality strength = value * 20 device.sconn.updatecache(strength, 'signal') device.sconn.emit_rssi(strength) elif itype is 5: # Service indicator pass elif itype is 7: # Message Received (SMS) pass elif itype is 8: # Call in progress indicator pass elif itype is 9: # roam indicator pass elif itype is 10: # SMS memory full pass else: log.msg('Unknown indication of "+CIEV: %s"' % args) return None class EricssonSIMClass(SIMBaseClass): """Ericsson SIM Class""" def __init__(self, sconn): super(EricssonSIMClass, self).__init__(sconn) def initialize(self, set_encoding=True): self.sconn.reset_settings() self.sconn.disable_echo() # So that phonebook size is returned self.sconn.send_at('AT+CPBS="SM"') def init_callback(size): # setup SIM storage defaults d = self.sconn.send_at('AT+CPMS="SM","SM","SM"') d.addCallback(lambda _: self.sconn.send_at('AT+CMER=3,0,0,1')) d.addCallback(lambda _: size) return d d = super(EricssonSIMClass, self).initialize(set_encoding=set_encoding) d.addCallback(init_callback) return d class EricssonWrapper(WCDMAWrapper): """Wrapper for all Ericsson cards""" def __init__(self, device): super(EricssonWrapper, self).__init__(device) def add_contact(self, contact): name = from_u(contact.name) number = from_u(contact.number) if 'UCS2' in self.device.sim.charset: name = pack_ucs2_bytes(name) number = pack_ucs2_bytes(number) # common arguments for both operations (name and number) args = [name, number] if contact.index: # contact.index is set, user probably wants to overwrite an # existing contact args.append(contact.index) d = super(WCDMAWrapper, self).add_contact(*args) d.addCallback(lambda _: contact.index) return d # contact.index is not set, this means that we need to obtain the # first slot free on the phonebook and then add the contact def get_next_id_cb(index): args.append(index) d2 = super(WCDMAWrapper, self).add_contact(*args) # now we just fake add_contact's response and we return the index d2.addCallback(lambda _: index) return d2 d = self._get_next_contact_id() d.addCallback(get_next_id_cb) return d def change_pin(self, oldpin, newpin): where = "SC" if 'UCS2' in self.device.sim.charset: where = pack_ucs2_bytes("SC") oldpin = pack_ucs2_bytes(oldpin) newpin = pack_ucs2_bytes(newpin) atstr = 'AT+CPWD="%s","%s","%s"' % (where, oldpin, newpin) d = self.queue_at_cmd(ATCmd(atstr, name='change_pin')) d.addCallback(lambda result: result[0].group('resp')) return d def enable_pin(self, pin, enable): where = "SC" if 'UCS2' in self.device.sim.charset: where = pack_ucs2_bytes("SC") pin = pack_ucs2_bytes(pin) at_str = 'AT+CLCK="%s",%d,"%s"' % (where, int(enable), pin) d = self.queue_at_cmd(ATCmd(at_str, name='enable_pin')) d.addCallback(lambda result: result[0].group('resp')) return d def enable_radio(self, enable): d = self.get_radio_status() def get_radio_status_cb(status): if status in [0, 4] and enable: return self.send_at('AT+CFUN=1') elif status in [1, 5, 6] and not enable: return self.send_at('AT+CFUN=4') d.addCallback(get_radio_status_cb) return d def get_apns(self): if self.device.sim.charset != 'UCS2': return super(EricssonWrapper, self).get_apns() d = super(WCDMAWrapper, self).get_apns() d.addCallback(lambda resp: [(int(r.group('index')), from_ucs2(r.group('apn'))) for r in resp]) return d def get_band(self): return defer.succeed(consts.MM_NETWORK_BAND_ANY) def get_charset(self): d = super(EricssonWrapper, self).get_charset() d.addCallback(from_ucs2) return d def get_charsets(self): d = super(EricssonWrapper, self).get_charsets() def get_charsets_cb(charsets): ret = [] for charset in charsets: if check_if_ucs2(charset): charset = from_ucs2(charset) ret.append(charset) return ret d.addCallback(get_charsets_cb) return d def get_network_mode(self): ERICSSON_CONN_DICT_REV = revert_dict(ERICSSON_CONN_DICT) def get_network_mode_cb(mode): if mode in ERICSSON_CONN_DICT_REV: return ERICSSON_CONN_DICT_REV[mode] raise E.General("unknown network mode: %d" % mode) d = self.get_radio_status() d.addCallback(get_network_mode_cb) return d def get_netreg_status(self): deferred = defer.Deferred() self.state_dict['creg_retries'] = 0 def get_it(auxdef=None): def get_netreg_status_cb((mode, status)): self.state_dict['creg_retries'] += 1 if self.state_dict['creg_retries'] > CREG_MAX_RETRIES: return auxdef.callback((mode, status)) if status == 4: reactor.callLater(CREG_RETRY_TIMEOUT, get_it, auxdef) else: return auxdef.callback((mode, status)) def get_netreg_status_eb(failure): return auxdef.errback(failure) d = super(EricssonWrapper, self).get_netreg_status() d.addCallbacks(get_netreg_status_cb, get_netreg_status_eb) return auxdef return get_it(deferred) def get_signal_quality(self): # On Ericsson, AT+CSQ only returns valid data in GPRS mode # So we need to override and provide an alternative. +CIND # returns an indication between 0-5 so let's just multiply # that by 20 to get a RSSI between 0-100% def get_signal_quality_cb(response): try: return int(response[0].group('sig')) * 20 except IndexError: # Sometimes it won't reply to a +CIND? command # we'll assume that we don't have RSSI right now return 0 cmd = ATCmd('AT+CIND?', name='get_signal_quality') d = self.queue_at_cmd(cmd) d.addCallback(get_signal_quality_cb) return d def get_pin_status(self): def ericsson_get_pin_status(facility): """ Checks whether the pin is enabled or disabled """ cmd = ATCmd('AT+CLCK="%s",2' % facility, name='get_pin_status') return self.queue_at_cmd(cmd) def pinreq_errback(failure): failure.trap(E.SimPinRequired) return 1 def aterror_eb(failure): failure.trap(E.General) # return the failure or wont work return failure facility = 'SC' if self.device.sim.charset == 'UCS2': facility = pack_ucs2_bytes('SC') d = ericsson_get_pin_status(facility) # call the local one d.addCallback(lambda response: int(response[0].group('status'))) d.addErrback(pinreq_errback) d.addErrback(aterror_eb) return d def _regexp_to_contact(self, match): """ Returns a :class:`core.contact.Contact` out of ``match`` :type match: ``re.MatchObject`` """ name = match.group('name') number = match.group('number') if self.device.sim.charset == 'UCS2': name = from_ucs2(name) number = from_ucs2(number) index = int(match.group('id')) return Contact(name, number, index=index) def send_pin(self, pin): """ Sends ``pin`` to authenticate We need to encode the PIN in the correct way, but also wait a little, as Ericsson devices return straight away on unlock but an immediate call to +CPIN? will still show the device as locked. """ from core.startup import attach_to_serial_port d = attach_to_serial_port(self.device) def _get_charset(_): if hasattr(self.device, 'sim') and \ hasattr(self.device.sim, 'charset'): return defer.succeed(self.device.sim.charset) else: return self.get_charset() d.addCallback(_get_charset) def _send_pin(charset, pin): if 'UCS2' in charset: pin = pack_ucs2_bytes(pin) return super(EricssonWrapper, self).send_pin(pin) d.addCallback(_send_pin, pin) d.addCallback(lambda x: deferLater(reactor, 1, lambda y: y, x)) return d def set_apn(self, apn): if self.device.sim.charset != 'UCS2': # no need to encode params in UCS2 return super(EricssonWrapper, self).set_apn(apn) def process_apns(apns, the_apn): for _index, _apn in apns: if _apn == the_apn: self.state_dict['conn_id'] = _index return defer.succeed('OK') try: conn_id = max([idx for idx, _ in apns]) + 1 if conn_id < self.apn_range[0] or conn_id > self.apn_range[1]: raise ValueError except (ValueError, TypeError): conn_id = self.apn_range[0] self.state_dict['conn_id'] = conn_id args = tuple([conn_id] + map(pack_ucs2_bytes, ["IP", the_apn])) cmd = ATCmd('AT+CGDCONT=%d,"%s","%s"' % args, name='set_apn') d = self.queue_at_cmd(cmd) d.addCallback(lambda response: response[0].group('resp')) return d d = self.get_apns() d.addCallback(process_apns, apn) d.addErrback(log.err) return d def set_band(self, band): if band == consts.MM_NETWORK_BAND_ANY: return defer.succeed('OK') raise KeyError("Unsupported band %d" % band) def set_charset(self, charset): # The oddity here is that the set command needs to have its charset # value encoded in the current character set if self.device.sim.charset == 'UCS2': charset = pack_ucs2_bytes(charset) d = super(EricssonWrapper, self).set_charset(charset) return d def set_allowed_mode(self, mode): if mode not in self.custom.allowed_dict: raise KeyError("Mode %d not found" % mode) def set_allowed_mode_cb(ign=None): self.device.set_property(consts.NET_INTFACE, "AllowedMode", mode) return ign return self.send_at("AT+CFUN=%d" % self.custom.allowed_dict[mode], callback=set_allowed_mode_cb) def set_network_mode(self, mode): if mode not in self.custom.conn_dict: raise KeyError("Mode %d not found" % mode) return self.send_at("AT+CFUN=%d" % self.custom.conn_dict[mode]) def reset_settings(self): cmd = ATCmd('AT&F', name='reset_settings') return self.queue_at_cmd(cmd) def sim_access_restricted(self, command, fileid=None, p1=None, p2=None, p3=None, data=None, pathid=None): d = super(EricssonWrapper, self).sim_access_restricted( command, fileid, p1, p2, p3, data, pathid) def cb(response): sw1, sw2, data = response if self.device.sim.charset == 'UCS2' and data is not None: data = from_ucs2(data) return (sw1, sw2, data) d.addCallback(cb) return d def get_ip4_config(self): """ Returns the ip4 config on a NDIS device Wrapper around _get_ip4_config that provides some error control """ ip_method = self.device.get_property(consts.MDM_INTFACE, 'IpMethod') if ip_method != consts.MM_IP_METHOD_STATIC: msg = "Cannot get IP4 config from a non static ip method" raise E.OperationNotSupported(msg) self.state_dict['num_of_retries'] = 0 def check_if_connected(deferred): def inform_caller(): if self.device.status > consts.MM_MODEM_STATE_REGISTERED: self.device.set_status(consts.MM_MODEM_STATE_REGISTERED) deferred.errback(RuntimeError('Connection attempt failed')) def get_ip4_eb(failure): failure.trap(E.General, E.Unknown) if self.state_dict.get('should_stop'): self.state_dict.pop('should_stop') return self.state_dict['num_of_retries'] += 1 if self.state_dict['num_of_retries'] > HSO_MAX_RETRIES: inform_caller() return failure reactor.callLater(HSO_RETRY_TIMEOUT, check_if_connected, deferred) # We received an unsolicited notification that we failed if self.device.connection_attempt_failed: inform_caller() return d = self._get_ip4_config() d.addCallback(deferred.callback) d.addErrback(get_ip4_eb) return deferred auxdef = defer.Deferred() return check_if_connected(auxdef) def _get_ip4_config(self): """Returns the ip4 config on a later Ericsson NDIS device""" cmd = ATCmd('AT*E2IPCFG?', name='get_ip4_config') d = self.queue_at_cmd(cmd) def _get_ip4_config_cb(resp): if not resp: raise E.General() ip = None dns = [] l = list(resp[0].groups()) while len(l) > 1: t = l.pop(0) a = l.pop(0) if t is None or a is None: continue if not '.' in a: a = from_ucs2(a) if t == '1': ip = a elif t == '3': dns.append(a) if ip is None: raise E.General() self.device.set_status(consts.MM_MODEM_STATE_CONNECTED) # XXX: We didn't get any, but we have to return something so use # the bogus values that 'pppd' returns sometimes. if not len(dns): dns = ['10.11.12.13', '10.11.12.14'] # XXX: caller expects exactly three! while len(dns) < 3: dns.append(dns[0]) return [ip] + dns[:3] d.addCallback(_get_ip4_config_cb) return d def hso_authenticate(self, user, passwd, auth): conn_id = self.state_dict.get('conn_id') if conn_id is None: raise E.CallIndexError("conn_id is None") # Bitfield '00111': MSCHAPv2, MSCHAP, CHAP, PAP, None if auth is None: iauth = consts.MM_ALLOWED_AUTH_UNKNOWN else: iauth = auth & 0x1f # only the lowest 5 bits if iauth == consts.MM_ALLOWED_AUTH_UNKNOWN: iauth = consts.MM_ALLOWED_AUTH_PAP # the old default # convert to string try: _auth = bin(iauth)[2:].zfill(5) # Python 2.6+ except: _auth = '' for i in range(5 - 1, -1, -1): _auth += "%d" % ((iauth >> i) & 1) if self.device.sim.charset == 'UCS2': args = (conn_id, pack_ucs2_bytes(user), pack_ucs2_bytes(passwd), _auth) else: args = (conn_id, user, passwd, _auth) return self.send_at('AT*EIAAUW=%d,1,"%s","%s",%s' % args) def hso_connect(self): conn_id = self.state_dict.get('conn_id') if conn_id is None: raise E.CallIndexError("conn_id is None") if self.device.status == consts.MM_MODEM_STATE_CONNECTED: # this cannot happen raise E.Connected("we are already connected") if self.device.status == consts.MM_MODEM_STATE_CONNECTING: raise E.SimBusy("we are already connecting") # AT*E2NAP=1 used to be used to enable unsolicited interface status # reports but not all devices support it and the existing code didn't # even handle them either. self.device.connection_attempt_failed = False self.device.set_status(consts.MM_MODEM_STATE_CONNECTING) d = self.send_at('AT*ENAP=1,%d' % conn_id) ip_method = self.device.get_property(consts.MDM_INTFACE, 'IpMethod') if ip_method == consts.MM_IP_METHOD_DHCP: # XXX: it would be nice to be using ipmethod == STATIC here, but # only later devices are capable of returning the negotiated DNS # values, so we default to DHCP mode and let the interface be # configured later by some other process. The most we can do is # check if the connection is up and report it as connected (that's # early, but actually no worse than ipmethod == PPP). def get_enap(): cmd = ATCmd('AT*ENAP?', name='get_enap') d = self.queue_at_cmd(cmd) def get_enap_cb(resp): if not resp: raise E.General() status = resp[0].group('status') if status is None: raise E.General() status = int(status) if status is ENAP_DISCONNECTED: self.device.connection_attempt_failed = True elif status is ENAP_CONNECTED: self.device.set_status(consts.MM_MODEM_STATE_CONNECTED) else: # ENAP_CONNECTING and spurious values raise E.General() return status d.addCallback(get_enap_cb) return d self.state_dict['num_of_retries'] = 0 def check_if_connected(deferred): def inform_caller(): if self.device.status > consts.MM_MODEM_STATE_REGISTERED: self.device.set_status( consts.MM_MODEM_STATE_REGISTERED) deferred.errback(RuntimeError('Connection attempt failed')) def get_enap_eb(failure): failure.trap(E.General, E.OperationNotSupported) if self.state_dict.get('should_stop'): self.state_dict.pop('should_stop') return self.state_dict['num_of_retries'] += 1 if self.state_dict['num_of_retries'] > HSO_MAX_RETRIES: inform_caller() return failure reactor.callLater(HSO_RETRY_TIMEOUT, check_if_connected, deferred) # We received an unsolicited notification that we failed if self.device.connection_attempt_failed: inform_caller() return d = get_enap() d.addCallback(deferred.callback) d.addErrback(get_enap_eb) return deferred auxdef = defer.Deferred() d.addCallback(lambda x: check_if_connected(auxdef)) return d def hso_disconnect(self): self.device.set_status(consts.MM_MODEM_STATE_DISCONNECTING) def disconnect_cb(ignored): # XXX: perhaps we should check the registration status here if self.device.status > consts.MM_MODEM_STATE_REGISTERED: self.device.set_status(consts.MM_MODEM_STATE_REGISTERED) d = self.send_at('AT*ENAP=0') d.addCallback(disconnect_cb) return d class EricssonF3607gwWrapper(EricssonWrapper): """Wrapper for F3307 / F3607gw Ericsson cards""" def find_contacts(self, pattern): d = self.list_contacts() d.addCallback(lambda contacts: [c for c in contacts if c.name.lower().startswith(pattern.lower())]) return d class EricssonCustomizer(WCDMACustomizer): """Base customizer class for Ericsson cards""" # Multiline so we catch and remove the ESTKSMENU async_regexp = re.compile("\r\n(?P[*+][A-Z]{3,}):(?P.*)\r\n", re.MULTILINE) allowed_dict = ERICSSON_ALLOWED_DICT band_dict = ERICSSON_BAND_DICT cmd_dict = ERICSSON_CMD_DICT conn_dict = ERICSSON_CONN_DICT device_capabilities = [S.SIG_SMS_NOTIFY_ONLINE, S.SIG_RSSI] signal_translations = { '+CIEV': (None, ericsson_new_indication), '*ESTKDISP': (None, None), '*ESTKSMENU': (None, None), '*EMWI': (None, None), '*E2NAP': (None, None), '+PACSP0': (None, None)} wrapper_klass = EricssonWrapper class EricssonF3607gwCustomizer(EricssonCustomizer): wrapper_klass = EricssonF3607gwWrapper class EricssonDevicePlugin(DevicePlugin): """DevicePlugin for Ericsson""" sim_klass = EricssonSIMClass custom = EricssonCustomizer() def __init__(self): super(EricssonDevicePlugin, self).__init__() wader-0.5.13/core/hardware/huawei.py000066400000000000000000000573131257646610200173050ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2009 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Common stuff for all Huawei cards""" import re from twisted.python import log from twisted.internet import defer from messaging.sms import is_gsm_text from messaging.utils import encode_str, unpack_msg from core.middleware import WCDMAWrapper from core.command import get_cmd_dict_copy, build_cmd_dict, ATCmd from core.contact import Contact from wader.common import consts from wader.common.encoding import (from_u, pack_ucs2_bytes, unpack_ucs2_bytes_in_ts31101_80, unpack_ucs2_bytes_in_ts31101_81, unpack_ucs2_bytes_in_ts31101_82) from core.hardware.base import WCDMACustomizer from core.plugin import DevicePlugin from core.sim import SIMBaseClass from wader.common.utils import rssi_to_percentage import wader.common.signals as S import wader.common.aterrors as E NETINFO_REGEXP = re.compile('[^a-zA-Z0-9.\-\s]*') BADOPER_REGEXP = re.compile('FFF*') HUAWEI_ALLOWED_DICT = { consts.MM_ALLOWED_MODE_ANY: (2, 0), consts.MM_ALLOWED_MODE_2G_ONLY: (13, 1), consts.MM_ALLOWED_MODE_3G_ONLY: (14, 2), consts.MM_ALLOWED_MODE_2G_PREFERRED: (2, 1), consts.MM_ALLOWED_MODE_3G_PREFERRED: (2, 2), } HUAWEI_CONN_DICT = { consts.MM_NETWORK_MODE_ANY: (2, 0), consts.MM_NETWORK_MODE_2G_ONLY: (13, 1), consts.MM_NETWORK_MODE_3G_ONLY: (14, 2), consts.MM_NETWORK_MODE_2G_PREFERRED: (2, 1), consts.MM_NETWORK_MODE_3G_PREFERRED: (2, 2), } HUAWEI_BAND_DICT = { consts.MM_NETWORK_BAND_ANY: 0x3FFFFFFF, consts.MM_NETWORK_BAND_DCS: 0x00000080, consts.MM_NETWORK_BAND_EGSM: 0x00000100, consts.MM_NETWORK_BAND_PCS: 0x00200000, consts.MM_NETWORK_BAND_G850: 0x00080000, consts.MM_NETWORK_BAND_U2100: 0x00400000, consts.MM_NETWORK_BAND_U1900: 0x00800000, consts.MM_NETWORK_BAND_U850: 0x04000000, # XXX: check this works with bit operations and all cards before enabling # consts.MM_NETWORK_BAND_U900: 0x0002000000000000, } # Hopefully this function will be exported from python-messaging in the future def compress_7_to_8_bit(txt): txt += '\x00' msgl = int(len(txt) * 7 / 8) op = [-1] * msgl c = shift = 0 for n in range(msgl): if shift == 6: c += 1 shift = n % 7 lb = ord(txt[c]) >> shift hb = (ord(txt[c + 1]) << (7 - shift) & 255) op[n] = lb + hb c += 1 return ''.join(map(chr, op)) def huawei_new_conn_mode(args, device): """Translates `args` to Wader's internal representation""" mode_args_dict = { '3,0': consts.MM_NETWORK_MODE_GPRS, '3,1': consts.MM_NETWORK_MODE_GPRS, '3,2': consts.MM_NETWORK_MODE_GPRS, '3,3': consts.MM_NETWORK_MODE_GPRS, '5,4': consts.MM_NETWORK_MODE_UMTS, '5,5': consts.MM_NETWORK_MODE_HSDPA, '5,6': consts.MM_NETWORK_MODE_HSUPA, '5,7': consts.MM_NETWORK_MODE_HSPA, '5,9': consts.MM_NETWORK_MODE_HSPA, # doc says HSPA+ } try: return mode_args_dict[args] except KeyError: return consts.MM_NETWORK_MODE_UNKNOWN def huawei_new_rssi(args, device): try: strength = rssi_to_percentage(int(args)) except (ValueError, TypeError): return None device.sconn.updatecache(strength, 'signal') device.sconn.emit_rssi(strength) return None HUAWEI_CMD_DICT = get_cmd_dict_copy() HUAWEI_CMD_DICT['get_syscfg'] = build_cmd_dict(re.compile(r""" \r\n \^SYSCFG: (?P\d+), (?P\d+), (?P[0-9A-F]*), (?P\d), (?P\d) \r\n """, re.VERBOSE)) HUAWEI_CMD_DICT['get_radio_status'] = build_cmd_dict( end=re.compile('\r\n\+CFUN:\s?\d\r\n'), extract=re.compile('\r\n\+CFUN:\s?(?P\d)\r\n')) HUAWEI_CMD_DICT['check_pin'] = build_cmd_dict(re.compile(r""" \r\n \+CPIN:\s* (?P READY | SIM\sPIN2? | SIM\sPUK2? ) \r\n """, re.VERBOSE)) #+CPBR: (1-200),80,14,0,0,0 HUAWEI_CMD_DICT['get_phonebook_size'] = build_cmd_dict(re.compile(r""" \r\n \+CPBR:\s \(\d+-(?P\d+)\).* \r\n """, re.VERBOSE)) HUAWEI_CMD_DICT['get_contact'] = build_cmd_dict(re.compile(r""" \r\n \^CPBR:\s(?P\d+), "(?P\+?\d+)", (?P\d+), "(?P.*)", (?P\d+) \r\n """, re.VERBOSE)) HUAWEI_CMD_DICT['list_contacts'] = build_cmd_dict( end=re.compile('(\r\n)?\r\n(OK)\r\n'), extract=re.compile(r""" \r\n \^CPBR:\s(?P\d+), "(?P\+?\d+)", (?P\d+), "(?P.*)", (?P\d+) """, re.VERBOSE)) HUAWEI_CMD_DICT['list_sms'] = build_cmd_dict(re.compile(r""" \r\n \+CMGL:\s* (?P\d+), (?P\d),["\w\s]*,\d+ \r\n(?P\w+)""", re.VERBOSE)) # +CMGR: 0,"416E64726577",23^M # 0791447758100650040C914497716247010000215032511142400444F3190D^M # ^M # OK^M HUAWEI_CMD_DICT['get_sms'] = build_cmd_dict(re.compile(r""" \r\n \+CMGR:\s (?P\d),["\w\s]*, \d+\r\n (?P\w+) \r\n""", re.VERBOSE)) class HuaweiWCDMAWrapper(WCDMAWrapper): """Wrapper for all Huawei cards""" def _get_syscfg(self): def parse_syscfg(resp): ret = {} mode_a = int(resp[0].group('modea')) mode_b = int(resp[0].group('modeb')) band = int(resp[0].group('theband'), 16) # keep original values ret['roam'] = int(resp[0].group('roam')) ret['srv'] = int(resp[0].group('srv')) ret['modea'] = mode_a ret['modeb'] = mode_b ret['theband'] = band ret['band'] = 0 # populated later on # network mode if mode_a == 2 and mode_b == 1: ret['mode'] = consts.MM_NETWORK_MODE_2G_PREFERRED elif mode_a == 2 and mode_b == 2: ret['mode'] = consts.MM_NETWORK_MODE_3G_PREFERRED elif mode_a == 13: ret['mode'] = consts.MM_NETWORK_MODE_2G_ONLY elif mode_a == 14: ret['mode'] = consts.MM_NETWORK_MODE_3G_ONLY elif mode_a == 2 and mode_b == 0: ret['mode'] = consts.MM_NETWORK_MODE_ANY # band if band == 0x3FFFFFFF: ret['band'] = consts.MM_NETWORK_BAND_ANY # this band is not combinable by firmware spec return ret for key, value in self.custom.band_dict.items(): if key == consts.MM_NETWORK_BAND_ANY: # ANY can't be combined continue if value & band: ret['band'] |= key return ret d = self.send_at('AT^SYSCFG?', name='get_syscfg', callback=parse_syscfg) return d def add_contact(self, contact): """ Adds ``contact`` to the SIM and returns the index where was stored :rtype: int """ name = from_u(contact.name) # common arguments for both operations (name and number) args = [name, from_u(contact.number)] if contact.index: # contact.index is set, user probably wants to overwrite an # existing contact args.append(contact.index) d = self._add_contact(*args) d.addCallback(lambda _: contact.index) return d # contact.index is not set, this means that we need to obtain the # first slot free on the phonebook and then add the contact def get_next_id_cb(index): args.append(index) d2 = self._add_contact(*args) # now we just fake add_contact's response and we return the index d2.addCallback(lambda _: index) return d2 d = self._get_next_contact_id() d.addCallback(get_next_id_cb) return d def _add_contact(self, name, number, index): """ Adds a contact to the SIM card """ raw = 0 try: # are all ascii chars name.encode('ascii') except: # write in TS31.101 type 80 raw format name = '80%sFF' % pack_ucs2_bytes(name) raw = 1 category = 145 if number.startswith('+') else 129 args = (index, number, category, name, raw) cmd = ATCmd('AT^CPBW=%d,"%s",%d,"%s",%d' % args, name='add_contact') return self.queue_at_cmd(cmd) def find_contacts(self, pattern): d = self.list_contacts() d.addCallback(lambda contacts: [c for c in contacts if c.name.lower().startswith(pattern.lower())]) return d def get_band(self): """Returns the current used band""" d = self._get_syscfg() d.addCallback(lambda ret: ret['band']) d.addErrback(log.err) return d def list_contacts(self): """ Returns all the contacts in the SIM :rtype: list """ exceptions = (E.NotFound, E.InvalidIndex, E.General) def not_found_eb(failure): failure.trap(*exceptions) return [] def get_them(ignored=None): cmd = ATCmd('AT^CPBR=1,%d' % self.device.sim.size, name='list_contacts', nolog=exceptions) d = self.queue_at_cmd(cmd) d.addCallback( lambda matches: map(self._regexp_to_contact, matches)) d.addErrback(not_found_eb) return d if self.device.sim.size: return get_them() else: d = self._get_next_contact_id() d.addCallback(get_them) return d def _regexp_to_contact(self, match): """ I process a contact match and return a `Contact` object out of it """ if int(match.group('raw')) == 0: # some buggy firmware appends this name = match.group('name').rstrip('\xff') else: encoding = match.group('name')[:2] hexbytes = match.group('name')[2:] if encoding != '82': # E220 pads '534E4E3AFFFFFFFFFFFFFFFFFF' # K2540 pads '534E4E3AFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' # 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' significant = hexbytes.find('FF') if significant != -1: hexbytes = hexbytes[:significant + 2] if encoding == '80': # example '80058300440586FF' name = unpack_ucs2_bytes_in_ts31101_80(hexbytes) elif encoding == '81': # example '810602A46563746F72FF' name = unpack_ucs2_bytes_in_ts31101_81(hexbytes) elif encoding == '82': # example '820505302D82D32D31' name = unpack_ucs2_bytes_in_ts31101_82(hexbytes) else: name = "Unsupported encoding" number = match.group('number') index = int(match.group('id')) return Contact(name, number, index=index) def get_contact(self, index): cmd = ATCmd('AT^CPBR=%d' % index, name='get_contact') d = self.queue_at_cmd(cmd) d.addCallback(lambda match: self._regexp_to_contact(match[0])) return d def get_network_info(self, _type=None): # Some E220 firmwares will append an extra char to AT+COPS? # (off-by-one) or reply as 'FFFFFFFFFF+'. The following callback # will handle this errors. def process_netinfo_cb(info): operator, tech = info m = BADOPER_REGEXP.match(operator) # sometimes the operator will come as a FFFFFFF+ if m: return "Unknown Network", tech # clean extra '@', 'x1a', etc return NETINFO_REGEXP.sub('', operator), tech d = super(HuaweiWCDMAWrapper, self).get_network_info(_type) d.addCallback(process_netinfo_cb) return d def get_network_mode(self): """Returns the current used network mode""" d = self._get_syscfg() d.addCallback(lambda ret: ret['mode']) d.addErrback(log.err) return d def set_band(self, band): """Sets the band to ``band``""" def get_syscfg_cb(info): mode_a, mode_b = info['modea'], info['modeb'] roam, srv = info['roam'], info['srv'] _band = 0 if band == consts.MM_NETWORK_BAND_ANY: # ANY cannot be combined by design _band = 0x3FFFFFFF else: # the rest can be combined for key, value in self.custom.band_dict.items(): if key == consts.MM_NETWORK_BAND_ANY: continue if key & band: _band |= value if _band == 0: raise KeyError("Unsupported band %d" % band) gsm_bands = (consts.MM_NETWORK_BAND_DCS | consts.MM_NETWORK_BAND_PCS | consts.MM_NETWORK_BAND_EGSM | consts.MM_NETWORK_BAND_G850) umts_bands = (consts.MM_NETWORK_BAND_U800 | consts.MM_NETWORK_BAND_U850 | consts.MM_NETWORK_BAND_U900 | consts.MM_NETWORK_BAND_U1900) if band & gsm_bands: mode_a, mode_b = 13, 1 elif band & umts_bands: mode_a, mode_b = 14, 2 else: # ANY and the rest mode_a, mode_b = 2, 0 at_str = 'AT^SYSCFG=%d,%d,%X,%d,%d' return self.send_at(at_str % (mode_a, mode_b, _band, roam, srv)) d = self._get_syscfg() d.addCallback(get_syscfg_cb) return d def set_allowed_mode(self, mode): """Sets the allowed mode to ``mode``""" def get_syscfg_cb(info): if mode not in self.custom.allowed_dict: # NOOP return "OK" _mode, acqorder = self.custom.allowed_dict[mode] band, roam, srv = info['theband'], info['roam'], info['srv'] band = 0x3FFFFFFF at_str = 'AT^SYSCFG=%d,%d,%X,%d,%d' def set_allowed_mode_cb(ign=None): self.device.set_property(consts.NET_INTFACE, "AllowedMode", mode) return ign return self.send_at(at_str % (_mode, acqorder, band, roam, srv), callback=set_allowed_mode_cb) d = self._get_syscfg() d.addCallback(get_syscfg_cb) d.addErrback(log.err) return d def set_network_mode(self, mode): """Sets the network mode to ``mode``""" def get_syscfg_cb(info): _mode, acqorder = info['modea'], info['modeb'] band, roam, srv = info['theband'], info['roam'], info['srv'] if mode not in self.custom.conn_dict: # NOOP return "OK" _mode, acqorder = self.custom.conn_dict[mode] band = 0x3FFFFFFF at_str = 'AT^SYSCFG=%d,%d,%X,%d,%d' return self.send_at(at_str % (_mode, acqorder, band, roam, srv)) d = self._get_syscfg() d.addCallback(get_syscfg_cb) d.addErrback(log.err) return d def set_smsc(self, smsc): """ Sets the SIM's smsc to `smsc` We wrap the operation with set_charset('IRA') and set_charset('UCS2') """ d = self.set_charset('IRA') d.addCallback(lambda _: super(HuaweiWCDMAWrapper, self).set_smsc(smsc)) d.addCallback(lambda _: self.set_charset('UCS2')) return d def _send_ussd_old_mode(self, ussd): """ Sends the USSD command ``ussd`` in Huawei old mode (^USSDMODE=0) Sends GSM 7bit compressed text Receives Hex encoded or GSM 7bit compressed text """ # AT+CUSD=1,"AA510C061B01",15 # (*#100#) # +CUSD: 0,"3037373935353033333035",0 # (07795503305) # +CUSD: 0,"C2303BEC1E97413D90140473C162A0221E9E96E741E430BD0CD452816" # "2B4574CF692C162301748F876D7E7A069737A9A837A20980B04",15 # +CUSD: 0,"D95399CD7EB340F9771D840EDBCB0AA9CD25CB81C269393DDD2E83143" # "0D0B43945CD53A0B09BACA0B964305053082287E9619722FBAE83C2F2" # "32E8ED0635A94E90F6ED2EBB405076393C2F83C8E9301BA48AD162AAD" # "808647ECB41E4323D9C6697C92071981D768FCB739742287FD7CF683A" # "88FE06E5DF7590380F6A529D2E",0 def send_request(ussd): if not is_gsm_text(ussd): raise ValueError gsm = ussd.encode("gsm0338") ussd_enc = encode_str(compress_7_to_8_bit(gsm)) cmd = ATCmd('AT+CUSD=1,"%s",15' % ussd_enc, name='send_ussd') return self.queue_at_cmd(cmd) def convert_response(response): index = response[0].group('index') if index == '1': self.device.set_property( consts.USD_INTFACE, 'State', 'user-response') else: self.device.set_property(consts.USD_INTFACE, 'State', 'idle') resp = response[0].group('resp') code = response[0].group('code') if code is not None: code = int(code) if ((code & 0x10) == 0x10): log.err("We don't yet handle ISO 639 encoded USSD" " - please report") raise E.MalformedUssdPduError(resp) if ((code & 0xf4) == 0xf4): log.err("We don't yet handle 8 bit encoded USSD" " - please report") raise E.MalformedUssdPduError(resp) try: ret = unpack_msg(resp).decode("gsm0338") if is_gsm_text(ret): return ret except UnicodeError: pass try: return resp.decode('hex') except TypeError: raise E.MalformedUssdPduError(resp) def reset_state(failure): if self.device.get_property(consts.USD_INTFACE, 'State') != 'idle': self.device.set_property(consts.USD_INTFACE, 'State', 'idle') failure.raiseException() # re-raise d = send_request(str(ussd)) d.addCallback(convert_response) d.addErrback(reset_state) return d def _send_ussd_ucs2_mode(self, ussd, loose_charset_check=False): """ Sends the USSD command ``ussd`` in UCS2 mode regardless of the current setting. Many Huawei devices that implement Vodafone specified USSD translation only function correctly in the UCS2 character set, so save the current charset, flip to UCS2, call the ancestor, restore the charset and then return the result """ def save_set_charset(old): if old == 'UCS2': if hasattr(self, 'old_charset'): del self.old_charset return defer.succeed('OK') self.old_charset = old return self.set_charset('UCS2') def restore_charset(result): if hasattr(self, 'old_charset'): self.set_charset(self.old_charset) del self.old_charset return result def restore_charset_eb(failure): if hasattr(self, 'old_charset'): self.set_charset(self.old_charset) del self.old_charset failure.raiseException() # re-raise d = defer.Deferred() d.addCallback(lambda _: self.get_charset()) d.addCallback(save_set_charset) d.addErrback(restore_charset_eb) d.addCallback(lambda _: super(HuaweiWCDMAWrapper, self).send_ussd(ussd, loose_charset_check=loose_charset_check)) d.addCallback(restore_charset) d.callback(True) return d class HuaweiWCDMACustomizer(WCDMACustomizer): """WCDMA Customizer class for Huawei cards""" wrapper_klass = HuaweiWCDMAWrapper async_regexp = re.compile( '\r\n(?P\^[A-Z]{3,9}):\s*(?P.*?)\r\n') allowed_dict = HUAWEI_ALLOWED_DICT band_dict = HUAWEI_BAND_DICT conn_dict = HUAWEI_CONN_DICT cmd_dict = HUAWEI_CMD_DICT device_capabilities = [S.SIG_NETWORK_MODE, S.SIG_SMS_NOTIFY_ONLINE, S.SIG_RSSI] signal_translations = { '^MODE': (S.SIG_NETWORK_MODE, huawei_new_conn_mode), '^RSSI': (None, huawei_new_rssi), '^DSFLOWRPT': (None, None), '^BOOT': (None, None), '^SRVST': (None, None), '^SIMST': (None, None), '^CEND': (None, None), '^EARST': (None, None), '^STIN': (None, None), '^SMMEMFULL': (None, None), '^CSNR': (None, None), } class HuaweiSIMClass(SIMBaseClass): """Huawei SIM Class""" def __init__(self, sconn): super(HuaweiSIMClass, self).__init__(sconn) def setup_sms(self): # Select SIM storage self.sconn.send_at('AT+CPMS="SM","SM","SM"') # Notification when a SMS arrives... self.sconn.set_sms_indication(1, 1, 0, 1, 0) # set PDU mode self.sconn.set_sms_format(0) def initialize(self, set_encoding=True): def at_curc_eb(failure): failure.trap(E.General) def init_cb(size): # enable unsolicited control commands d = self.sconn.send_at('AT^CURC=1') d.addErrback(at_curc_eb) self.sconn.set_network_mode(consts.MM_NETWORK_MODE_ANY) self.sconn.send_at('AT+COPS=3,0') return size d = super(HuaweiSIMClass, self).initialize(set_encoding=set_encoding) d.addCallback(init_cb) return d class HuaweiWCDMADevicePlugin(DevicePlugin): """DevicePlugin for Huawei""" sim_klass = HuaweiSIMClass custom = HuaweiWCDMACustomizer() wader-0.5.13/core/hardware/icera.py000066400000000000000000000324351257646610200171040ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2009-2011 Vodafone España, S.A. # Author: Andrew Bird # # 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. """ Common stuff for all ZTE's Icera based cards """ import re from twisted.internet import defer, reactor from wader.common import consts import wader.common.aterrors as E import wader.common.signals as S from wader.common.utils import revert_dict, rssi_to_percentage from core.command import get_cmd_dict_copy, build_cmd_dict, ATCmd from core.exported import HSOExporter from core.hardware.base import WCDMACustomizer from core.middleware import WCDMAWrapper from core.sim import SIMBaseClass from core.plugin import DevicePlugin HSO_MAX_RETRIES = 10 HSO_RETRY_TIMEOUT = 3 ICERA_ALLOWED_DICT = { consts.MM_ALLOWED_MODE_ANY: 5, consts.MM_ALLOWED_MODE_2G_ONLY: 0, consts.MM_ALLOWED_MODE_3G_ONLY: 1, consts.MM_ALLOWED_MODE_2G_PREFERRED: 2, consts.MM_ALLOWED_MODE_3G_PREFERRED: 3, } ICERA_MODE_DICT = { consts.MM_NETWORK_MODE_ANY: 5, consts.MM_NETWORK_MODE_2G_ONLY: 0, consts.MM_NETWORK_MODE_3G_ONLY: 1, consts.MM_NETWORK_MODE_2G_PREFERRED: 2, consts.MM_NETWORK_MODE_3G_PREFERRED: 3, } ICERA_BAND_DICT = { } ICERA_CMD_DICT = get_cmd_dict_copy() # \r\n+CPBR: (1-200),80,14,0,0,0\r\n\r\nOK\r\n ICERA_CMD_DICT['get_phonebook_size'] = build_cmd_dict( re.compile(r""" \r\n \+CPBR:\s \(\d+-(?P\d+)\).* \r\n """, re.VERBOSE)) # \r\n+CMGL: 1,1,"616E64726577",23\r\n # 0791447758100650040C9144977162470100009011503195310004D436390C\r\n ICERA_CMD_DICT['list_sms'] = build_cmd_dict( re.compile(r""" \r\n \+CMGL:\s* (?P\d+), (?P\d), (?P"\w*?")?, \d+ \r\n(?P\w+) """, re.VERBOSE)) # \r\n+CMGR: 0,"616E64726577",29\r\n # 0791447758100650040C914497716247010000117082812392400B4AB29A3C5693D56BF2 # 18\r\n ICERA_CMD_DICT['get_sms'] = build_cmd_dict( re.compile(r""" \r\n \+CMGR:\s* (?P\d+), (?P.*), \d+\r\n (?P\w+) \r\n """, re.VERBOSE)) # \r\n%IPSYS: 1,2\r\n ICERA_CMD_DICT['get_network_mode'] = build_cmd_dict( re.compile(r""" %IPSYS:\s (?P\d+), (?P\d+) """, re.VERBOSE)) # K3805-Z :- %IPDPADDR:,,,,[,,]\r\n # K4510-Z :- %IPDPADDR:,,,,[,,, # ,]\r\n ICERA_CMD_DICT['get_ip4_config'] = build_cmd_dict( re.compile(r""" %IPDPADDR: \s*(?P\d+), \s*(?P[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+), \s*(?P[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+), \s*(?P[0-9.]*), \s*(?P[0-9.]*) (?P.*|,.*) """, re.VERBOSE)) ICERA_CMD_DICT['hso_authenticate'] = build_cmd_dict() ICERA_CONN_DICT_REV = { '2G-GPRS': consts.MM_NETWORK_MODE_GPRS, '2G-EDGE': consts.MM_NETWORK_MODE_EDGE, '3G': consts.MM_NETWORK_MODE_UMTS, '3G-HSDPA': consts.MM_NETWORK_MODE_HSDPA, '3G-HSUPA': consts.MM_NETWORK_MODE_HSUPA, '3G-HSDPA-HSUPA': consts.MM_NETWORK_MODE_HSPA, } # Icera proprietary error E.ERROR_DICT['+CME ERROR: 767'] = E.General # Operation failed E.ERROR_DICT['+CME ERROR: 769'] = E.SimBusy def icera_new_conn_mode(args, device): try: args = args.replace(' ', '') # ['4', '23415', '3G-HSDPA', '-', '0'] rssi, network, tech, connected, regulation = args.split(',') strength = rssi_to_percentage(int(rssi)) netmode = ICERA_CONN_DICT_REV.get(tech, consts.MM_NETWORK_MODE_UNKNOWN) except (ValueError, TypeError, IndexError): return None device.sconn.updatecache(strength, 'signal') device.sconn.emit_rssi(strength) device.sconn.emit_network_mode(netmode) return None def icera_connection_state(args, device): if not args: return args = args.replace(' ', '') args = args.split(',') if len(args) >= 2: if str(args[1]) == '3': device.connection_attempt_failed = True return None class IceraSIMClass(SIMBaseClass): """ Icera SIM Class I perform an initial setup in the device """ def __init__(self, sconn): super(IceraSIMClass, self).__init__(sconn) def initialize(self, set_encoding=True): self.sconn.reset_settings() self.sconn.disable_echo() d = super(IceraSIMClass, self).initialize(set_encoding=set_encoding) def init_callback(size): # setup SIM storage defaults d = self.sconn.send_at('AT+CPMS="SM","SM","SM"') # turn on unsolicited network notifications d.addCallback(lambda _: self.sconn.send_at('AT%NWSTATE=1')) d.addCallback(lambda _: size) return d d.addCallback(init_callback) return d class IceraWrapper(WCDMAWrapper): """Wrapper for all ZTE Icera based cards""" def enable_radio(self, enable): d = self.get_radio_status() def get_radio_status_cb(status): if status in [0, 4] and enable: return self.send_at('AT+CFUN=1') elif status in [1, 5, 6] and not enable: return self.send_at('AT+CFUN=4') d.addCallback(get_radio_status_cb) return d def get_band(self): return defer.succeed(consts.MM_NETWORK_BAND_ANY) def get_network_mode(self): """Returns the current network mode""" def get_network_mode_cb(resp): mode = int(resp[0].group('mode')) ICERA_MODE_DICT_REV = revert_dict(self.custom.conn_dict) if mode in ICERA_MODE_DICT_REV: return ICERA_MODE_DICT_REV[mode] raise KeyError("Unknown network mode %s" % mode) d = self.send_at('AT%IPSYS?', name='get_network_mode', callback=get_network_mode_cb) return d def set_band(self, band): if band == consts.MM_NETWORK_BAND_ANY: return defer.succeed('OK') raise KeyError("Unsupported band %d" % band) def set_allowed_mode(self, mode): if mode not in self.custom.allowed_dict: raise KeyError("Mode %s not found" % mode) def set_allowed_mode_cb(orig=None): self.device.set_property(consts.NET_INTFACE, "AllowedMode", mode) return orig return self.send_at("AT%%IPSYS=%d" % self.custom.allowed_dict[mode], callback=set_allowed_mode_cb) def set_network_mode(self, mode): """Sets the network mode to ``mode``""" # Note: Icera devices drop signal and network acquisition on bearer # preference set. This causes the connection sequence to take # 7 secs instead of 2. So we test for current value and only # set if required. Perhaps a similar thing will happen on # band set, but as there are no bands defined for Icera at # the moment, the band set is a no-op. if mode not in self.custom.conn_dict: raise KeyError("Mode %s not found" % mode) d = self.get_network_mode() def get_network_mode_cb(_mode): if _mode == mode: return defer.succeed('OK') return self.send_at("AT%%IPSYS=%d" % self.custom.conn_dict[mode]) d.addCallback(get_network_mode_cb) return d def get_ip4_config(self): """ Returns the ip4 config on a NDIS device Wrapper around _get_ip4_config that provides some error control """ ip_method = self.device.get_property(consts.MDM_INTFACE, 'IpMethod') if ip_method != consts.MM_IP_METHOD_STATIC: msg = "Cannot get IP4 config from a non static ip method" raise E.OperationNotSupported(msg) self.state_dict['num_of_retries'] = 0 def real_get_ip4_config(deferred): def inform_caller(): if self.device.status > consts.MM_MODEM_STATE_REGISTERED: self.device.set_status(consts.MM_MODEM_STATE_REGISTERED) deferred.errback(RuntimeError('Connection attempt failed')) def get_ip4_eb(failure): failure.trap(E.General, E.OperationNotSupported) if self.state_dict.get('should_stop'): self.state_dict.pop('should_stop') return self.state_dict['num_of_retries'] += 1 if self.state_dict['num_of_retries'] > HSO_MAX_RETRIES: inform_caller() return failure reactor.callLater(HSO_RETRY_TIMEOUT, real_get_ip4_config, deferred) # We received an unsolicited notification that we failed if self.device.connection_attempt_failed: inform_caller() return d = self._get_ip4_config() d.addCallback(deferred.callback) d.addErrback(get_ip4_eb) return deferred auxdef = defer.Deferred() return real_get_ip4_config(auxdef) def _get_ip4_config(self): """Returns the ip4 config on a Icera NDIS device""" conn_id = self.state_dict.get('conn_id') if conn_id is None: raise E.CallIndexError("conn_id is None") cmd = ATCmd('AT%%IPDPADDR=%d' % conn_id, name='get_ip4_config') d = self.queue_at_cmd(cmd) def _get_ip4_config_cb(resp): if not resp: raise E.General() ip, dns1 = resp[0].group('ip'), resp[0].group('dns1') # XXX: Fix dns3 dns2 = dns3 = resp[0].group('dns2') self.device.set_status(consts.MM_MODEM_STATE_CONNECTED) return [ip, dns1, dns2, dns3] d.addCallback(_get_ip4_config_cb) return d def hso_authenticate(self, user, passwd, auth): """Authenticates using ``user`` and ``passwd`` on Icera NDIS devices""" conn_id = self.state_dict.get('conn_id') if conn_id is None: raise E.CallIndexError("conn_id is None") # Note: auth is now a bitfield, but icera can only support one mode so # unless it's only NONE or CHAP, default to PAP if auth == consts.MM_ALLOWED_AUTH_NONE: _auth = 0 # No auth elif auth == consts.MM_ALLOWED_AUTH_CHAP: _auth = 2 # CHAP else: _auth = 1 # PAP args = (conn_id, _auth, user, passwd) cmd = ATCmd('AT%%IPDPCFG=%d,0,%d,"%s","%s"' % args, name='hso_authenticate') d = self.queue_at_cmd(cmd) d.addCallback(lambda resp: resp[0].group('resp')) return d def hso_connect(self): conn_id = self.state_dict.get('conn_id') if conn_id is None: raise E.CallIndexError("conn_id is None") if self.device.status == consts.MM_MODEM_STATE_CONNECTED: # this cannot happen raise E.Connected("we are already connected") if self.device.status == consts.MM_MODEM_STATE_CONNECTING: raise E.SimBusy("we are already connecting") self.device.connection_attempt_failed = False self.device.set_status(consts.MM_MODEM_STATE_CONNECTING) return self.send_at('AT%%IPDPACT=%d,1' % conn_id) def hso_disconnect(self): conn_id = self.state_dict.get('conn_id') if conn_id is None: raise E.CallIndexError("conn_id is None") self.device.set_status(consts.MM_MODEM_STATE_DISCONNECTING) def disconnect_cb(ignored): # XXX: perhaps we should check the registration status here if self.device.status > consts.MM_MODEM_STATE_REGISTERED: self.device.set_status(consts.MM_MODEM_STATE_REGISTERED) d = self.send_at('AT%%IPDPACT=%d,0' % conn_id) d.addCallback(disconnect_cb) return d class IceraWCDMACustomizer(WCDMACustomizer): """WCDMA customizer for ZTE Icera based devices""" async_regexp = re.compile(""" \r\n (?P%[A-Z]{3,}):\s*(?P.*) \r\n""", re.VERBOSE) allowed_dict = ICERA_ALLOWED_DICT band_dict = ICERA_BAND_DICT conn_dict = ICERA_MODE_DICT cmd_dict = ICERA_CMD_DICT device_capabilities = [S.SIG_NETWORK_MODE, S.SIG_SMS_NOTIFY_ONLINE, S.SIG_RSSI] signal_translations = { '%NWSTATE': (None, icera_new_conn_mode), '%IPDPACT': (None, icera_connection_state), } wrapper_klass = IceraWrapper exporter_klass = HSOExporter class IceraWCDMADevicePlugin(DevicePlugin): """WCDMA device plugin for ZTE Icera based devices""" sim_klass = IceraSIMClass custom = IceraWCDMACustomizer() wader-0.5.13/core/hardware/longcheer.py000066400000000000000000000166611257646610200177720ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2012 Sphere Systems Ltd # Author: Andrew Bird # # 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. """Common stuff for all Longcheer cards and rebranded variants""" import re from twisted.internet import defer # also registers gsm0338 codec from messaging.sms import is_gsm_text from messaging.utils import unpack_msg from wader.common import consts from wader.common.aterrors import MalformedUssdPduError from wader.common.encoding import pack_ucs2_bytes from wader.common.utils import revert_dict from core.command import get_cmd_dict_copy, build_cmd_dict from core.hardware.base import WCDMACustomizer from core.middleware import WCDMAWrapper from core.plugin import DevicePlugin from core.sim import SIMBaseClass LONGCHEER_ALLOWED_DICT = { consts.MM_ALLOWED_MODE_ANY: '2', # 3g preferred consts.MM_ALLOWED_MODE_3G_ONLY: '1', consts.MM_ALLOWED_MODE_3G_PREFERRED: '2', consts.MM_ALLOWED_MODE_2G_ONLY: '3', consts.MM_ALLOWED_MODE_2G_PREFERRED: '4', } LONGCHEER_CONN_DICT = { consts.MM_NETWORK_MODE_ANY: '2', # 3g preferred consts.MM_NETWORK_MODE_3G_ONLY: '1', consts.MM_NETWORK_MODE_3G_PREFERRED: '2', consts.MM_NETWORK_MODE_2G_ONLY: '3', consts.MM_NETWORK_MODE_2G_PREFERRED: '4', } LONGCHEER_CMD_DICT = get_cmd_dict_copy() # +CPBR: 1,"+4917XXXXXX",145,"005400650073007400200053007400720069","",129,"" LONGCHEER_CMD_DICT['get_contact'] = build_cmd_dict(re.compile(r""" \r\n \+CPBR:\s(?P\d+), "(?P[+0-9a-fA-F*#]+)", (?P\d+), "(?P.*?)" (?P,\S*)? \r\n""", re.X)) LONGCHEER_CMD_DICT['list_contacts'] = build_cmd_dict( end=re.compile('(\r\n)?\r\n(OK)\r\n'), extract=re.compile(r""" \r\n \+CPBR:\s(?P\d+), "(?P[+0-9a-fA-F*#]+)", (?P\d+), "(?P.*?)" (?P,\S*)? """, re.X)) LONGCHEER_CMD_DICT['get_network_mode'] = build_cmd_dict( re.compile("\r\n\+MODODR:\s?(?P\d+)\r\n")) class LongcheerSIMClass(SIMBaseClass): """Longcheer SIM Class""" def __init__(self, sconn): super(LongcheerSIMClass, self).__init__(sconn) def initialize(self, set_encoding=True): def init_callback(size): # make sure we are in most promiscuous mode before registration self.sconn.set_network_mode(consts.MM_NETWORK_MODE_ANY) # set SMS storage default self.sconn.send_at('AT+CPMS="SM","SM","SM"') return(size) d = super(LongcheerSIMClass, self).initialize(set_encoding) d.addCallback(init_callback) return d class LongcheerWCDMAWrapper(WCDMAWrapper): """Wrapper for all Longcheer cards""" def enable_radio(self, enable): d = self.get_radio_status() def get_radio_status_cb(status): if status in [0, 4] and enable: return self.send_at('AT+CFUN=1') elif status in [1] and not enable: return self.send_at('AT+CFUN=4') d.addCallback(get_radio_status_cb) return d def get_band(self): """Returns the current used band""" return defer.succeed(consts.MM_NETWORK_BAND_ANY) def set_band(self, band): """Sets the band to ``band``""" if band == consts.MM_NETWORK_BAND_ANY: return defer.succeed('OK') else: raise KeyError("Unsupported band %d" % band) def get_network_mode(self): """Returns the current network mode""" def cb(resp): mode = resp[0].group('mode') return revert_dict(self.custom.conn_dict)[mode] return self.send_at("AT+MODODR?", name='get_network_mode', callback=cb) def set_allowed_mode(self, mode): """Sets the allowed mode to ``mode``""" if mode not in self.custom.allowed_dict: raise KeyError("Unknown network mode %d" % mode) def cb(ign=None): self.device.set_property(consts.NET_INTFACE, "AllowedMode", mode) return ign return self.send_at("AT+MODODR=%s" % self.custom.allowed_dict[mode], callback=cb) def set_network_mode(self, mode): """Sets the network mode to ``mode``""" if mode not in self.custom.conn_dict: raise KeyError("Unknown network mode %d" % mode) return self.send_at("AT+MODODR=%s" % self.custom.conn_dict[mode]) def send_ussd(self, ussd): """ Sends the USSD command ``ussd`` Sends plain or UCS2 encoded text Receives GSM 7bit compressed text """ # AT+CUSD=1,"*100#",15 # (*100#) # or # AT+CUSD=1,"002A0031003000300023",15 # (*100#) # +CUSD: 1,"5079191E4E935BCDB2DBAF88818E753A3A2C2EBBD76F37FDAD90818E7" # "53A3A2C2EBB5BD6B2DCEC3F8BC3F275394D57CC40C1BA991D26D7DD67" # "D0B14E4787C565F70B1A1EAF5BF477EBF856D040D0F0780D6A86DDE17" # "359AEB881A86179DA9C769BDF0A1C0899669BCB",15 # Prepaid-Menü\n # 1 Guthabenkonto\n # 2 Guthaben-Verfügbarkeit\n # 3 Aufladung Guthaben/Pack-to-Go\n # 4 Pack Manager\n # 7 Tarifinfo\n # 8 Hilfe def convert_response(response): index = response[0].group('index') if index == '1': self.device.set_property( consts.USD_INTFACE, 'State', 'user-response') else: self.device.set_property(consts.USD_INTFACE, 'State', 'idle') resp = response[0].group('resp') if resp is None: return "" # returning the Empty string is valid try: ret = unpack_msg(resp).decode("gsm0338") if is_gsm_text(ret): return ret except UnicodeError: pass try: return resp.decode('hex') except TypeError: raise MalformedUssdPduError(resp) def reset_state(failure): if self.device.get_property(consts.USD_INTFACE, 'State') != 'idle': self.device.set_property(consts.USD_INTFACE, 'State', 'idle') failure.raiseException() # re-raise if 'UCS2' in self.device.sim.charset: ussd = pack_ucs2_bytes(ussd) d = super(WCDMAWrapper, self).send_ussd(str(ussd)) d.addCallback(convert_response) d.addErrback(reset_state) return d class LongcheerWCDMACustomizer(WCDMACustomizer): """WCDMA customizer for Longcheer devices""" allowed_dict = LONGCHEER_ALLOWED_DICT cmd_dict = LONGCHEER_CMD_DICT conn_dict = LONGCHEER_CONN_DICT wrapper_klass = LongcheerWCDMAWrapper class LongcheerWCDMADevicePlugin(DevicePlugin): """WCDMA device plugin for Longcheer devices""" sim_klass = LongcheerSIMClass custom = LongcheerWCDMACustomizer() wader-0.5.13/core/hardware/novatel.py000066400000000000000000000223531257646610200174670ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2009 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Common stuff for all Novatel's cards""" import re from twisted.internet import defer, reactor from wader.common import consts from core.command import get_cmd_dict_copy, build_cmd_dict from core.hardware.base import WCDMACustomizer from core.middleware import WCDMAWrapper from core.plugin import DevicePlugin from wader.common.utils import revert_dict from core.sim import SIMBaseClass import wader.common.signals as S NOVATEL_ALLOWED_DICT = { consts.MM_ALLOWED_MODE_ANY: '0,2', consts.MM_ALLOWED_MODE_2G_ONLY: '1,2', consts.MM_ALLOWED_MODE_3G_ONLY: '2,2', consts.MM_ALLOWED_MODE_3G_PREFERRED: '0,2', } NOVATEL_MODE_DICT = { consts.MM_NETWORK_MODE_ANY: '0,2', consts.MM_NETWORK_MODE_2G_ONLY: '1,2', consts.MM_NETWORK_MODE_3G_ONLY: '2,2', consts.MM_NETWORK_MODE_3G_PREFERRED: '0,2', # just a dup of automatic } NOVATEL_BAND_DICT = { consts.MM_NETWORK_BAND_ANY: 0x3FFFFFFF, consts.MM_NETWORK_BAND_EGSM: 0x00000100, # 900 MHz consts.MM_NETWORK_BAND_DCS: 0x00000080, # 1800 MHz consts.MM_NETWORK_BAND_PCS: 0x00200000, # 1900 MHz consts.MM_NETWORK_BAND_G850: 0x00080000, # 850 MHz consts.MM_NETWORK_BAND_U2100: 0x00400000, # WCDMA 2100 MHz consts.MM_NETWORK_BAND_U1800: 0x01000000, # WCDMA 3GPP UMTS1800 MHz consts.MM_NETWORK_BAND_U17IV: 0x02000000, # WCDMA 3GPP AWS 1700/2100 MHz consts.MM_NETWORK_BAND_U800: 0x08000000, # WCDMA 3GPP UMTS800 MHz consts.MM_NETWORK_BAND_U850: 0x04000000, # WCDMA 3GPP UMTS850 MHz consts.MM_NETWORK_BAND_U1900: 0x00800000, # WCDMA 3GPP UMTS MHz } # Following band definitions found from X950D # AT$nwband=? # $NWBAND: bit definitions # $NWBAND: 00 CDMA2000 Band Class 0, A-System # $NWBAND: 01 CDMA2000 Band Class 0, B-System # $NWBAND: 02 CDMA2000 Band Class 1, all blocks # $NWBAND: 03 CDMA2000 Band Class 2 place holder # $NWBAND: 04 CDMA2000 Band Class 3, A-System # $NWBAND: 05 CDMA2000 Band Class 4, all blocks # $NWBAND: 06 CDMA2000 Band Class 5, all blocks # $NWBAND: 07 GSM DCS band # $NWBAND: 08 GSM Extended GSM (E-GSM) band # $NWBAND: 09 GSM Primary GSM (P-GSM) band # $NWBAND: 10 CDMA2000 Band Class 6 # $NWBAND: 11 CDMA2000 Band Class 7 # $NWBAND: 12 CDMA2000 Band Class 8 # $NWBAND: 13 CDMA2000 Band Class 9 # $NWBAND: 14 CDMA2000 Band Class 10 # $NWBAND: 15 CDMA2000 Band Class 11 # $NWBAND: 16 GSM 450 band # $NWBAND: 17 GSM 480 band # $NWBAND: 18 GSM 750 band # $NWBAND: 19 GSM 850 band # $NWBAND: 20 GSM Band # $NWBAND: 21 GSM PCS band # $NWBAND: 22 WCDMA I IMT 2000 band # $NWBAND: 23 WCDMA II PCS band # $NWBAND: 24 WCDMA III 1700 band # $NWBAND: 25 WCDMA IV 1700 band # $NWBAND: 26 WCDMA V US850 band # $NWBAND: 27 WCDMA VI JAPAN 800 band # $NWBAND: 28 Reserved for BC12/BC14 # $NWBAND: 29 Reserved for BC12/BC14 # $NWBAND: 30 Reserved # $NWBAND: 31 Reserved # Following additional band definitions from MiFi 2352 # at$NWBAND2=? # $NWBAND2: bit definitions # $NWBAND2: 00 WLAN US 2400 band # $NWBAND2: 01 WLAN ETSI 2400 band # $NWBAND2: 02 WLAN FRANCE 2400 band # $NWBAND2: 03 WLAN SPAIN 2400 band # $NWBAND2: 04 WLAN JAPAN 2400 band # $NWBAND2: 05 WLAN US 2400 band # $NWBAND2: 06 WLAN EUROPE 5000 band # $NWBAND2: 07 WLAN FRANCE 5000 band # $NWBAND2: 08 WLAN SPAIN 5000 band # $NWBAND2: 09 WLAN JAPAN 5000 band # $NWBAND2: 10 Reserved # $NWBAND2: 11 Reserved # $NWBAND2: 12 Reserved # $NWBAND2: 13 Reserved # $NWBAND2: 14 Reserved # $NWBAND2: 15 Reserved # $NWBAND2: 16 WCDMA EUROPE 2600 band # $NWBAND2: 17 WCDMA EUROPE & JAPAN 900 band # $NWBAND2: 18 WCDMA JAPAN 1700 band # $NWBAND2: 19 Reserved for WLAN # $NWBAND2: 20 Reserved for WLAN # $NWBAND2: 21 Reserved for WLAN # $NWBAND2: 22 Reserved for WLAN # $NWBAND2: 23 Reserved for WLAN # $NWBAND2: 24 Band Class 16 # $NWBAND2: 25 Reserved # $NWBAND2: 26 Reserved # $NWBAND2: 27 Reserved # $NWBAND2: 28 Reserved # $NWBAND2: 29 Reserved # $NWBAND2: 30 Persistent value from NV # $NWBAND2: 31 Reserved NOVATEL_CMD_DICT = get_cmd_dict_copy() NOVATEL_CMD_DICT['get_network_mode'] = build_cmd_dict( re.compile("\r\n\$NWRAT:\s?(?P\d,\d),\d\r\n")) NOVATEL_CMD_DICT['get_band'] = build_cmd_dict( re.compile("\r\n\$NWBAND:\s?(?P[0-9A-Fa-f]+)")) class NovatelSIMClass(SIMBaseClass): """Novatel SIM Class""" def __init__(self, sconn): super(NovatelSIMClass, self).__init__(sconn) def setup_sms(self): # Select SIM storage self.sconn.send_at('AT+CPMS="SM","SM","SM"') # Notification when a SMS arrives... self.sconn.set_sms_indication(1, 1, 0, 1, 0) # set PDU mode self.sconn.set_sms_format(0) def initialize(self, set_encoding=True): def init_callback(size): # make sure we are in most promiscuous mode before registration self.sconn.set_network_mode(consts.MM_NETWORK_MODE_ANY) return(size) d = super(NovatelSIMClass, self).initialize(set_encoding) d.addCallback(init_callback) return d class NovatelWrapper(WCDMAWrapper): """Wrapper for all Novatel cards""" # XXX: Needs updating for use of bands in second chunk # of bit settings (U900 etc) def get_band(self): """Returns the current used band""" if not len(self.custom.band_dict): return defer.succeed(consts.MM_NETWORK_BAND_ANY) def get_band_cb(resp): band = int(resp[0].group('band'), 16) if band == 0x3FFFFFFF: return consts.MM_NETWORK_BAND_ANY ret = 0 for key, value in NOVATEL_BAND_DICT.items(): if value & band: ret |= key return ret return self.send_at("AT$NWBAND?", name='get_band', callback=get_band_cb) def get_network_mode(self): """Returns the current network mode""" def get_network_mode_cb(resp): mode = resp[0].group('mode') return revert_dict(self.custom.conn_dict)[mode] return self.send_at("AT$NWRAT?", name='get_network_mode', callback=get_network_mode_cb) # XXX: Needs updating for use of bands in second chunk # of bit settings (U900 etc) def set_band(self, band): """Sets the band to ``band``""" if not len(self.custom.band_dict): if band == consts.MM_NETWORK_BAND_ANY: return defer.succeed('') else: raise KeyError("Unsupported band %d" % band) if band == consts.MM_NETWORK_BAND_ANY: _band = 0x3FFFFFFF else: _band = 0 for key, value in self.custom.band_dict.items(): if key == consts.MM_NETWORK_BAND_ANY: continue if key & band: _band |= value if _band == 0: # if we could not satisfy the request, tell someone raise KeyError("Unsupported band %d" % band) deferred = defer.Deferred() d = self.send_at("AT$NWBAND=%08x" % _band) def delay_result(result): reactor.callLater(1, deferred.callback, True) d.addCallback(delay_result) return deferred def set_allowed_mode(self, mode): """Sets the allowed mode to ``mode``""" if mode not in self.custom.allowed_dict: raise KeyError("Unknown network mode %d" % mode) def set_allowed_mode_cb(ign=None): self.device.set_property(consts.NET_INTFACE, "AllowedMode", mode) return ign return self.send_at("AT$NWRAT=%s" % self.custom.allowed_dict[mode], callback=set_allowed_mode_cb) def set_network_mode(self, mode): """Sets the network mode to ``mode``""" if mode not in self.custom.conn_dict: raise KeyError("Unknown network mode %d" % mode) return self.send_at("AT$NWRAT=%s" % self.custom.conn_dict[mode]) class NovatelWCDMACustomizer(WCDMACustomizer): """WCDMA customizer for Novatel cards""" async_regexp = None allowed_dict = NOVATEL_ALLOWED_DICT band_dict = {} # let the cards that support band switching define # the bands they support cmd_dict = NOVATEL_CMD_DICT conn_dict = NOVATEL_MODE_DICT device_capabilities = [S.SIG_SMS_NOTIFY_ONLINE] wrapper_klass = NovatelWrapper class NovatelWCDMADevicePlugin(DevicePlugin): """WCDMA device plugin for Novatel cards""" sim_klass = NovatelSIMClass custom = NovatelWCDMACustomizer() wader-0.5.13/core/hardware/option.py000066400000000000000000000440711257646610200173300ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Common stuff for all Option's datacards/devices""" import re from twisted.internet import defer, reactor from wader.common import consts import wader.common.aterrors as E import wader.common.signals as S from wader.common.utils import rssi_to_percentage, revert_dict from core.command import get_cmd_dict_copy, build_cmd_dict, ATCmd from core.exported import HSOExporter from core.hardware.base import WCDMACustomizer from core.middleware import WCDMAWrapper from core.plugin import DevicePlugin from core.sim import SIMBaseClass NUM_RETRIES = 30 RETRY_TIMEOUT = 4 HSO_MAX_RETRIES = 10 HSO_RETRY_TIMEOUT = 3 OPTION_ALLOWED_DICT = { consts.MM_ALLOWED_MODE_ANY: 5, consts.MM_ALLOWED_MODE_2G_PREFERRED: 2, consts.MM_ALLOWED_MODE_3G_PREFERRED: 3, consts.MM_ALLOWED_MODE_2G_ONLY: 0, consts.MM_ALLOWED_MODE_3G_ONLY: 1, } # The MM band dictionary was modelled after the Option band dict, but there are # now some differences OPTION_BAND_DICT = { consts.MM_NETWORK_BAND_ANY: 'ANY', consts.MM_NETWORK_BAND_EGSM: 'EGSM', consts.MM_NETWORK_BAND_DCS: 'DCS', consts.MM_NETWORK_BAND_PCS: 'PCS', consts.MM_NETWORK_BAND_G850: 'G850', consts.MM_NETWORK_BAND_U2100: 'U2100', consts.MM_NETWORK_BAND_U1800: 'U1700', # Option calls this U1700 consts.MM_NETWORK_BAND_U17IV: 'U17IV', consts.MM_NETWORK_BAND_U800: 'U800', consts.MM_NETWORK_BAND_U850: 'U850', consts.MM_NETWORK_BAND_U900: 'U900', consts.MM_NETWORK_BAND_U17IX: 'U17IX', consts.MM_NETWORK_BAND_U1900: 'U1900', # Option has no U2600 } OPTION_CONN_DICT = { consts.MM_NETWORK_MODE_2G_ONLY: 0, consts.MM_NETWORK_MODE_3G_ONLY: 1, consts.MM_NETWORK_MODE_2G_PREFERRED: 2, consts.MM_NETWORK_MODE_3G_PREFERRED: 3, consts.MM_NETWORK_MODE_ANY: 5, } # Option devices like to append its serial number after the IMEI, ignore it OPTION_CMD_DICT = get_cmd_dict_copy() OPTION_CMD_DICT['get_imei'] = build_cmd_dict(re.compile( "\r\n(?P\d+),\S+\r\n", re.VERBOSE)) OPTION_CMD_DICT['get_sim_status'] = build_cmd_dict(re.compile(r""" _OBLS:\s(?P\d), (?P\d), (?P\d) """, re.VERBOSE)) OPTION_CMD_DICT['get_band'] = build_cmd_dict(re.compile(r""" \r\n(?P.*):\s+(?P\d) """, re.VERBOSE)) # AT_OPBM=? # _OPBM: ("ANY","EGSM","DCS","PCS","G850","U2100","U1900","U1700","U17IV", # "U850","U800","U900","U17IX") OPTION_CMD_DICT['get_bands'] = build_cmd_dict(re.compile(r""" "(?P[A-Za-z0-9]+)"(?:,|\)) """, re.VERBOSE)) OPTION_CMD_DICT['get_network_mode'] = build_cmd_dict(re.compile(r""" _OPSYS:\s (?P\d), (?P\d) """, re.VERBOSE)) OPTION_CMD_DICT['hso_authenticate'] = build_cmd_dict() OPTION_CMD_DICT['get_ip4_config'] = build_cmd_dict(re.compile(r""" \r\n _OWANDATA:\s (?P\d),\s (?P.*),\s (?P.*),\s (?P.*),\s (?P.*),\s (?P.*),\s (?P.*),\s (?P\d+) \r\r\n""", re.X)) def option_new_mode(args, device): """ Translates Option's unsolicited notifications to Wader's representation """ ossysi_args_dict = { '0': consts.MM_NETWORK_MODE_GPRS, '2': consts.MM_NETWORK_MODE_UMTS, '3': consts.MM_NETWORK_MODE_UNKNOWN, } netmode = ossysi_args_dict.get(args, consts.MM_NETWORK_MODE_UNKNOWN) device.sconn.emit_network_mode(netmode) return None def option_new_rssi(args, device): try: strength = rssi_to_percentage(int(args.split(',')[0])) except (ValueError, TypeError, IndexError): return None device.sconn.updatecache(strength, 'signal') device.sconn.emit_rssi(strength) return None class OptionSIMClass(SIMBaseClass): """ Option SIM Class I perform an initial setup in the device and will not return until the SIM is *really* ready """ def __init__(self, sconn): super(OptionSIMClass, self).__init__(sconn) self.num_retries = 0 def initialize(self, set_encoding=True): deferred = defer.Deferred() def init_callback(size): # make sure we aren't looking for a network that isn't available self.sconn.set_network_mode(consts.MM_NETWORK_MODE_ANY) # setup asynchronous notifications self.sconn.send_at('AT_OSSYS=1') # cell change notification self.sconn.send_at('AT_OSQI=1') # signal quality notification deferred.callback(size) def sim_ready_cb(ignored): d2 = super(OptionSIMClass, self).initialize(set_encoding) d2.addCallback(init_callback) def sim_ready_eb(failure): deferred.errback(failure) d = self.is_sim_ready() d.addCallback(sim_ready_cb) d.addErrback(sim_ready_eb) return deferred def is_sim_ready(self): deferred = defer.Deferred() def process_sim_state(auxdef): def parse_response(resp): status = tuple(map(int, resp[0].groups())) if status == (1, 1, 1): auxdef.callback(True) else: self.num_retries += 1 if self.num_retries < NUM_RETRIES: reactor.callLater(RETRY_TIMEOUT, process_sim_state, auxdef) else: msg = "Max number of attempts reached %d" auxdef.errback(E.General(msg % self.num_retries)) return self.sconn.send_at('AT_OBLS', name='get_sim_status', callback=parse_response) return auxdef return process_sim_state(deferred) class OptionWrapper(WCDMAWrapper): """Wrapper for all Option cards""" def _get_band_dict(self): """Returns a dict with the currently selected bands""" def callback(resp): bands = {} for r in resp: name, active = r.group('name'), int(r.group('active')) bands[name] = active return bands d = self.send_at('AT_OPBM?', name='get_band', callback=callback) return d def get_band(self): """Returns the current used band""" def get_band_dict_cb(current): if 'ANY' in current and current['ANY'] == 1: # can't be combined by design return consts.MM_NETWORK_BAND_ANY ret = 0 for key, value in self.custom.band_dict.items(): for name, active in current.items(): if name == value and active: ret |= key return ret d = self._get_band_dict() d.addCallback(get_band_dict_cb) return d def get_bands(self): """ Returns the supported bands from the device, but filtered to the set supported in the custom band_dict. This means we can remove any awkward bands from problematic devices e.g. K3760 / U2100 """ def callback(resp): bands = [r.group('name') for r in resp] ret = 0 for key, value in self.custom.band_dict.items(): if key == consts.MM_NETWORK_BAND_ANY: # skip ANY continue if value in bands: ret |= key return ret return self.send_at('AT_OPBM=?', name='get_bands', callback=callback) def get_network_mode(self): """Returns the current network mode""" def get_network_mode_cb(resp): mode = int(resp[0].group('mode')) OPTION_CONN_DICT_REV = revert_dict(OPTION_CONN_DICT) if mode in OPTION_CONN_DICT_REV: return OPTION_CONN_DICT_REV[mode] raise KeyError("Unknown network mode %d" % mode) return self.send_at('AT_OPSYS?', name='get_network_mode', callback=get_network_mode_cb) def get_roaming_ids(self): # Many Option devices panic if AT+CPOL is sent while in UCS2, we will # switch to IRA, perform the operation and switch back to UCS2 self.set_charset("IRA") d = super(OptionWrapper, self).get_roaming_ids() def get_roaming_ids_cb(rids): d2 = self.set_charset("UCS2") d2.addCallback(lambda _: rids) return d2 d.addCallback(get_roaming_ids_cb) return d def set_band(self, band): """Sets the band set to requested ``band``""" def get_band_dict_cb(current): at_str = 'AT_OPBM="%s",%d' todo = [] if band == consts.MM_NETWORK_BAND_ANY: # enabling ANY should suffice todo.append(self.send_at(at_str % ('ANY', 1))) else: supported = self.custom.band_dict.copy() supported.pop(consts.MM_NETWORK_BAND_ANY) # turn on the bits we want first for key, value in supported.items(): if key & band and current[value] != 1: # enable required band todo.append(self.send_at(at_str % (value, 1))) # turn off the bits we don't want for key, value in supported.items(): if (not key & band) and current[value] != 0: # disable unrequired band todo.append(self.send_at(at_str % (value, 0))) if todo: dlist = defer.DeferredList(todo, consumeErrors=1) dlist.addCallback(lambda l: [x[1] for x in l]) return dlist raise KeyError("OptionWrapper: Unknown band %d" % band) # due to Option's band API, we'll start by obtaining the current bands d = self._get_band_dict() d.addCallback(get_band_dict_cb) return d def set_allowed_mode(self, mode): """Sets the allowed mode to ``_mode``""" if mode not in self.custom.allowed_dict: raise KeyError("Unknown mode %d for set_allowed_mode" % mode) def set_allowed_mode_cb(orig=None): self.device.set_property(consts.NET_INTFACE, "AllowedMode", mode) return orig return self.send_at("AT_OPSYS=%d,2" % self.custom.allowed_dict[mode], callback=set_allowed_mode_cb) def set_network_mode(self, mode): """Sets the network mode to ``_mode``""" if mode not in OPTION_CONN_DICT: raise KeyError("Unknown mode %d for set_network_mode" % mode) return self.send_at("AT_OPSYS=%d,2" % OPTION_CONN_DICT[mode]) class OptionHSOWrapper(OptionWrapper): """Wrapper for all Option HSO cards""" def get_ip4_config(self): """ Returns the ip4 config on a HSO device Wrapper around _get_ip4_config that provides some error control """ ip_method = self.device.get_property(consts.MDM_INTFACE, 'IpMethod') if ip_method != consts.MM_IP_METHOD_STATIC: msg = "Cannot get IP4 config from a non static ip method" raise E.OperationNotSupported(msg) self.state_dict['num_of_retries'] = 0 def real_get_ip4_config(deferred): def inform_caller(): if self.device.status > consts.MM_MODEM_STATE_REGISTERED: self.device.set_status(consts.MM_MODEM_STATE_REGISTERED) deferred.errback(RuntimeError('Connection attempt failed')) def get_ip4_eb(failure): failure.trap(E.General) if self.state_dict.get('should_stop'): self.state_dict.pop('should_stop') return self.state_dict['num_of_retries'] += 1 if self.state_dict['num_of_retries'] > HSO_MAX_RETRIES: inform_caller() return failure reactor.callLater(HSO_RETRY_TIMEOUT, real_get_ip4_config, deferred) d = self._get_ip4_config() d.addCallback(deferred.callback) d.addErrback(get_ip4_eb) return deferred auxdef = defer.Deferred() return real_get_ip4_config(auxdef) def _get_ip4_config(self): """Returns the ip4 config on a HSO device""" conn_id = self.state_dict.get('conn_id') if not conn_id: raise E.CallIndexError("conn_id is None") cmd = ATCmd('AT_OWANDATA=%d' % conn_id, name='get_ip4_config') d = self.queue_at_cmd(cmd) def _get_ip4_config_cb(resp): ip, dns1 = resp[0].group('ip'), resp[0].group('dns1') # XXX: Fix dns3 dns2 = dns3 = resp[0].group('dns2') self.device.set_status(consts.MM_MODEM_STATE_CONNECTED) return [ip, dns1, dns2, dns3] d.addCallback(_get_ip4_config_cb) return d def hso_authenticate(self, user, passwd, auth): """Authenticates using ``user`` and ``passwd`` on HSO devices""" conn_id = self.state_dict.get('conn_id') if not conn_id: raise E.CallIndexError("conn_id is None") # Note: auth is now a bitfield, but option can only support one mode so # unless it's only NONE or CHAP, default to PAP if auth == consts.MM_ALLOWED_AUTH_NONE: _auth = 0 # No auth elif auth == consts.MM_ALLOWED_AUTH_CHAP: _auth = 2 # CHAP else: _auth = 1 # PAP args = (conn_id, _auth, user, passwd) cmd = ATCmd('AT$QCPDPP=%d,%d,"%s","%s"' % args, name='hso_authenticate') d = self.queue_at_cmd(cmd) d.addCallback(lambda resp: resp[0].group('resp')) return d def hso_connect(self): # clean should_stop if 'should_stop' in self.state_dict: self.state_dict.pop('should_stop') conn_id = self.state_dict.get('conn_id') if not conn_id: raise E.CallIndexError("conn_id is None") if self.device.status == consts.MM_MODEM_STATE_CONNECTED: # this cannot happen raise E.Connected("we are already connected") if self.device.status == consts.MM_MODEM_STATE_CONNECTING: raise E.SimBusy("we are already connecting") self.device.set_status(consts.MM_MODEM_STATE_CONNECTING) return self.device.sconn.send_at('AT_OWANCALL=%d,1,0' % conn_id) def hso_disconnect(self): conn_id = self.state_dict.get('conn_id') if not conn_id: raise E.CallIndexError("conn_id is None") self.device.set_status(consts.MM_MODEM_STATE_DISCONNECTING) self.state_dict['should_stop'] = True def disconnect_cb(ignored): # XXX: perhaps we should check the registration status here if self.device.status > consts.MM_MODEM_STATE_REGISTERED: self.device.set_status(consts.MM_MODEM_STATE_REGISTERED) d = self.device.sconn.send_at('AT_OWANCALL=%d,0,0' % conn_id) d.addCallback(disconnect_cb) return d class OptionWCDMACustomizer(WCDMACustomizer): """Customizer for Option's cards""" async_regexp = re.compile(r""" \r\n (?P_O[A-Z]{3,}):\s(?P.*) \r\n""", re.VERBOSE) allowed_dict = OPTION_ALLOWED_DICT band_dict = OPTION_BAND_DICT conn_dict = OPTION_CONN_DICT cmd_dict = OPTION_CMD_DICT device_capabilities = [S.SIG_NETWORK_MODE, S.SIG_SMS_NOTIFY_ONLINE, S.SIG_RSSI] signal_translations = { '_OSSYSI': (None, option_new_mode), '_OSIGQ': (None, option_new_rssi), } wrapper_klass = OptionWrapper class OptionHSOWCDMACustomizer(OptionWCDMACustomizer): """Customizer for HSO WCDMA devices""" exporter_klass = HSOExporter wrapper_klass = OptionHSOWrapper class OptionWCDMADevicePlugin(DevicePlugin): """DevicePlugin for Option""" sim_klass = OptionSIMClass custom = OptionWCDMACustomizer() def __init__(self): super(OptionWCDMADevicePlugin, self).__init__() class OptionHSOWCDMADevicePlugin(OptionWCDMADevicePlugin): """DevicePlugin for Option HSO devices""" custom = OptionHSOWCDMACustomizer() dialer = 'hso' ipmethod = consts.MM_IP_METHOD_STATIC def __init__(self): super(OptionHSOWCDMADevicePlugin, self).__init__() wader-0.5.13/core/hardware/qualcomm.py000066400000000000000000000077341257646610200176430ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2010 Vodafone España, S.A. # Author: Andrew Bird # # 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. """Common stuff for all Qualcomm's cards""" from twisted.internet import defer, reactor from twisted.internet.task import deferLater from wader.common import consts from core.command import get_cmd_dict_copy from core.hardware.base import WCDMACustomizer from core.middleware import WCDMAWrapper from core.plugin import DevicePlugin from core.sim import SIMBaseClass # Gobi 2000 notes: # Unfortunately there is only one tty port available so it has to be # shared between data and status functions. # There also doesn't seem to be any way of specifying the bearer # preference for 3G only, 3G preferred, or 2G only. QUALCOMM_CMD_DICT = get_cmd_dict_copy() class QualcommSIMClass(SIMBaseClass): """Qualcomm SIM Class""" def __init__(self, sconn): super(QualcommSIMClass, self).__init__(sconn) def initialize(self, set_encoding=True): def init_callback(size): # make sure we are in most promiscuous mode before registration self.sconn.set_network_mode(consts.MM_NETWORK_MODE_ANY) # set SMS storage default self.sconn.send_at('AT+CPMS="SM","SM","SM"') return(size) d = super(QualcommSIMClass, self).initialize(set_encoding) d.addCallback(init_callback) return d class QualcommWCDMAWrapper(WCDMAWrapper): """Wrapper for all Qualcomm cards""" def enable_radio(self, enable): d = self.get_radio_status() def get_radio_status_cb(status): if status in [0, 4] and enable: self.send_at('AT+CFUN=1') # delay here to give the device chance to wake up return deferLater(reactor, 2, lambda: None) elif status == 1 and not enable: return self.send_at('AT+CFUN=4') d.addCallback(get_radio_status_cb) return d def get_band(self): """Returns the current used band""" return defer.succeed(consts.MM_NETWORK_BAND_ANY) def set_band(self, band): """Sets the band to ``band``""" if band == consts.MM_NETWORK_BAND_ANY: return defer.succeed('OK') else: raise KeyError("Unsupported band %d" % band) def get_network_mode(self): """Returns the current network mode""" return defer.succeed(consts.MM_NETWORK_MODE_ANY) def set_network_mode(self, mode): """Sets the network mode to ``mode``""" if mode == consts.MM_NETWORK_MODE_ANY: return defer.succeed('OK') else: raise KeyError("Unknown network mode %d" % mode) def set_allowed_mode(self, mode): """Sets the allowed mode to ``mode``""" if mode == consts.MM_ALLOWED_MODE_ANY: self.device.set_property(consts.NET_INTFACE, "AllowedMode", mode) return defer.succeed('OK') else: raise KeyError("Unknown allowed mode %d" % mode) class QualcommWCDMACustomizer(WCDMACustomizer): """WCDMA customizer for Qualcomm devices""" cmd_dict = QUALCOMM_CMD_DICT wrapper_klass = QualcommWCDMAWrapper class QualcommWCDMADevicePlugin(DevicePlugin): """WCDMA device plugin for Qualcomm devices""" sim_klass = QualcommSIMClass custom = QualcommWCDMACustomizer() wader-0.5.13/core/hardware/sierra.py000066400000000000000000000107411257646610200173020ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Common stuff for all SierraWireless cards""" import re from twisted.internet import defer from wader.common import consts from core.command import get_cmd_dict_copy, build_cmd_dict from core.hardware.base import WCDMACustomizer from core.middleware import WCDMAWrapper from core.plugin import DevicePlugin from wader.common.utils import revert_dict SIERRA_ALLOWED_DICT = { consts.MM_ALLOWED_MODE_ANY: '00', consts.MM_ALLOWED_MODE_3G_ONLY: '01', consts.MM_ALLOWED_MODE_2G_ONLY: '02', consts.MM_ALLOWED_MODE_3G_PREFERRED: '03', consts.MM_ALLOWED_MODE_2G_PREFERRED: '04', } SIERRA_MODE_DICT = { consts.MM_NETWORK_MODE_ANY: '00', consts.MM_NETWORK_MODE_3G_ONLY: '01', consts.MM_NETWORK_MODE_2G_ONLY: '02', consts.MM_NETWORK_MODE_3G_PREFERRED: '03', consts.MM_NETWORK_MODE_2G_PREFERRED: '04', } SIERRA_BAND_DICT = { consts.MM_NETWORK_BAND_EGSM: '03', # EGSM (900MHz) consts.MM_NETWORK_BAND_DCS: '03', # DCS (1800MHz) consts.MM_NETWORK_BAND_PCS: '04', # PCS (1900MHz) consts.MM_NETWORK_BAND_G850: '04', # GSM (850 MHz) consts.MM_NETWORK_BAND_U2100: '02', # WCDMA 2100Mhz (Class I) consts.MM_NETWORK_BAND_U800: '02', # WCDMA 3GPP UMTS800 (Class VI) consts.MM_NETWORK_BAND_ANY: '00', # any band } SIERRA_CMD_DICT = get_cmd_dict_copy() SIERRA_CMD_DICT['get_band'] = build_cmd_dict(re.compile( "\r\n\!BAND:\s?(?P\d+)")) SIERRA_CMD_DICT['get_network_mode'] = build_cmd_dict(re.compile( "\r\n\!SELRAT:\s?(?P\d+)")) class SierraWrapper(WCDMAWrapper): """Wrapper for all Sierra cards""" def get_band(self): """Returns the current used band""" def get_band_cb(resp): band = resp[0].group('band') return revert_dict(self.custom.band_dict)[band] return self.send_at("AT!BAND?", name='get_band', callback=get_band_cb) def get_network_mode(self): """Returns the current used network mode""" def get_network_mode_cb(resp): mode = resp[0].group('mode') return revert_dict(self.custom.conn_dict)[mode] return self.send_at("AT!SELRAT?", name='get_network_mode', callback=get_network_mode_cb) def set_band(self, band): """Sets the band to ``band``""" if band not in self.custom.band_dict: raise KeyError("Unknown band %d" % band) return self.send_at("AT!BAND=%s" % self.custom.band_dict[band]) def set_allowed_mode(self, mode): """Sets the allowed mode to ``mode``""" if mode not in self.custom.allowed_dict: raise KeyError("Unknown mode %d" % mode) def set_allowed_mode_cb(ign=None): self.device.set_property(consts.NET_INTFACE, "AllowedMode", mode) return ign return self.send_at("AT!SELRAT=%s" % self.custom.allowed_dict[mode], callback=set_allowed_mode_cb) def set_network_mode(self, mode): """Sets the network mode to ``mode``""" if mode not in self.custom.conn_dict: raise KeyError("Unknown mode %d" % mode) return self.send_at("AT!SELRAT=%s" % self.custom.conn_dict[mode]) class SierraWirelessWCDMACustomizer(WCDMACustomizer): """WCDMA customizer for sierra wireless cards""" async_regexp = None allowed_dict = SIERRA_ALLOWED_DICT band_dict = SIERRA_BAND_DICT conn_dict = SIERRA_MODE_DICT cmd_dict = SIERRA_CMD_DICT wrapper_klass = SierraWrapper class SierraWCDMADevicePlugin(DevicePlugin): """WCDMA device plugin for sierra wireless cards""" custom = SierraWirelessWCDMACustomizer() wader-0.5.13/core/hardware/sonyericsson.py000066400000000000000000000024051257646610200205510ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Common stuff for all SonyEricsson's cards""" from core.hardware.base import WCDMACustomizer SONYERICSSON_ALLOWED_DICT = {} SONYERICSSON_CONN_DICT = {} SONYERICSSON_BAND_DICT = {} class SonyEricssonCustomizer(WCDMACustomizer): """WCDMA customizer for sonny ericsson devices""" async_regexp = None allowed_dict = SONYERICSSON_ALLOWED_DICT conn_dict = SONYERICSSON_CONN_DICT band_dict = SONYERICSSON_BAND_DICT wader-0.5.13/core/hardware/zte.py000066400000000000000000000174101257646610200166170ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2007 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Common stuff for all zte's cards""" import re from twisted.internet import defer from wader.common import consts from core.command import get_cmd_dict_copy, build_cmd_dict from core.hardware.base import WCDMACustomizer from core.middleware import WCDMAWrapper from core.plugin import DevicePlugin from core.sim import SIMBaseClass from wader.common.utils import revert_dict import wader.common.signals as S ZTE_ALLOWED_DICT = { consts.MM_ALLOWED_MODE_ANY: (0, 0), consts.MM_ALLOWED_MODE_2G_ONLY: (1, 0), consts.MM_ALLOWED_MODE_3G_ONLY: (2, 0), consts.MM_ALLOWED_MODE_2G_PREFERRED: (0, 1), consts.MM_ALLOWED_MODE_3G_PREFERRED: (0, 2), } ZTE_MODE_DICT = { consts.MM_NETWORK_MODE_ANY: (0, 0), consts.MM_NETWORK_MODE_2G_ONLY: (1, 0), consts.MM_NETWORK_MODE_3G_ONLY: (2, 0), consts.MM_NETWORK_MODE_2G_PREFERRED: (0, 1), consts.MM_NETWORK_MODE_3G_PREFERRED: (0, 2), } ZTE_BAND_DICT = { consts.MM_NETWORK_BAND_ANY: 0, # any band (consts.MM_NETWORK_BAND_U850 | consts.MM_NETWORK_BAND_EGSM | consts.MM_NETWORK_BAND_DCS): 1, (consts.MM_NETWORK_BAND_U2100 | consts.MM_NETWORK_BAND_EGSM | consts.MM_NETWORK_BAND_DCS): 2, # Europe (consts.MM_NETWORK_BAND_U850 | consts.MM_NETWORK_BAND_U2100 | consts.MM_NETWORK_BAND_EGSM | consts.MM_NETWORK_BAND_DCS): 3, (consts.MM_NETWORK_BAND_U850 | consts.MM_NETWORK_BAND_U1900 | consts.MM_NETWORK_BAND_G850 | consts.MM_NETWORK_BAND_PCS): 4, } # AT+ZBANDI=0 : Automatic (Auto) - Default # AT+ZBANDI=1 : UMTS 850 + GSM 900/1800 # AT+ZBANDI=2 : UMTS 2100 + GSM 900/1800 (Europe) # AT+ZBANDI=3 : UMTS 850/2100 + GSM 900/1800 # AT+ZBANDI=4 : UMTS 850/1900 + GSM 850/1900 ZTE_CMD_DICT = get_cmd_dict_copy() ZTE_CMD_DICT['get_band'] = build_cmd_dict(re.compile(r""" \r\n \+ZBANDI:\s? (?P\d) \r\n """, re.VERBOSE)) ZTE_CMD_DICT['get_network_mode'] = build_cmd_dict(re.compile(r""" \r\n \+ZSNT:\s (?P\d+), (?P\d+), (?P\d+) \r\n """, re.VERBOSE)) def zte_new_conn_mode(what, device): zpasr = re.search(r'"(?P.*?)"(?:,"(?P.*?)")?', what) mode = zpasr.group('mode') modes = { "GSM": consts.MM_NETWORK_MODE_GPRS, "GPRS": consts.MM_NETWORK_MODE_GPRS, "EDGE": consts.MM_NETWORK_MODE_EDGE, "UMTS": consts.MM_NETWORK_MODE_UMTS, "HSDPA": consts.MM_NETWORK_MODE_HSDPA, "HSUPA": consts.MM_NETWORK_MODE_HSUPA, } # "No Service", "Limited Service", non-match return modes.get(mode, consts.MM_NETWORK_MODE_UNKNOWN) class ZTEWrapper(WCDMAWrapper): """Wrapper for all ZTE cards""" def get_band(self): """Returns the current used band""" if not len(self.custom.band_dict): return defer.succeed(consts.MM_NETWORK_BAND_ANY) def get_band_cb(resp): band = int(resp[0].group('band')) return revert_dict(self.custom.band_dict)[band] return self.send_at("AT+ZBANDI?", name='get_band', callback=get_band_cb) def get_network_mode(self): """Returns the current network mode preference""" def get_network_mode_cb(resp): only = int(resp[0].group('only')) order = int(resp[0].group('order')) return revert_dict(self.custom.conn_dict)[(only, order)] return self.send_at("AT+ZSNT?", name='get_network_mode', callback=get_network_mode_cb) def set_band(self, band): """Sets the band to ``band``""" if not len(self.custom.band_dict): if band == consts.MM_NETWORK_BAND_ANY: return defer.succeed('') else: raise KeyError("Unsupported band %d" % band) for key, value in self.custom.band_dict.items(): if band & key: return self.send_at("AT+ZBANDI=%d" % value) raise KeyError("Unsupported band %d" % band) def set_allowed_mode(self, mode): """Sets the allowed mode to ``mode``""" if mode not in self.custom.allowed_dict: raise KeyError("Mode %s not found" % mode) def set_allowed_mode_cb(ign=None): self.device.set_property(consts.NET_INTFACE, "AllowedMode", mode) return ign return self.send_at("AT+ZSNT=%d,0,%d" % self.custom.allowed_dict[mode], callback=set_allowed_mode_cb) def set_network_mode(self, mode): """Sets the network mode to ``mode``""" if mode not in self.custom.conn_dict: raise KeyError("Mode %s not found" % mode) return self.send_at("AT+ZSNT=%d,0,%d" % self.custom.conn_dict[mode]) class ZTEWCDMACustomizer(WCDMACustomizer): """WCDMA customizer for ZTE devices""" async_regexp = re.compile(""" \r\n (?P\+Z[A-Z]{3,}):\s*(?P.*) \r\n""", re.VERBOSE) allowed_dict = ZTE_ALLOWED_DICT band_dict = {} conn_dict = ZTE_MODE_DICT cmd_dict = ZTE_CMD_DICT device_capabilities = [S.SIG_NETWORK_MODE, S.SIG_SMS_NOTIFY_ONLINE] signal_translations = { '+ZDONR': (None, None), '+ZPASR': (S.SIG_NETWORK_MODE, zte_new_conn_mode), '+ZUSIMR': (None, None), '+ZPSTM': (None, None), '+ZEND': (None, None), } wrapper_klass = ZTEWrapper class ZTEWCDMASIMClass(SIMBaseClass): """WCDMA SIM class for ZTE devices""" def __init__(self, sconn): super(ZTEWCDMASIMClass, self).__init__(sconn) def setup_sms(self): # Select SIM storage self.sconn.send_at('AT+CPMS="SM","SM","SM"') # Notification when a SMS arrives... self.sconn.set_sms_indication(2, 1, 0, 2, 0) # XXX: We have to set +CDSI indication as original ZTE devices don't # support +CDS mode. At some point we will have to implement # processing of +CDSI notifications in core # Sample notification # '+CDSI: "SR",50' # # Sample retrieval # AT+CPMS="SR";+CMGR=50;+CMGD=50;+CPMS="SM" # +CPMS: 1,100,1,15,1,15 # # +CMGR: ,,25 # 079144775810065006FD0C91449771624701117013908522401170139085624000 # # +CPMS: 0,100,1,15,1,15 # # OK # set PDU mode self.sconn.set_sms_format(0) class ZTEWCDMADevicePlugin(DevicePlugin): """WCDMA device plugin for ZTE devices""" sim_klass = ZTEWCDMASIMClass custom = ZTEWCDMACustomizer() wader-0.5.13/core/mal.py000066400000000000000000000405171257646610200147750ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2010 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Message Assembly Layer for Wader""" from time import mktime from twisted.internet import reactor from twisted.internet.defer import succeed, gatherResults, Deferred from twisted.python import log from messaging.sms import SmsDeliver from messaging.sms.wap import (extract_push_notification, is_a_wap_push_notification, is_mms_notification) from wader.common.aterrors import (CMSError314, SimBusy, SimNotStarted, SimFailure) from wader.common.encoding import pack_dbus_safe_string from wader.common.signals import SIG_MMS, SIG_SMS, SIG_SMS_COMP, SIG_SMS_DELV from wader.common.sms import Message from core.mms import dbus_data_to_mms STO_INBOX, STO_DRAFTS, STO_SENT = 1, 2, 3 # XXX: What should this threshold be? SMS_DATE_THRESHOLD = 5 MAL_RETRIES = 3 MAL_RETRY_TIMEOUT = 3 def debug(s): # Change this to remove debugging if 1: print s def should_fragment_be_assembled(sms, fragment): """Returns True if ``fragment`` can be assembled to ``sms``""" if sms.completed: # SMS is completed, no way to assemble it return False if sms.ref != fragment.ref: # different sms id return False if sms.datetime is not None and fragment.datetime is not None: # if datetime is present convert it to unix time time1 = mktime(sms.datetime.timetuple()) time2 = mktime(fragment.datetime.timetuple()) if abs(time1 - time2) > SMS_DATE_THRESHOLD: return False if sms.cnt != fragment.cnt: # number of parts return False if sms.number != fragment.number: # different sender return False if sms.csca != fragment.csca: # different SMSC return False debug("MAL: Assembling fragment %s with sms %s" % (fragment, sms)) return True class CacheIncoherenceError(Exception): """Raised upon a cache incoherence error""" class NotificationContainer(object): """ I am a WAP push notification container I keep a list with all the wap_push notifications and provide some operations on it """ def __init__(self, tx_id=None): self.notifications = [] self.tx_id = tx_id def add_notification(self, wap_push, notification): self.notifications.append((wap_push, notification)) def get_last_notification(self): """Returns the last received notification""" # comp function must return an int ret = sorted(self.notifications, lambda _, n: int(mktime(n[0].datetime.timetuple()))) # return the last element return ret[-1][1] class MessageAssemblyLayer(object): """I am a transparent layer to perform operations on concatenated SMS""" def __init__(self, wrappee): self.wrappee = wrappee self.last_sms_index = 0 self.last_wap_index = 0 self.sms_map = {} self.wap_map = {} self.sms_pending = [] self.cached = False def initialize(self, obj=None): debug("MAL::initialize obj: %s" % obj) if obj is not None: self.wrappee = obj # revert to initial state self.last_sms_index = self.last_wap_index = 0 self.sms_map = {} self.sms_pending = [] self.cached = False # populate sms cache return self._do_initialize() def _do_initialize(self): # init counter self.wrappee.state_dict['mal_init_retries'] = 0 deferred = Deferred() def list_sms(auxdef): def sim_busy_eb(failure): failure.trap(SimBusy, SimNotStarted, CMSError314) self.wrappee.state_dict['mal_init_retries'] += 1 if self.wrappee.state_dict['mal_init_retries'] > MAL_RETRIES: raise SimFailure("Could not initialize MAL") reactor.callLater(MAL_RETRY_TIMEOUT, list_sms, auxdef) d = self.list_sms() d.addErrback(sim_busy_eb) d.chainDeferred(auxdef) return auxdef return list_sms(deferred) def _do_add_sms(self, sms, indexes=None): """ Adds ``sms`` to the cache using ``indexes`` if defined It returns the logical index where it was stored """ #debug("MAL::_do_add_sms sms: %s indexes: %s" % (sms, indexes)) # save the real index if indexes is None if indexes: map(sms.real_indexes.add, indexes) else: sms.real_indexes.add(sms.index) #debug("MAL::_do_add_sms sms.real_indexes %s" % sms.real_indexes) # assign a new logical index self.last_sms_index += 1 sms.index = self.last_sms_index # reference the sms by this logical index self.sms_map[self.last_sms_index] = sms return self.last_sms_index def _add_sms(self, sms, emit=False): """ Adds ``sms`` to the cache It returns the logical index where it was stored """ debug("MAL::_add_sms: %s" % sms) if not sms.cnt: index = self._do_add_sms(sms) debug("MAL::_add_sms single part SMS added with " "logical index: %d" % index) # being a single part sms, completed == True if emit: for signal in [SIG_SMS, SIG_SMS_COMP]: self.wrappee.emit_signal(signal, index, True) return index else: for index, value in self.sms_map.iteritems(): if should_fragment_be_assembled(value, sms): # append the sms and emit the different signals completed = self.sms_map[index].append_sms(sms) debug("MAL::_add_sms multi part SMS with logical " "index %d, completed %s" % (index, completed)) # check if we have just assembled a WAP push notification if completed: notification = self.sms_map[index] if self._is_a_wap_push_notification(notification): if self._process_wap_push_notification(index, emit): debug("MAL::_add_sms MMS processed OK") # There's no need to return an index here as we # have been called by gen_cache and MMS has a # different index scheme than SMS. return # XXX: Must have been a non-MMS notification WAP # push, there's nothing we can do with those # presently. Leave them to be displayed as # SMS, although poorly, so that they may be # removed by the user if desired. if emit: # only emit signals in runtime, not startup self.wrappee.emit_signal(SIG_SMS, index, completed) if completed: self.wrappee.emit_signal(SIG_SMS_COMP, index, completed) # return sms logical index return index # this is the first fragment of this multipart sms, add it # to cache, emit signal and wait for the rest of fragments # to arrive. It returns the logical index where was stored index = self._do_add_sms(sms) if emit: self.wrappee.emit_signal(SIG_SMS, index, False) debug("MAL::_add_sms first part of a multi part SMS added with " "logical index %d" % index) return index def _after_ack_delete_notifications(self, _, index): try: container = self.wap_map.pop(index) except KeyError: debug("MessageAssemblyLayer::_after_ack_delete_notifications" " NotificationContainer %d does not exist" % index) return indexes = [] for wap_push, _ in container.notifications: indexes.extend(wap_push.real_indexes) ret = map(self.wrappee.do_delete_sms, indexes) return gatherResults(ret) def acknowledge_mms(self, index, extra_info): d = self.wrappee.do_acknowledge_mms(index, extra_info) d.addCallback(self._after_ack_delete_notifications, index) return d def delete_sms(self, index): """Deletes sms identified by ``index``""" debug("MAL::delete_sms: %d" % index) if index in self.sms_map: sms = self.sms_map.pop(index) ret = map(self.wrappee.do_delete_sms, sms.real_indexes) debug("MAL::delete_sms deleting %s" % sms.real_indexes) return gatherResults(ret) error = "SMS with logical index %d does not exist" raise CacheIncoherenceError(error % index) def download_mms(self, index, extra_info): container = self.wap_map[index] d = self.wrappee.do_download_mms(container.get_last_notification(), extra_info) return d def get_sms(self, index): """Returns the sms identified by ``index``""" if index in self.sms_map: return succeed(self.sms_map[index]) error = "SMS with logical index %d does not exist" raise CacheIncoherenceError(error % index) def list_available_mms_notifications(self): """Returns all the lingering sms wap push notifications""" ret = [] for index, container in self.wap_map.items(): notification = container.get_last_notification() ret.append((index, self._clean_headers(notification))) return succeed(ret) def _list_sms(self): ret = [] for sms in self.sms_map.values(): if sms.fmt != 0x04: ret.append(sms.to_dict()) else: # WAP Pushes are binary and may not be valid DBus strings _sms = sms.to_dict() _sms['text'] = pack_dbus_safe_string(_sms['text']) ret.append(_sms) return ret def list_sms(self): """Returns all the sms""" debug("MAL::list_sms") def gen_cache(messages): debug("MAL::list_sms::gen_cache") self.sms_map = {} for sms in messages: self._add_sms(sms) self.cached = True return self._list_sms() if self.cached: debug("MAL::list_sms::cached path") return succeed(self._list_sms()) d = self.wrappee.do_list_sms() d.addCallback(gen_cache) return d def list_sms_raw(self): """Returns all the raw sms, not assembled via the mal""" debug("MAL::list_sms_raw") return self.wrappee.do_list_sms() def send_mms(self, mms, extra_info): debug("MAL::send_mms: %s" % mms) d = self.wrappee.do_send_mms(dbus_data_to_mms(mms), extra_info) return d def send_sms(self, sms): debug("MAL::send_sms: %s" % sms) if not sms.status_request: return self.wrappee.do_send_sms(sms) d = self.wrappee.do_send_sms(sms) d.addCallback(self._save_sms_reference, sms) return d def send_sms_from_storage(self, index): debug("MAL::send_sms_from_storage: %d" % index) if index in self.sms_map: sms = self.sms_map.pop(index) indexes = sorted(sms.real_indexes) debug("MAL::send_sms_from_storage sending %s" % indexes) ret = map(self.wrappee.do_send_sms_from_storage, indexes) return gatherResults(ret) error = "SMS with logical index %d does not exist" raise CacheIncoherenceError(error % index) def save_sms(self, sms): """Saves ``sms`` in the cache memorizing the resulting indexes""" debug("MAL::save_sms: %s" % sms) d = self.wrappee.do_save_sms(sms) d.addCallback(lambda indexes: self._do_add_sms(sms, indexes)) d.addCallback(lambda logical_index: [logical_index]) return d def _save_sms_reference(self, indexes, sms): sms.status_references.extend(indexes) sms.status_reference = indexes[0] self.sms_pending.append(sms) return [sms.status_reference] def on_sms_delivery_report(self, pdu): """Executed when a SMS delivery report is received""" data = SmsDeliver(pdu).data sms = Message.from_dict(data) assert sms.is_status_report(), "SMS IS NOT STATUS REPORT" # XXX: O(N) here for _sms in self.sms_pending: if sms.ref in _sms.status_references: # one confirmation received _sms.status_references.remove(sms.ref) # no more status references? Then we are done, remove it from # the status_references list and emit signal if not _sms.status_references: self.sms_pending.remove(_sms) return self.wrappee.emit_signal(SIG_SMS_DELV, _sms.status_reference) break else: log.err("Received status report with " "unknown reference: %d" % sms.ref) def on_sms_notification(self, index): """Executed when a SMS notification is received""" debug("MAL::on_sms_notification: %d" % index) d = self.wrappee.do_get_sms(index) d.addCallback(self._add_sms, emit=True) return d def _is_a_wap_push_notification(self, sms): """Returns True if ``sms`` is a WAP push notification""" if sms.fmt != 0x04: return False return is_a_wap_push_notification(sms.text) def _process_wap_push_notification(self, index, emit): """ Processes WAP push notification identified by ``index`` If ``emit`` is True, it will emit a MMSReceived signal if this the first time we see this notification. """ notification = extract_push_notification(self.sms_map[index].text) if not is_mms_notification(notification): debug("MAL::_process_wap_push_notification: is not for MMS") return False wap_push = self.sms_map.pop(index) index = None new = False _from = notification.headers['From'] tx_id = notification.headers['Transaction-Id'] for i, container in self.wap_map.items(): if container.tx_id != tx_id: continue noti = container.get_last_notification() if _from == noti.headers['From']: index = i break else: # this is the first time we see this tx_id index = self.last_wap_index self.last_wap_index += 1 new = True container = self.wap_map.get(index, NotificationContainer(tx_id)) container.add_notification(wap_push, notification) self.wap_map[index] = container if emit and new: # emit the signal if this is the first time we # see this notification notification = container.get_last_notification() headers = self._clean_headers(notification) self.wrappee.emit_signal(SIG_MMS, index, headers) return True def _clean_headers(self, notification): """Clean ``headers`` so its safe to send the dict via DBus""" hdrs = notification.headers.copy() hdrs['Content-Type'] = notification.content_type return hdrs wader-0.5.13/core/middleware.py000066400000000000000000001536511257646610200163450ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """ Wrapper around :class:`~core.protocol.WCDMAProtocol` It basically provides error control and more high-level operations. N-tier folks can see this as a Business Logic class. """ from collections import deque import dbus import serial from gobject import timeout_add_seconds, source_remove from math import floor from time import time from twisted.python import log from twisted.internet import defer, reactor, task import wader.common.aterrors as E from wader.common.consts import (WADER_SERVICE, MDM_INTFACE, CRD_INTFACE, NET_INTFACE, USD_INTFACE, MM_NETWORK_BAND_ANY, MM_NETWORK_MODE_ANY, MM_NETWORK_MODE_LAST, MM_MODEM_STATE_DISABLED, MM_MODEM_STATE_ENABLING, MM_MODEM_STATE_ENABLED, MM_MODEM_STATE_SEARCHING, MM_MODEM_STATE_REGISTERED, MM_MODEM_STATE_DISCONNECTING, MM_MODEM_STATE_CONNECTING, MM_MODEM_STATE_CONNECTED, MM_GSM_ACCESS_TECH_GSM_COMPAT, MM_GSM_ACCESS_TECH_GPRS, MM_GSM_ACCESS_TECH_EDGE, MM_GSM_ACCESS_TECH_UMTS, MM_GSM_ACCESS_TECH_HSDPA, MM_GSM_ACCESS_TECH_HSUPA, MM_GSM_ACCESS_TECH_HSPA, MM_GSM_ACCESS_TECH_HSPA_PLUS, MM_GSM_ACCESS_TECH_LTE, MM_IP_METHOD_PPP) from wader.common.encoding import (from_ucs2, from_u, unpack_ucs2_bytes, pack_ucs2_bytes, check_if_ucs2, LATIN_EX_B, from_8bit_in_gsm_or_ts31101) import wader.common.exceptions as ex from wader.common.signals import SIG_CREG from wader.common.sms import Message from wader.common.utils import rssi_to_percentage from core.contact import Contact from core.mal import MessageAssemblyLayer from core.mms import (send_m_send_req, send_m_notifyresp_ind, get_payload) from core.oal import get_os_object from core.protocol import WCDMAProtocol from core.sim import (COM_READ_BINARY, EF_AD, EF_SPN, EF_ICCID, SW_OK, RETRY_ATTEMPTS, RETRY_TIMEOUT) class WCDMAWrapper(WCDMAProtocol): """ I am a wrapper around :class:`~core.protocol.WCDMAProtocol` Its main objective is to provide some error control on some operations and a cleaner API to deal with its results. """ def __init__(self, device): super(WCDMAWrapper, self).__init__(device) # unfortunately some methods require me to save some state # between runs. This dict contains 'em. self.state_dict = {} # message assembly layer (initted on do_enable_device) self.mal = MessageAssemblyLayer(self) self.signal_matchs = [] # timeout_add_seconds task ID self.__time = 0 self.__rx_bytes = 0 self.__tx_bytes = 0 self.stats_id = None # iface name self.iface = None self.apn_range = None self.caches = { 'signal': (0, 0), 'registration': (0, (0, '', '')), } def maybecached(self, name, fn, cblist): if self.caches[name][0] >= time(): # cache hasn't expired log.msg("get '%s' (cached path)" % name, system='CACHE') d = defer.succeed(self.caches[name][1]) else: log.msg("get '%s' (noncached path)" % name, system='CACHE') d = fn() for cb in cblist: d.addCallback(cb) d.addCallback(self.updatecache, name) return d def updatecache(self, value, name): CACHETIME = 5 self.caches[name] = (time() + CACHETIME, value) return value def emit_network_mode(self, value): """ Method to validate, convert ``value`` to dbus UInt32 and emit """ if value < 0 or value > MM_NETWORK_MODE_LAST: return self.device.exporter.NetworkMode(dbus.UInt32(value)) def emit_rssi(self, value): """ Method to validate, convert ``value`` to dbus UInt32 and emit """ if value < 0 or value > 100: return self.device.exporter.SignalQuality(dbus.UInt32(value)) def connect_to_signals(self): bus = dbus.SystemBus() device = bus.get_object(WADER_SERVICE, self.device.sconn.device.opath) sm = device.connect_to_signal(SIG_CREG, self.on_creg_cb) self.signal_matchs.append(sm) def clean_signals(self): while self.signal_matchs: sm = self.signal_matchs.pop() sm.remove() def __str__(self): return self.device.__remote_name__ def acknowledge_mms(self, index, extra_info): """ Acknowledges the Mms identified by ``index`` using ``extra_info`` """ return self.mal.acknowledge_mms(index, extra_info) def do_acknowledge_mms(self, index, extra_info): if 'wap2' not in extra_info: raise ValueError("Only WAP2.0 is supported at the moment") if 'mmsc' not in extra_info: raise ValueError("No mmsc key in %s" % extra_info) try: notification = self.mal.wap_map[index].get_last_notification() except IndexError: raise E.ExpiredNotification("Could not find " "notification %d" % index) d = send_m_notifyresp_ind(extra_info, notification.headers['Transaction-Id']) return d def add_contact(self, contact): """ Adds ``contact`` to the SIM and returns the index where was stored """ ucs2 = 'UCS2' in self.device.sim.charset name = pack_ucs2_bytes(contact.name) if ucs2 else from_u(contact.name) # common arguments for both operations (name and number) args = [name, from_u(contact.number)] if contact.index: # contact.index is set, user probably wants to overwrite an # existing contact args.append(contact.index) d = super(WCDMAWrapper, self).add_contact(*args) d.addCallback(lambda _: contact.index) return d def get_next_id_cb(index): args.append(index) d2 = super(WCDMAWrapper, self).add_contact(*args) # now we just fake add_contact's response and we return the index d2.addCallback(lambda _: index) return d2 # contact.index is not set, this means that we need to obtain the # first free slot on the phonebook and then add the contact d = self._get_next_contact_id() d.addCallback(get_next_id_cb) return d def cancel_ussd(self): """Cancels an ongoing USSD session""" d = super(WCDMAWrapper, self).cancel_ussd() def set_idle(result): self.device.set_property(USD_INTFACE, 'State', 'idle') return result[0].group('resp') d.addCallback(set_idle) return d def change_pin(self, oldpin, newpin): """Changes PIN from ``oldpin`` to ``newpin``""" d = super(WCDMAWrapper, self).change_pin(oldpin, newpin) d.addCallback(lambda result: result[0].group('resp')) return d def check_pin(self): """ Returns the SIM's auth state :raise SimPinRequired: Raised if SIM PIN is required :raise SimPukRequired: Raised if SIM PUK is required :raise SimPuk2Required: Raised if SIM PUK2 is required """ if self.device.quirks.get('needs_enable_before_pin_check', False): # Note: this device needs to be enabled before pin can be checked d = self.enable_radio(True) d.addCallback(lambda x: super(WCDMAWrapper, self).check_pin()) else: d = super(WCDMAWrapper, self).check_pin() def process_result(resp): result = resp[0].group('resp') if result == 'READY': return result elif result == 'SIM PIN': raise E.SimPinRequired() elif result == 'SIM PUK': raise E.SimPukRequired() elif result == 'SIM PUK2': raise E.SimPuk2Required() else: log.err("unknown authentication state %s" % result) d.addCallback(process_result) return d def delete_contact(self, index): """Deletes contact at ``index``""" d = super(WCDMAWrapper, self).delete_contact(index) d.addCallback(lambda result: result[0].group('resp')) return d def delete_sms(self, index): return self.mal.delete_sms(index) def do_delete_sms(self, index): """Deletes SMS at ``index``""" d = super(WCDMAWrapper, self).delete_sms(index) d.addCallback(lambda result: result[0].group('resp')) return d def download_mms(self, index, extra_info): """Downloads the Mms identified by ``index``""" return self.mal.download_mms(index, extra_info) def do_download_mms(self, notification, extra_info): uri = notification.headers['Content-Location'] return get_payload(uri, extra_info) def disable_echo(self): """Disables echo""" d = super(WCDMAWrapper, self).disable_echo() d.addCallback(lambda result: result[0].group('resp')) return d def enable_pin(self, pin, enable): """ Enables or disables PIN auth with ``pin`` according to ``enable`` """ def cache_and_return_response(response): self.device.set_property(CRD_INTFACE, 'PinEnabled', enable) return response[0].group('resp') d = super(WCDMAWrapper, self).enable_pin(pin, enable) d.addCallback(cache_and_return_response) return d def enable_echo(self): """ Enables echo Use this with caution as it might leave Wader on an unusable state """ d = super(WCDMAWrapper, self).enable_echo() d.addCallback(lambda result: result[0].group('resp')) return d def find_contacts(self, pattern): """ Returns all the `Contact` objects whose name matches ``pattern`` :rtype: list """ if 'UCS2' in self.device.sim.charset: pattern = pack_ucs2_bytes(pattern) d = super(WCDMAWrapper, self).find_contacts(pattern) d.addCallback(lambda matches: map(self._regexp_to_contact, matches)) return d def get_apns(self): """ Returns all the APNs in the SIM :rtype: list """ d = super(WCDMAWrapper, self).get_apns() d.addCallback(lambda resp: [(int(r.group('index')), r.group('apn')) for r in resp]) return d def get_apn_range(self): """ Returns the range of context IDs in the SIM :rtype: tuple """ d = super(WCDMAWrapper, self).get_apn_range() d.addCallback(lambda r: (int(r.group('lo4')), int(r.group('hi4')))) return d def get_band(self): """Returns the current band used""" raise NotImplementedError() def get_bands(self): """ Returns the available bands :rtype: list """ bands = self.custom.band_dict.keys() if MM_NETWORK_BAND_ANY in bands: bands.pop(MM_NETWORK_BAND_ANY) # cast it to UInt32 return defer.succeed(dbus.UInt32(sum(bands))) def get_card_model(self): """Returns the card model""" d = super(WCDMAWrapper, self).get_card_model() d.addCallback(lambda response: response[0].group('model')) return d def get_card_version(self): """Returns the firmware version""" d = super(WCDMAWrapper, self).get_card_version() d.addCallback(lambda response: response[0].group('version')) return d def get_charset(self): """Returns the current charset""" d = super(WCDMAWrapper, self).get_charset() d.addCallback(lambda response: response[0].group('lang')) return d def get_charsets(self): """ Returns the available charsets :rtype: list """ d = super(WCDMAWrapper, self).get_charsets() d.addCallback(lambda resp: [match.group('lang') for match in resp]) return d def get_contact(self, index): """Returns the contact at ``index``""" d = super(WCDMAWrapper, self).get_contact(index) d.addCallback(lambda match: self._regexp_to_contact(match[0])) return d def get_hardware_info(self): """Returns the manufacturer name, card model and firmware version""" dlist = [self.get_manufacturer_name(), self.get_card_model(), self.get_card_version()] return defer.gatherResults(dlist) def sim_access_restricted(self, command, fileid=None, p1=None, p2=None, p3=None, data=None, pathid=None): d = super(WCDMAWrapper, self).sim_access_restricted( command, fileid, p1, p2, p3, data, pathid) def cb(response): try: sw1 = int(response[0].group('sw1')) except (IndexError, TypeError, ValueError): sw1 = None try: sw2 = int(response[0].group('sw2')) except (IndexError, TypeError, ValueError): sw2 = None if (sw1 not in SW_OK) or (sw1 == 0x90 and sw2 != 0): # Status error. raise E.General('Bad Status response for AT+CRSM') data = response[0].group('response') return (sw1, sw2, data) d.addCallback(cb) return d def get_iccid(self): """Returns ICC identification number""" d = self.sim_access_restricted(COM_READ_BINARY, EF_ICCID, 0, 0, 10) def get_iccid_cb(response): sw1, sw2, data = response if data is None: raise E.General('ICCID not available') if len(data) != 20: raise E.General('ICCID length not correct') # Parse BCD F padded string. result = '' i = 0 while (i + 1 < len(data)): msd = data[i] lsd = data[i + 1] i += 2 if lsd in ['f', 'F']: break result += lsd if msd in ['f', 'F']: break result += msd return result d.addCallback(get_iccid_cb) return d def get_imei(self): """Returns the IMEI""" d = super(WCDMAWrapper, self).get_imei() d.addCallback(lambda response: response[0].group('imei')) return d def get_imsi(self): """Returns the IMSI""" d = super(WCDMAWrapper, self).get_imsi() d.addCallback(lambda response: response[0].group('imsi')) return d def get_ip4_config(self): """Returns the IP4Config info related to IpMethod""" raise NotImplementedError() def get_manufacturer_name(self): """Returns the manufacturer name""" d = super(WCDMAWrapper, self).get_manufacturer_name() d.addCallback(lambda response: response[0].group('name')) return d def _get_netreg_info(self, status): # Ugly but it works. The naive approach with DeferredList won't work # as the call order is not guaranteed resp = [status] def get_netinfo_cb(info): new = info[1] cur = self.device.get_property(NET_INTFACE, 'AccessTechnology') # Don't stamp on the value provided by a richer method if (new == MM_GSM_ACCESS_TECH_GPRS and cur == MM_GSM_ACCESS_TECH_EDGE) or \ (new == MM_GSM_ACCESS_TECH_UMTS and cur in [MM_GSM_ACCESS_TECH_HSDPA, MM_GSM_ACCESS_TECH_HSUPA, MM_GSM_ACCESS_TECH_HSPA, MM_GSM_ACCESS_TECH_HSPA_PLUS]): self.device.set_property(NET_INTFACE, 'AccessTechnology', cur) else: self.device.set_property(NET_INTFACE, 'AccessTechnology', new) return resp.append(info[0]) def get_netinfo_eb(failure): failure.trap(E.NoNetwork, ex.LimitedServiceNetworkError) resp.append('') d = self.get_network_info('numeric') d.addCallback(get_netinfo_cb) d.addErrback(get_netinfo_eb) d.addCallback(lambda _: self.get_network_info('name')) d.addCallback(get_netinfo_cb) d.addErrback(get_netinfo_eb) d.addCallback(lambda _: tuple(resp)) return d def _get_netreg_info_emit(self, _reginfo): """Convert type and emit RegistrationInfo signal""" if self.device.status <= MM_MODEM_STATE_REGISTERED: if _reginfo[0] in [1, 5]: self.device.set_status(MM_MODEM_STATE_REGISTERED) else: self.device.set_status(MM_MODEM_STATE_SEARCHING) reginfo = (dbus.UInt32(_reginfo[0]), _reginfo[1], _reginfo[2]) self.device.exporter.RegistrationInfo(*reginfo) return reginfo def get_netreg_info(self): """Get the registration status and the current operator""" return self.maybecached('registration', self.get_netreg_status, [lambda info: info[1], self._get_netreg_info, self._get_netreg_info_emit]) def on_creg_cb(self, status): """Callback for +CREG notifications""" d = defer.succeed(status) d.addCallback(self._get_netreg_info) d.addCallback(self._get_netreg_info_emit) d.addCallback(self.updatecache, 'registration') return d def get_netreg_status(self): """Returns a tuple with the network registration status""" d = super(WCDMAWrapper, self).get_netreg_status() def convert_cb(resp): # convert them to int return int(resp[0].group('mode')), int(resp[0].group('status')) d.addCallback(convert_cb) return d def get_network_info(self, _type=None): """ Returns the network info (a.k.a AT+COPS?) The response will be a tuple as (OperatorName, ConnectionType) if it returns a (None, None) that means that some error occurred while obtaining the info. The class that requested the info should take care of insisting before this problem. This method will convert numeric network IDs to alphanumeric. """ d = super(WCDMAWrapper, self).get_network_info(_type) def get_net_info_cb(netinfo): """ Returns a (Networkname, ConnType) tuple It returns None if there's no info """ if not netinfo: return None netinfo = netinfo[0] if netinfo.group('error'): # this means that we've received a response like # +COPS: 0 which means that we don't have network temporaly # we should raise an exception here raise E.NoNetwork() # TS 27007 got updated as of 10.4 _map = { '0': MM_GSM_ACCESS_TECH_GPRS, # strictly GSM '1': MM_GSM_ACCESS_TECH_GSM_COMPAT, '2': MM_GSM_ACCESS_TECH_UMTS, # strictly UTRAN '3': MM_GSM_ACCESS_TECH_EDGE, '4': MM_GSM_ACCESS_TECH_HSDPA, '5': MM_GSM_ACCESS_TECH_HSUPA, '6': MM_GSM_ACCESS_TECH_HSPA, '7': MM_GSM_ACCESS_TECH_LTE, } conn_type = _map.get(netinfo.group('status')) netname = netinfo.group('netname') if netname in ['Limited Service', pack_ucs2_bytes('Limited Service')]: raise ex.LimitedServiceNetworkError # netname can be in UCS2, as a string, or as a network id (int) if check_if_ucs2(netname): return unpack_ucs2_bytes(netname), conn_type else: # now can be either a string or a network id (int) try: netname = int(netname) except ValueError: # we got a string ID return netname, conn_type # if we have arrived here, that means that the network id # is a five, six or seven digit integer return str(netname), conn_type d.addCallback(get_net_info_cb) return d def get_network_mode(self): """Returns the current network mode""" raise NotImplementedError() def get_network_modes(self): """Returns the supported network modes""" modes = self.custom.conn_dict.copy() if MM_NETWORK_MODE_ANY in modes: modes.pop(MM_NETWORK_MODE_ANY) return defer.succeed(sum(modes.keys())) def get_network_names(self): """ Performs a network search :rtype: list of :class:`NetworkOperator` """ d = super(WCDMAWrapper, self).get_network_names() d.addCallback(lambda resp: [NetworkOperator(*match.groups()) for match in resp]) return d def _get_free_contact_ids(self): """Returns a deque with the not used contact ids""" def list_contacts_cb(contacts): if not contacts: return deque(range(1, self.device.sim.size)) busy_ids = [contact.index for contact in contacts] free = set(range(1, self.device.sim.size)) ^ set(busy_ids) return deque(list(free)) def list_contacts_eb(failure): failure.trap(E.NotFound, E.General) return deque(range(1, self.device.sim.size)) d = self.list_contacts() d.addCallbacks(list_contacts_cb, list_contacts_eb) return d def _get_next_contact_id(self): """ Returns the next unused contact id Provides some error control and won't fail if sim.size is None as the card might be a bit difficult """ def do_get_it(): d = self._get_free_contact_ids() d.addCallback(lambda free: free.popleft()) return d if self.device.sim.size and self.device.sim.size != 0: return do_get_it() deferred = defer.Deferred() self.state_dict['phonebook_retries'] = 0 def get_it(auxdef=None): def get_phonebook_size_cb(size): self.device.sim.size = size d = do_get_it() d.chainDeferred(deferred) def get_phonebook_size_eb(failure): self.state_dict['phonebook_retries'] += 1 if self.state_dict['phonebook_retries'] > RETRY_ATTEMPTS: raise RuntimeError("Could not obtain phonebook size") reactor.callLater(RETRY_TIMEOUT, get_it, auxdef) d = self.get_phonebook_size() d.addCallback(get_phonebook_size_cb) d.addErrback(get_phonebook_size_eb) return auxdef return get_it(deferred) def get_operator_id(self): """ Returns the ID of the network operator that issued the SIM card, formatted as a 5 or 6-digit MCC/MNC code (ex "310410"). :raise General: When MCC+MNC cannot be retrieved. """ d = defer.Deferred() # Another way to handle global variables. d.imsi = None d.mnc_length = None def get_op_id_eb(failure): log.msg("get_operator_id FAILURE: %s" % failure.value) failure.raiseException() d.addErrback(get_op_id_eb) d_mnc = self.sim_access_restricted(COM_READ_BINARY, EF_AD, 0, 0, 4) def get_op_id_mnc_digits_cb(response): sw1, sw2, number = response if number is None or len(number) < 8: raise E.General('Bad MNC length response') number = int(number[6:8], 16) if number in range(2, 5): # We got MNC number of digits right. return number else: raise E.General('Incorrect MNC length') def get_op_id_mnc_digits_eb(failure): log.msg("get_operator_id mnc_digits FAILURE %s" % failure.value) failure.raiseException() d_mnc.addCallback(get_op_id_mnc_digits_cb) d_mnc.addCallback(d.callback) d_mnc.addErrback(get_op_id_mnc_digits_eb) d_mnc.addErrback(d.errback) def store_mnc_length(mnc_length): self.mnc_length = mnc_length d_imsi = self.get_imsi() d_imsi.addErrback(get_op_id_imsi_eb) d_imsi.addErrback(d.errback) return d_imsi d.addCallback(store_mnc_length) def get_op_id_imsi_eb(failure): log.msg("get_operator_id imsi FAILURE %s" % failure.value) failure.raiseException() def store_imsi(imsi): self.imsi = imsi return None d.addCallback(store_imsi) def get_op_id_cb(response): number = self.mnc_length # An integer. imsi = self.imsi if number is None or imsi is None: raise E.General('Bad MNC length or IMSI') length = number + 3 if len(imsi) < length: raise E.General('IMSI length too short') return imsi[0:length] d.addCallback(get_op_id_cb) return d def get_phonebook_size(self): """Returns the phonebook size""" d = super(WCDMAWrapper, self).get_phonebook_size() d.addCallback(lambda resp: int(resp[0].group('size'))) return d def get_pin_status(self): """Returns 1 if PIN auth is active and 0 if its not""" def pinreq_errback(failure): failure.trap(E.SimPinRequired) return 1 def aterror_eb(failure): failure.trap(E.General) # return the failure or wont work return failure d = super(WCDMAWrapper, self).get_pin_status() d.addCallback(lambda response: int(response[0].group('status'))) d.addErrback(pinreq_errback) d.addErrback(aterror_eb) return d def get_radio_status(self): """Returns whether the radio is enabled or disabled""" d = super(WCDMAWrapper, self).get_radio_status() d.addCallback(lambda resp: int(resp[0].group('status'))) return d def get_roaming_ids(self): """Returns the network ids stored in the SIM to roam""" # a.k.a. AT+CPOL? d = super(WCDMAWrapper, self).get_roaming_ids() d.addCallback(lambda raw: [BasicNetworkOperator( obj.group('netid')) for obj in raw if int(obj.group('netid'))]) return d def get_signal_quality(self): """Returns the signal level quality""" if self.device.status < MM_MODEM_STATE_ENABLED: return defer.succeed(0) return self.maybecached('signal', super(WCDMAWrapper, self).get_signal_quality, [lambda response: int(response[0].group('rssi')), rssi_to_percentage]) def get_sms(self, index): return self.mal.get_sms(index) def do_get_sms(self, index): """ Returns a ``Message`` object representing the SMS at ``index`` """ d = super(WCDMAWrapper, self).get_sms(index) def get_sms_cb(rawsms): try: sms = Message.from_pdu(rawsms[0].group('pdu')) sms.where = int(rawsms[0].group('where')) sms.index = index except IndexError: # handle bogus CMTI notifications, see #180 return None return sms d.addCallback(get_sms_cb) return d def get_sms_format(self): """ Returns 1 if SMS format is text and 0 if SMS format is PDU """ d = super(WCDMAWrapper, self).get_sms_format() d.addCallback(lambda response: int(response[0].group('format'))) return d def get_smsc(self): """Returns the SMSC number stored in the SIM""" d = super(WCDMAWrapper, self).get_smsc() def get_smsc_cb(response): try: smsc = response[0].group('smsc') if not smsc.startswith('+'): if check_if_ucs2(smsc): smsc = from_u(unpack_ucs2_bytes(smsc)) return smsc except KeyError: raise E.NotFound() d.addCallback(get_smsc_cb) return d def get_spn(self): """ Returns SPN Service Provider Name from SIM. """ # AT+CRSM=176,28486,0,1,16 d = self.sim_access_restricted(COM_READ_BINARY, EF_SPN, 0, 1, 16) def get_spn_cb(response): sw1, sw2, spn = response if spn is None: raise E.General('SPN not returned.') if spn: spn = from_8bit_in_gsm_or_ts31101(spn) return spn or '' d.addCallback(get_spn_cb) return d def list_available_mms(self): return self.mal.list_available_mms_notifications() def list_contacts(self): """ Returns all the contacts in the SIM :rtype: list """ def not_found_eb(failure): failure.trap(E.NotFound, E.InvalidIndex, E.General) return [] def get_them(ignored=None): d = super(WCDMAWrapper, self).list_contacts() d.addCallback(lambda matches: map(self._regexp_to_contact, matches)) d.addErrback(not_found_eb) return d if self.device.sim.size: return get_them() else: d = self._get_next_contact_id() d.addCallback(get_them) return d def _regexp_to_contact(self, match): """ Returns a :class:`core.contact.Contact` out of ``match`` :type match: ``re.MatchObject`` """ name = match.group('name') if self.device.sim.charset == 'UCS2': name = from_ucs2(name) number = match.group('number') index = int(match.group('id')) return Contact(name, number, index=index) def list_sms(self): return self.mal.list_sms() def do_list_sms(self): """ Returns all the SMS in the SIM card :rtype: list """ d = super(WCDMAWrapper, self).list_sms() def get_all_sms_cb(messages): sms_list = [] for rawsms in messages: try: sms = Message.from_pdu(rawsms.group('pdu')) sms.index = int(rawsms.group('id')) sms.where = int(rawsms.group('where')) sms_list.append(sms) except ValueError: log.err(ex.MalformedSMSError, "Malformed PDU: %s" % rawsms.group('pdu')) return sms_list d.addCallback(get_all_sms_cb) return d def save_sms(self, sms): return self.mal.save_sms(sms) def do_save_sms(self, sms): """ Stores ``sms`` and returns a list of indexes ``sms`` might span several messages if it is a multipart SMS """ save_sms = super(WCDMAWrapper, self).save_sms ret = [save_sms(p.pdu, p.length) for p in sms.to_pdu(store=True)] d = defer.gatherResults(ret) # the order is important! You need to run gatherResults and add # the callback to its result, not the other way around! d.addCallback(lambda response: [int(resp[0].group('index')) for resp in response]) return d def send_at(self, atstr, name='send_at', callback=None): """Sends an arbitrary AT string ``atstr``""" d = super(WCDMAWrapper, self).send_at(atstr, name=name) if callback is None: d.addCallback(lambda response: response[0].group('resp')) else: d.addCallback(callback) return d def send_pin(self, pin): """ Sends ``pin`` to authenticate Most devices need some time to settle after a successful auth it is the caller's responsability to give at least 15 seconds to the device to settle, this time varies from device to device """ from core.startup import attach_to_serial_port d = attach_to_serial_port(self.device) d.addCallback(lambda _: super(WCDMAWrapper, self).send_pin(pin)) d.addCallback(lambda response: response[0].group('resp')) return d def send_puk(self, puk, pin): """ Send ``puk`` and ``pin`` to authenticate Most devices need some time to settle after a successful auth it is the caller's responsability to give at least 15 seconds to the device to settle, this time varies from device to device """ d = super(WCDMAWrapper, self).send_puk(puk, pin) d.addCallback(lambda response: response[0].group('resp')) return d def send_mms(self, mms, extra_info): """Send ``mms`` and returns the Message-Id""" return self.mal.send_mms(mms, extra_info) def do_send_mms(self, mms, extra_info): if 'wap2' not in extra_info: raise ValueError("Only WAP2.0 is supported at the moment") if 'mmsc' not in extra_info: raise ValueError("No mmsc key in %s" % extra_info) return send_m_send_req(extra_info, mms) def send_sms(self, sms): """ Sends ``sms`` and returns the indexes ``sms`` might span several messages if it is a multipart SMS """ return self.mal.send_sms(sms) def do_send_sms(self, sms): def send_sms_cb(response): return int(response[0].group('index')) ret = [] for pdu in sms.to_pdu(): d = super(WCDMAWrapper, self).send_sms(pdu.pdu, pdu.length) d.addCallback(send_sms_cb) ret.append(d) return defer.gatherResults(ret) def send_sms_from_storage(self, index): """Sends the SMS stored at ``index`` and returns the new index""" return self.mal.send_sms_from_storage(index) def do_send_sms_from_storage(self, index): d = super(WCDMAWrapper, self).send_sms_from_storage(index) d.addCallback(lambda response: int(response[0].group('index'))) return d def send_ussd(self, ussd, force_ascii=False, loose_charset_check=False): """Sends the ussd command ``ussd``""" def convert_response(response): index = response[0].group('index') if index == '1': self.device.set_property(USD_INTFACE, 'State', 'user-response') else: self.device.set_property(USD_INTFACE, 'State', 'idle') resp = response[0].group('resp') if resp is None: return "" # returning the Empty string is valid if not check_if_ucs2(resp, limit=LATIN_EX_B): if 'UCS2' in self.device.sim.charset and \ not loose_charset_check: raise E.MalformedUssdPduError(resp) else: return resp else: try: return unpack_ucs2_bytes(resp) except (TypeError, UnicodeDecodeError): raise E.MalformedUssdPduError(resp) def reset_state(failure): if self.device.get_property(USD_INTFACE, 'State') != 'idle': self.device.set_property(USD_INTFACE, 'State', 'idle') failure.raiseException() # re-raise if 'UCS2' in self.device.sim.charset and not force_ascii: ussd = pack_ucs2_bytes(ussd) d = super(WCDMAWrapper, self).send_ussd(str(ussd)) d.addCallback(convert_response) d.addErrback(reset_state) return d def set_allowed_mode(self, mode): raise NotImplementedError("Implement it in the device family wrapper") def set_apn(self, apn): """Sets the APN to ``apn``""" def process_apns(apns, the_apn): for _index, _apn in apns: if _apn == the_apn: self.state_dict['conn_id'] = _index return defer.succeed('OK') try: conn_id = max([idx for idx, _ in apns]) + 1 if conn_id < self.apn_range[0] or conn_id > self.apn_range[1]: raise ValueError except (ValueError, TypeError): conn_id = self.apn_range[0] self.state_dict['conn_id'] = conn_id d = super(WCDMAWrapper, self).set_apn(conn_id, the_apn) d.addCallback(lambda response: response[0].group('resp')) return d d = self.get_apns() d.addCallback(process_apns, apn) return d def set_band(self, band): """Sets the device band to ``band``""" raise NotImplementedError() def set_charset(self, charset): """Sets the SIMs charset to ``charset``""" d = super(WCDMAWrapper, self).set_charset(charset) d.addCallback(lambda ignored: self.device.sim.set_charset(charset)) return d def set_error_level(self, level): """Sets the modem's error reporting level to ``level``""" d = super(WCDMAWrapper, self).set_error_level(level) d.addCallback(lambda response: response[0].group('resp')) return d def set_network_mode(self, mode): """Sets the network mode to ``mode``""" raise NotImplementedError() def enable_radio(self, enable): """ Enables the radio according to ``enable`` It will not enable it if its already enabled and viceversa """ def check_if_necessary(status): if (status == 1 and enable) or (status == 0 and not enable): return defer.succeed('OK') d = super(WCDMAWrapper, self).enable_radio(enable) d.addCallback(lambda response: response[0].group('resp')) return d d = self.get_radio_status() d.addCallback(check_if_necessary) return d def set_sms_format(self, _format=0): """Sets PDU mode or text mode in the SIM""" d = super(WCDMAWrapper, self).set_sms_format(_format) d.addCallback(lambda response: response[0].group('resp')) return d def set_smsc(self, smsc): """Sets the SIMS's SMSC number to ``smsc``""" if 'UCS2' in self.device.sim.charset: smsc = pack_ucs2_bytes(smsc) d = super(WCDMAWrapper, self).set_smsc(smsc) d.addCallback(lambda response: response[0].group('resp')) return d # some high-level methods exported over DBus def init_properties(self): # XXX: Implement UnlockRetries self.device.set_property(MDM_INTFACE, 'UnlockRetries', dbus.UInt32(999)) # There's no way to query this, so we have to assume :-( self.device.set_property(USD_INTFACE, 'State', 'idle') d = self.get_bands() d.addCallback(lambda bands: self.device.set_property(CRD_INTFACE, 'SupportedBands', dbus.UInt32(bands))) d.addCallback(lambda _: self.get_network_modes()) d.addCallback(lambda modes: self.device.set_property(CRD_INTFACE, 'SupportedModes', dbus.UInt32(modes))) d.addCallback(lambda _: self.get_pin_status()) d.addCallback(lambda active: self.device.set_property(CRD_INTFACE, 'PinEnabled', dbus.Boolean(active))) d.addCallback(lambda _: self.get_imei()) d.addCallback(lambda imei: self.device.set_property(MDM_INTFACE, 'EquipmentIdentifier', imei)) d.addCallback(lambda _: self.get_iccid()) def iccid_eb(failure): failure.trap(E.General) self.device.set_property(CRD_INTFACE, 'SimIdentifier', ''), d.addCallbacks(lambda iccid: self.device.set_property(CRD_INTFACE, 'SimIdentifier', iccid), iccid_eb) return d def get_simple_status(self): """Returns the status for o.fd.MM.Modem.Simple.GetStatus""" if self.device.status < MM_MODEM_STATE_ENABLED: return defer.succeed(dict(state=self.device.status)) def get_simple_status_cb((sigstrength, netinfo, band, net_mode)): return dict(state=self.device.status, signal_quality=sigstrength, operator_code=netinfo[1], operator_name=netinfo[2], band=band, network_mode=net_mode) deferred_list = [] deferred_list.append(self.get_signal_quality()) deferred_list.append(self.get_netreg_info()) deferred_list.append(self.get_band()) deferred_list.append(self.get_network_mode()) d = defer.gatherResults(deferred_list) d.addCallback(get_simple_status_cb) d.addErrback(log.err) return d def connect_simple(self, settings): """Connects with the given ``settings``""" if self.device.status == MM_MODEM_STATE_CONNECTED: # this cannot happen raise E.Connected("we are already connected") if self.device.status == MM_MODEM_STATE_CONNECTING: raise E.SimBusy("we are already connecting") def connect_eb(failure): log.msg("connect_simple errorback") if self.device.status >= MM_MODEM_STATE_REGISTERED: self.device.set_status(MM_MODEM_STATE_REGISTERED) failure.raiseException() # re-raise simplesm = self.device.custom.simp_klass(self.device, settings) d = simplesm.start_simple() d.addCallback(lambda _: self.device.set_status(MM_MODEM_STATE_CONNECTED)) d.addErrback(connect_eb) return d def connect_to_internet(self, settings): # Note: this is called by: # 1/ connect_simple via simple state machine # 2/ directly by Connect() dbus method self.device.set_property(NET_INTFACE, 'LastApn', unicode(settings.get('apn', ''))) ip_method = self.device.get_property(MDM_INTFACE, 'IpMethod') if ip_method == MM_IP_METHOD_PPP: d = self.connect_to_internet_ppp(settings) else: d = self.connect_to_internet_hso(settings) d.addCallback(self.start_traffic_monitoring) return d def connect_to_internet_hso(self, settings): username = settings.get('username', '') password = settings.get('password', '') auth = settings.get('allowed_auth', None) d = self.hso_authenticate(username, password, auth) d.addCallback(lambda _: self.hso_connect()) return d def connect_to_internet_ppp(self, settings): """Opens data port and dials ``settings['number']`` in""" if self.device.status == MM_MODEM_STATE_CONNECTED: # this cannot happen raise E.Connected("we are already connected") if self.device.status == MM_MODEM_STATE_CONNECTING: raise E.SimBusy("we are already connecting") self.device.set_status(MM_MODEM_STATE_CONNECTING) # open the data port port = self.device.ports.dport # this will raise a SerialException if port is busy port.obj = serial.Serial(port.path) port.obj.flush() # send ATDT and convert number to string as pyserial does # not like to write unicode to serial ports number = settings.get('number') d = defer.maybeDeferred(port.obj.write, "ATDT%s\r\n" % str(number)) # we should detect error or success here and set state return d def disconnect_from_internet(self): """Disconnects the modem""" ip_method = self.device.get_property(MDM_INTFACE, 'IpMethod') if ip_method == MM_IP_METHOD_PPP: d = self.disconnect_from_internet_ppp() else: d = self.disconnect_from_internet_hso() d.addCallback(self.stop_traffic_monitoring) return d def disconnect_from_internet_hso(self): # NM usually issues disconnect as part of a connect sequence if self.device.status < MM_MODEM_STATE_CONNECTED: return defer.succeed(True) return self.hso_disconnect() def disconnect_from_internet_ppp(self): """Disconnects the modem temporarily lowering the DTR""" # NM usually issues disconnect as part of a connect sequence if self.device.status < MM_MODEM_STATE_CONNECTED: return defer.succeed(True) port = self.device.ports.dport if not port.obj.isOpen(): raise AttributeError("Data serial port is not open") self.device.set_status(MM_MODEM_STATE_DISCONNECTING) # XXX: should check that we did stop the connection and set status def restore_speed(speed): try: port.obj.setBaudrate(speed) except serial.SerialException: pass port.obj.close() # XXX: perhaps we should check the registration status here if self.device.status > MM_MODEM_STATE_REGISTERED: self.device.set_status(MM_MODEM_STATE_REGISTERED) return True # lower and raise baud speed speed = port.obj.getBaudrate() try: port.obj.setBaudrate(0) except serial.SerialException: pass # restore the speed in .1 seconds return task.deferLater(reactor, .1, restore_speed, speed) def get_stats(self): """ Returns a tuple with the connection statistics for this dialer :return: (in_bytes, out_bytes) """ if self.iface is not None: now = time() rx_bytes, tx_bytes = get_os_object().get_iface_stats(self.iface) # if any of these three are not 0, it means that this is at # least the second time this method is executed, thus we # should have cached meaningful data if self.__rx_bytes or self.__tx_bytes or self.__time: rx_delta = rx_bytes - self.__rx_bytes tx_delta = tx_bytes - self.__tx_bytes interval = now - self.__time raw_rx_rate = int(floor(rx_delta / interval)) raw_tx_rate = int(floor(tx_delta / interval)) rx_rate = raw_rx_rate if raw_rx_rate >= 0 else 0 tx_rate = raw_tx_rate if raw_tx_rate >= 0 else 0 else: # first time this is executed, we cannot reliably compute # the rate. It is better to lie just once rx_rate = tx_rate = 0 self.__rx_bytes, self.__tx_bytes = rx_bytes, tx_bytes self.__time = now return rx_bytes, tx_bytes, rx_rate, tx_rate def register_with_netid(self, netid): """ I will try my best to register with ``netid`` If ``netid`` is an empty string, I will register with my home network """ netr_klass = self.device.custom.netr_klass netsm = netr_klass(self.device.sconn, netid) return netsm.start_netreg() def start_traffic_monitoring(self, x=None): ip_method = self.device.get_property(MDM_INTFACE, 'IpMethod') if ip_method == MM_IP_METHOD_PPP: self.iface = 'ppp0' # XXX: don't hardcode to first PPP instance else: self.iface = self.device.get_property(MDM_INTFACE, 'Device') if self.stats_id is None: self.stats_id = timeout_add_seconds(1, self._emit_dial_stats) return x def stop_traffic_monitoring(self, x=None): # remove the emit stats task if self.stats_id is not None: source_remove(self.stats_id) self.stats_id = None return x def enable_device(self, enable): """ I enable or disable myself according to ``enable`` If enable is True, I check the auth state of a device and will try to initialize it. Otherwise I will disable myself """ if enable: return self._do_enable_device() else: return self._do_disable_device() def _do_disable_device(self): self.clean_signals() if self.device.status == MM_MODEM_STATE_CONNECTED: def on_disconnect_from_internet(_): if self.device.status >= MM_MODEM_STATE_REGISTERED: self.device.set_status(MM_MODEM_STATE_REGISTERED) self.device.close() d = self.disconnect_from_internet() d.addCallback(on_disconnect_from_internet) d.addErrback(log.err) return d if self.device.status >= MM_MODEM_STATE_ENABLED: self.device.close() return defer.succeed(None) def _do_enable_device(self): if self.device.status >= MM_MODEM_STATE_ENABLED: return defer.succeed(self.device) if self.device.status == MM_MODEM_STATE_ENABLING: raise E.SimBusy() self.device.set_status(MM_MODEM_STATE_ENABLING) def signals(resp): self.connect_to_signals() # XXX: This netreg notification seems to be unrelated to enable, # perhaps it should be moved? self.device.sconn.set_netreg_notification(1) if self.device.status < MM_MODEM_STATE_ENABLED: self.device.set_status(MM_MODEM_STATE_ENABLED) return resp def setdefaults(resp): def apn_range_cb(result): self.apn_range = result return resp def apn_range_eb(e): self.apn_range = (1, 3) return resp d = self.device.sconn.get_apn_range() d.addCallback(apn_range_cb) d.addErrback(apn_range_eb) return d from core.startup import attach_to_serial_port def process_device_and_initialize(device): self.device = device auth_klass = self.device.custom.auth_klass authsm = auth_klass(self.device) def set_status(failure): self.device.set_status(MM_MODEM_STATE_DISABLED) failure.raiseException() # re-raise d = authsm.start_auth() # if auth is ready, the device will initialize straight away # if auth isn't ready the callback chain won't be executed and # will just return the given exception d.addErrback(set_status) d.addCallback(self.device.initialize) d.addCallback(setdefaults) d.addCallback(signals) return d d = attach_to_serial_port(self.device) d.addCallback(process_device_and_initialize) return d def _check_initted_device(self, result): """ To be executed after successful authentication over DBus Network Manager calls this via SendPin() even when not performing an Enable or SimpleConnect, it's just done as part of noticing a new device has appeared. So after we have unlocked we save a timestamp so that any subsequent initialisation can check if the requisite settling DELAY has elapsed, but we can't continue blindly to initialisation as used to be the case. """ self.device.set_property(MDM_INTFACE, 'UnlockRequired', '') self.device.set_authtime(time()) return result def _emit_dial_stats(self): stats = self.get_stats() if stats is not None: self.device.exporter.DialStats(stats) # make sure this is repeatedly called return True class BasicNetworkOperator(object): """A Network operator with a netid""" def __init__(self, netid): super(BasicNetworkOperator, self).__init__() self.netid = from_ucs2(netid) def __repr__(self): return '' % self.netid def __cmp__(self, o): try: a = int(self.netid) except (AttributeError, NameError, TypeError, ValueError): a = None try: b = int(o.netid) except (AttributeError, NameError, TypeError, ValueError): b = None return cmp(a, b) def __eq__(self, o): return self.netid == o.netid def __ne__(self, o): return not self.__eq__(o) class NetworkOperator(BasicNetworkOperator): """I represent a network operator on a mobile network""" def __init__(self, stat, long_name, short_name, netid, rat): super(NetworkOperator, self).__init__(netid) self.stat = int(stat) self.long_name = from_ucs2(long_name) self.short_name = from_ucs2(short_name) self.rat = int(rat) def __repr__(self): args = (self.long_name, self.netid) return '' % args wader-0.5.13/core/mms.py000066400000000000000000000125361257646610200150200ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2010 Warp Networks, S.L. # Author: Pablo Martí # # 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. """MMS related classes and functions""" from array import array from cStringIO import StringIO import socket import dbus from twisted.internet import threads from twisted.python import log from messaging.mms.message import MMSMessage, DataPart from wader.common.aterrors import ExpiredNotification def remove_headers_and_convert_to_array(payload): _, data = payload.split('\r\n\r\n') return array("B", data) def mms_to_dbus_data(mms): """Converts ``mms`` to a tuple ready to be sent via DBus""" headers = {} data_parts = [] # Convert headers for key, val in mms.headers.items(): if key == 'Content-Type': headers[key] = val[0] else: headers[key] = val del headers['Date'] # Set up data for data_part in mms.data_parts: part = {'Content-Type': data_part.content_type, 'data': dbus.ByteArray(data_part.data)} if data_part.headers['Content-Type'][1]: part['parameters'] = data_part.headers['Content-Type'][1] data_parts.append(part) return headers, data_parts def dbus_data_to_mms(headers, data_parts): """Returns a `MMSMessage` out of ``dbus_data``""" mms = MMSMessage() content_type = '' for key, val in headers.items(): if key == 'Content-Type': content_type = val else: mms.headers[key] = val mms.content_type = content_type # add data parts for data_part in data_parts: content_type = data_part['Content-Type'] data = array("B", data_part['data']) parameters = data_part.get('parameters', {}) dp = DataPart() dp.set_data(data, content_type, parameters) # XXX: MMS message with no SMIL support # Content-Type: application/vnd.wap.multipart.mixed mms.add_data_part(dp) return mms def do_get_payload(url, extra_info): host, port = extra_info['wap2'].split(':') s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host, int(port))) s.send("GET %s HTTP/1.0\r\n\r\n" % url) buf = StringIO() while True: data = s.recv(4096) if not data: break buf.write(data) s.close() data = buf.getvalue() buf.close() return data def get_payload(uri, extra_info): """ Downloads ``uri`` and returns a `MMSMessage` from it :param extra_info: dict with connection information """ d = threads.deferToThread(do_get_payload, uri, extra_info) d.addCallback(remove_headers_and_convert_to_array) d.addCallback(MMSMessage.from_data) return d def do_post_payload(extra_info, payload): host, port = extra_info['wap2'].split(':') mmsc = extra_info['mmsc'] s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host, int(port))) s.send("POST %s HTTP/1.0\r\n" % mmsc) s.send("Content-Type: application/vnd.wap.mms-message\r\n") s.send("Content-Length: %d\r\n\r\n" % len(payload)) s.sendall(payload) buf = StringIO() while True: data = s.recv(4096) if not data: break buf.write(data) s.close() data = buf.getvalue() buf.close() log.msg("do_post_payload: %r" % data) return data def post_payload(extra_info, data): return threads.deferToThread(do_post_payload, extra_info, data) def send_m_notifyresp_ind(extra_info, tx_id): mms = MMSMessage() mms.headers['Transaction-Id'] = tx_id mms.headers['Message-Type'] = 'm-notifyresp-ind' mms.headers['Status'] = 'Retrieved' def process_response(data): if data.startswith("HTTP/1.0 404"): # notification has expired try: text = data.split('\r\n')[0][13:] except IndexError: text = "Message not found in MMSC" raise ExpiredNotification(text) d = post_payload(extra_info, mms.encode()) d.addCallback(process_response) return d def send_m_send_req(extra_info, dbus_data): # sanitize headers headers = dbus_data['headers'] if 'To' not in headers: raise ValueError("You need to provide a recipient 'To'") if not headers['To'].endswith('/TYPE=PLMN'): headers['To'] += '/TYPE=PLMN' # set headers mms = dbus_data_to_mms(dbus_data) for key, val in headers.items(): mms.headers[key] = val # set type the last one so is always the right type mms.headers['Message-Type'] = 'm-send-req' d = post_payload(extra_info, mms.encode()) d.addCallback(remove_headers_and_convert_to_array) d.addCallback(MMSMessage.from_data) return d wader-0.5.13/core/modemmanager.py000066400000000000000000000043071257646610200166550ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """ A simple wrapper to access the devices on a hassle-free way Note that it will not use async methods (for clarity's sake) and thus it will block. If you want to use it in async environments, you'll have to roll out your own wrapper. """ import dbus from wader.common import consts class ModemManager(object): """I provide an easy to use interface to interact with ModemManager""" def __init__(self): super(ModemManager, self).__init__() self.bus = dbus.SystemBus() self.mm_obj = None self.dial_obj = None self._opaths = [] self._init_modemmanager() def _init_modemmanager(self): self.mm_obj = self.bus.get_object(consts.WADER_SERVICE, consts.WADER_OBJPATH) self._opaths = self.mm_obj.EnumerateDevices() if not self._opaths: raise RuntimeError("No devices found") def get_devices(self): """Returns a list with the devices present in the system""" devices = [self.bus.get_object(consts.WADER_SERVICE, opath) for opath in self._opaths] return devices def get_dial_manager(self): """Returns a proxy to DialerManager""" if not self.dial_obj: self.dial_obj = self.bus.get_object(consts.WADER_DIALUP_SERVICE, consts.WADER_DIALUP_OBJECT) return self.dial_obj wader-0.5.13/core/oal.py000066400000000000000000000027431257646610200147760ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """ OS Abstraction Layer OS provides an abstraction layer so path differences between OSes/distros won't affect Wader """ _os_obj = None def get_os_object(): """ Returns a ``OSPlugin`` instance corresponding to current OS used If the OS is unknown it will return None """ global _os_obj if _os_obj is not None: return _os_obj from core.plugin import PluginManager from wader.common.interfaces import IOSPlugin for osplugin in PluginManager.get_plugins(IOSPlugin): if osplugin.is_valid(): osplugin.initialize() _os_obj = osplugin return _os_obj return None wader-0.5.13/core/oses/000077500000000000000000000000001257646610200146145ustar00rootroot00000000000000wader-0.5.13/core/oses/__init__.py000066400000000000000000000015531257646610200167310ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """OSes base classes""" wader-0.5.13/core/oses/bsd.py000066400000000000000000000034041257646610200157370ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """BSD-based OS plugins""" from os import uname from core.oses.unix import UnixPlugin class FreeBSDPlugin(UnixPlugin): """Plugin for FreeBSD""" def __init__(self): super(FreeBSDPlugin, self).__init__() def get_iface_stats(self, iface): # XXX: implementation missing return 0, 0 def is_valid(self): if 'FreeBSD' in uname()[0]: return True return False def update_dns_cache(self): raise NotImplementedError() class OpenBSDPlugin(UnixPlugin): """Plugin for OpenBSD""" def __init__(self): super(OpenBSDPlugin, self).__init__() def get_iface_stats(self, iface): # XXX: implementation missing return 0, 0 def is_valid(self): try: __import__("openbsd") return True except ImportError: return False def update_dns_cache(self): raise NotImplementedError() wader-0.5.13/core/oses/linux.py000066400000000000000000000474411257646610200163370ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Linux-based OS plugin""" import dbus from functools import partial from os.path import join, exists import re import subprocess from zope.interface import implements from twisted.internet import defer, reactor, utils from twisted.python import log, reflect from twisted.python.procutils import which from wader.common import consts from wader.common.interfaces import IHardwareManager from wader.common.utils import get_file_data, natsort from core.hardware.base import identify_device, probe_ports from core.plugin import PluginManager from core.oses.unix import UnixPlugin from core.startup import setup_and_export_device from core.serialport import Ports IDLE, BUSY = range(2) ADD_THRESHOLD = 6 MODEL, VENDOR, DRIVER = "ID_MODEL_ID", "ID_VENDOR_ID", "ID_USB_DRIVER" SUBSYSTEMS = ["tty", "usb", "net"] REQUIRED_PROPS = [VENDOR, MODEL, DRIVER, "ID_BUS", "DEVNAME"] BAD_DEVFILE = re.compile('^/dev/(tty\d*?|console|ptmx)$') def get_ancestor(device): """ Returns the greatest ancestor with matching vid / pid """ def get_vidpid(device): vendor = None model = None for key in device.get_property_keys(): if key == VENDOR: try: vendor = int(device.get_property(key), 16) except (ValueError, TypeError): pass if key == MODEL: try: model = int(device.get_property(key), 16) except (ValueError, TypeError): pass return (vendor, model) vidpid = get_vidpid(device) ancestor = parent = device while parent is not None: if get_vidpid(parent) == vidpid: ancestor = parent parent = parent.get_parent() return ancestor def is_valid_device(device): """Checks whether ``device`` is valid""" if not device.get_device_file(): return False # before checking all the properties, filter out all the /dev/tty%d if BAD_DEVFILE.match(device.get_device_file()): return False # filter out /dev/usb/foo/bar/foo like too parts = device.get_device_file().split('/') if len(parts) > 3: return False # check that it has all the required properties # otherwise we are not interested on it props = device.get_property_keys() for prop in REQUIRED_PROPS: if prop not in props: return False return True def get_devices(client): devices = [] # get all the devices under the tty, usb and net subsystems for subsystem in SUBSYSTEMS: for device in client.query_by_subsystem(subsystem): if is_valid_device(device): devices.append(device) return devices class HardwareManager(object): """ I find and configure devices on Linux I am resilient to ports assigned in unusual locations and devices sharing ids. """ implements(IHardwareManager) def __init__(self): super(HardwareManager, self).__init__() #: dictionary with all my configured clients self.clients = {} #: reference to StartupController self.controller = None self._waiting_deferred = None # remember the total client count for opath generation self._client_count = -1 gudev = reflect.namedAny("gudev") self.gudev_client = gudev.Client(SUBSYSTEMS) # temporary place to store hotplugged devices to process self._hotplugged_devices = [] self._call = None self._connect_to_signals() def _connect_to_signals(self): self.gudev_client.connect("uevent", self._on_uevent) def _on_uevent(self, client, action, device): msg = "UEVENT device: %s action: %s" log.msg(msg % (device.get_sysfs_path(), action)) if action == 'remove': # handle remove for opath, plugin in self.clients.items(): if plugin.sysfs_path == device.get_sysfs_path(): self.controller.DeviceRemoved(plugin.opath) self._unregister_client(plugin) elif action == 'add': # if valid, append it to the list of hotplugged devices # for later processing if is_valid_device(device): self._hotplugged_devices.append(device) if self._call is None: # the first time we set a small delay and whenever a device # is added we will reset the call 2 seconds self._call = reactor.callLater(2, self._process_hotplugged_devices) elif self._call.active(): # XXX: this can be optimized by substracting x milliseconds # for every device added to the reset call. However it # introduces some more logic and perhaps should live outside. self._call.reset(ADD_THRESHOLD) def register_controller(self, controller): """ See `IHardwareManager.register_controller` """ self.controller = controller def get_devices(self): """See :meth:`wader.common.interfaces.IHardwareManager.get_devices`""" # If clients is an empty dict we assume that this is the first # time get_devices is executed. If not, we just return the current # devices. If get_devices is executed in the middle of a hotplugging # event, the "just added" device won't be returned, but it will be # processed in a few seconds by _process_hotplugged_devices anyway. if self.clients: return defer.succeed(self.clients.values()) return self._process_found_devices(get_devices(self.gudev_client)) def _process_hotplugged_devices(self): # get DevicePlugin out of a list of gudev.Device self._process_found_devices(self._hotplugged_devices) self._hotplugged_devices, self._call = [], None def _process_found_devices(self, devices=None, emit=True): """ Processes gudev ``devices`` and returns ``DevicePlugin``s Find devices with a common parent and merge them, identify the ones that need it, register and emit a signal if ``emit`` is True. """ deferreds = [] for device in self._setup_devices(devices): d = identify_device(device) d.addCallback(self._register_client, emit=emit) deferreds.append(d) return defer.gatherResults(deferreds) def _setup_devices(self, devices): """Sets up ``devices``""" found_devices = {} for device in devices: props = {} for prop in REQUIRED_PROPS: value = device.get_property(prop) # values are either string or hex try: props[prop] = int(value, 16) except ValueError: props[prop] = value if 'DEVNAME' in props: abspath = device.get_device_file() if props['DEVNAME'] != abspath: # Sometimes the DEVNAME property obtained from 'gudev' is # just a relative pathname. This seems to occur on boot # with the device already inserted, rather than upon a # hotplug insertion event. if abspath.endswith(props['DEVNAME']) and exists(abspath): props['DEVNAME'] = abspath else: log.msg("DEVNAME != device.get_device_file() but " + "fixup not possible") log.msg("'%s' != '%s'" % (props['DEVNAME'], abspath)) # if these properties are present, we should use them as the # data and control ports for mm_prop in ['ID_MM_PORT_TYPE_MODEM', 'ID_MM_PORT_TYPE_AUX']: if mm_prop in device.get_property_keys(): props[mm_prop] = bool(int(device.get_property(mm_prop))) # now find out the device parent parent = get_ancestor(device).get_sysfs_path() if parent in self.clients: # this device has already been setup return if parent in found_devices: # we have already found a device with the same parent, update # the attributes found_devices[parent].update(props) else: # a new parent has been found, store its sysfs_path as key # as all the childs have the same property DEVNAME, we need to # create a new and temporal property named DEVICES found_devices[parent] = props found_devices[parent]['DEVICES'] = [] if 'DEVNAME' in props: # append the device name as usual found_devices[parent]['DEVICES'].append(props['DEVNAME']) # if any of this is present save them for later use for _prop in ['ID_MM_PORT_TYPE_MODEM', 'ID_MM_PORT_TYPE_AUX']: if props.get(_prop, False): found_devices[parent][_prop] = props['DEVNAME'] result = [] for sysfs_path, info in found_devices.items(): device = self._get_device_from_info(sysfs_path, info) if device: result.append(device) else: log.msg("Unknown device: %s" % info) try: lsusb_path = which('lsusb')[0] except IndexError: lsusb_path = '/usr/bin/lsusb' try: id_vendor = info['ID_VENDOR_ID'] id_model = info['ID_MODEL_ID'] p = subprocess.Popen([lsusb_path, '-v', '-d %x:%x' % (id_vendor, id_model)], bufsize=-1, stderr=subprocess.PIPE, stdout=subprocess.PIPE, universal_newlines=True, shell=False) device_info, device_info_error = p.communicate() log.msg('lsusb device info:%s' % device_info) if len(device_info_error): log.msg('lsusb device info_error:%s' % device_info_error) except (OSError, ValueError): pass from core.hardware.base import raw_identify_device for port in info.get('DEVICES', []): try: log.msg('try to identify model on port "%s"' % port) raw_identify_device(port) except ValueError: pass return result def _register_client(self, plugin, emit=False): """ Registers `plugin` in `self.clients` indexes by its object path Will emit a DeviceAdded signal if emit is True """ log.msg("registering plugin %s with opath %s" % (plugin, plugin.opath)) self.clients[plugin.opath] = setup_and_export_device(plugin) if emit: self.controller.DeviceAdded(plugin.opath) return plugin def _unregister_client(self, client): """Removes client identified by ``opath``""" try: plugin = self.clients.pop(client.opath) plugin.close(removed=True) except KeyError: log.err("_unregister_client: Could not " "unregister %s. Is not present." % client.opath) def _generate_opath(self): self._client_count += 1 return "/org/freedesktop/ModemManager/Devices/%d" % self._client_count def _get_hso_ports(self, ports): dport = cport = None for port in ports: name = port.split('/')[-1] path = join('/sys/class/tty', name, 'hsotype') if exists(path): what = get_file_data(path).strip().lower() if what == 'modem': dport = port elif what == 'application': cport = port if dport and cport: break return dport, cport def _get_hso_device(self, sysfs_path): for device in self.gudev_client.query_by_subsystem("net"): if device.get_sysfs_path().startswith(sysfs_path): return device.get_property("INTERFACE") raise ValueError("Cannot find hso device for device %s" % sysfs_path) def _get_device_from_info(self, sysfs_path, info): """Returns a `DevicePlugin` out of ``info``""" # order the ports before probing ports = info['DEVICES'] natsort(ports) query = [info.get(key) for key in [VENDOR, MODEL]] plugin = PluginManager.get_plugin_by_vendor_product_id(*query) if plugin: dport = cport = None plugin.sysfs_path = sysfs_path plugin.opath = self._generate_opath() set_property = partial(plugin.set_property, emit=False) # set DBus properties (Modem interface) set_property(consts.MDM_INTFACE, 'IpMethod', consts.MM_IP_METHOD_PPP) set_property(consts.MDM_INTFACE, 'MasterDevice', 'udev:%s' % sysfs_path) # XXX: Fix CDMA set_property(consts.MDM_INTFACE, 'Type', consts.MM_MODEM_TYPE_REV['GSM']) set_property(consts.MDM_INTFACE, 'Driver', info[DRIVER]) set_property(consts.MDM_INTFACE, 'Enabled', dbus.Boolean(False)) # set to unknown set_property(consts.NET_INTFACE, 'AccessTechnology', dbus.UInt32(0)) # set to preposterous initial number so any comparison will fail set_property(consts.NET_INTFACE, 'AllowedMode', dbus.UInt32(0xffff)) set_property(consts.NET_INTFACE, 'LastApn', '') # preprobe stuff if hasattr(plugin, 'preprobe_init'): # this plugin requires special initialisation before probing plugin.preprobe_init(ports, info) # now get the ports ports_need_probe = True if info[DRIVER] == 'hso': dport, cport = self._get_hso_ports(ports) ports_need_probe = False # if these two properties are present, use them right away and # do not probe if ('ID_MM_PORT_TYPE_MODEM' in info or 'ID_MM_PORT_TYPE_AUX' in info): try: dport = info['ID_MM_PORT_TYPE_MODEM'] log.msg("%s: ID_MM_PORT_TYPE_MODEM" % dport) except KeyError: pass try: cport = info['ID_MM_PORT_TYPE_AUX'] log.msg("%s: ID_MM_PORT_TYPE_AUX" % cport) except KeyError: pass ports_need_probe = False if ports_need_probe: # the ports were not hardcoded nor was an HSO device dport, cport = probe_ports(ports) if not dport and not cport: # this shouldn't happen msg = 'No data port and no control port with ports: %s' raise RuntimeError(msg % ports) if dport is not None: set_property(consts.MDM_INTFACE, 'Device', dport.split('/')[-1]) if info[DRIVER] == 'cdc_acm': # MBM device # XXX: Not all CDC devices support DHCP, to override see # plugin attribute 'ipmethod' # XXX: Also need to support Ericsson devices via 'hso' dialer # so that we can use the plain backend. At least F3607GW # supports a get_ip4_config() style AT command to get # network info, else we need to implement a DHCP client # set DBus properties (Modem.Gsm.Hso interface) hso_device = self._get_hso_device(sysfs_path) set_property(consts.MDM_INTFACE, 'Device', hso_device) set_property(consts.MDM_INTFACE, 'IpMethod', consts.MM_IP_METHOD_DHCP) if plugin.dialer in 'hso': # set DBus properties (Modem.Gsm.Hso interface) hso_device = self._get_hso_device(sysfs_path) set_property(consts.MDM_INTFACE, 'Device', hso_device) if hasattr(plugin, 'ipmethod'): # allows us to specify a method in a driver independent way set_property(consts.MDM_INTFACE, 'IpMethod', plugin.ipmethod) if hasattr(plugin, 'conntype') and plugin.conntype: set_property(consts.MDM_INTFACE, 'ConnType', dbus.UInt32(plugin.conntype)) else: set_property(consts.MDM_INTFACE, 'ConnType', dbus.UInt32(consts.WADER_CONNTYPE_UNKNOWN)) plugin.ports = Ports(dport, cport) return plugin log.msg("Could not find a plugin with info %s" % info) return None def get_hw_manager(): try: return HardwareManager() except: return None class LinuxPlugin(UnixPlugin): """OSPlugin for Linux-based distros""" dialer = None hw_manager = get_hw_manager() def __init__(self): super(LinuxPlugin, self).__init__() def is_valid(self): raise NotImplementedError() def add_default_route(self, iface): """See :meth:`wader.common.interfaces.IOSPlugin.add_default_route`""" args = ['add', 'default', 'dev', iface] return utils.getProcessValue('/sbin/route', args, reactor=reactor) def delete_default_route(self, iface): """ See :meth:`wader.common.interfaces.IOSPlugin.delete_default_route` """ args = ['delete', 'default', 'dev', iface] return utils.getProcessValue('/sbin/route', args, reactor=reactor) def configure_iface(self, iface, ip='', action='up'): """See :meth:`wader.common.interfaces.IOSPlugin.configure_iface`""" assert action in ['up', 'down'] if action == 'down': args = [iface, action] else: args = [iface, ip, 'netmask', '255.255.255.255', '-arp', action] return utils.getProcessValue('/sbin/ifconfig', args, reactor=reactor) def get_iface_stats(self, iface): """See :meth:`wader.common.interfaces.IOSPlugin.get_iface_stats`""" stats_path = "/sys/class/net/%s/statistics" % iface rx_b = join(stats_path, 'rx_bytes') tx_b = join(stats_path, 'tx_bytes') try: return map(int, [get_file_data(rx_b), get_file_data(tx_b)]) except (IOError, OSError): return 0, 0 def get_additional_wvdial_ppp_options(self): return "" wader-0.5.13/core/oses/osx.py000066400000000000000000000123341257646610200160020ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """DevicePlugin for OSX""" from functools import partial import sys from twisted.internet import defer from twisted.python import log, reflect from zope.interface import implements from wader.common.consts import (MDM_INTFACE, MM_MODEM_TYPE_REV, NET_INTFACE, MM_IP_METHOD_PPP) from core.hardware.base import raw_identify_device from wader.common.interfaces import IHardwareManager from core.oses.unix import UnixPlugin from core.plugin import PluginManager from core.serialport import Ports from core.startup import setup_and_export_device class OSXPlugin(UnixPlugin): """OSPlugin for OSX""" dialer = None def __init__(self): super(OSXPlugin, self).__init__() self.hw_manager = HardwareManager() def get_iface_stats(self, iface): """See :meth:`wader.common.interfaces.IOSPlugin.get_iface_stats`""" # XXX: implementation missing return 0, 0 def is_valid(self): """See :meth:`wader.common.interfaces.IOSPlugin.is_valid`""" # XXX: Disable OSX support until we can find the provenance of the C # module, or replace it with something else. return False return sys.platform == 'darwin' def update_dns_cache(self): # XXX: implementation missing pass class HardwareManager(object): """I find and configure devices""" implements(IHardwareManager) def __init__(self): super(HardwareManager, self).__init__() self.controller = None self._device_count = -1 self.clients = {} def register_controller(self, controller): """ See `IHardwareManager.register_controller` """ self.controller = controller def get_devices(self): """See :meth:`wader.common.interfaces.IHardwareManager.get_devices`""" # so pylint does not complain on Linux osxserialports = reflect.namedAny('core.oses.osxserialports') devs_info = [d for d in osxserialports.modems() if 'Modem' in d['suffix']] deferreds = [] for dev in devs_info: port = dev['dialin'] if dev['dialin'] else dev['callout'] d = defer.maybeDeferred(raw_identify_device, port) d.addCallback(self._get_device_from_model, dev) deferreds.append(d) d = defer.gatherResults(deferreds) d.addCallback(self._check_if_devices_are_registered) return d def _get_device_from_model(self, model, dev_info): plugin = PluginManager.get_plugin_by_remote_name(model) if plugin: device = dev_info['callout'].split('/')[-1] set_property = partial(plugin.set_property, emit=True) set_property(MDM_INTFACE, 'Device', device) # XXX: Fix MasterDevice set_property(MDM_INTFACE, 'MasterDevice', 'iokit:com.vodafone.BMC.NotImplemented') # XXX: Fix CDMA set_property(MDM_INTFACE, 'Type', MM_MODEM_TYPE_REV['GSM']) set_property(MDM_INTFACE, 'Driver', 'notimplemented') set_property(MDM_INTFACE, 'IpMethod', MM_IP_METHOD_PPP) set_property(MDM_INTFACE, 'UnlockRequired', "") # import here else we start the dbus too early in startup from dbus import Boolean set_property(MDM_INTFACE, 'Enabled', Boolean(False)) # set to unknown set_property(NET_INTFACE, 'AccessTechnology', 0) # set to -1 so any comparison will fail and will update it set_property(NET_INTFACE, 'AllowedMode', -1) plugin.opath = self._generate_opath() plugin.ports = Ports(dev_info['callout'], dev_info['dialin']) return plugin def _check_if_devices_are_registered(self, devices): for device in devices: if device.opath not in self.clients: self._register_client(device, emit=True) return devices def _register_client(self, plugin, emit=False): """ Registers `plugin` in `self.clients` Will emit a DeviceAdded signal if emit is True """ log.msg("registering plugin %s with opath %s" % (plugin, plugin.opath)) self.clients[plugin.opath] = setup_and_export_device(plugin) if emit: self.controller.DeviceAdded(plugin.opath) def _generate_opath(self): self._device_count += 1 return '/org/freedesktop/ModemManager/Devices/%d' % self._device_count wader-0.5.13/core/oses/unix.py000066400000000000000000000044671257646610200161640ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """OSPlugin for Unix-based OSes""" from twisted.python import log from core.plugin import OSPlugin from core.resolvconf import NamedManager from wader.common.utils import get_file_data class UnixPlugin(OSPlugin): """Plugin for Unix""" def __init__(self): super(UnixPlugin, self).__init__() self.named_manager = NamedManager() def add_dns_info(self, dns, iface=None): """See :meth:`wader.common.interfaces.IOSPlugin.add_dns_info`""" self.named_manager.add_dns_info(dns) try: self.update_dns_cache() except NotImplementedError: klass = self.__class__.__name__ log.err("%s: update_dns_cache not implemented" % klass) def delete_dns_info(self, dns, iface=None): """See :meth:`wader.common.interfaces.IOSPlugin.delete_dns_info`""" self.named_manager.delete_dns_info(dns) try: self.update_dns_cache() except NotImplementedError: klass = self.__class__.__name__ log.err("%s: update_dns_cache not implemented" % klass) def is_valid(self): # DO NOT modify this unless you know what you are doing. This plugin # is the parent class of LinuxPlugin/OSXPlugin/*BSDPlugin/etc. This # is not a final implementation as there's no such thing as a Unix OS. return False def get_iface_stats(self, iface): raise NotImplementedError() def update_dns_cache(self): raise NotImplementedError() wader-0.5.13/core/plugin.py000066400000000000000000000265161257646610200155250ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Plugin system for Wader""" # import inspect # for debugging only from dbus import Boolean, UInt32 from pytz import timezone from time import time from twisted.internet import defer, reactor from twisted.internet.task import deferLater from twisted.python import log from twisted.plugin import IPlugin, getPlugins from zope.interface import implements from wader.common.consts import (MDM_INTFACE, HSO_INTFACE, CRD_INTFACE, NET_INTFACE, USD_INTFACE, MM_MODEM_STATE_DISABLED, MM_MODEM_STATE_ENABLED) import wader.common.aterrors as at import wader.common.exceptions as ex import wader.common.interfaces as interfaces from wader.common.utils import flatten_list from core.daemon import build_daemon_collection from core.sim import SIMBaseClass import plugins class DevicePlugin(object): """Base class for all plugins""" implements(IPlugin, interfaces.IDevicePlugin) __properties__ = {} # at what speed should we talk with this device? baudrate = 115200 # Class that will initialize the SIM, by default SIMBaseClass sim_klass = SIMBaseClass # Response of AT+CGMM __remote_name__ = "" # instance of a custom adapter class if device needs customization custom = None # instance of the exporter class that will export AT methods exporter = None # dialer dialer = 'default' # place for device specific hints to the core quirks = {} def __init__(self): super(DevicePlugin, self).__init__() # sim instance self.sim = None # serial connection reference self.sconn = None # device internal state self._status = MM_MODEM_STATE_DISABLED # time SIM authentication was done self._authtime = None # collection of daemons self.daemons = None # onyl used in devices that like to share ids, like # huawei's exxx family. It should have at least a # 'default' key mapping to a safe device that can be # used to identify the rest of the family self.mapping = {} # object path self.opath = None # sysfs_path self.sysfs_path = None # dictionary with org.freedesktop.DBus.Properties self.props = {MDM_INTFACE: {}, HSO_INTFACE: {}, CRD_INTFACE: {}, NET_INTFACE: {}, USD_INTFACE: {}} self.ports = None def __repr__(self): args = (self.__class__.__name__, self.ports) return "<%s %s>" % args def get_property(self, iface, name): return self.props[iface][name] def get_properties(self, iface=None): if iface is None: return self.props return self.props[iface] def set_property(self, iface, name, value, emit=True): self.props[iface][name] = value if hasattr(self.exporter, 'MmPropertiesChanged') and emit: self.exporter.MmPropertiesChanged(iface, {name: value}) def set_status(self, status, reason=UInt32(0)): """Sets internal device status to ``status``""" # # uncomment for debugging only # caller = inspect.stack()[1] # try: # log.msg("set_status called by %s" % str(caller)) # finally: # del caller if status >= MM_MODEM_STATE_ENABLED and \ self._status < MM_MODEM_STATE_ENABLED: self.exporter.DeviceEnabled(self.opath) self.set_property(MDM_INTFACE, 'Enabled', Boolean(status >= MM_MODEM_STATE_ENABLED)) self.set_property(MDM_INTFACE, 'State', status) # Default reason is MM_MODEM_STATE_CHANGED_REASON_UNKNOWN self.exporter.StateChanged(self._status, status, reason) self._status = status @property def status(self): """Returns the internal device status""" return self._status def set_authtime(self, authtime): self._authtime = authtime @property def authtime(self): """ Returns the time in secs (UTC) that authentication was done or None if it's not occurred yet """ return self._authtime def close(self, remove_from_conn=False, removed=False): """Closes the plugin and frees all the associated resources""" log.msg("Closing plugin %s" % self) if remove_from_conn or removed: try: self.exporter.remove_from_connection() except LookupError, e: log.err(e) def free_resources(ign=None): """Frees the resources held by :meth:`initialize`""" if self.sconn is not None and self.sconn.transport: self.sconn.transport.unregisterProducer() if self.ports.cport.obj is not None: self.ports.cport.obj.loseConnection("Bye!") self.ports.cport.obj = None # if our device was pulled out whilst we were connected if self.ports.dport.obj is not None: if self.ports.dport.obj.isOpen(): self.ports.dport.obj.close() self.ports.dport.obj = None if self.daemons is not None and self.daemons.running: self.daemons.stop_daemons() d = defer.succeed(True) d.addCallback(self.sconn.stop_traffic_monitoring) if not removed: d.addCallback(lambda _: self.sconn.enable_radio(False)) d.addCallback(lambda _: self.set_status(MM_MODEM_STATE_DISABLED)) d.addCallback(free_resources) return d def initialize(self, init_obj=None): """Initializes the SIM""" def on_init(size): if not self.daemons: self.daemons = build_daemon_collection(self) self.daemons.start_daemons() d = self.sconn.init_properties() def set_enable(_): if self._status <= MM_MODEM_STATE_ENABLED: self.set_status(MM_MODEM_STATE_ENABLED) return _ d.addCallback(set_enable) d.addCallback(lambda _: self.sconn.mal.initialize(obj=self.sconn)) d.addCallback(lambda _: size) return d def initialize_sim(_): self.sim = self.sim_klass(self.sconn) d = self.sim.initialize() d.addCallback(on_init) return d # XXX: Sometimes, right after a combination of Modem.Enable operations # and hot pluggings, the core will not reply to the first AT # command sent, but it will to the second. This was an old comment # and it's not certain that this still occurs; if symptoms persist # we'll need to re-add handling for this case. if self.authtime is None: log.err("Being asked to initialise before auth has been checked") raise RuntimeError def do_init(): if not (self.sconn and self.sconn.transport): return defer.fail(at.SerialSendFailed()) log.msg('Enabling radio and initialising SIM') d = self.sconn.enable_radio(True) d.addCallback(initialize_sim) d.addErrback(log.err) return d remaining = (self.authtime + self.custom.auth_klass.DELAY) - time() + 1 if remaining > 0: log.msg('Delaying SIM initialisation for %d secs' % remaining) d = deferLater(reactor, remaining, do_init) else: d = do_init() return d def patch(self, other): """Patch myself in-place with the settings of another plugin""" if not isinstance(other, DevicePlugin): raise ValueError("Cannot patch myself with a %s" % type(other)) self.opath = other.opath self.sysfs_path = other.sysfs_path self.ports = other.ports self.props = other.props.copy() self.baudrate = other.baudrate class RemoteDevicePlugin(DevicePlugin): """ Base class from which all the RemoteDevicePlugins should inherit from """ implements(IPlugin, interfaces.IRemoteDevicePlugin) class OSPlugin(object): """Base class from which all the OSPlugins should inherit from""" implements(IPlugin, interfaces.IOSPlugin) dialer = None hw_manager = None def __init__(self): super(OSPlugin, self).__init__() def get_iface_stats(self, iface): """ Returns ``iface`` network statistics :rtype: tuple """ raise NotImplementedError() def is_valid(self): """Returns True if we are on the given OS/Distro""" raise NotImplementedError() def initialize(self): """Initializes the plugin""" pass class PluginManager(object): """I manage Wader's plugins""" @classmethod def get_plugins(cls, interface=IPlugin, package=plugins): """ Returns all the plugins under ``package`` that implement ``interface`` """ return getPlugins(interface, package) @classmethod def get_plugin_by_remote_name(cls, name, interface=interfaces.IDevicePlugin): """ Get a plugin by its remote name :raise UnknownPluginNameError: When we don't know about the plugin """ for plugin in cls.get_plugins(interface, plugins): if not hasattr(plugin, '__remote_name__'): continue if plugin.__remote_name__ == name: return plugin if name in plugin.mapping: return plugin.mapping[name]() raise ex.UnknownPluginNameError(name) @classmethod def get_plugin_by_vendor_product_id(cls, product_id, vendor_id): """Get a plugin by its product and vendor ids""" log.msg("get_plugin_by_id called with 0x%04x and 0x%04x" % (product_id, vendor_id)) for plugin in cls.get_plugins(interfaces.IDevicePlugin): props = flatten_list(plugin.__properties__.values()) if int(product_id) in props and int(vendor_id) in props: if not plugin.mapping: # regular plugin return plugin # device has multiple personalities... # this will just return the default plugin for # the mapping, we keep a reference to the mapping # once the device is properly identified by # core.hardware.base::identify_device _plugin = plugin.mapping['default']() _plugin.mapping = plugin.mapping return _plugin return None wader-0.5.13/core/protocol.py000066400000000000000000000775071257646610200160760ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Twisted protocols for serial communication""" import re from twisted.internet import protocol, defer, reactor from twisted.python.failure import Failure from twisted.python import log import wader.common.aterrors as E import wader.common.signals as S from core.command import ATCmd # Standard unsolicited notifications CALL_RECV = re.compile('\r\nRING\r\n') STK_DEBUG = re.compile('\r\n\+STC:\s\d+\r\n') # Standard solicited notifications SMS_RECEIVED = re.compile('\r\n\+CMTI:\s"(?P\w{2,})",(?P\d+)\r\n') SMS_DELIVERY = re.compile('\r\n\+CDS:\s\d+\r\n(?P[A-Za-z0-9]+)\r\n') SPLIT_PROMPT = re.compile('\r?\r\n>\s$') CREG_REGEXP = re.compile('\r\n\+CREG:\s*(?P\d)\r\n') class BufferingStateMachine(object, protocol.Protocol): """A simple SM that handles low level communication with the device""" def __init__(self, device): super(BufferingStateMachine, self).__init__() self.device = device # a reference to the customizer class for notifications self.custom = device.custom # current AT command self.cmd = None self.state = 'idle' # idle and wait buffers self.idlebuf = "" self.waitbuf = "" # log prefix for situations where the prefix is not appended self._prefix = "" def _get_log_prefix(self): try: if not self._prefix: port = self.device.ports.get_application_port() self._prefix = port.obj.logPrefix() except AttributeError: self._prefix = '' return self._prefix def _timeout_eb(self): """Executed when a command exceeds its timeout""" msg = "Command '%r' timed out, this is my waitbuf: %s" e = E.SerialResponseTimeout(msg % (self.cmd, self.waitbuf)) self.notify_failure(e) self.transition_to_idle() def cancel_current_delayed_call(self): """ Cancels current :class:`~core.command.ATCmd` delayed call """ if self.cmd.call_id and self.cmd.call_id.active(): self.cmd.call_id.cancel() def notify_success(self, result): """ Notify success to current :class:`~core.command.ATCmd` """ self.cancel_current_delayed_call() try: self.cmd.deferred.callback(result) except Exception, e: args = (self.cmd, result) log.err(e, "'%r' callback failed with args '%s'" % args) def notify_failure(self, failure): """Notify failure to current :class:`~core.command.ATCmd`""" self.cancel_current_delayed_call() self.cmd.deferred.errback(failure) def set_cmd(self, cmd): """ Sets ``cmd`` as the next command to process It also sets an initial timeout and transitions to waiting state """ self.cmd = cmd # set the timeout for this command self.cmd.call_id = reactor.callLater(cmd.timeout, self._timeout_eb) self.set_state('waiting') def set_state(self, new_state): """Sets the new state ``new_state``""" log.msg("state change: %s -> %s" % (self.state, new_state), system=self._get_log_prefix()) # the system line got added because no suffix was being added # to the log in set_state self.state = new_state def transition_to_idle(self): """Transitions to idle state and cleans internal buffers""" self.cmd = None self.set_state('idle') self.idlebuf = "" self.waitbuf = "" def send_splitcmd(self): """ Used to send the second part of a split command after prompt appears """ raise NotImplementedError() def emit_signal(self, signal, *args, **kwds): """ Emits ``signal`` :param signal: The name of the signal to emit :param args: The arguments for the signal ``signal`` :param kwds: The keywords for the signal ``signal`` """ method = getattr(self.device.exporter, signal, None) if method: method(*args, **kwds) else: log.err("No method registered for signal %s" % signal) def connectionLost(self, reason): super(BufferingStateMachine, self).connectionLost(reason) log.msg("Serial connection was lost") self.transport = None def dataReceived(self, data): """See `twisted.internet.protocol.Protocol.dataReceived`""" # XXX: Change the following zero to one to log all data from the modem if 0: log.msg('dataReceived: %s' % str(data)) state = 'handle_%s' % self.state getattr(self, state)(data) def process_notifications(self, _buffer): """ Processes unsolicited notifications in ``_buffer`` :param _buffer: Buffer to scan """ if not self.device.custom or not self.device.custom.async_regexp: return _buffer custom = self.device.custom # we have to use re.finditer as some cards like to pipeline # several asynchronous notifications in one for match in re.finditer(custom.async_regexp, _buffer): name, value = match.groups() if name in custom.signal_translations: # we obtain the signal name and the associated function # that will translate the device unsolicited message to # the signal used in Wader internally signal, func = custom.signal_translations[name] # if we have a transform function defined, then use it # otherwise use value as args if func: try: args = func(value, self.device) except Exception, e: msg = "%s can not handle notification %s" log.err(e, msg % (func, value)) args = value if signal is not None: self.emit_signal(signal, args) # remove from the idlebuf the match (but only once please) _buffer = _buffer.replace(match.group(), '', 1) return _buffer def process_notification_sms_received(self, _buffer): while True: match = SMS_RECEIVED.match(_buffer) if not match: break mal = getattr(self, 'mal', None) if mal: index = int(match.group('id')) mal.on_sms_notification(index) _buffer = _buffer.replace(match.group(), '', 1) return _buffer def process_notification_creg_received(self, _buffer): while True: match = CREG_REGEXP.match(_buffer) if not match: break status = int(match.group('status')) self.emit_signal(S.SIG_CREG, status) _buffer = _buffer.replace(match.group(), '', 1) return _buffer def handle_idle(self, data): """ Processes ``data`` in `idle` state Being in `idle` state, there are six possible events that must be handled: - STK init garbage - Call received (we're not handling it in waiting) - A SMS arrived - SMS notification (Not handled yet) - Device's own unsolicited notifications - Default: i.e. this device originated a notification that we don't understand yet, the point is to log it and make it visible so the user can report it to us """ log.msg("idle: %r" % data) self.idlebuf += data # most possible event: # device's own unsolicited notifications # signal translations stuff self.idlebuf = self.process_notifications(self.idlebuf) if not self.idlebuf: return # second most possible event: # new SMS arrived self.idlebuf = self.process_notification_sms_received(self.idlebuf) if not self.idlebuf: return # third most possible event: # SMS delivery report match = SMS_DELIVERY.match(self.idlebuf) if match: mal = getattr(self, 'mal', None) if mal: pdu = match.group('pdu') mal.on_sms_delivery_report(pdu) # the report might be repeated, consume as many as possible # idle: unmatched data '\r\n+CDS: 27\r\n07914306073011F006C # 00B914306565711F90120910134454001209101344540000100\r\n\r # \n+CDS: 27\r\n07914306073011F006C10B914306565711F90120910 # 144454001209101444540000100\r\n' self.idlebuf = self.idlebuf.replace(match.group(), '') if not self.idlebuf: return # fourth most possible event match = STK_DEBUG.match(self.idlebuf) if match: self.idlebuf = self.idlebuf.replace(match.group(), '') if not self.idlebuf: return # fifth most possible event self.idlebuf = self.process_notification_creg_received(self.idlebuf) if not self.idlebuf: return # sixth most possible event: match = CALL_RECV.match(self.idlebuf) if match: self.emit_signal(S.SIG_CALL) self.idlebuf = self.idlebuf.replace(match.group(), '') if not self.idlebuf: return log.msg("idle: unmatched data %r" % self.idlebuf) def handle_waiting(self, data): """Process ``data`` in the wait state""" self.waitbuf += data self.waitbuf = self.process_notifications(self.waitbuf) if not self.waitbuf: return # new SMS arrived self.waitbuf = self.process_notification_sms_received(self.waitbuf) if not self.waitbuf: return # CREG arrived self.waitbuf = self.process_notification_creg_received(self.waitbuf) if not self.waitbuf: return try: cmdinfo = self.custom.cmd_dict[self.cmd.name] except KeyError, e: log.err(e, 'command %s not present in my cmd dict' % self.cmd) return self.transition_to_idle() match = cmdinfo['end'].search(self.waitbuf) if match: # end of response if cmdinfo['extract']: # There's an regex to extract info from data response = list(re.finditer(cmdinfo['extract'], self.waitbuf)) resp_repr = str([m.groups() for m in response]) log.msg("%s: callback = %s" % (self.state, resp_repr)) self.notify_success(response) # now clean self.waitbuf for _m in response: self.waitbuf = self.waitbuf.replace(_m.group(), '', 1) # now clean end of command endmatch = cmdinfo['end'].search(self.waitbuf) if endmatch: self.waitbuf = self.waitbuf.replace(endmatch.group(), '', 1) else: # there's no regex in cmdinfo to extract info log.msg("%s: no callback registered" % self.state) self.notify_success(self.waitbuf) self.waitbuf = self.waitbuf.replace(match.group(), '', 1) self.transition_to_idle() else: # there is no end of response detected, so we have either an error # or a split command (like send_sms, save_sms, etc.) match = E.extract_error(self.waitbuf) if match: exception, error, m = match e = exception(error) f = Failure(e) if not f.check(*self.cmd.nolog): log.err(e, "waiting") # send the failure back self.notify_failure(f) # remove the exception string from the waitbuf self.waitbuf = self.waitbuf.replace(m.group(), '', 1) self.transition_to_idle() else: match = SPLIT_PROMPT.search(data) if match: log.msg("waiting: split command prompt detected") self.send_splitcmd() self.waitbuf = self.waitbuf.replace(match.group(), '', 1) else: log.msg("waiting: unmatched data %r" % data) class SerialProtocol(BufferingStateMachine): """ I define the protocol used to communicate with the SIM card SerialProtocol communicates with the SIM synchronously, only one command at a time. However, SerialProtocol offers an asynchronous interface :meth:`SerialProtocol.queue_at_cmd` which accepts and queues an :class:`~core.command.ATCmd` and returns a :class:`~twisted.internet.defer.Deferred` that will be callbacked with the commands response, or errback if an exception is raised. SerialProtocol actually is an specially tailored Finite State Machine. After several redesigns and simplifications, this FSM has just two states: - idle: sitting idle for user input or an unsolicited response, when a command is received we send the command and transition to the waiting state. - waiting: the FSM is buffering and parsing all the SIM's response to the command till it matches the regexp that signals the end of the command. If the command has an associated regexp to extract information, the buffered response will be parsed and the command's deferred will be callbacked with the regexp as argument. There are commands that don't have an associated regexp to extract information as we are not interested in the "all went ok" response, only if an exception occurred -e.g. when deleting a contact we are only interested if something went wrong, not if all went ok. The transition to each state is driven by regular expressions, each command has associated a set of regular expressions that make the FSM change states. This regexps are defined in :obj:`core.command.CMD_DICT` although the plugin mechanism offers the possibility of customizing the CMD_DICT through :class:`~core.hardware.base.Customizer` if a card uses a different AT string than the rest for that particular command. """ def __init__(self, device): super(SerialProtocol, self).__init__(device) self.queue = defer.DeferredQueue() self.mutex = defer.DeferredLock() self._check_queue() def transition_to_idle(self): """Transitions to idle state and processes next queued `ATCmd`""" super(SerialProtocol, self).transition_to_idle() # release the lock and check the queue if self.mutex.locked: self.mutex.release() self._check_queue() def send_splitcmd(self): """ Used to send the second part of a split command after prompt appears """ self.transport.write(self.cmd.splitcmd) def _process_at_cmd(self, cmd): def _transition_and_send(_): self.set_cmd(cmd) msg = "%s: sending %r" % (self.state, cmd.cmd) if self.transport is None: # Seems that the device went away log.msg(msg + ' cancelled', system=self._get_log_prefix()) self.notify_failure(E.SerialSendFailed('port disappeared')) self.transition_to_idle() else: log.msg(msg, system=self._get_log_prefix()) self.transport.write(cmd.get_cmd()) d = self.mutex.acquire() d.addCallback(_transition_and_send) def _check_queue(self): # when the next element of the queue is put, _process_at_cmd will be # callbacked with it d = self.queue.get() d.addCallback(self._process_at_cmd) def queue_at_cmd(self, cmd): """ Queues an :class:`~core.command.ATCmd` ``cmd`` This deferred will be callbacked with the command's response :rtype: `Deferred` """ self.queue.put(cmd) return cmd.deferred class WCDMAProtocol(SerialProtocol): """ A Twisted protocol to chat with WCDMA devices I am able to speak with most WCDMA devices, if you want to customize the command being sent for a particular command, subclass me. """ def __init__(self, device): super(WCDMAProtocol, self).__init__(device) def add_contact(self, name, number, index): """ Adds a contact to the SIM card :param name: The contact name :param number: The contact number :param index: The contact index """ category = 145 if number.startswith('+') else 129 args = (index, number, category, name) cmd = ATCmd('AT+CPBW=%d,"%s",%d,"%s"' % args, name='add_contact') return self.queue_at_cmd(cmd) def cancel_ussd(self): """Cancels an ongoing USSD session""" cmd = ATCmd("AT+CUSD=2", name='cancel_ussd') return self.queue_at_cmd(cmd) def change_pin(self, oldpin, newpin): """ Changes ``oldpin`` to ``newpin`` in the SIM card :type oldpin: str :type newpin: str :raise General: When the password is incorrect. :raise IncorrectPassword: When the password is incorrect. :raise InputValueError: When the PIN != \d{4} """ atstr = 'AT+CPWD="SC","%s","%s"' % (str(oldpin), str(newpin)) cmd = ATCmd(atstr, name='change_pin') return self.queue_at_cmd(cmd) def check_pin(self): """ Checks what's necessary to authenticate against the SIM card :raise SimBusy: When the SIM is not ready :raise SimNotStarted: When the SIM is not ready :raise SimFailure: This exception is raised by Option's colt when authentication is disabled :rtype: str """ # Note: don't log these exceptions exceptions = (E.SimFailure,) cmd = ATCmd('AT+CPIN?', name='check_pin', nolog=exceptions) return self.queue_at_cmd(cmd) def delete_all_contacts(self): """Deletes all the contacts in SIM card, function useful for tests""" d = self.get_used_contact_ids() def list_contacts_ids_cb(used): if not used: return True return defer.gatherResults(map(self.delete_contact, used)) d.addCallback(list_contacts_ids_cb) return d def delete_all_sms(self): """Deletes all the messages in SIM card, function useful for tests""" d = self.get_used_sms_ids() def delete_all_sms_cb(used): if not used: return True return defer.gatherResults(map(self.delete_sms, used)) d.addCallback(delete_all_sms_cb) return d def delete_contact(self, index): """Deletes the contact specified by ``index``""" cmd = ATCmd('AT+CPBW=%d' % index, name='delete_contact') return self.queue_at_cmd(cmd) def delete_sms(self, index): """Deletes the message specified by ``index``""" cmd = ATCmd('AT+CMGD=%d' % index, name='delete_sms') return self.queue_at_cmd(cmd) def disable_echo(self): """Disables echo of AT cmds""" cmd = ATCmd('ATE0', name='disable_echo') return self.queue_at_cmd(cmd) def enable_echo(self): """Enables echo of AT cmds""" cmd = ATCmd('ATE1', name='enable_echo') return self.queue_at_cmd(cmd) def enable_pin(self, pin, enable): """ Enables pin authentication at startup :type pin: int :type enable: bool :raise General: If ``pin`` is incorrect. :raise IncorrectPassword: If ``pin`` is incorrect. :raise ValueError: When ``pin`` != \d{4} """ at_str = 'AT+CLCK="SC",%d,"%s"' % (int(enable), str(pin)) cmd = ATCmd(at_str, name='enable_pin') return self.queue_at_cmd(cmd) def enable_radio(self, enable): """ Enables/disable radio stack """ cmd = ATCmd("AT+CFUN=%d" % int(enable), name='enable_radio') cmd.timeout = 30 return self.queue_at_cmd(cmd) def find_contacts(self, pattern): """Returns a list of contacts that match ``pattern``""" cmd = ATCmd('AT+CPBF="%s"' % pattern, name='find_contacts') return self.queue_at_cmd(cmd) def get_apns(self): """Returns all the APNs in the SIM""" cmd = ATCmd('AT+CGDCONT?', name='get_apns') return self.queue_at_cmd(cmd) def get_apn_range(self): """Returns the range of APN context IDs in the SIM""" cmd = ATCmd('AT+CGDCONT=?', name='get_apn_range') return self.queue_at_cmd(cmd) def get_card_model(self): """Returns the SIM card model""" cmd = ATCmd('AT+CGMM', name='get_card_model') return self.queue_at_cmd(cmd) def get_card_version(self): """Returns the SIM card version""" cmd = ATCmd('AT+CGMR', name='get_card_version') return self.queue_at_cmd(cmd) def get_charset(self): """Returns the current character set name""" cmd = ATCmd('AT+CSCS?', name='get_charset') return self.queue_at_cmd(cmd) def get_charsets(self): """Returns the available charsets""" cmd = ATCmd('AT+CSCS=?', name='get_charsets') return self.queue_at_cmd(cmd) def get_contact(self, index): """Returns the contact at ``index``""" cmd = ATCmd('AT+CPBR=%d' % index, name='get_contact') return self.queue_at_cmd(cmd) def list_contacts(self): """ Returns all the contacts stored in the SIM card :raise General: When no contacts are found. :raise NotFound: When no contacts are found. :raise SimBusy: When the SIM is not ready. :raise SimNotStarted: When the SIM is not ready. :rtype: list """ # Note: don't log these exceptions exceptions = (E.General, E.NotFound) cmd = ATCmd('AT+CPBR=1,%d' % self.device.sim.size, name='list_contacts', nolog=exceptions) return self.queue_at_cmd(cmd) def get_imei(self): """Returns the IMEI number of the SIM card""" cmd = ATCmd('AT+CGSN', name='get_imei') return self.queue_at_cmd(cmd) def get_imsi(self): """Returns the IMSI number of the SIM card""" cmd = ATCmd('AT+CIMI', name='get_imsi') return self.queue_at_cmd(cmd) def get_manufacturer_name(self): """Returns the manufacturer name of the SIM card""" cmd = ATCmd('AT+GMI', name='get_manufacturer_name') return self.queue_at_cmd(cmd) def get_netreg_status(self): """Returns the network registration status""" cmd = ATCmd('AT+CREG?', name='get_netreg_status') return self.queue_at_cmd(cmd) def get_network_info(self, _type=None): """Returns a tuple with the network info""" if _type is 'name': s = 'AT+COPS=3,0;+COPS?' elif _type is 'numeric': s = 'AT+COPS=3,2;+COPS?' else: s = 'AT+COPS?' cmd = ATCmd(s, name='get_network_info') return self.queue_at_cmd(cmd) def get_network_names(self): """Returns a tuple with the network info""" cmd = ATCmd('AT+COPS=?', name='get_network_names') cmd.timeout = 300 return self.queue_at_cmd(cmd) def get_phonebook_size(self): """ Returns the phonebook size of the SIM card :raise General: When the SIM is not ready. :raise SimBusy: When the SIM is not ready. :raise CMSError500: When the SIM is not ready. """ cmd = ATCmd('AT+CPBR=?', name='get_phonebook_size') cmd.timeout = 15 return self.queue_at_cmd(cmd) def get_pin_status(self): """Checks whether the pin is enabled or disabled""" cmd = ATCmd('AT+CLCK="SC",2', name='get_pin_status') return self.queue_at_cmd(cmd) def get_radio_status(self): """Returns whether the radio is enabled or disabled""" cmd = ATCmd("AT+CFUN?", name='get_radio_status') return self.queue_at_cmd(cmd) def get_roaming_ids(self): """Returns a list with the networks we can register with""" cmd = ATCmd('AT+CPOL?', name='get_roaming_ids') return self.queue_at_cmd(cmd) def get_signal_quality(self): """Returns a tuple with the RSSI and BER of the connection""" cmd = ATCmd('AT+CSQ', name='get_signal_quality') return self.queue_at_cmd(cmd) def list_sms(self): """ Returns all the messages stored in the SIM card :raise General: When no messages are found. :raise NotFound: When no messages are found. :rtype: list """ cmd = ATCmd('AT+CMGL=4', name='list_sms') return self.queue_at_cmd(cmd) def get_sms(self, index): """Returns the message stored at ``index``""" cmd = ATCmd('AT+CMGR=%d' % index, name='get_sms') return self.queue_at_cmd(cmd) def get_sms_format(self): """Returns the message stored at ``index``""" cmd = ATCmd('AT+CMGF?', name='get_sms_format') return self.queue_at_cmd(cmd) def get_smsc(self): """Returns the SMSC stored in the SIM""" cmd = ATCmd('AT+CSCA?', name='get_smsc') return self.queue_at_cmd(cmd) def get_used_contact_ids(self): """Returns a list with the used contact ids""" def errback(failure): failure.trap(E.NotFound, E.General) return [] d = self.list_contacts() d.addCallback(lambda contacts: [int(c.group('id')) for c in contacts]) d.addErrback(errback) return d def get_used_sms_ids(self): """Returns a list with used SMS ids in the SIM card""" d = self.list_sms() def errback(failure): failure.trap(E.NotFound, E.General) return [] d.addCallback(lambda smslist: [int(s.group('id')) for s in smslist]) d.addErrback(errback) return d def register_with_netid(self, netid, mode=1, _format=2): """Registers with ``netid``""" atstr = 'AT+COPS=%d,%d,"%s"' % (mode, _format, netid) cmd = ATCmd(atstr, name='register_with_netid') cmd.timeout = 30 return self.queue_at_cmd(cmd) def reset_settings(self): """Resets the settings to factory settings""" cmd = ATCmd('ATZ', name='reset_settings') return self.queue_at_cmd(cmd) def save_sms(self, pdu, pdu_len): """Returns the index where ``pdu`` was stored""" cmd = ATCmd('AT+CMGW=%s' % pdu_len, name='save_sms', eol='\r') cmd.splitcmd = '%s\x1a' % pdu return self.queue_at_cmd(cmd) def send_pin(self, pin): """ Authenticates using ``pin`` :raise General: Exception raised by Nozomi when PIN is incorrect. :raise IncorrectPassword: Exception raised when the PIN is incorrect """ cmd = ATCmd('AT+CPIN="%s"' % str(pin), name='send_pin') return self.queue_at_cmd(cmd) def send_puk(self, puk, pin): """ Authenticates using ``puk`` and ``pin`` :raise General: Exception raised by Nozomi when PUK is incorrect. :raise IncorrectPassword: Exception raised when the PUK is incorrect """ atstr = 'AT+CPIN="%s","%s"' % (str(puk), str(pin)) cmd = ATCmd(atstr, name='send_puk') return self.queue_at_cmd(cmd) def send_sms(self, pdu, pdu_len): """Sends the given pdu and returns the index""" cmd = ATCmd('AT+CMGS=%d' % pdu_len, name='send_sms', eol='\r') cmd.splitcmd = '%s\x1a' % pdu return self.queue_at_cmd(cmd) def send_sms_from_storage(self, index): """Sends the SMS stored at ``index`` and returns the new index""" cmd = ATCmd('AT+CMSS=%d' % index, name='send_sms_from_storage') return self.queue_at_cmd(cmd) def send_ussd(self, ussd): """Sends the USSD command ``ussd``""" dcs = 15 cmd = ATCmd('AT+CUSD=1,"%s",%d' % (ussd, dcs), name='send_ussd') cmd.timeout = 30 return self.queue_at_cmd(cmd) def set_apn(self, index, apn): """Sets the APN to ``apn`` using ``index``""" cmd = ATCmd('AT+CGDCONT=%d,"IP","%s"' % (index, apn), name='set_apn') return self.queue_at_cmd(cmd) def set_charset(self, charset): """Sets the character set used on the SIM""" cmd = ATCmd('AT+CSCS="%s"' % charset, name='set_charset') return self.queue_at_cmd(cmd) def set_error_level(self, level): """Sets the modem error level""" cmd = ATCmd('AT+CMEE=%d' % level, name='set_error_level') return self.queue_at_cmd(cmd) def set_netreg_notification(self, val=1): """Sets CREG unsolicited notification""" cmd = ATCmd('AT+CREG=%d' % val, name='set_netreg_notification') return self.queue_at_cmd(cmd) def set_network_info_format(self, mode=0, _format=2): """Sets the network information format for +COPS queries""" cmd = ATCmd('AT+COPS=%d,%d' % (mode, _format), name='set_network_info_format') return self.queue_at_cmd(cmd) def set_sms_format(self, _format=0): """Sets the format of the SMS""" cmd = ATCmd('AT+CMGF=%d' % _format, name='set_sms_format') return self.queue_at_cmd(cmd) def set_sms_indication(self, mode=2, mt=1, bm=0, ds=0, bfr=0): """Sets the SMS indication mode""" args = 'AT+CNMI=' + ','.join(map(str, [mode, mt, bm, ds, bfr])) cmd = ATCmd(args, name='set_sms_indication') return self.queue_at_cmd(cmd) def set_smsc(self, number): """Sets the SMSC""" cmd = ATCmd('AT+CSCA="%s"' % number, name='set_smsc') return self.queue_at_cmd(cmd) def send_at(self, at_str, name='send_at', timeout=None): """Send an arbitrary AT string to the SIM card""" cmd = ATCmd(at_str, name=name) if timeout: cmd.timeout = timeout return self.queue_at_cmd(cmd) def sim_access_restricted(self, command, fileid=None, p1=None, p2=None, p3=None, data=None, pathid=None): """ Implements CRSM AT command. command is only mandatory argument. :param command: command integer number. :type command: int """ if fileid is None: cmd = ATCmd('AT+CRSM=%d' % command, name='sim_access_restricted') elif p1 is None or p2 is None or p3 is None: cmd = ATCmd('AT+CRSM=%d,%d' % (command, fileid), name='sim_access_restricted') elif data is None: cmd = ATCmd('AT+CRSM=%d,%d,%d,%d,%d' % (command, fileid, p1, p2, p3), name='sim_access_restricted') elif pathid is None: cmd = ATCmd('AT+CRSM=%d,%d,%d,%d,%d,%s' % (command, fileid, p1, p2, p3, data), name='sim_access_restricted') else: cmd = ATCmd('AT+CRSM=%d,%d,%d,%d,%d,%s,%s' % (command, fileid, p1, p2, p3, data, pathid), name='sim_access_restricted') return self.queue_at_cmd(cmd) wader-0.5.13/core/resolvconf.py000066400000000000000000000040121257646610200163720ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 from twisted.python import log from wader.common.utils import get_file_data, save_file class NamedManager(object): def __init__(self, resolvconf_path='/etc/resolv.conf'): self.resolvconf_path = resolvconf_path self.old_contents = None def add_dns_info(self, dns): if self.old_contents is not None: # just log it, don't fail msg = "NamedManager old_contents not empty: %s" log.err(msg % self.old_contents) return # backup old data real_path = os.path.realpath(self.resolvconf_path) self.old_contents = get_file_data(real_path) # set new DNS data to_write = [] if dns: to_write.append("# Generated by Wader") for d in dns: to_write.append("nameserver %s" % d) to_write.append('') save_file(real_path, '\n'.join(to_write)) def delete_dns_info(self, dns): if self.old_contents is None: # just log it, don't fail log.err("NamedManager old_contents is None") return real_path = os.path.realpath(self.resolvconf_path) save_file(real_path, self.old_contents) self.old_contents = None wader-0.5.13/core/serialport.py000066400000000000000000000061451257646610200164070ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Logging Serial Port and related classes""" from twisted import version as TwistedVersion from twisted.internet.serialport import SerialPort as _SerialPort from twisted.python import log class Port(object): """I represent a serial port in Wader""" def __init__(self, path): self._port_path = path self._sport_obj = None def get_port_path(self): return self._port_path def set_port_path(self, path): self._port_path = path def get_sport_obj(self): return self._sport_obj def set_sport_obj(self, obj): self._sport_obj = obj path = property(get_port_path, set_port_path) obj = property(get_sport_obj, set_sport_obj) class Ports(object): """I am a pair of :class:`~core.serialport.Port` objects""" def __init__(self, dport, cport): self.dport = Port(dport) self.cport = Port(cport) def get_application_port(self): """Returns the application port""" if self.cport.path: return self.cport elif self.dport.path: return self.dport else: raise AttributeError("No application port") def has_two(self): """ Check if there are two active ports :rtype: bool """ return all([self.dport.path, self.cport.path]) def __repr__(self): if not self.cport.path: return "dport: %s" % (self.dport.path) return "dport: %s cport: %s" % (self.dport.path, self.cport.path) class SerialPort(_SerialPort, log.Logger): """Small wrapper over Twisted's serial port to make it loggable""" def __init__(self, protocol, port, reactor, baudrate=115200, timeout=.1): super(SerialPort, self).__init__(protocol, port, reactor, baudrate=baudrate, timeout=timeout) self._port = port # Note: fixup a long standing bug in twisted itself def connectionLost(self, reason): super(SerialPort, self).connectionLost(reason) if TwistedVersion.major < 11 or \ (TwistedVersion.major == 11 and TwistedVersion.minor < 1): self.protocol.connectionLost(reason) def logPrefix(self): """Returns the last part of the port being used""" return self._port.split('/')[-1] wader-0.5.13/core/shell.py000066400000000000000000000031111257646610200153200ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Module to obtain an introspection shell""" from twisted.cred import portal, checkers from twisted.conch import manhole, manhole_ssh def get_manhole_factory(namespace, **passwords): """ Returns a ``ConchFactory`` instance configured with given settings :param namespace: The namespace to use :param passwords: The passwords to use :rtype: `twisted.conch.manhole_ssh.ConchFactory` """ realm = manhole_ssh.TerminalRealm() def getManhole(_): return manhole.Manhole(namespace) realm.chainedProtocolFactory.protocolFactory = getManhole p = portal.Portal(realm) checker = checkers.InMemoryUsernamePasswordDatabaseDontUse(**passwords) p.registerChecker(checker) return manhole_ssh.ConchFactory(p) wader-0.5.13/core/sim.py000066400000000000000000000114371257646610200150130ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """SIM startup module""" from twisted.python import log from twisted.internet import defer, reactor import wader.common.aterrors as E from wader.common.encoding import from_ucs2 RETRY_ATTEMPTS = 3 RETRY_TIMEOUT = 3 # SIM files: # 3GPP TS 31.102 V10.21.0 (2011-064) EF_AD = 0x6fad # 28589 EF_SPN = 0x6f46 # 28486 # ETSI TS 102 221 V8.2.0 (2009-06) EF_ICCID = 0x2fe2 # 12285 # SIM Commands: # ETSI TS 102 221 V8.2.0 (2009-06): Commands # 3GPP TS 27.007 V10.4.0 (2011-06): Restricted SIM access +CRSM COM_READ_BINARY = 0xb0 # 176 COM_READ_RECORD = 0xb2 # 178 COM_GET_RESPONSE = 0xc0 # 192 COM_UPDATE_BINARY = 0xd6 # 214 COM_UPDATE_RECORD = 0xdc # 220 COM_STATUS = 0xf2 # 242 COM_RETRIEVE_DATA = 0xcb # 203 COM_SET_DATA = 0xdb # 219 # Successfull status bytes # ETSI TS 102 221 V8.2.0 (2009-06) SW_OK = [0x90, 0x91, 0x92] # Response APDU structure. class SIMBaseClass(object): """ I take care of initing the SIM The actual details of initing the SIM vary from mobile to datacard, so I am the one to subclass in case your device needs a special startup """ def __init__(self, sconn): super(SIMBaseClass, self).__init__() self.sconn = sconn self.size = None self.charset = 'IRA' self.num_of_failures = 0 def set_size(self, size): log.msg("Setting size to %d" % size) self.size = size def set_charset(self, charset): try: self.charset = from_ucs2(charset) except (UnicodeDecodeError, TypeError): self.charset = charset return charset def setup_sms(self): # Notification when a SMS arrives... self.sconn.set_sms_indication(2, 1, 0, 1, 0) # set PDU mode self.sconn.set_sms_format(0) def initialize(self, set_encoding=True): """ Initializes the SIM card This method sets up encoding, SMS format and notifications in the SIM. It returns a deferred with the SIM size. """ # set up extended error reporting self.sconn.set_error_level(1) if set_encoding: self._setup_encoding() self.setup_sms() deferred = defer.Deferred() def get_size(auxdef): d = self.sconn.get_phonebook_size() def phonebook_size_cb(resp): self.set_size(resp) auxdef.callback(self.size) def phonebook_size_eb(failure): failure.trap(E.General, E.SimBusy, E.SimFailure) self.num_of_failures += 1 if self.num_of_failures > RETRY_ATTEMPTS: # fail gracefully for now, we'll try again # the next time a contact operation is required auxdef.callback(None) return reactor.callLater(RETRY_TIMEOUT, get_size, auxdef) d.addCallback(phonebook_size_cb) d.addErrback(phonebook_size_eb) return auxdef return get_size(deferred) def _set_charset(self, charset): """ Checks whether is necessary the change and memorizes the used charset """ def process_charset(reply): """ Only set the new charset if is different from current encoding """ if reply != charset: return self.sconn.set_charset(charset) # we already have the wanted charset self.charset = reply return defer.succeed(True) d = self.sconn.get_charset() d.addCallback(process_charset) return d def _process_charsets(self, charsets): for charset in ["UCS2", "IRA", "GSM"]: if charset in charsets: return self._set_charset(charset) msg = "Couldn't find an appropriated charset in %s" raise E.CharsetError(msg % charsets) def _setup_encoding(self): d = self.sconn.get_charsets() d.addCallback(self._process_charsets) return d wader-0.5.13/core/startup.py000066400000000000000000000270521257646610200157250ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Utilities used at startup""" import os from os.path import join import dbus from dbus.service import Object, BusName, method, signal from twisted.application.service import Application, Service from twisted.internet import reactor, defer from twisted.plugin import IPlugin, getPlugins from twisted.python import log # Logger related imports. import glob import time from twisted.python import threadable from twisted.python.log import ILogObserver, FileLogObserver from twisted.python.logfile import BaseLogFile, LogReader import wader.common.consts as consts from wader.common._dbus import DBusExporterHelper from wader.common.provider import NetworkProvider from core.serialport import SerialPort DELAY = 10 ATTACH_DELAY = 1 OLDLOCK = join(consts.DATA_DIR, '.setup-done') _application = None # Wow, this is fantastic if you need to know where to add an errorback #defer.setDebugging(True) class WaderLogFile(BaseLogFile): """ A log file that can be rotated. A rotateLength of None disables automatic log rotation. """ def __init__(self, name, directory, defaultMode=None, maxRotatedFiles=None): """ Create a log file rotating on length. @param name: file name. @type name: C{str} @param directory: path of the log file. @type directory: C{str} @param defaultMode: mode used to create the file. @type defaultMode: C{int} @param maxRotatedFiles: if not None, max number of log files the class creates. Warning: it removes all log files above this number. @type maxRotatedFiles: C{int} """ BaseLogFile.__init__(self, name, directory, defaultMode) self.maxRotatedFiles = maxRotatedFiles def _openFile(self): BaseLogFile._openFile(self) self.lastDate = self.toDate(os.stat(self.path)[8]) def toDate(self, *args): """Convert a unixtime to (year, month, day) localtime tuple, or return the current (year, month, day) localtime tuple. This function primarily exists so you may overload it with gmtime, or some cruft to make unit testing possible. """ # primarily so this can be unit tested easily return time.localtime(*args)[:3] def shouldRotate(self): """Rotate when the date has changed since last write""" return self.toDate() > self.lastDate def getLog(self, identifier): """ Given an integer, return a LogReader for an old log file. """ filename = "%s.%d" % (self.path, identifier) if not os.path.exists(filename): raise ValueError("no such logfile exists") return LogReader(filename) def write(self, data): """ Write some data to the file. """ BaseLogFile.write(self, data) # Guard against a corner case where time.time() # could potentially run backwards to yesterday. # Primarily due to network time. self.lastDate = max(self.lastDate, self.toDate()) def rotate(self): """ Rotate the file and create a new one. If it's not possible to open new logfile, this will fail silently, and continue logging to old logfile. """ if not (os.access(self.directory, os.W_OK) and \ os.access(self.path, os.W_OK)): return logs = self.listLogs() logs.reverse() for i in logs: if self.maxRotatedFiles is not None and i >= self.maxRotatedFiles: os.remove("%s.%d" % (self.path, i)) else: os.rename("%s.%d" % (self.path, i), "%s.%d" % (self.path, i + 1)) self._file.close() os.rename(self.path, "%s.1" % self.path) self._openFile() def listLogs(self): """ Return sorted list of integers - the old logs' identifiers. """ result = [] for name in glob.glob("%s.*" % self.path): try: counter = int(name.split('.')[-1]) if counter: result.append(counter) except ValueError: pass result.sort() return result def __getstate__(self): state = BaseLogFile.__getstate__(self) del state["lastDate"] return state threadable.synchronize(WaderLogFile) def _get_application(): """ Factory function that returns an Application object. If the object does not exist then it creates a new Application object. (Internal use only). """ global _application if _application is not None: return _application _application = Application(consts.APP_NAME) logfile = WaderLogFile(consts.LOG_NAME, consts.LOG_DIR, maxRotatedFiles=consts.LOG_NUMBER) _application.setComponent(ILogObserver, FileLogObserver(logfile).emit) return _application def create_skeleton_and_do_initial_setup(): """I perform the operations needed for the initial user setup""" PLUGINS = join(consts.BASE_DIR, 'usr', 'share', consts.APP_SLUG_NAME, 'plugins') if os.path.exists(OLDLOCK): # old way to signal that the setup is complete os.unlink(OLDLOCK) # maybe create new networks DB provider = NetworkProvider() try: if provider.is_current(): log.msg("Networks DB is current") else: log.msg("Networks DB creation started") provider.populate_networks() log.msg("Networks DB population complete") except Exception, e: log.err(str(e)) finally: provider.close() # maybe regenerate plugin cache regenerate = False # remove any compiled python files that are orphans for cmpname in glob.glob(join(PLUGINS, '*.py[co]')): pyname = cmpname[:-1] if not os.path.exists(pyname): regenerate = True os.unlink(cmpname) # check if any python is newer than the cache if not regenerate: try: timestamp = os.stat(join(PLUGINS, 'dropin.cache'))[8] for pyname in glob.glob(join(PLUGINS, '*.py')) + \ glob.glob(join(PLUGINS, '*.py[co]')): if timestamp < os.stat(pyname)[8]: raise ValueError except: regenerate = True if regenerate: log.msg("Plugin cache generation started") import plugins list(getPlugins(IPlugin, package=plugins)) log.msg("Plugin cache generation complete") else: log.msg("Plugin cache is current") class WaderService(Service): """I am a Twisted service that starts up Wader""" def __init__(self): self.ctrl = None self.prof = None self.dial = None def startService(self): """Starts the Wader service""" log.msg("%s (%s) started" % (consts.APP_NAME, consts.APP_VERSION)) create_skeleton_and_do_initial_setup() # check if we have an OSPlugin for this OS/Distro from core.oal import get_os_object if get_os_object() is None: message = 'OS/Distro not registered' details = """ The OS/Distro under which you are running %s is not registered in the OS database. Check the documentation for what you can do in order to support your OS/Distro """ % consts.APP_NAME raise SystemExit("%s\n%s" % (message, details)) from core.dialer import DialerManager self.ctrl = StartupController() self.dial = DialerManager(self.ctrl) def get_clients(self): """ Helper method for SSH sessions :rtype: dict """ return self.ctrl.hm.clients class StartupController(Object, DBusExporterHelper): """ I manage devices in the system Discovery, identification, hotplugging, etc. :ivar clients: Dict with a reference to every configured device """ def __init__(self): name = BusName(consts.WADER_SERVICE, bus=dbus.SystemBus()) super(StartupController, self).__init__(bus_name=name, object_path=consts.WADER_OBJPATH) from core.oal import get_os_object self.hm = get_os_object().hw_manager assert self.hm is not None, "Running Wader on an unsupported OS?" self.hm.register_controller(self) @method(consts.WADER_INTFACE, in_signature='', out_signature='ao', async_callbacks=('async_cb', 'async_eb')) def EnumerateDevices(self, async_cb, async_eb): """ Returns a list of object paths with all the found devices It also includes the object paths of already handled devices """ d = self.hm.get_devices() d.addCallback(lambda devs: [d.opath for d in devs]) return self.add_callbacks(d, async_cb, async_eb) @signal(consts.WADER_INTFACE, signature='o') def DeviceAdded(self, opath): """Emitted when a 3G device is added""" log.msg("emitting DeviceAdded('%s')" % opath) @signal(consts.WADER_INTFACE, signature='o') def DeviceRemoved(self, opath): """Emitted when a 3G device is removed""" log.msg("emitting DeviceRemoved('%s')" % opath) def get_wader_application(): """ Returns the application object required by twistd on startup In the future this will be the point that will load startup plugins and modify the application object """ service = WaderService() application = _get_application() service.setServiceParent(application) return application def attach_to_serial_port(device): """Attaches the serial port in ``device``""" port = device.ports.get_application_port() if port.obj is not None: return defer.succeed(device) d = defer.Deferred() port.obj = SerialPort(device.sconn, port.path, reactor, baudrate=device.baudrate) reactor.callLater(ATTACH_DELAY, lambda: d.callback(device)) return d def setup_and_export_device(device): """Sets up ``device`` and exports its methods over DBus""" if not device.custom.wrapper_klass: raise AttributeError("No wrapper class for device %s" % device) wrapper_klass = device.custom.wrapper_klass log.msg("wrapping plugin %s with class %s" % (device, wrapper_klass)) device.sconn = wrapper_klass(device) # Use the exporter that device specifies if not device.custom.exporter_klass: raise AttributeError("No exporter class for device %s" % device) exporter_klass = device.custom.exporter_klass log.msg("exporting %s methods with class %s" % (device, exporter_klass)) exporter = exporter_klass(device) device.exporter = exporter device.__repr__ = device.__str__ return device wader-0.5.13/core/statem/000077500000000000000000000000001257646610200151405ustar00rootroot00000000000000wader-0.5.13/core/statem/__init__.py000066400000000000000000000016021257646610200172500ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Different state machines used in the app""" wader-0.5.13/core/statem/auth.py000066400000000000000000000160741257646610200164630ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Authentication state machine""" from epsilon.modal import mode, Modal from twisted.internet import reactor, defer from twisted.python import log import wader.common.aterrors as E SIM_FAIL_DELAY = 15 MAX_NUM_SIM_ERRORS = 3 MAX_NUM_SIM_BUSY = 5 class AuthStateMachine(Modal): """I authenticate against a device""" modeAttribute = 'mode' initialMode = 'get_pin_status' DELAY = 15 def __init__(self, device): self.device = device self.deferred = defer.Deferred() # it will be set to True if AT+CPIN? == +CPIN: READY self.auth_was_ready = False self.num_sim_errors = 0 self.num_sim_busy = 0 log.msg("starting %s ..." % self.__class__.__name__) def __repr__(self): return "authentication_sm" # keyring stuff def notify_auth_ok(self): """Called when authentication was successful""" self.deferred.callback(True) def notify_auth_failure(self, failure): """Called when we faced a failure""" log.msg("%s: notifying auth failure %s" % (self, failure)) self.deferred.errback(failure) # states callbacks def check_pin_cb(self, resp): """Callbacked with check_pin's result""" self.auth_was_ready = True self.notify_auth_ok() def get_pin_status_cb(self, enabled): """Callbacked with get_pin_status's result""" if int(enabled): self.notify_auth_failure(E.SimPinRequired()) else: self.notify_auth_ok() def incorrect_pin_eb(self, failure): """Executed when PIN is incorrect""" failure.trap(E.IncorrectPassword) self.notify_auth_failure(failure) def incorrect_puk_eb(self, failure): """Executed when the PUK is incorrect""" failure.trap(E.IncorrectPassword, E.General) self.notify_auth_failure(E.IncorrectPassword()) def incorrect_puk2_eb(self, failure): """Executed when the PUK2 is incorrect""" failure.trap(E.IncorrectPassword, E.General) self.notify_auth_failure(E.IncorrectPassword()) def pin_required_eb(self, failure): """Executed when SIM PIN is required""" failure.trap(E.SimPinRequired, E.General) self.notify_auth_failure(E.SimPinRequired()) def puk_required_eb(self, failure): """Executed when PUK/PUK2 is required""" failure.trap(E.SimPukRequired, E.SimPuk2Required) self.notify_auth_failure(failure) def sim_failure_eb(self, failure): """Executed when there's a SIM failure, try again in a while""" failure.trap(E.SimFailure) log.msg("device returned SimFailure") self.num_sim_errors += 1 if self.num_sim_errors >= MAX_NUM_SIM_ERRORS: # we can now consider that there's something wrong with the # device, probably there's no SIM self.notify_auth_failure(E.SimNotInserted()) return reactor.callLater(SIM_FAIL_DELAY, self.do_next) def sim_busy_eb(self, failure): """Executed when SIM is busy, try again in a while""" failure.trap(E.SimBusy, E.SimNotStarted, E.General) self.num_sim_busy += 1 if self.num_sim_busy >= MAX_NUM_SIM_BUSY: # we can now consider that there's something wrong with the # device, probably a firmwarebug self.notify_auth_failure(E.SimFailure()) return reactor.callLater(SIM_FAIL_DELAY, self.do_next) def sim_no_present_eb(self, failure): """Executed when there's no SIM, errback it""" failure.trap(E.SimNotInserted) self.notify_auth_failure(failure) # entry point def start_auth(self): """ Starts the authentication Returns a deferred that will be callbacked if everything goes alright :raise SimFailure: SIM unknown error :raise SimNotInserted: SIM not inserted :raise DeviceLockedError: Device is locked """ self.do_next() return self.deferred # states class get_pin_status(mode): """ Ask the SIM what's the PIN status The SIM can be in one of the following states: - SIM is ready (already authenticated, or PIN disabled) - PIN is needed - PIN2 is needed (not handled) - PUK/PUK2 is needed - SIM is not inserted - SIM's firmware error """ def __enter__(self): pass def __exit__(self): pass def do_next(self): log.msg("%s: transition to get_pin_status mode...." % self) from core.startup import attach_to_serial_port d = attach_to_serial_port(self.device) d.addCallback(lambda _: self.device.sconn.set_error_level(1)) d.addCallback(lambda _: self.device.sconn.check_pin()) d.addCallback(self.check_pin_cb) d.addErrback(self.pin_required_eb) d.addErrback(self.puk_required_eb) d.addErrback(self.sim_failure_eb) d.addErrback(self.sim_busy_eb) d.addErrback(self.sim_no_present_eb) class pin_needed_status(mode): """ Three things can happen: - Auth went OK - PIN is incorrect - After three failed PIN auths, PUK is needed """ def __enter__(self): pass def __exit__(self): pass def do_next(self): """ Three things can happen: - Auth went OK - PIN is incorrect - After three failed PIN auths, PUK is needed """ pass class puk_needed_status(mode): """ Three things can happen: - Auth went OK - PUK/PIN is incorrect - After five failed attempts, PUK2 is needed """ def __enter__(self): pass def __exit__(self): pass def do_next(self): pass class puk2_needed_status(mode): """ Three things can happen: - Auth went OK - PUK2/PIN is incorrect - After ten failed attempts, device is locked """ def __enter__(self): pass def __exit__(self): pass def do_next(self): pass wader-0.5.13/core/statem/networkreg.py000066400000000000000000000274141257646610200177110ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Network registration state machine""" import dbus from epsilon.modal import mode, Modal from twisted.python import log from twisted.internet import defer, reactor #import wader.common.exceptions as ex import wader.common.aterrors as E from wader.common.signals import SIG_CREG from wader.common.consts import (WADER_SERVICE, STATUS_IDLE, STATUS_HOME, STATUS_SEARCHING, STATUS_DENIED, STATUS_UNKNOWN, STATUS_ROAMING) REGISTER_TIMEOUT = 15 MAX_WAIT_TIMES = 6 class NetworkRegistrationStateMachine(Modal): """I am a network registration state machine""" modeAttribute = 'mode' initialMode = 'check_registration' def __init__(self, sconn, netid=""): self.sconn = sconn self.netid = netid self.deferred = defer.Deferred() self.call_id = None # used to track how many times we've been waiting for a new event self.wait_counter = 0 self.registering = False self.tried_manual_registration = False self.signal_matchs = [] self.connect_to_signals() log.msg("starting %s ..." % self.__class__.__name__) def __repr__(self): return "network_sm" def connect_to_signals(self): bus = dbus.SystemBus() device = bus.get_object(WADER_SERVICE, self.sconn.device.opath) sm = device.connect_to_signal(SIG_CREG, self.on_netreg_cb) self.signal_matchs.append(sm) def clean_signals(self): while self.signal_matchs: sm = self.signal_matchs.pop() sm.remove() def start_netreg(self): """ Starts the network registration process Returns a deferred that will be callbacked upon success and errbacked with a CMEError30 or a NetworkRegistrationError if fails """ self.do_next() return self.deferred def notify_success(self, ignored=True): """Notifies the caller that we have succeed""" try: self.deferred.callback(ignored) except defer.AlreadyCalledError: pass self.cancel_counter() self.clean_signals() def notify_failure(self, failure): """Notifies the caller that we have failed""" try: self.deferred.errback(failure) except defer.AlreadyCalledError: pass self.cancel_counter() self.clean_signals() def cancel_counter(self): if self.call_id is not None and not self.call_id.called: self.call_id.cancel() self.call_id = None def restart_counter_or_transition(self, timeout=REGISTER_TIMEOUT): self.cancel_counter() self.wait_counter += 1 if self.wait_counter <= MAX_WAIT_TIMES: self.call_id = reactor.callLater(timeout, self.check_if_registered) elif not self.tried_manual_registration: self.transitionTo('manual_registration') self.do_next() else: # we have already tried to register manually and it failed self.notify_failure(E.Unknown()) def register_with_netid(self, netid): self.tried_manual_registration = True d = self.sconn.register_with_netid(netid) d.addCallback(lambda ign: self.check_if_registered()) def check_if_registered(self): d = self.sconn.get_netreg_status() d.addCallback(self.process_netreg_status) def on_netreg_cb(self, status): """Callback for +CREG notifications""" # we fake 'mode == 1' as we've already enabled it self.process_netreg_status((1, status)) def process_netreg_status(self, info): """Processes get_netreg_status callback and reacts accordingly""" _mode, status = info if status == STATUS_IDLE: # we are not looking for a network # set up +CREG notification and start network search if not _mode: self.sconn.set_netreg_notification(1) if not self.registering: self.sconn.send_at('AT+COPS=0,,') self.registering = True self.restart_counter_or_transition() elif status in [STATUS_SEARCHING, STATUS_UNKNOWN]: # we are looking for a network, give it some time. # +CREG: 4 is officially unknown, but it's been seen during # Ericsson F3507g radio power up, and with Huawei E173 immediately # before registration; in neither case is it fatal. if not _mode: self.sconn.set_netreg_notification(1) self.restart_counter_or_transition() elif status in [STATUS_HOME, STATUS_ROAMING]: # We have already found our network -unless a netid was # specified. Lets check if the contraints are satisfied self.registering = False self.transitionTo('check_constraints') self.do_next() elif status == STATUS_DENIED: # Network registration has been denied self.registering = False msg = 'Net registration failed: +CREG: %d,%d' % (_mode, status) self.notify_failure(E.NetworkNotAllowed(msg)) return def process_netreg_info(self, info): """ Checks if we are registered with the supplied operator (if any) It will transition to manual_registration if necessary """ status, netid, long_name = info if self.netid == netid: return self.notify_success() # turns out we're registered with an operator we shouldn't be self.transitionTo('manual_registration') self.do_next() def find_netid_to_register_with(self, imsi): """ Registers with the first netid that appears in both +COPS=? and +CPOL? """ # we have tried to register with our home network and it has # failed. We'll try to register with the first netid present # in AT+COPS=? and AT+CPOL? def process_netnames(networks): # Sort so that networks with longer prefixes are found first networks.sort(reverse=True) for n in networks: try: is_imsi_prefix = imsi.startswith(n.netid) \ if len(n.netid) else False except (AttributeError, NameError, TypeError): is_imsi_prefix = False if n.netid == self.netid or is_imsi_prefix: assert self.registering == False, "Registering again?" self.register_with_netid(n.netid) self.registering = True def process_roaming_ids_cb(roam_operators): for roam_operator in roam_operators: if roam_operator in networks: assert self.registering == False, "Registering again?" self.register_with_netid(roam_operator.netid) self.registering = True break else: # Simplify for message cpol = list(set([i.netid for i in roam_operators])) netids = list(set([i.netid for i in networks])) msg = "No match in +CPOL %s / networks %s" % (cpol, netids) log.err(msg) self.notify_failure(E.NoNetwork(msg)) # raise ex.NetworkRegistrationError(msg) def process_roaming_ids_eb(failure): # +CME ERROR 3, +CME ERROR 4 failure.trap(E.OperationNotAllowed, E.OperationNotSupported) # Simplify for message netids = list(set([i.netid for i in networks])) msg = "No +CPOL list to compare with networks %s" % netids log.err(msg) self.notify_failure(E.NoNetwork(msg)) # raise ex.NetworkRegistrationError(msg) d = self.sconn.get_roaming_ids() d.addCallback(process_roaming_ids_cb) d.addErrback(process_roaming_ids_eb) d = self.sconn.get_network_names() d.addCallback(process_netnames) # states class check_registration(mode): """I check +CREG to see whats the initial status""" def __enter__(self): log.msg("%s: check_registration entered" % self) def __exit__(self): log.msg("%s: check_registration exited" % self) def do_next(self): d = self.sconn.get_netreg_status() d.addCallback(self.process_netreg_status) class check_constraints(mode): """ We are registered with our home network or roaming We are going to check whether it satisfies our constraints or not """ def __enter__(self): log.msg("%s: check_constraints entered" % self) def __exit__(self): log.msg("%s: check_constraints exited" % self) def do_next(self): if not self.netid: # no netid specified and we're already registered with our # home network or roaming, this is success self.notify_success() return d = self.sconn.get_netreg_info() d.addCallback(self.process_netreg_info) class manual_registration(mode): """ I start the manual registration process This is due to a +CREG: 1,2 or because the card automatically registered with an operator and its netid doesn't matches with the one specified by the user """ def __enter__(self): log.msg("%s: manual_registration entered" % self) def __exit__(self): log.msg("%s: manual_registration exited" % self) def do_next(self): def process_imsi_cb(imsi): assert self.registering == False, "Registering again?" log.msg("manual_registration self.netid = %s, imsi = %s" % (str(self.netid), str(imsi))) try: is_imsi_prefix = imsi.startswith(self.netid) \ if len(self.netid) else False except (AttributeError, NameError, TypeError): is_imsi_prefix = False if is_imsi_prefix: log.msg("manual_registration self.netid is IMSI prefix") # if we've been specified a netid, we cannot do # much more than trying to register with it and # if it fails return asap self.register_with_netid(self.netid) self.registering = True else: log.msg("manual_registration self.netid not IMSI prefix") # look for a netid to register with self.find_netid_to_register_with(imsi) log.msg("%s: obtaining the IMSI..." % self) d = self.sconn.get_imsi() d.addCallback(process_imsi_cb) wader-0.5.13/core/statem/simple.py000066400000000000000000000200431257646610200170020ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """org.freedesktop.ModemManager.Modem.Simple state machine""" from epsilon.modal import mode, Modal from twisted.internet import defer, reactor from twisted.python import log import wader.common.aterrors as E from wader.common.consts import STATUS_HOME, STATUS_ROAMING from wader.common.utils import convert_network_mode_to_allowed_mode # poll at 'n' second intervals for 'm' tries INTERVAL = 3 TRIES = 30 class SimpleStateMachine(Modal): """I am a state machine for o.fd.ModemManager.Modem.Simple""" modeAttribute = 'mode' initialMode = 'begin' def __init__(self, device, settings): self.device = device self.sconn = device.sconn # XXX: Currently, as of NM 0.8, nm-applet always passes # 'network_mode'=ANY in ConnectSimple. We have to # remove it here or we stamp on the card setting # done from the user's profile if 'network_mode' in settings: del settings['network_mode'] self.settings = settings self.deferred = defer.Deferred() def transition_to(self, state): self.transitionTo(state) self.do_next() def start_simple(self): """Starts the whole process""" self.do_next() return self.deferred def notify_success(self, ignored=True): """Notifies the caller that we have succeed""" self.deferred.callback(ignored) def notify_failure(self, failure): """Notifies the caller that we have failed""" self.deferred.errback(failure) class begin(mode): def __enter__(self): log.msg("Simple SM: begin entered") def __exit__(self): log.msg("Simple SM: begin exited") def do_next(self): self.registration_tries = TRIES self.transition_to('check_pin') class check_pin(mode): """We are going to check whether auth is ready or not""" def __enter__(self): log.msg("Simple SM: check_pin entered") def __exit__(self): log.msg("Simple SM: check_pin exited") def do_next(self): # check the auth state, if its ready go to next state # otherwise try to auth with the provided pin, give it # some seconds to settle and go to next state def check_pin_cb(ignored, wait=False): if not wait: self.transition_to('register') else: DELAY = self.device.custom.auth_klass.DELAY reactor.callLater(DELAY, self.transition_to, 'register') def check_pin_eb_pin_needed(failure): failure.trap(E.SimPinRequired) if 'pin' not in self.settings: self.notify_failure(E.SimPinRequired("No pin provided")) return d = self.sconn.send_pin(self.settings['pin']) d.addCallback(check_pin_cb, wait=True) d.addErrback(self.notify_failure) d = self.sconn.check_pin() d.addCallback(check_pin_cb) d.addErrback(check_pin_eb_pin_needed) class register(mode): """Registers with the given network id""" def __enter__(self): log.msg("Simple SM: register entered") def __exit__(self): log.msg("Simple SM: register exited") def do_next(self): if 'network_id' in self.settings: netid = self.settings['network_id'] d = self.sconn.register_with_netid(netid) d.addCallback(lambda _: self.transition_to('set_apn')) else: self.transition_to('set_apn') class set_apn(mode): def __enter__(self): log.msg("Simple SM: set_apn entered") def __exit__(self): log.msg("Simple SM: set_apn exited") def do_next(self): if 'apn' in self.settings: d = self.sconn.set_apn(self.settings['apn']) d.addCallback(lambda _: self.transition_to('set_band')) else: self.transition_to('set_band') class set_band(mode): def __enter__(self): log.msg("Simple SM: set_band entered") def __exit__(self): log.msg("Simple SM: set_band exited") def do_next(self): if 'band' in self.settings: d = self.sconn.set_band(self.settings['band']) d.addCallback(lambda _: reactor.callLater(1, self.transition_to, 'set_allowed_mode')) else: self.transition_to('set_allowed_mode') class set_allowed_mode(mode): def __enter__(self): log.msg("Simple SM: set_allowed_mode entered") def __exit__(self): log.msg("Simple SM: set_allowed_mode exited") def do_next(self): def get_network_mode_cb(mode): allowed = convert_network_mode_to_allowed_mode(mode) if allowed == self.settings['allowed_mode']: log.msg("Simple SM: set_allowed_mode is current") self.transition_to('wait_for_registration') else: log.msg("Simple SM: set_allowed_mode change required") d2 = self.sconn.set_allowed_mode( self.settings['allowed_mode']) # We need to wait long enough for the device to start # switching and lose the current registration d2.addCallback(lambda _: reactor.callLater(5, self.transition_to, 'wait_for_registration')) if 'allowed_mode' in self.settings: d = self.sconn.get_network_mode() d.addCallback(get_network_mode_cb) else: self.transition_to('wait_for_registration') class wait_for_registration(mode): def __enter__(self): log.msg("Simple SM: wait_for_registration entered") def __exit__(self): log.msg("Simple SM: wait_for_registration exited") def do_next(self): def get_netreg_status_cb(info): if info[1] in [STATUS_HOME, STATUS_ROAMING]: self.transition_to('connect') elif self.registration_tries <= 0: self.notify_failure(E.NoNetwork("Not registered")) else: self.registration_tries -= 1 reactor.callLater(INTERVAL, self.do_next) d = self.sconn.get_netreg_status() d.addCallback(get_netreg_status_cb) class connect(mode): def __enter__(self): log.msg("Simple SM: connect entered") def __exit__(self): log.msg("Simple SM: connect exited") def do_next(self): self.settings['number'] = \ "*99***%d#" % self.sconn.state_dict.get('conn_id') d = self.sconn.connect_to_internet(self.settings) d.addCallback(lambda _: self.transition_to('done')) class done(mode): def __enter__(self): log.msg("Simple SM: done entered") def __exit__(self): log.msg("Simple SM: done exited") def do_next(self): self.notify_success() wader-0.5.13/doc/000077500000000000000000000000001257646610200134605ustar00rootroot00000000000000wader-0.5.13/doc/Makefile000066400000000000000000000034461257646610200151270ustar00rootroot00000000000000# You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html htmlhelp latex changes coverage help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview over all changed/added/deprecated items" @echo " coverage to check documentation coverage for library and C API" clean: -rm -rf _build/* html: mkdir -p _build/html _build/doctrees $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html @echo @echo "Build finished. The HTML pages are in _build/html." latex: mkdir -p _build/latex _build/doctrees $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex @echo @echo "Build finished; the LaTeX files are in _build/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." htmlhelp: mkdir -p _build/htmlhelp _build/doctrees $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in _build/htmlhelp." changes: mkdir -p _build/changes _build/doctrees $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes @echo @echo "The overview file is in _build/changes." coverage: mkdir -p _build/coverage _build/doctrees $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) _build/coverage @echo @echo "Coverage finished; see _build/coverage/python.txt" wader-0.5.13/doc/_static/000077500000000000000000000000001257646610200151065ustar00rootroot00000000000000wader-0.5.13/doc/_static/wader.css000066400000000000000000000000261257646610200167200ustar00rootroot00000000000000@import url(foo.css); wader-0.5.13/doc/conf.py000066400000000000000000000116371257646610200147670ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2009 Warp Networks, S.L. # Author: David Francos Cuartero # # 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. """ Wader documentation build configuration file, created by sphinx-quickstart on Mon Apr 20 09:16:21 2009. """ import os import sys sys.path.insert(0, os.path.abspath('..')) from wader.common.consts import APP_VERSION, APP_NAME # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.coverage'] # Add any paths that contain templates here, relative to this directory. templates_path = [] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'contents' # General substitutions. project = APP_NAME copyright = 'The Wader project and contributors' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # # The short X.Y version. version = APP_VERSION release = version # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%d %B, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = False # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # Sphinx will recurse into subversion configuration folders and try to read # any document file within. These should be ignored. # Note: exclude_dirnames is new in Sphinx 0.5 exclude_dirnames = ['.svn', '.git'] # Options for HTML output # ----------------------- # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. #html_style = 'wader.css' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". #html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. html_use_smartypants = True # Content template for the index page. #html_index = '' # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. html_additional_pages = {} # If false, no module index is generated. html_use_modindex = True # If true, the reST sources are included in the HTML build as _sources/. html_copy_source = True # Output file base name for HTML help builder. htmlhelp_basename = 'Waderdoc' html_show_sourcelink = True # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, # document class [howto/manual]). #latex_documents = [] latex_documents = [ ('contents', 'wader.tex', 'Wader Documentation', 'The Wader project', 'manual'), ] # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # If this isn't set to True, the LaTex writer can only handle six levels of # headers. latex_use_parts = True wader-0.5.13/doc/contents.rst000066400000000000000000000010351257646610200160460ustar00rootroot00000000000000.. _contents: ============================= Wader documentation contents ============================= .. toctree:: :maxdepth: 9 user/index devel/index glossary Indices, glossary and tables ============================ * :ref:`genindex` * :ref:`modindex` * :ref:`glossary` * :ref:`search` .. toctree:: :maxdepth: 9 :hidden: :glob: modules/core/* modules/core/backends/* modules/core/hardware/* modules/core/oses/* modules/core/statem/* modules/wader/common/* modules/wader/common/backends/* wader-0.5.13/doc/devel/000077500000000000000000000000001257646610200145575ustar00rootroot00000000000000wader-0.5.13/doc/devel/add-new-device.rst000066400000000000000000000165731257646610200201010ustar00rootroot00000000000000=================================== How to add support for a new device =================================== How to support a new device =========================== All devices should inherit from :class:`~wader.common.middleware.WCDMAWrapper` in order to override its base methods. It accepts two argument for its constructor, an instance or subclass of :class:`~wader.common.plugin.DevicePlugin` and the object path of the device to use. A DevicePlugin contains all the necessary information to speak with the device: a data port, a control port (if exists), a baudrate, etc. It also has the following attributes and members that you can customise for your device plugin: - ``custom``: An instance or subclass of :class:`wader.common.hardware.base.WCDMACustomizer`. See bellow. - ``sim_klass``: An instance or subclass of :class:`wader.common.sim.SIMBaseClass`. - ``baudrate``: At what speed are we going to talk with this device (default 115200). - ``__remote_name__``: As some devices share the same vendor and product ids, we will issue an `AT+GMR` command right at the beginning to find out the real device model. Set this attribute to whatever your device replies to the `AT+GMR` command. - ``mapping``: A dictionary that, when is not empty, means that this particular combination of vendor and product ids is shared between several models from the same company. As the ids are the same, the only way to differentiate them is issuing an `AT+GMR` command to get the device model. This dict must have an entry for each model and a `default` entry that will be used in cases where we do not know about the card model. For more information about the mapping dictionary, have a look at the module :class:`wader.common.plugins.huawei_exxx`. The object :class:`wader.common.hardware.base.WCDMACustomizer` acts as a container for all the device-specific customizations such as: - ``wrapper_klass``: Specifies the class that will be used to wrap the `Device` object. Used in situations where the default commands supplied by :class:`wader.common.middleware.WCDMAWrapper` are not enough: (i.e. devices with "interesting" firmwares). Some of its commands may need an special parsing or an specific workaround. - ``exporter_klass``: Specifies the class that will export the wrapper methods over :term:`DBus`. - State Machines: Each device its a world on its own, and even though they are supposed to support the relevant GSM and 3GPP standards, some devices prefer to differ from them. The `custom` object contains references to the state machines that the device should use, (on situations where it applies, such as with WCDMA devices of course): - ``auth_klass``: The state machine used to authenticate against the device, default is :class:`wader.common.statem.auth.AuthStateMachine`. - ``netr_klass``: The state machine used to register on the network, default is :class:`wader.common.statem.networkreg.NetworkRegistrationStateMachine`. - ``simp_klass``: The *simple* state machine specified by :term:`ModemManager` v0.2. This state machine basically comprises the previous two on a single (and simpler) one. - ``async_regexp``: regular expression object that will match whatever pattern of unsolicited notifications the given device sends us. - ``signal_translations``: Dictionary of tuples, each tuple has two members: the first is the signal id and the second is a function that will translate the signal arguments and the signal to the internal representation that Wader uses. You can find some sample code in the :class:`~wader.common.hardware.huawei` module. If a notification should be ignored, then add it as a key like the rest, but its value should be a tuple ``(None, None)``. - ``band_dict``: Dictionary with the different bands supported by the device. The keys will *always* be a `MM_NETWORK_BAND_FOO` and the value is up to the implementor. You can see the supported bands in the :mod:`~wader.common.consts` module. - ``conn_dict``: Dictionary with the different network modes supported by the device. The keys will *always* be a `MM_NETWORK_MODE_FOO` and the value is up to the implementor. You can see the supported network modes in the :mod:`~wader.common.consts` module. - ``cmd_dict``: Dictionary with information about how each command should be processed. ``cmd_dict`` most of the time will be a shallow copy of the :class:`~wader.common.command` dict with minor modifications about how a particular command is processed on the given device. - ``device_capabilities``: List with all the unsolicited notifications that this device will send us. If the device sends us every RSSI change that detects, we don't need to poll manually the device for that information. Overview of a simple DevicePlugin ================================= Lets have a look at the NovatelXU870 plugin: .. literalinclude:: ../../plugins/devices/novatel_xu870.py :lines: 18- In an ideal world, devices have a unique vendor and product id tuple, they conform to the relevant CDMA or WCDMA specs, and that's it. The device is identified by its vendor and product ids and double-checked with its `__remote_name__` attribute (the response to an `AT+GMR` command). This vendor and product id tuple will usually use the `usb` bus, however some devices might end up attached to the ``pci`` or `pcmcia` buses. The last line in the plugin will create an instance of the plugin in wader's plugin system -otherwise it will not be found!. Overview of a *relatively* simple DevicePlugin ============================================== Take for example the HuaweiE620 class: .. literalinclude:: ../../plugins/devices/huawei_e620.py :lines: 19- The E620 plugin is identical to the XU870 except one small difference regarding the parsing of the `get_roaming_ids` command. The E620 omits some information that other devices do output, and the regular expression object that parses it has to be updated. We get a new copy of the `cmd_dict` dictionary attribute and modify it with the new regexp the `get_roaming_ids` entry. The new `cmd_dict` is specified in its Customizer object. Overview of a not so simple DevicePlugin ======================================== .. literalinclude:: ../../plugins/devices/huawei_e220.py :lines: 19- Huawei's E220, despite sharing its manufacturer with the E620, has a couple of minor differences that deserve some explanation. There's a bug in its firmware that will reset the device if you ask its SMSC. The workaround is to get once the SMSC before switching to UCS2, you'd be amazed of how long took me to discover the fix. The second difference with the E620 is that the E220 can have several product_ids, thus its product_id list has two elements. Overview of a complex DevicePlugin ================================== .. literalinclude:: ../../plugins/devices/option_colt.py :lines: 24- This data card is the buggiest card we've found so far, and has proven to be an excellent challenge for the extensibility and granularity of our plugin system. Basically we've found the following bugs on the card's firmware: - If PIN authentication is disabled and you issue an `AT+CPIN?`, the card will reply with a `+CPIN: SIM PUK2`. - Don't ask me why, but `AT+CPBR=1,250` does not work once the application is running. I have tried replacing the command with an equivalent one (`AT+CPBF=""`) without luck. So we had to modify the AuthStateMachine for this particular device and its `cmd_dict`. wader-0.5.13/doc/devel/add-new-os.rst000066400000000000000000000031061257646610200172470ustar00rootroot00000000000000How to add support for a new distro/OS ====================================== How to add support for a new distro +++++++++++++++++++++++++++++++++++ Adding support for a new distro is relatively straightforward, it basically boils down to: - Inheriting from :class:`~wader.common.oses.linux.LinuxPlugin`. - Implementing the ``is_valid`` method. This will return True if we are in the given OS/Distro and False otherwise. Usually you will just need to check for a well known file that your distro or OS ships with. - Implement the ``get_timezone`` method. This method returns a string with the timezone name (i.e. "Europe/Madrid"). Implementing this method is not strictly necessary, and Wader can start up without it, but your SMS dates will probably be off by some hours. Lets have a look at the Fedora plugin: .. literalinclude:: ../../plugins/oses/fedora.py :lines: 21- As we can see, the Fedora plugin just defines ``is_valid`` and provides an implementation for ``get_timezone``. How to add support for a new OS +++++++++++++++++++++++++++++++ Adding support for a new OS is not as easy as the previous point. You need to add a new os class to ``wader.common.oses`` with a working implementation for the following methods/objects: - ``get_iface_stats``: Accepts just one parameter, the iface name, and returns a tuple with tx,rx bytes. - ``is_valid``: Returns True if the plugin is valid in the context where is being run, otherwise returns False. - ``hw_manager``: A instance of a class that implements the :class:`~wader.common.interfaces.IHardwareManager` interface. wader-0.5.13/doc/devel/contacts-backend.rst000066400000000000000000000103721257646610200205170ustar00rootroot00000000000000========================== Pluggable contacts backend ========================== Overview ======== During the BMC/Wader merge it became obvious that a pluggable contacts backend was going to be required in order to support all the different contact sources that `Vodafone`_ had in mind: * SIM contacts * Persistent contacts (SQL) * Web services contacts (`ZYB`_) * Desktop contacts (Evolution, KMail, etc.) The design envisioned a central entity that would deal with all the different plugins in a homogeneous way (ContactStore). Every plugin would provide its own ``Contact`` class extending :class:`~wader.common.contact.Contact`. .. _Vodafone: http://www.vodafone.com .. _ZYB: http://www.zyb.com Classes overview ================ :class:`~wader.common.contact.ContactStore` is is the central point to perform operations with the different backends. It is a simple class that performs operations on an array of different backends. Every operation will contact every registered backend, and it is up to the backend to decide if it should accept or ignore the given operation. For example, a ``list_contacts`` operation will contact every backend and will return an iterator that will yield all the contacts present in the backend. On the other hand a specific operation, such as ``edit_contact`` will be ignored by all the backends but the one that the target belongs to. How to determine that a contact object belongs to a given backend? Easy, every backend must provide a :class:`~wader.common.contact.Contact` subclass that will identify the contact type. When a foreign contact is received in a backend for an specific operation, such as ``edit_contact`` the backend will just ignore the request. Design rationale ================ The design of this feature had to take into account that the core and UI run separatedly in different processes. While the former runs as uid 0 (root), the latter runs as a user process. Why is this important? Because one of the first contact plugins developed was an SQLite backend. Any database created at core level, could then be read by any user unless we complicated the whole design and schema with user separation at the SQL level. Instead we chose to instantiate the plugins at the GUI level, not only the design was simpler, but also we had less data to worry at the core level. The following diagram illustrates the design: .. image:: images/contacts-backend/design.png :alt: Current design diagram :align: center Not implemented ================ The following functionalities need some planning and discussion, thus they are not present in the current implementation. Plugin manager ala gedit ++++++++++++++++++++++++ Soon it became clear that a manager interface would be required so the user can control what plugins are active at a given time, and also as a mean to configure plugins that require some config. Every row in the interface represents a plugin found in the system, they can be enabled by ticking the checkbox next to it. In addition, there is a configure button at the bottom of the interface, by clicking this button a new interface specific to every plugin is shown. In the case of ZYB it would contain widgets to be able to register online or enter an existing username and password. Not all the plugins require a further screen to configure them, right now ZYB is the only one. .. image:: images/contacts-backend/gedit.png :alt: Gedit plugin manager interface :align: center Widgets to edit contacts ++++++++++++++++++++++++ There are certain backends, such as ZYB, that can handle way more data than what other backends usually deal with (i.e. name, email and phone). ZYB can configure middle name, family name, time zone, country, and more. Instead of providing a base contact class with all this attributes, we chose to have a simple contact class that is extended by subclasses with all this new attributes. Similarly, every plugin module can provide widgets in a well known location -i.e. ``plugin.widgets``- to edit this augmented contact classes. ``plugin.widgets`` could well be a dictionary with the following keys: add, config, edit, read. Every one of them has as its value a class tailored for the situation that implements an interface for the task. When needed, the class will be instantiated, used and then disposed. wader-0.5.13/doc/devel/images/000077500000000000000000000000001257646610200160245ustar00rootroot00000000000000wader-0.5.13/doc/devel/images/contacts-backend/000077500000000000000000000000001257646610200212275ustar00rootroot00000000000000wader-0.5.13/doc/devel/images/contacts-backend/design.dia000066400000000000000000000036501257646610200231630ustar00rootroot00000000000000\Yo8~WITݤ@ZaŠ-V N}߾C2İBGpȼ{4'~ 0{'ӛoy>{+10Qnz3!oG&$| 6W-c{]&D쏗!ޘM~LhzUnQl<>z\`G 6㘳Myn Q&b(5~o[%Q8} YyUsO%d@®lH-l"?teŗ"E|QGQYxɛ$H7,\/Dt,H@v{=NOޝ5R}OFOԕI_i'8UCq6/7LZ(u.}'lMYlpHv*&Up)DV<ظj#9l"o[:Ucg1>DOM"߻}6wU&yE6)DbP:1p J 24&Q#s+I C|V,̟w-yua},jE0r袡N TaVE ;2pfCZxؼslv WPvl@cUPk2O5a8+bivtGfPdP %TCmgH0 >2šalI"T R ;*䓨y!*l}B% Oc7.l'CB( sa8CHd4ՑaGB'uc d(c3`%!B6R6MA/Y=y|lnse׸Շ: *C$ApɆ 5}xbTƎ^?}_(o_r Zwader-0.5.13/doc/devel/images/contacts-backend/design.png000066400000000000000000000571001257646610200232110ustar00rootroot00000000000000PNG  IHDRwbKGD IDATxy|Lϙ$3If&%!ABj+JIi^tnսEJ]Tޫzio[jjEEIbHdߗٟj$Ld>+sIF}r9# !ٌӧ`׮$,++G~~f̘ŋ79Gau5_ JKر,^.P(ݷ oI]ѪWMXwPСuf4ΠNDDDDT::Q4%:9y!%%eݲea2%uu"rtG}]ݕػf.aw=.e`瑖<{>l=:5;/ `2=c=_~R)˚FQQmg52e $Iƍu:1qD2dv dٳ`y"##shDÇgX'4`~Dtv _ooXpF**rTJ(6_GA6{Ayyym%%%4hF777^ݻwǺu0}t V /X[`^yFڵ ƍCDDƎkz$a ÁpX"RS#77(r*!IL& z= #TΝ;@(r9ݭM&f̘;bƤI0d\xk֬ѣG!˭JBBѣG#44Ν܅?-[@#88_5:w ̙3{B@\\>kaaaxo>b>CϞ=1w\ I~dgg!!! G}Zϟ5k@`„ dz> <=ڠzꯪ ̓74 VZo[7ˋ"::>ƪ%;;/0wtsyϯWmܸ<0/%"mطoE!эv$Isc9ذ{TWly bZ킎ٴ""""S7TJ,'boеk' aСM y 8wK޽oo-fϞ""""fE2 酂l۶|~:Z^^JR3z=QUUB nnpsS{N;w"J 0L5#"""""5zԩY_WTT̙tdd ?00P(PRڵBhhuHMvfV0͸x1 0P*]ѸCooOGhhPc_&5hwwWzտ%W!>lهRGǎѧO?U+_h89ռ* /.eԩ38p &EXXtPjDDDDDE7b07;ѻw$xѣkǹ_2vЫWw2{7o￟ELLg4ݖf֫txu1֭f}{xh0fH3 G{}DDDDDԬVYG :Mbbzb͚QF;h6agϦcIMr> ? IIMr>""""lºN\.!77yB:|DD#̜9k#""""zyyu5uݺ"%MƮ]!I2BvBa2Q\\R!'h G~Uhh(~g ֯_C.#&&wFF`@^q .fB&7Lce*dtw'|=pS""";vػzb޽1bN>]c}݇ `˖-ضmvލX`ڶmѣGc>|sSsVm0 r u;X,(,+GaY9NNNBox7/DDDd?{ڵ޽{]c{RRBBBSO_~Aaa!;̜9tV^8`ȑXbΟ?o+X,d"O.@74p~#98r;ÅM.lw3Rq;wߏ]va6cܸq7n8vn݊m۶!!! ~@XXFX 0MlƙL\χ"lCz}dd\F&}}&nJq'""=;99福 0 PQj5mۆI&~={]tV$DEE!** .D~~>oߎ-[_ӧqi{j>|8bcc1j(-نE^Bkj), _ l%!$""#=k?`, ||)Jlܸ> [$%%֯ɓ'c0L8pn݊~ ظq#6nIQFC^ }6[\0.\.,D^q:oL,5u77;KBCrr2&Mt4 [o4;^}UYR1bZO@u:v< 2-'2p=C;%Q=AndM?u$Ixq+((f¬YP]]]vYǺaݺuXn QFa̘1 \VZUV6!t#++QZQҪ*T 0LL0 Hf3,P(䐄\&B]Uк [B-KJFNAW`_r zu!""gn6[w5ߦT*t8qmۆ[bصkvڅgy:t@ll,bcc1d(oA«wrj ),BNQ1ʪ`6P$ r  ل*E֦W |yyKl$\4ˑY$t:=O`f8X 9d 9..pwqnJnpy""&arKcf<<<#<<?>JJJ}vl۶ ?.^+WbʕpuuŰaì3̴kttDmv  i9q)k+@NJ]>s!d'0Lj|N(p$E@ͨPr  wwC wީ'""jvޞ.I&aҤIX,߱uVlݺ~GnݺYWR߿uz( z@=pa- UTBV_BX,0d+w2Hw͂$d Eee@F&ND;??܉lazS//O892 111lٲ?3oߎd$''7ހFX{I&᫯ 6 =zӵWVxEUU5pKEPMF!=7Jt @ϕ;DDDt++pstz ̙31sL ݻ[nŖ-[p9_ׯ\.G>}0f{hѧOkX///ѣa5E )5l:5*.tz&:k V|?"u!,4fh4p*:8;;cذa6l{=?[lO?xc|pqqulƣ>̛73rrq"=.PU!g~"""rdvf&ӭ(swwG\\ cǬSC:tc"k5sx7nDZ:t lorvYY᡹z\\\ѨO$DEE!** Çwߍz/^!ɐ 1SI.@쌠7 #{XW(hn-rQJ߱BDrr2Fuݠ~>)/ҭ8 /c؉aC{K aw›UZZMa!5kM?A i4j3-\*!p6#aoܖ=ꪲ>}¦MVGSƨQjldFdd$uݻ{СuKNQ=o/.h3^s=0` jrw D͞˹7\a.FZGKJJ0i$nF.]0`Ǟ˼/t$ Uc{ADD͐ú~~>7}YwԠ...ػwM'W&HXlJJJ]C+//GVV]Cw D͖úJ1f^^tNGRڰru[Dw _F||<] Q===|oGyeKB.I`Qc++++_0b;WDDT42axѵڵnOiVZe/hJ0a`0_oK@mڻ rpUUUx?ڱ""IB'MKKC^=`67ݝZL`DZ&;gKc6.`Zw7dw˖-"##qQLD͊]º=ϸ{]h2%_R 3IJ%bvҙs}t:tٵ}W8q""Äujbd棸 flm|}ۋcn>#̙3}]tArr-AD `2[pb 5&U邐:΃3,F"--6}LtE]úHIojbJFvmyڵk#\Mǎqi(vݘaݑᏳga2[] 98Et3Q#2ҥ Ν;wö׿c5AUDDOTl>yAш=ǓQVYiROjuOOOWczk$"ú:3.qz2jV 8j$jBX_ą Ю];ҥK8kիW7yDDbXwRx IDAT0ypSt%摭}pyW^Z-QQQشiN8ɓ'cҥsD HƩYH˾] HMM v 5ADDucXw GϞw D r2:o;ھ"a0Bg0Bo4ADZDDD |V6$Iw @:5sF;WBDT7uQXZjnL=Qsss>HJD5tz%5eú8 >Z(xgaQάNw.A.Y`Xwn.N.A:Ri2AT*@uu+!"Yw~^(ʲwDҸ"C.3`0ع"κnPjѧK:5xgA|{BdPKv寒DDDuuѱr gu""z0; @߮]!IF<s.Z (++s%DDucXw07; Lػr`WWQQİ|<r9d:=Q=â{Kw)@^ˇWUUٰ""a݁% 1]ЧK..M඗/__|3ggϞHOOoʨн&ރDDMS7E8ʪ OD:` {nܹvɓ'6mڄ.]4EB9EE+!":Yy)_`rȁH?+//ݻ;wX,5k⚲L""&ǰN(..UVƳÌ;Vj∈c֩|T@WU+W6(>ztSL&-WPOȾ֩ouaVǡC>00k֬޽{QYYQK?b޼ytRJjl?}4&M-[4YDDua"Zd:w |Z@.]PRR1c\OcРA@TTfϞkɓuo'믿]v8p .^pq2O.\@nХK$$$ࡇ[DD FBuǠ7] ` w>ɱX,x嗱xZ%yyywÇZZ-ѷo_111hժU\5o7nĄ M +2e T*֮] [r%x {KDaRNw5ؾ}=NۓЭ[7@uu5СCȨWPPbbb_QQQPT jքpww$IB@.ךk";urcrrrÇСCk~'''tΝ;Um6ްݻヒMP1 gaBIdCaۢC@ ە֭[\ҐYcX,8u_'N5|ܯ.ヴ45aUDDucX1L8r,Jـ"!klł{ | j5ߊJ$&&;v࣢$ҝ~ݿd,X +"":ݔ˅EHx&ӭqV7+na,M0m4ر}i]YYYֱ 8rH%蝝Y#wɦu'55*  4nF|||HKKYanٸ~2IBΝq}g_FUu5` 8$ ^.喕#11>ZBCCk}um޽{ȑ#xWhѢэ=?;mǪjbX'EVA`#! I7s !;UfffG zi 111sŚ5k=VXggg\#JLLD޽ϟ?K.sUDD51͙!Eee0vU$7%|xh-*FvAz'[sS))jWW{$L&RRRp!ӧOZY׷FxӧSYY /// 부|ׯ_]OKH<]Q- "-$^BaYa$l:MDxPڲ~QTWWhRгg~طo_v...Xr%fΜٔbL>ڵkgRjaXbeɼ@X lݔJi__8g4d~a9s{ͭ֘f͚˗[DZϛ7)))^b@4TLL /^l2 :8U8²7vP?b$h_O>kC%%%~=殻·~  8]x-[ػ "wDB]SPәPۻfC&IWt2 -<= LKHyxx`ȑ9r$`ŊvcߏHl޼ٺ^€Vjٶn݊e9$uj}㍜"\B)6mZhDqE*QӡZ /IPH289TBRM wwsib{O?ݠO3 >an^gX'uj*d!Fe5:I&FNmZCn99~wDҥKur82>Z-|Zw@Eu5 JJ[\Ҋ Y9$ nJ%|=h K"3gN>޽{c_Hr{FDDMaJw A\XQ] WUz=&ӕiOJj  5hcUPGUPP3gb5G"""NtὭuhDeu5zW_y(Zlh0any(Er Nr(dr.puqZU7 k>>>.a=)`b>*P7d6TNNP8)`X ,W@C! DDDTu"$ JZ9n """"j։)u""jtGll,P,\6?226lI_{jԩϟh5 DD~ 6 GRRJJJ7 //Ǐwy b00rHٳĦMfs!`jAm0QX,x'3^@۶m=z`ׯ ##w4 #OXX^~e >ݻwGDD= ;w.NٳgVX]bŊHKKç~ ر#QQQ~ k׮_|a/..eee8u˱ah4:k߾=:t| kV1 [b8u ǣ ϟGAACςL DDhZMRR{ARK, f͚www@ll,k)ָqжm[@xx8O;wK .֥Knݺξ ۇh APP۽.V 8y$vލUVx7ϻDQQg˗/Cˎh4mڴkgggff(uSa,_P((--YYYj"  W^zuYgffB$ݻF_Z&Y'"Fm/s(++Cqqu{zz:ݠHTuzz:NK"33iiix'w۴iR4k⩧ZFRRRu@.#%%iii֯b_@D-:5L?o.]\F8qsѣGct(**… 1qD(ϟgXX$IB^^^_"""Я_?̞=O:˗/_NN-Zd wEee%QðNDDjررc777?>>>ѣ$I?l!$$裏|c͚5h40a"&&c=C8ᅦ BCCV\]]q% ꫯߢ[nus?@R!<<jQQQ_oND-$tݱ}a޽0`ˡ;֭[q}!66[lw9Dw։)u""""fabX'""""j։)u""""fabX'""""j։) "" {A||<Ο?5keQ։&***o>#>> Jݻw3u"""%ؿ50LD޽P!ѝab`ǎǚ5kPTTxc͛S6QD-:Q ct:{Aw;v$I֭[7Çn҈Z#1m4&$I3 ڷo/&N(-[&8 t:]#^QKMMӧo2D6mĢEj[lB!,^x:ի#'NNNGS !(**SNBVcNJzk/((ӦM]tz"66Vj)f̘!˭vYKbذa[n"<<\$&&^~[tQVZ^xAX,S_K,BшVZ_~kmJ DD T#|{uKKK~O'O|[}m۶_|Q9RhZEĈzJ_^Jh4:Zرz|NN v)hxXB=zh3d1aQ\\,z;w:th0@7N !8yECL>]TUU1h 1e뱝;w}{O}w"33S!DJJqMSW];wN!Dyy8~xڔ։l߾V y{"TVVx@m֦fq '3g!jŸq[o%#***lZGc?DddP("00P[N!Ď;Q\\\8" 6!n/8qBHd BQQQ!d2YwדkرcB&"붽{ '''Q]]-֯^B9sFr,X f̘qz/55UT*ָpu""r(B,Z}V3<&equuo… I2 ]vE׮]#ˑCСCHHH@NN6mڄM6 za:2&&:uIM!  W@nnni^^X܎LH޽{خhPPPVZ؞V[>/_F1~PPF# ѦMVwvvllBQw\ݰa/_L( b7.Xv-/_S"22* ֠0%""?u+--Ŋ+kСCCVcXp!lق|9s}̙={j*L2}݇%K~Cyyyy\]]SOAV#)) hӦ Zmׯ_V~y$I: r)))HKK~#<>>ɚIL8""aÆ:Ǫ_oۻTjdiii/'bbbsV{xO?Tcuc)--ӧOBPJ%z-v!,\]]5 |G?Rի.kC?1cx։3ܹ31vX[o{>,|MdggcصkڵkS3a4qqZWi_߿~DIDAT5ѣ֧:_zz}%L&t*&:X,flB]qU.C@&rJ8;YWWuS_J`X'"oS0wǽxs7ްnOOOkbڴi(~$&&֚YFR!::}ށ:]#BŨ,Auuşxo''j-T*7z@ypUr!@d Pc]!RDqAkcm+\SnuĩVL+PqE"T,fr~k58[%O>9sr )ӞaJYYoU[[+qD"*((G}]v?֡Cڍ y晚5kVl~銋3)Ԩ2ԔJpɉJL+9y J?]L6~$uRVV}OTYY?86GHEEEzW$<ٳc7PO.׉F#(QE VAd(Q0X/x6JCV||B&I:-G4a袋tEIj￰Pv튍w U\/KghԨdv6(j}@LOөI&iҤIZb$)z3%%%ڼy6o,e;Oٱ &YѣG+;;[/^[[[/,PccbUUj1=z)edZp.\[3޽{w^[NR˓8g͚5kOn-^X<֬YӣzĿtDI}N1JJPMM&M)٠`[?O롇wmvK`P{Շ~/))iɓ5{legg+;;[r:06-YDe!E:z?) 9vFD͟?_Ϗ-+**|߿ TPPg}V̙3CK.զM$I7oٳkCOٚ>>|9(۪Rf3ԙgZRMӟ|I<[o魷ޒR'N+̔RNNNKRAAfΜ|-Z箪P(dTPEWu􍸸8͙3Gs~I҉'F٣CСCڰa$)))I3f9k?alyI9)p:9r,Y%KHjOO?Շ~?rvء;vtxH$Gڷo}Y(jszN}j^o0n6<O+W_ÇUVV-[hƌ]^iii:~Nʎ{:0 54̎BY-Z{v`03fhjh E~H? P0X/MN҂`Lv76n[s.2-X@Æ%(V0X' 5d(|IOuB.Smm :?~\sivkQNN͛f}?755ka nшjj*U[akpCY&-]4iӦi{4TÐ*UAr:+-=FU ͎!:ZoZ3>NCMr:KYѰшBBfEϴP`0 5 KR<EQ IJ!t^Icu(EQF%IN'X;l+HRtu^}}\ii(=cvS X 樯$%$$xƌhvS0 1u}B!I!Zr 3QFLcPprfμH~1\4jh˕bv:N4bxIq!ק f$Y"$rx\G?Ӿ};T__-av^1 CCMߒ3;RhI 4H555VJh4/(PaTK!+NƝIf))iفڡl:UU%t <Ѩ<8Ә15zt\.^lBYe`PYY**NRh_Fe|JK!CFjĈqJK)?@(EQr N[rKzhv^4h1z)>>r͎kϩ|/>|XTSSO>ώ>;^Xhhϟ;wX=e]٣jct+(W ͎kho sE$=Er N{R_|RSSuYgqc3 kz){ڸqZ]s5z饗̎s: b͘1ks<l0[jqh7:Z+h7:`Qu([jb ^([jm >}PEY,I39 tfI~`u(EQF%IN' XCl)H6''GCJof̘q9߯޿;2L:U=NOuz ##C?|e=&MdR;c1S@\yڳg;&I*--_-_vEEEK+V.ȑ# u>hUUz+Piiil}FFy͟?_>OsՉ'kJIIу>xR_VRRӕ'0|_ .y睧Lݻ7o~N~ozrk֬QNN&O;w{***t 7(--MRaaa^oʕ?+Wjر{tΝx>Fhw:=˗^$mذA/mc.r :TuvmTyy~m=sm>Onnuak̘1kla *//Wjjrrrܬ"m۶M4$?^;vP]]}]_^/Rmz-OuM7[oahɒ%9s***;_o۶M[luZxjkkuAwtw~O>&O'|R_~6lУYgy::^AAz!UMM>%K;G، ]$㷿m^x~3c߾}Ƙ1cH$bL0عscӧO7 0o8N2ox<ؿv|#110 aN())1 0&Nh<3/fDز,cƍޑ{Ǹc8qbWaar|߷ox`0[kt{ceee/r?Ϲ<ȑ#36md.?샑u-B!I>SNՐ!Ct}tjܹmkecǎU(REEcC?~|ǎ7 ;VcǎՔ)SU^^nС?kȐ!r8m555*$k5jƎunmU$Q$v 2D Gs6lXܭN86_Vݽ6=9tu]ȸq~z=>|Νw}G6;7߬o]k׮m.==]£GhȐ!JOOWuuBP쇄&{Ĉr\*((8%3tѣj޼yr8Zf<أcwzz***T__+K}s#GTMM˕f]wMOD=9tt>^YYY?~֭['et׿uls=WfhrUUlrʳh!á2mܸn,s9S8__}q={n1M2E:;Y<)SDf҅^[nEַz|w8z״{njZ|K8_]qqq0auWצ'.=sZtifx;5d~[N6mۜ0m~!lK/նmoK.1;-6|򉊊$|\K.59`m` ѣGxbriٲe3;`iu/rsskv `@2-AI-7]UQ%l$`5u(EQe0:@@Y,Xe(:$IIII&'QD$I.$9:`Qu([ B$cre`K@(EQ0 0;r)*d 5Qp8$I|`e %EY,&I59 tFI39 tXe(:`QuH~Ikum> Xe(:vZY39 t'ǛFY,8m9rDF{O$џS z8m?^>M?Oa۶Y?222t Nkyyy:p.rzwT[[+IJIIQccz)oV ʕ+MN -FG & Ðvl*++KYgF={;&t8mڴIwnȑ:|0 .rssv{/EP=өիWw͸qt7S":/_quD=:nM4I^{m?'q)65~x?~?ZlIsljժUmeeeh0#[ihh$I[ne]fr*#[|뮻$IgqE1:3FZhqSugڿ1loϞ=1c1l---MSN5;`in޽[^z1]r%z7̎XeLpB#nfG :QEնmCXe(:`Qu(EQEY,Xe(:`Qu(EQEY,Xe(:`@t%(55U^W&LP^^`l{O7͝;WoFpp(..NSNՖ-[:?:/ (g֭Z`̙O?TzWTVVH^}U]|Zx;RtMꪫ /9=ܣP(R]|Zllj*6'Ns`OnhT~VZիWǖgeegaD";j*qmVXZ?˕ Ir8r4h=C*,,g())N:`)((uvp@'NpZUUUڽ{wi}Ne0|גÇwMyyy 6LTVV[+==]IIIӶmb۵zG;v{*-:`@:te?IJKKۭkݯ8~W7effjkݦ?zP2335j(mܸa(33S#GK/n/A);;;,11Q6m6nܨ|ܹ~5jTcĈ}{b:`@q:zꩧ_Bk׮UiiB>3vmڵk\.x ]VO<񄪫UWWkzGb7:K+VК5k,D"jlllFe0\~ھ}vء %&&*77Wiiiʒ$]yڶm6oެ#Gjذazg+VtyիWk׮]zb~a|6Nn Hs՛o6 ,Ђ fǎ햍=Z]nu(EQEY,Xe(:`Qu(EQEY,Xe(:`Qu(EQEY,Xe(:`Qu(EQrjھ}1~ǿ{(`&-Z kܙ`IENDB`wader-0.5.13/doc/devel/images/contacts-backend/gedit.png000066400000000000000000001550561257646610200230450ustar00rootroot00000000000000PNG  IHDRsBITOtEXtSoftwaregnome-screenshot> IDATxu\ǿ4(HRR! vݏ]؊ "( ]W{;?;8x^>w>;ٙ`I߁B>[E\C0 9rar (((ʅQPP1 c(FAA!P.B\C0 9^(|^|TXnv"" 44͛3 &dM?\N>B40LAIuoCV005572 o9R@()>:oS+BQT KK2?fefi YIAQ3FiV#R(406\B(j\ WK0;C@h4LK| $``4I KQmdra! 0!B޽FQ0 B[ \EIg.?mJGFӽZxTFoi)^j&L8Kӿ[ xY 4Ljd*̼3Ti\!TSP 0%NYbJw)0Tݜa4F}x"P4iץJ$S~8ɃT/X 1¤3ú(Pm{ f+QE6ɃaA٤4SeS:`;y;›|RөTOЙsm5Í 7vagʀ>Vf M =`Aէ棺(hOYRKfo$+Jq $K+a66eV?'pCvs2i9QtinԅIcFZn=K޵[\Ya$%I(P9WFS3477Ws+AC>iRBGDo|@mjx0ޅJ5>`ݚ>/:ݿwO5ec{NJ.!'ξSn1tݾ} qu724gf RRܐ$:>0M'/wzשDaWHr-vҧɗFv{x7Ѯ={:y ]}"0[엋\zEigtC> 9o{z fGWgY[[ڈ?fBdVбyZ;yvQ"OXLQ[Q;yvȜs+'su}֡ oߕMS9:ux*^V%fJT3îӤIIJu0R 4v73cB%HɤhicnasƣZ(HӃ aEq2<;:yضwX^0 cc BTȒ"~ߘ7eo;ޟzu|I?M)‚_}oG+sV4Ypt4P(*^%ߢ=֫NnۜUS᳡cѹ"+Wϩ7/S|Lp>!vU^ÚS_=ͪnǙ͘s~|2F[:|U=o̙}&':XsWa쁷^aXU%9z7ݰYbYUv#|m^j ]]K HLBU[>4pUq +ʾl݌dC~z͋04hS>:7a` =ƴg<9{{[]CK kjL^wrw% I4ȹL/0' Ȁe9nD+3q.ZIsޣ8C#Wk [i+^C8`> X֝,UYH;{A,g`΅phjiS\8MIv)%}NTfV봱tiە2'8Yj\VȋsM/ ON&6M\zTL"-fbme`f׭2F7jjkG7wҔn$$মbwhlxK0-th* RkDM_۸d!՜dݻf?yS27(?)84~LV̛~qy-b MwUX']rbtY%?’0 ͔L,/*"H K G+f< P_(W֣p+22?rR#&/S ?S5{83<Fإ-4U. IAWz}[zummTgrs FSF|!Fo2;e2 9"W@qs^r@~>!,("iHϡ7@H &x7s׉K57&!Vl.S%]ܿ7w%׵-hy|aD%8|Z}ekҿ|ASobۙ52qT;<0l1\C)<$G|!ZM T̑\p@\H Gr8b\!_5x\n~nɳA ϫ, A,@RU3t{++RTĊ%5SX?RJPj9?[ھ]1oj׸=Zֽ-OIdpi2詂fc13;MݨR 0J|}+ڶQ o0:qʻfI/vo061qqۉǟ%|}*жaQsF gu>JˣCrM͇^pk`ګ&<"deԅͧ46-*#I 0Ze_?4?_(k_ŤeepAe#C?t.ڰ`IgQ%hU5[Z6у| H@PD @;GX]6q'?|m菘O;xP=2jEԴbM3RY 0W)@E +vEoֽV}㮂Fy4]%z?wj[%LZ g2S6{GQGS>єQ| U /D,Mr`y Z;OQ!`Y0n}1sojm 8%[^zNP0ȔŇ4΂ݖHڼwLߓL 6rkxPn3fݚ##G/l]ƞfOC+[2ǘ{/A k,eU*z)5\u#Gg [ͣ1 trlH[g4Z3ٶdN1>NPַo} E!YeI3ƤysfOJMEhJ/y(Q3=EtWߏ,Fj#k:>|w6hufJ0vô]- ~~PlK(ې)aLXA݇R!$muoCV0Ƴ' rC>ʴrm ϭ[u̐9$^>pp\") g"ĿKQ@k G^Y4>ȃ뼍5`#Uiax y0LeW kV)ThY])fE=Fuoy |Sj#pa#TwHH֦iBh5% HaT-oΗ8  SL.,/';+#H.CQA$Yz ZC|їO[E%`8HĢ^i\ؗO;](fxQP:eJ4dWzp JAQ1=,4>WYm5U0 qbʅQPP1 c(FAA!P.B\C0 9rar (((ʅQPP1 c(FAA!4$F$ߺ8AֹFAAQ-jÅ Sn17WD‰>% Y]dXbPֽᶦVuK.`5FD !VoތVnsֺՌ6?AWz*=դ6 k_EM[=ޖ)lMl܇xtZ=-lMV5ֈ^v 漙2i{&y5m{,юi,ff-/]5Iƫ'nG|'t樁ٯ׍[xmWw80i CLe|LMIaʭ3'~ߩ*wΗNy¤.Ѿ|4H1oky^~wk 'L͗(((DM0^ԕ[i=u骎#:^_@t]^[gu110jѮ߈v f[޽{Qy틽ۘ6I;DWТM u;؛QQP5`q_MPb{UmgO-~BPj6.2Xt-7"fRګa@q4E-2 (!,1@h5‹=}ɞɻx e}IvpϝC#/=I&L!9NAZlnw}:Ӱ)DϬ46MHʀ)7VLT@Cƀ}ɯo[|۵ڦ?ʭBZ S.,etזVWoHγ{+_߿}ujGKz]@1G8yƵax So2K??IJwasx4psT x!N>^qιt1煝rȤԟAOE:: aʃ\9 P.OX~Jlb$XǺS{ .sˡ ԫg.9et{9?bRWc#ѩC#\U5TY(iҹmZv]~|; c >C.[3I}95PJ׍'17%K`\nYMAAQTa)5Q:qMu /ﱃ[J LS+[c|!5vA{ mH 8'fa'((‡S6W%2(sAF `WB4\xePPP5jB\C0 9rar (((ʅQPP159FAA`q6@!"Ty@ ÔT YD 5jvhh?jL*~J2߿Yה!IAAQ}s0-9Y5(@\ɓ'[d"e_"0 CpU@\ظq[EE\4(&3Y,@ B#.ˆstkcbX,3N#^J 9M }X:br/yjl~K|.^j˳F4u}l瘄Ągg֍wl\b$v~^,ۄ#A@`I?W[3bZx _s3_3uTDCLX,c\lbX]N5T7Y,mȪk?$.-^ s4gX,C!|SdMAQOYudvl=/;3,cH<,}|(û&80;M+\$j7ѺG@ۑ%q}?~-Mc_GziI.{~~ŎeWsDtmd0̹sWvi YSPAHD0L]C_ J’e(5kMAkWnM 6{p2 HE_'|UM@w5S\e3 »!p]aN )^&~Cl*d4mJ]O>$hsij5ihC2}UJpUDB0#VêgGЎ(4 0>;ml`ڍZ6Y+զUWCnLU0SQ$v6tT+Դ؊eWn8}c'o7-s4$+EY$\~@PSm;f7H=7uχ$.OdXE?@哇j.+ L^ Ƌ$W>7p kGf׹׮]vڕs[:5S-SPvEڄ,qֳ߃}:0 }<_)(ꝺqs?z;$ˎVqZ}T$T}N~ z!4y ^qB3@o_&',_A4Fʒ),){}ycH!8{BBR=b#acR"Qy}Aqw°br^&֥I!BQ"iL6HL)&Dz:tu9ke0]Ҍˊ jo{ y) 2AF,لWpi݅{5uP0۪u*~X2}a Vcs$D.*4Fә!D1˵뿆2D['l#@c:p h?ɴxyY!oAjx,\IGS}MQT9 ծQw I]CӨom$m mjV%$I84jmiSɊIE !vphύQr4Vyyg.MjhfFj-`hR &jS$O!ʽkjbܝoZs?I?ݻTɠP(@ DN* @ !@H4?cD?[,}u.-ml; [tR[E sLbX6)815bs:$t<ʽ1i LsT[3En5C\-X,˦3 ~\\ޘb[xN<]j@qׄ׺Xs?H)~i7V,ebkdB J&C.dN̮lCլ% ejt*8klRcʝi%)%-ahW\r3\"IE)!C΋Z=M[L-0Qu ê:կ*]-LIEսj$酕ן𔬷$Bê栉/aXr3]Cm _Z8}R7sIn9|=DߑElyQ@ާn~;ʔ맙rc=)C>;x<| Ymùî=js^>|@CZVׯyR7ZǓt4dc:>Jԩ# 5b2S,ЋT)I(0ά!ún~]53.s3;v]ˆ*]"ctsʝjْ$^-V'[c~p <A<߿$Ѻ4`(X/((`2tL)HMR2uXi:K;4O[=v&[pO/ڱ ISjL{Mlf(N/G>GZV hƀ{=/0wm f@9[fzUZo_tcwV&z4m**uJ?pn&?ya`h":/'<[0arI?q -~DNRU˻DB)3%Ke*%mG_) Nx߮rH35p.{L_%bRkK"'ǵvRJalbAABPH$Bit:N]Ag9*. 2W L{{5}CĭV9H mK]q-"gO5k&Bhjpx??Fs:.IB&O/pqеg3QN.A4 CZj@ *XH&K._Xſt]G&Q9B1kKQż_H;&OBZb8eKOKf*2E.nu WZץ  D+]aYarq F doyFS͍ᴢ[L<ЅL4>.5T~4x /i1D4w锽g /lc׈ O6{4N#l s5 [cR^DBR)e= ii?4F6D+*27c"@o*!ĻH$%Y|~- }#J$[N\KaYB`dbqq"y:B2kXy']\ )OJ&Wd6&X7[E'XN,0YMQp/"?Myeq|c$iɲc_*XnS&S@fZ7ƌS;Do}"/!F+I9pau$hv ^~SHXKۦtit#5wB~%>ضb`MAjL|KL¯g7:~?RRS,ʴ]s\ѵ^K_<Ϳ8kB8,L\h}]Z:tCv]Ts~^Riv^ҎteK+%-MkpW k[/l*d1?_3֜}3J@a{_[g 畻ߒ$XqdLk!qԊZʳBRGcgƾ~4QޞSR>e}-l4;8`(/;!@BD _C&ΪِrP :]{Ÿo6ߟrDjY/!|4I`ZA  -Lvr|(F񷀉AiF rhԧO==u ^`COAQ]JFiGqZ{+) I 9!$OQDj/K9paW__ aMYԮ5e}a3J4%K.Rp_XfffxIQ= (((T P-pa.\o Q4C&د >%PPPTNMdײj9pa͓D#?ۑ{dǫ&MRQP+` G4o9pauFğ.Gas] L|H͒tfER4d0JL]k9paFr~{zzͨ3E[:<+V4_GGvLio7/:-@Ovhf.='nUsfvdlô'E>(?ڊnl6u$,Oa {grd ɓ_Y9s6f:u?q?@9l6{?$~'~e#Nm۱lv×U]qhV6llv?a fZ?).ǛM/F0ItEƆny~, yvle,?-TA @xC @f\L(Ԩﭰ|m(Tlfho}L\LB8c~yn%M[;u³ec\}{[s,9E66Q&>nū@U^(5RU)Y4mŴLLL5Oai @7>_X}lA [qv<_:yֵ9f7GU[zwn @I/7> T=.JC_b ŕnP Kpi9ROH>|7_ 6,$U/q/o9L/?{qzğ:5G7,>f߹+ݻ܇VjH's"H81ԑfklRf{wm@|)# >@΃TK5YбdgV`PntC[C_AܝgK:@򹱮->{ۃDZDؾdNdP utT[toDԗ$Aa&=z0:SWJ~\ {~O )&ϞoYx9@YMaPt Ȉ hCٔן<hufFcJhP2Bf#Kmm 컨x!Qミs#4w)DBNB*_ފ :0d1ta7wfdj2+u rE0㙥5CWWr$gf~{?2UP'[2;LYԮDfO4S @Y۶@Jg[TdP"O|#&[/sw¼ nN8 ?: 72bR \#T}A{q5V-nN }ts$SoF>n(qϗhh!\ū\w.,@M;z((Ox…2mI!+ i#0QѸ*ɺ1X*k iCW5ץ;ƚ3Flwm_iݿ2cCx^vRiֻx> T JIthZ:떫qgL6c[ `hEv-o&?(@?[dYvm[·:&$Y5"V .|H%{1uha{0񀰢Ӣe-N/KG8^#IadD[[׮ `((2"+)aJJ2W/?ݨ иqt~*qߏ12mݭ ;5Q"ä󃻢_oY J 3mHAٴM:ioѴ~~!Zb q|I-U/y/ݠ}ێy ]F_4N p2-F/·Pn aH7ºXysa) z*i->ayy$q(JH99#GLJn_UT>."}4K拷T/ ޵9x#)븰 ~qgG0faR )-:_۹S?9,-_L?;' +5댗י_ϒDQMpAar?.LSidEKGiC[5a?n 㨠It$OUR#[0e}NŨ4],i%1Lgr/dT! {Lc޽8IPe:W]e0s+Z+8Y o<ΫUW6nx#qiaci'A `4N+.+%*,~sr׍c[|g[UK|!?PHI-_]@pCsމ/ja\uQ痍fvC_3mj1f<wl65,! NԍC=[lv+s/IkXcc8;s🱽[9vau;fdǃ:b[z(qߎvnö:lQ3J{}WLն5xpZw6>tKSݖʉ_O / H翛~f߭'׍dP$iL5f;vo~SeW =Y_}` eHSm5bNHX4K}t_3(7{ɻ(6 P%n_%rtRUZQ37aeϣ|1}k[{_3pd96vT؂iaPHH -SiaBDo~~ay;SLbo) ///45[lР/o~~޽W~pA}LJZܑ|z[r%i͇Mrkal``f8 5n}  L{~͋6c50y/e9Y.nl#mcW/O ⿘ROp_K2]M'9KA mcl}⺙W/&,vY8MӕeiX0Q)wՖ*K$?;6d6,.sV Ыb:;6Cì6?dT]T+/p}O/GrօlH]qÕvҫH~zʏgAM;<"rV.[e`zV.ڿ^}F7')L%c*XMɫ5or9.l™ۇj<|`7WmCVb0p]#ι'-=__MW\/qܟA^K0J{dyyen:xwH.+Z ̜w瞝*V:Zj8]ήQVx\>YR2i)MES!'aS)̘*.22 Z6i7 '[J5A7e69_}ą@oi$HW!ځ2US+HILF[߇u5sBcsPa9JFnlWyV3ӍĥUzMɓ nmΗKV(XxRufnZxb2Oh2 IDATY3FyѨ"/`YEqh:_}*cQW4"vó}&}o޼7!Wj=FVq";Xk*N?J̯-B%gv EߐwȈ+`7I/=]O/m>w#g\hUJ Bl,PXYXR({s iCk"l185#`P8EaEѧAզ>b3Mxs4*U/?-ƌt7TaDƏx#wu8f=qe&"dJxn^8{}Sƣa6 NYwg=td+H$G |am\ =}2?^S g)C~G[v%>wdTR :w&R/[ңz J|7O47VyVtJQ5 }[{)Ff[Ux~8 TYDlwg7oFh6R4tE׮s=̕+?S4LZϑV/&c/{McOӶ2b81wQׅ֤S?y@2y{'%ƅ]ٽR4~f ~,жЖmX?Q[Blg﬜a1 +_~y{|M7FÁicU%&xo,6jFr=|?Ǽ9@Dd^b\ttTD{ w>mҎZnqcwv-+PȢpt;m'z_?\7dꥫti?GgfH&`Sk{-]X Plaw[ *ۋGL9@׍(&/$Uq[x|TVՐ]Yq4V_yZ#!,KIt ƫ:Х{7Y7x.N 35p,Y03L&Bxkm3nߚ֬LrŸI Gؘ=d)Ls^gyY31*vU+'z)Ȉ 2 0/6kXlu€ nIb˲^e K^A_>4So}oU1r `C^T?AQ]JR[w֭.ct&^F0 $)$ P( I+aY!_XۻV /ﱃ[JAC^ב@y!7o''>޳'Sz)#un_ב?̏r4>q{F]._ F %I0|/prM -KmCF<ð?j9hHRPPQ[k9paT_Q4"Ɂ ((ѢkhC kXX* KJZkTA-B0ypa'Oo U!ptXYSR\ظqrxx@E-+.B 8سX,灘9vBK]_{vn';_UꙜ,tȓ\)GY_N.ri nɚJytB paF><1P Nҧ_x_!ۤqM#c]a%3G(Ga,޻G&oSO$<]67r21 pygm-ZۤYfl ҡ 1CdFL_05\P%$3՛T!+ձC7H[MK- ^8+6b yHQ'ooj m{k=ˬ%2c1.lD/@~V `V`uMM sx<Z5[bɥpD;5UKuSMU{4 qFaaܨŷFH,fd"-MFqi~Jp>YCϽ%#2h%ǵi+_Km{Kle %v?FU }^._|cNr྆p(@2eDOZӔyrGq3E 1E`{rbJR,]a# ^I} @w17bE0t͉hY9 }]KZw(ePMLZCE67lQgEyݚg8% &-6ҘYWK7ggO+XSwCW'L4 &֓w?#c;yO#+"ϲ%JӃ~p).U3򠒘;.ݱ]ƽ>iuS=M!2G|e;ZXW]Z,aАZxtO >F  @\1cbo'W)kN;Mc=|ߦvw1$R24,?Y|wty񯙽[r A zu!@Ibw}oG0쯋NTvƽuWOY1ۘ<2{L%Str5RTc4ee׷3;OfuqL;tse߾v^tS)ĭ_H @\ΰFN2fOelKIJ]a~ B_0fh!#€p==j``¢mͣMK$AAADa0 Zx6  ՘ mQ f`4C b1@3ReB,,̈́"mE\ [5; 1A@@"̂^:VMXV..(-˒DB@ f@ T3Œ"ii\[Mk7de$hʻ֦t^&Fw& OSY,UL#jVH 1HJhZ0LDc]\V3.&i!/"GMoVtx\$ ˎT XkYi"Ԁ^RL Η\X>|#03DXOǹ baabŒUOc F9qEb6sw _X_;|"7'f #1R - zufEvK\i{9ˠ}M]{L`c7Nry]xoկa?<uFXk601(C0.WVgl茐u$:ZoW<1` +-}")Ol?G7JfNWr˶Y|`ꨉ|HƸgڶP$ad 2CQ<Z[jkH,cpؠ1h]@]|Z"@Cq7x/qqSVGfd6 $!Շ,Is7"NE_h>6i#Vq/rJ^䖚_嗛88{A d#_%lKDi4-0`ǏN9/eƺe݊O)7uf16,8"9] l=Ӣ]Qa qޙ&b'N:l@ (o;@ +ڄˢ"T/kU_,N:bT@ {|s];o.WD. veVn~6 ?W='SZTE% lXG@ ͉HC8|^Bk@7rtٛiN}DZ[G] x~v~E#5:*zʞRx9c9Mw6zʝ{/~WCc7*FGLru_e=* 0ILЦۄ {T .yՉȤK_x> :j?߿B'Kc2;oI@osd{RA>F* ]4toݽ_1_dC3^sXp'aiͫd2wһ7vYk#}~sAj" 7 wnF(~?tTj/UQY]VELəc&=ޡ˿L×>p[x,-JwMM׸nk{|UONoL'ҝQ_NiLݼ쳎.6vfՃ=]@_=mIBWϐ 8]/gT_u: qM^8 ˺_]|?+ВGr9DC~ٵ S֞*3.9rO=l ^z`8Cׯޣϼ Wڽ㈯?ˋԝjJЭ'_OuϿs/[AsjJ:Eݍ@^AJAx՞:#0'k-8-U\CK-iITOye I$:#Tv:T4#kv`XC?҄f]r^sA{]:@<%}3o=ywӇ՘ 3K״'b/GT?MjjAYEH")xhE:\VzESnϩ@*df)LXsDyvB!>*h8bgST.Yk[2W4`QܑTkzjtwݶу,|Ak7D"LY{mէ$&_Ӓ=Rm?n9y/:5++ŋYGƒ~n̫2U*5 柝M#ӓ-{,Z(O=Wc:թǪyWW8I}.;:oQ\4fb[FԢIQԣIKpͅ{[O~M{E.1jяٝL0v!:,fx%o(A?ϭgׯBgbjw6 q!+'3\ۓF?1Y]:v 6$e]\3G'Ο/ۿp1uŽyH *ʯ~aК[>ǿٻھ 7MͱU{>_u;թe1d󮩰%ýM|%m>p~ſCQhu9&bqz %Wy/=|G$ n`cG`AQ/{hz&XX!m$% *HQi 11 h0)-8NH6.x qM[@٤bqklo$dݙ;"m?d C6!ˇ  Q[MFLA4 Mh&ţmGBQ,Η;$*4|vZ(T)ATnB/v] PuOd|I_I}a,,X/R>7 kd_![ Kp>Hr7~idb G= WG k~RE 3Grobs3yۙO)6[E,QS/h'e7\jVO˳O+=\a/ދm?t+||HT5~V?h_Iq=WF#0Wb-`H+p!I\H|ta4/8m IDATͯ^&Dn[Ŕ; cˁ̌Q s_sk}()bڜ#MigǓe&\wJ0C;'=಴Y$C#z uNJh ~>6!ANgGmNVQUMSu57`Yaj7_Oxߞ @DŽb>"juk k Lك-+pl7zܑV**fUzQJLܷNQn̋"UA_Til3yӥe zrSZ6H-g Qr ~˃-edRF&U= Iq^ tq^3141d83 H3i):{Q% OӢcE6N|TQ`rQ 5 ~R-W],Cy 7;.w:p>pD {^p?p<P?oIbbGyUE]KbCrD(Nf?n!YV֎z3^=_3"x|AeLu58SE::f`; iiop\&`ـvm]VхFk@M2EaG3͒˗b€XRiH Hkh-KjRB JI…wk?(rhD!֭u=|}B*Lɔ"~I"`2D: Ƭ.}@˔G15/pxKo l1tȂZ)tlр@O"°|ȷF*`6d=вp2Bv҉L-Q1q*kYiR*l)H UYQ@Z$sS.jF[xA7*z o#tEy/ *p322r{(h4Kݺ<=YYr.2yV(˼tmG}w,68:pcBɫ;G}M8-4NLj[MSO?~޻`(.dyYLKU~0bPe%^e˔Q!Ca L*m>ف$kB 11jߨ~9`$9@#z03H&+|*4|^s|my4p؈Vg?>*GZpn3<5l4xf\u6WvƮ> +<1b]ĽWN`~7sC\f$o?of?9SYb *QQv+K^==dzW@>^{,p>4zWT}j0xKBܪWJ,FQZf4CcF!@Ce=ЁbXXX^cյg$lm$%HҢ"1!A0Nm$l #U H Ka΀&1r%fDM8ԡ5!RPL0! 3 ~aŒ"iL #ry-R*uip15|aΨ,,E2ahA CӴM\&^(AK;] zL`㰊 DS kdV&S91~+17:bѴaY$ s sb؁ Kqei 4d ^Esh(uaX/4   8RM,1A/P+ܔ $$v@(Oɶ̨}Y4]`Q<)2ƈP8\/e[a Jsɱ!PXD [q|*XwX}jtsuCiz(p0FcQ*cckZ#TJ @(БYe?XCkd"ƈA\^WA~{ksG?ބP{Vi.f6U-P Kkr;>0WNJ< 7@h0nkrJ H@0ƸV(c_T,6t~G'(04~,J@s$莒A%IIQ^\L d3@'גҴLLH>CVNC;ko싋V"Y_@/}>Hn? <+OhSu`L[c{e>6on}%(*%˹I6Q*8Vnq+qͤO h7O|ߏò٣wGKS-@a=-nyXk쌎5i3;&r뽍;xYys$=xW( u/S%*Ǻ-nJTBG;ҸcW(]94B$??TۏHUEiO\ ,YE*brQ1 c7IJN$``h F ¬R 2!_ʽr@1cF-E vcV~kZפ>s}$@~~މ,Y2W %EhYk7QYUtQUr9KUǨg_ԲqDzߎ0=5jIL1Ag{O^(&MM^r$5H#שلӚTǸ,ݗcgD>*%y /wh ۵?5/CTeM]ozitnL{ :4{o#Av(F*+cvϣ'J7qzjԒ]H.xo(0XuaJC;H@a1LA I5IR 4áHi_<,^=btyM} ~/G?|RN:J&3=찹Aq /#x*QY[Xkʩn$v܏e ;G"‡RF.t҈).bq;N`t8*KŐ0\5H<-H>ü\IDyj ԅv? Id @KvmK#_H@jq>Bɳ5_,xu{yQ7 5g jk8UEy׮<`>')pFlgzWUGkJ<b1j2Z @Q`BakOYG _P,1{9O/SCʓ_h3ie@gKRTIB>5T .Mʪ2CEyzQHVaG t1Pے*LGYkPFz8IHQvATF6tIB(OBf)12qI7B"QFz8a",I>茫!=?H}?Ĺq*J钄 Y?K\=>6,4S5XE3ikl& NT&o%4P(wH@1̍>ּYAիU,5JTv5 W;" ]Fh^^mDA6oZB Bj@`,ъҗV}߱㐕]]Kέny#Un5tBlS,%/&2KeV/_5J|*2nmʉzQ~cJ^N5bZ4lm[Yָ]2yq`aaK*)͋IaTUJϴ1K@na>V֤S{g3şAٍӪGݜh>O/[.sN:wt7467tm; O+..&s0M[N)3݄OmgJLYq>ۺ&̪TUS8@ptzi5K@ߦS)cL^Okά*VJU]BgJp-1O>M[c]yMG&B0#t{C{caayOq "!*%!3"a H0: I-NQ%!ƸMըKltPL-iZ04e}E㒒RSKF-x|F̋9c HmU&m T&/E-0U4Drs[ ƹ}޵i0Uk׮w- {J 0a&Lx"¢ քG pK"]LX藿 jb;jIV4r--WPr8pﷳYҬ^4VOLLLC#C1Xi$7#?(Sr #A=1Jqg$M/ K-?މKT.@y=Tms?×;i{4߹a2d]V0gb,;bLV }z/`?<ݝ.s$ AL|@`}K*eV! E63}goWL<;Ȓn`YQƫRTP50f~JPWob VyƓc?|:xRҴ}kHHS>-G3dL{`e7ew e!}Y!RKjb4T0@_`nۦ驨%[NY.gM壦w.٢. ̬/WJl:{S/-t]_3Z*cduapb" "~=9> ^Xo׮[;] --|v; xYX}-}lՋW`5s泗rՓ*öG|V ? yGp&~~?ڸ}x+8w*Xu[/_͏מ䗿A3|^x{@ : q|^zsҘP|?ndW1fa 8<(Q&ǰ`ݶ UӃE~z4M颅^\|){jidƾMOPK*b1  9=u;(xʶO/G>[=7iF+NiiNdggK$5utt Mkܲ&͂ tWx-)i]=Og?{Lo[(FzvwشΔ[Ȁ:m ]4:u+<&6K/\z wOTFKfebVDI[_9m"yv\GПɬɆ' 9$@4] iFȄݸqٹ^ӪU+ v"fw! t~;Ni*rUW{{Kx ώ\4ܪ<\vүf/'Dks`JR,]\\pa{ܰ,•0E 1E`@7q[5#GK?,kg)GʩU```wUu.+p ^xQ{t0=~Zޑ1~ ibOCZEⷉeT1h=y^7Dth!N(I IM`O+F6S2 r'lzY3I[۩Y-ۃ2li 9+KhBunxfdi>}Úҗ/tjgN{8Z09I^ӥYa«Lxu\1 U˾+-+u#S`_6kQa7gq@}p5fp|]ByxmVz$4.%4,MZtJ;Zۣg+AZgkmvvғ۱ y&9qT֞'-o-̚JYhStI3|4r13U}:o7/Sm *4lWXZ0aaamڴ8`ъ /1ELInY)4jc%ù}j$ak퍛Fa M@& dhmgSL&ijr< HӌT,ʕXq1-#$b IrI KY _Der}5օ!hFIE\ 8b間HV$8 P&*hrXXXe IjF$AotQ]o!kiR@h%2,)Dzr,-2Q]RnOŌ)eʲLiaVknhVVNkg{_&e" 2EbHB9r,-CB,)cJL$ʢHΐaQ R,!Tcaai40 R$VXvR_BO!B`4M4--΢3eqh@HgD+?{xu^ fԵ,,,-fa F9qEb6p:J}a2TVr\.A\.C$b^a9֘f0zL*zp[@4Z~be@Т({) v!վFk:Di$7Fby4 8`l5J_,E)1 cHa$X,e1R4Wqȝ,%".83fæ1(t}wȨߺ: ޤӎ--IWzYM=ۂJn*[Oj9ڧ|ɶ:zϧE ta!e0(0)G RT.! 99%9h+&uJM9t5厾y 9c^UA2p-d*i0w-:4_4{c9%/rJbrK R/rJMZO@r0X>r1IpK[\Xc6>_."઀+ (GMxHbv.}gy&E1x G] R:y{o;p(3Cɖܞz!PQtfp@ h< ά IDAT&!ߜ{E8%8B/ 7JdP_gm8Iq齄oʋ240xMc<-*1@ dz\uY4z@ }_JE@ hgmTITi@ D]l.>p t;x \t&UVǃoj}cZ lJ0Cm{1;5^ v&Jo XT!*,WDCY䳇+///wJo C2uԪn߿㟅OQrhk y$y=y:dwCF}1AӆNğcxg0t9wfv~|uf2*p_/ܹmkQR}R]]G3NG&]}~ syˠ]Fmx?̿Wx>]5noy/9z3JyK&?Q d]vF0]x>ӆ(Ləc&=ޡ͍Zdq;,_lܝ/  yՉȤKWW(R*j8Nơy Q+ yru ߿u7Ea:`7k =hptȥ˯lRVE@/{ ,mNKoQ;?{}=~5|4}SS.̝6L=CGTT69!X JN[i-[+QMT ~V%,5ЃFw%yW~1}7n&8thHSZ򢹆ZTR UJFpWr t坮? X{+e*4̏L,Ee<8kZQc!IQOgxoftHzӆ(x ptś iFK1n@@'}^(~q/SuzE>JZ kLaJ"3DYk[2W4`Qܑ#m?n9y/:5++ŋ\izjtwݶуo$e2oSWz~mOKV}Q 柝M#ӓ-+?W-8}.;:oQ\4fb[TR%'ݘWeaJqN霷{?qޑvrda?.s?)=yFxpP7lb0}_b]x3?,_ʹ;?,k}=xpP yq!z󨗋=^|783օ6` y_00]tSeaiɴ߆>duu~c!ǎii&mGB>>>CwIR6\h/ 6O^}-߰&~w/ee Iw7{ {q ,Z|܏~ufg_oo$n䜓23'cg}+Gm](\lҸ_8(4%݊.bCߴׅ98rck|)HQX( Y|V7.DoR뉇޵Y?'UYfg @v\ٳg 6~SKrj٤ٱY`1߷nq'}ffaYX9$J*+ژx X'Av< ,ua!_F.e7MBűO2v^bUe#l\RVZ6 ^￟khʻsȘí](ƤzIܡgQ'g.4n |UDrOɀɤg}\[y%T٘El'i;x޳k"Tdw( ^<04}ˁ/JIF+?%\-  à/w1 .r;s,(IDR4bHRE `rNV^l4-߼W? ^}mi£4 kXgwJ^< -D$obʷ_SI=4X,KbP#36t 3[nG]|~.6dT6fzo埚'%Q2"*C=tT 3oma!9]XGI^NJYgOPWrj_?o$tc^?uK6]4{M󙰘' ɚOUFUYv1@",2,G2P ?c1Fa;XltEl*\rÔ5Bηڶ.S GĭGwgЮwad3F؅.;/5RļkBşaLf;Nsz׆nLWJ=7#@0B0`BO+\~,,,p' [B$U{Y# @34B/9b.jbaiyĒBQyMc f1a1 3 -aLӌsՔu9E=Ǐ˃۴ (zkx[V3Nq/m0F: *N(Miz]3O91m4v,fHyqI aQ(jvHB02($YUϺ;fe?OYyɽ97w9XL>dCJWͫ7i1 e 7%hPV24e(m8ps8q<\@!CHL~՘uR&w)prF>. |rWwVP }{E{A[t;S[uGHtLLLLLLpSAyng uFn_)S+m UT虇 :Iy;Y1EOE,x^?z}/k?_.{2ONWP-l;s⾽lP\U$oRӿX>}?81jܓ˝n_͉:Bt-\vTsAy`/3:J# Pծiջwl*`[GN [q>u붺ZU; YyKx|o~Nx7ƷG}h΄_J܂w=߻+S۾ؾwԲZ]4 e|hRC뮜,heyv"P;OXKMTvyϼNߩ x.~Hz0@*ȉBu,zs(?A~m9*8ث84/$\mS%)):5 t$XBSթ0 Rp P(GP P(. H P(p ca |_fHP(QB8$lŊ RGq {g  FP(F$Qߧ<3#P. a7ZVʬoV.)$3q Ƿ/7;wbEhHLzɔ/Eө(ckVc=cB  +fVFm>loo;_HZn(%V}fb]g=KIMjzY¿Ϗ޸q֥MQ][44H{M!YGxK{;4,;bإSRRRRN}k~1GÙJSÆ Xzݙ[juϹ뾛=F{>RZ707Ᏽp&/Gq̓p:򵸂WQ%a˫[]L\ΩMlTm6kKb/"'VluyŦ 7n޼uns;ñ -Md6 tm'5*.>TV{S܏bf"v3$mݗ}TSߦk !!V֔ ! ;p@ll}H* d=|k6y!#f~3z&)>ƛSO6 3 8cV_Kb(I$8l(5/AK:wh7LUrǎ3g|1tst._kLC@IX~νkɑn1M{]v+Vߗ 쑡4 hyl5bLy̰q֍|3ǍFREN~=KBvjtRÚyI"i}/j7^\[E^sX)6mrg5gǙwm5ٿ{m{uP5s*b$lق/5Be"㧟 SБ~DH&<&" q5Z0#8ɝoLqo~|@O;<~h'$^ljÀ s;֊yyo}Y-D60w'nk v'm^;K?X|a#H֎3mvA-;vrTK؆Mקj䩔#WחM+N݀S~~yӷr,1L0 G6wO[7k/Nmk +ם^Ʊ,B @ egύ OEh"s#cBXTx9 SAHCP IDAT n9):I!Ha@Cn\,D8hJM&-#8Vo("e9CIiV8)æB:tqBqZ?h"]!#{gDxXXء3)2hሹacYsr˲FܳNnxZX=|(JmScєc)Y iyOvM),>ݩzfyF%asjYVqЋ$#z\/.-Q(AjUR!cPZa?nh ! |1F?H`|nsBIqD 9ebBypX!LX/"^QFd Nn3ֱ1^opNnN6q!0eJyBH08+أݬCO?^Ilگ⬍=,[ Ԟr RqדÄ#dCB0#B%?cp:L{YI`8LPktpDRѰpv¦/O.ptVu6f0 nZ[Ԗ::-u̺૒!L6U O7#G@kաS2Ę5wk^`8R{sZ Zz9Tksca 86&[_wVPNx;a|ݵk9%7r5~>wrJub=7Jeq&Vwm˚*LI{FrռݛFÛ ~{53T'H6ُwSfp26']#ju'Kd%oo3{cZj3읭Ym"E直|ֺ~nZ$2,lO+SVVPV}gC0W#6QVC^ݬZbݬ&Y(cF#iJZ3߂OVGuzEڷZn5zޑ| \XbjZRG'J_$̿^_-}ջ{ֳ fgY6+JvQ1" Tڙn,[Kͪw`Κ~gϝعaJ ~0捣13V8ql¡'?沿U{njC(:~ea\{YI}䥵0f'nY0Gq;^QH_82N+ӟ}cL~61&|ב'nsL)xfVĚNql2fۺ{߼9̤g;u];uo/\eJi'Ϳa~ϭT>` ͌e$ soȲ^yʕRml\.d *.VT*fQ%H=lrS${-Gv: oev۔-5Q@k_yosnjkCJ+3";YY%? ,*tY,)GEY-,[Qٌ?t⯏ǶpމlI͉žlIա{|5 +}k92xųݽb_jiZBoM^?gm"s`y%@>s>Aw'I78`!T*[7eYe98cLHHv2\.e~jFԨ>?m+pGU=;1>CF wN&nPqTZ rU)Tqv&0=s4zC6eZhaHHCq6'hԻ_r~_v^;CYC̱%%3t8Jvulm̾y7Vnhy7F|-W5+潮0bgJ_%% aFPh)'B lߵfׁ`"k̿[w]MILeU/:6SžMY,Wr L6 pގ7]7y?DOL8޻=Y"Xʀ BVKB!U~:V\9` U?w rƽǿҍ:Uxh0`d ,IU&+ijUx(ts"Cq>Rb SK+~i *TRKƌ[zW-L.ClkqVqd5rY^NNLx. au=+?fg>cM~`77sʼMR&'[9n( 9v#+7Tc/+irYo<2Ż.^>}2{k%DqwWHg-t`T@C^%'mfLcY,ms^Y',q#cd:ҝ3̙+yw~e .HwG=ޑb_fW֥½j. au F;mmܢ!.{1N_2g׻#zts[${t}lתEs*̿ϗk\3up׎]GxvxweC7lm_ZhԳK.Y1ּInSx?.J_%rʫN6u- T+y}ųgKyΜ$KvbٺGMyHPԩ)] sca V(J$BPA%B0. at,B8$Q(GTF՛!B Da ?A@P(. a&Lx&P(: HB8JXM=N1+x_#Ie.R(X|To.r}5>@4{Vش+Nv(sΝ[?)Uե<=zD|kIIѣjD{jŖ4p}D.eWSGuo߾~=ǫsh[Ƿ6 Ý =ګu||||硓/ضm۶c~#Wz?9>>P˛IJSǷ~)&9}{|dV>5Z"0; q3mea1lBϺg^pﯼ, '9ج3ǽ@b~@|֑;'V3_g,ޯ-XN_^!! oݵ{`S׋SsSMӰke>3aצO]@j0yȠzAֆ_wleO y[ejT㍕(;sRW/9vU o]0[%4aϭU׽@wՉ'!տ[2H=|P8O㽛`?,]˚-Gy\X;/ګ-ٰE|^Ef_#-_),j4Q*O_1g\/ 챧x~ʼn MF j>gʿؤs& etCFaÛ2Җ\,,- vʮ~}"xyB8ӭugVMJٴMa<ZP(cVi&a*%ITj5ʆ O=~8:Fd'(/Br0^qC"UEEDl"(=U 蒌S:zl?~9)99ԋ.UO׭Nп_UGFGG߇)e 07SB^X=ŏs.)z<;X-QR6\t(kȣ Wwyl[u@x|\ȁGc}^'Mؿϋoc-,~OU5iHឩ}g1&I3P 0/M~8d2·^PT\nnߗ Tl4(J"f@:(j }Tϧ旰u>ѯ=1uR?Ŀ̮DoZqf\h&M?~y*˱@00L`#7npϐB8ñ-+-/apYB>Ըuŗq !Ŀ=72<e;@&,b€9zRxe%ĩ~!$/󮧏_5Pc[V:iӏ]?7qۣ =, L=fV*C R97̼|/3*r 0TYbK W(c>@\| Bq0 P P(. 0 P P(. }\V8eMjarL&+' 8d"sw6Qb S,TjFStrQA>Ua_f^޾ c1Ӗdj9r <|эJ%}C%0)77iP]W )77mF+k2fUV9₼) V{` rs5iPoQ?=YNlu͎T0D>yP9/^\h*!(#k*VodD"'1bHܸRkJX1r*寺/lZe^mjqjSABHU/qr:xzj9WѲr+D,nnb}=YHe!Dޓ'LY mJ2rSQV.m{TFY)I~е*aQ+>bB0D-E|&VNr%5V TJQxF(WJUU*a!òDwHb!5VGihNปhiH"I Kh4SdُA+$4WbR%v">V*d kaU>L=Tg<ђ"%H@pHI (jZT4*a8K`Y6es<## GV,K$uҺ(4p!Bl+!4KdiֹH$A9pVHlD%$'en+cdAJ8z*cXR!dkJ6~Dфh *Z].߅i >A?ql>v@\i;u:)igl3&L6 0f|ÀYd'nZ5Ӡ_s&ٱ Q@q|Ǽ C.Dx? 3tJ#  Yd `gMrJo7I-xA&C 1Ee}"!oeAئ `͝k,f!J6 G5 Bqe" 4 }3bHNՖ$~/eosa/>-VoHV1k W~|=iشq,@*f5N)pw9N.M2GyiMfSbmH|>֛HV <~^/i&  0&dmdSB! Q}r󋍂ҰW4G% i{(IXq`Ө~MSfƫ$ :bGjY~HU>bvHu0$5YS fvLĺ$0t&MU_ P8 Ҟá%;K\| f "W߼ӣcArSP]"~B(c^=6 {i&lءc _*¬׷şjB(trV{ysAڼ) #O~/0R6M+$BO,g3x*P u1DCʔkubKM>5eګhG\/h8T%~%[Ux'?|@?Lg H{lJ}msn`ู~}xnGwt@cg_5aTC@KlhNϻsO~J35bƣ_- X6+u^Ro}?jw?NjܛYC I0fA4B,!f @8'H M >]4%&FʼTiwN&-0=Qv E){d;^]jyK,e -CVcY%X6U@m;5uT2m=$>e6yצyWrnFuo*wL ȆmD!"w0J?_OTZl5*qCvMS B8c;( YVtEO`e@xXݕe-7 ֵ7Õݒ>ZڽVnݳY"V;H@jT5dyf5tvKfԾvߴ)7~8w[眍}-zD)-j@BȻOf5V6~wr;g2 2ߑhny*3aq]ZKi+ezÎÿ=~Ri0e)qտVpj;A>lW...G<-r/.#uYdgJuhkYEY^ l>őO>#K<"وcFXf٫^"W>{6O?㓉3>yj@£~\qjo.WywRFxd@^DzJkx?j#z/x/FNڽJ9z"5׈~@feǂ]! , C*Ez+'kڏc3/eP% 5)^m ^A.d>LԳJ> Oj j?e+U7fOhOg6N]Fi֞ F5ӛZe }@EAELU*^Q7޻SsgotDŽ]cb|zfN@AOvW\wKoU}*R`m/t 0vDz+ 3E7ڶ)tx*v`|Lq#*ees*6901d奥f氄6v2 @|Z7`$ǂS#B3e{i'eF5WdF鷋r~xΪ3 Cѭ4?"@FAK.55WTJ=#Z5`db5`t( 'zF)Q(Ml!уZp,.\6*N9]0&=eng~WnfJ"&E1&7u`tFL&`ب7 b|ӆ'̝̓5qk4Do` 荘lin]HCSCQ%Per6 : 1 RlҳDB(iJWhb\c섀(KX~R31B F#ҽirt\ht /7}~R a&[5rՙJ%l:q,լ&W.`DkyIDAT0˜cG+M%/nHYj/e7aLw{ڥ>Gtr f+80g-Ձh}F]j qg!8lrivqpDΰZ%&&~pT= ,'\n{6҄W|[$4=aD̗HcBܘmΚc6[`hh rbARH^qD=($oi\_l !@0*_%M2_v# F r~tX̏}sEHb2l !1) l4B!]&iHqw 2tPH9R+-* j"ahLFD0vp93j}7 !Պè>'fYTL+7!_sכ2Sߓ5 $=r/\(kZ&,ɓ{bHh K9fh Ḳ 㭎2 pm# [x'CoO M2E"C oǂ0 mZI#Q*CBO]sz̸wIӢiJ` $ s s(TWt'1!Y B3 PYtB1-ʼr,H[Dl*cƛy҂WN)1c#kԱ&GeqGFaHu['$C%%Ρ6 )埝FPM|:tr~Fr?]ŗ_O[Juǖ{jIoGU`̠IOw;`LgǗ'}Q>wjgHO,]FB>l'v;+&LϿJx:'Sda"%0A Ri1?cI\Hۺ_pVʥg/8w@D6Am4Opi܃w:)53.Mҍtitٝ9E@XA%d 6u?ʷ~AYW&[ j{)oHn$Ok" %=-k%?uJ8kQe_e iߛxӜ"@.HsQ 8l{I‹ǵj'2T=}yڠT{:ޝڶU( o+{MzQrlzKsJYRvHIRp˟ $-ޒfC"*=K1/;׭ wwu0hK&4 #D"bb')< hV7Te%"^%u-'^GN Oz!Eme%jG4 \iOgp2n &lf$K֚eS@u X_\_]4:6nB D<6' Zvˇ(\v`_/pTq5޼]1ݻb%Jr;U;T VZшxSd 4 Dl֤z?a]QfwWVmyESDRFZ`SZ@x @{D%*J !lq `VW^ Fe7tds8%kaĐJټܼ|B*|ɥTNf71D]$ORg+bIH3Ur)))z NTYԆZ9VV -f&zw 8mfLQۏmwa,MϹ.-թ:kVR` I~lG{ގaV;Kou%/~%-xlUG@e9 /)t!!eCiwjq6vpwuE%`&^pnNN š\!xi |i!Bo}4CP6l$/ҷy4 6³\HMJT!ոg2VcTja<|mOv`F.W12\o0L{lB0k[R Ud2Y 8Ry0Vo21o P P(. 0 P P(. 0 P P(. 0 P P(. 0 8%aat%ud B0*0 NFFm f!d׫Lb`ı+y>LPj03N>1^ўBQhӫ833U|BBke0TQ?3Ӌ b7Q*U5^b*LdF ķP(ݪ`Bv,ՇY3c' lߢi}\nn| !%V1MfQqmz<ͮL!@Gtii?0Bvl$X$G9֚퇶|;Rjʤ]52\gCG7FP7)>4GO_ .7fZnzH:H,gob;%>:Vq{lvq(OmHG-Ԏr3f6?" di-߅UzՅ nPfȆ$2,igf?AW^LD<ӫet}ԯֈwywl^1TMՔ(M&W ,%#ag1yw[y~4\^8cdЗ&JѧHRzԝR@XѠ'&GA2eT#+}b:K~6$?ꄈi̋| B  ZM9 3 z.5! E>` m/_%Ç=d[=ѧg}F :s;jljU2 #$ 8ʇ=Bx5k-"@# !oxfMx/5r `Z ~.ɏ Xy`a/ .˧ىi,k%t4RZӓ7ۮђFJ@AAt෈# !TqSW|#.Ў`"k"2YW*(X$O aBxOYA!!m70&6w6b)lB_0bY'+3B)dAm[1H$rīIss3f=8aLn#_|gus;\wU m [OsooUF$9pt>BհE 1!/cTϫ#}7/%_:f`~,ۙ钤 E9(߿NN F#TB~bOz+7=qћO[xUoc^w7nxs\߿nFLjm'nee&۟cz}<4ow\r蟋#PMIa8~x-@k0&w.oL-yבy!9BA&4)l{Cr~F ;V;X)1!=Yʃ:5~z߯Z#gX~0KB._==N/ lTʲ~F2(F^칟v:UoPj95^;{Xm dK67wHܹju D23.}|qk߀a6/ԏv&¹s:wussEJMP|?dӫL(!2Vbg ĻvŷQN9W-纪E' I`w r8RGrӋQW@Yp*nz ʔd5_"?] D^҉j@. _:yPQ#)icPjʇv( C%B0T( C%B0T( C%B0T( C%B0T( C%B0T( :L ׎IENDB`wader-0.5.13/doc/devel/images/deviceplugin.dia000066400000000000000000000066301257646610200211660ustar00rootroot00000000000000]]s6}ϯШ*,~Id]:ۇdiG D6a,Y0{<; \=ǟΣ=KR.⛮EC1'M:oĩ~uӝIDJ/IʮG^FWݷo:LTʄub:g71 L'ݼպ]("tit.^百t _ %"媉\-?inFw,.?l!9ɔLJDds3|Cq^ʛk.j\Ү9" X8*%n' iNeUK)Ns;MyiQ諡{_|cGcXf_ _y6^"&>W1] KOfm z]vNL;3KTw F %|dN,CmDӴ BT'7_3?6ՓZAгzpE}:~O#1*#][.ehEl> E +6H'Uә|WO5c{N}|4d*c(Uq-2Wۄr9X}W9澁.hX$,MGR㦄 TaA\U 9盱{Hh2+N(ܠۯzF*fCF0z6$NwF$ c0In+vs,HUC,ԭc49V7ݹE Rq .n2,fP|YD_3/Dd1a#?ZTT(6K !f U>ԛ5b<#uOd'͜YqxCFsS枞gL`f%TdaXΣMÜɬ޽CL䍟I]wof_&:,٘0sPG\cr>zʛ9rwfi8~T6w4uՒ:|ʁ}? }3P5zٻQB^qxR739<19مu#i\LP;uc͝ ӄ&hBX%C6*Z$d꒱ϚCVVK$,}X2!@ 1$ $B!F!$T.S)KȈDʀ`l-#ӈ@ edAe8v; ;)拈JfJj+V]='mTekgHNϲmV^{dgI‡u4/8' PCE8PCE8PCE8O$mWGn#^4[8It)g-{;e򳤒}Lݝ‘HH1m_HƦ\W˯R>_WɈJ/]T~utwi*B3{M:͵[[؞S]L< ojQѱy$B_|IࡂVXuhd;CN%IZlƭ¸U\U8M|95{g]zүjq$=JIlOY;B?8Y>f#tҥX~WgllKce 9 IgFɺ8/ETӛnpbGkk~:|']:أVck>Iq(3~\=+XdvYl.2ܡ]m!q$b>[ 4L0A 4L0A 4L0A|}4Ipꑨ$xYEyjNM >zVy7`3l&DQI丝˧CQw,Rx<%O%t,'ir!i &H.^94eڞk+ ÁkHxsՒv@EMUF䴯jjo%OܠZ7:}-π-< x~ |VÆ͔hEa6tqJ9TB8Sn߯9u^chE<5'SIMM}{wBڻB!~[5̄ B:<%B!B!ӠB!tB!N_B!iP𫢨(x< ###c֬YHIIi~L2CfffXy<aff={mB!Co3UWWؾ}; ŋwZӧOq]O6lۻKB!MFo3ȑ#ؾ};&Mĝ߸q#6lЪm/[ HLLeo._SN ;ziB!Bo3Ǐ̙3~|\Cܿ=‘#GЯ_&Yw+W#<<;.]|}"ɰ|rA]C|嗸sJTk gbʕJ(//GRRN8Ѭ(+++g3mۆ[n)䳳9޽{|[OB!6?ooo̘1/Z "|>l TVVb̘1033u놷~ =ݻw韷aoW^ SSSahV_^Ԕ)SO̞= |>֬YvIDpp0d.(hFB!-fRWW!o"%%Ei ={SL%JJJ#aaaJjjj9s&wlbb??F2vX\pӧOP(χ`oo߬(ooo| ЀP(ľ}쬔7Ķmлwohjj ݻ7аEG!ΉǞ}LAK73v ޽ Ds'NDMM s/ۗҐAC!=vw-8;;CGG(++CII @WW_~e;B! Z@ڝ%yyyx1zyի B!/f~IDBBB{wB!B!N_B!iPK!B: ~ !BHf7]7os~ !@3B!h_ፐ֧O !ҡ/!B4(%B!jjjٳgcшݻwۻ[BHoBBlmmۻ 455aaa׷hm10,]U S:u χƍm۶_U:WRR%BEFTw^̛7qqq3f rssqܹVM6 ݍWc Νݻo>siĉ ǎCrr21|xyyaʔ)@ hQB!m2f&MĶo^oZ\\߿?aݻwg}P.<<;w0ooo СCYyy9ck׮=z0SSSY=X=ؖ-[lL 0SSStR&c^ >{7300`gf u=\M0Ջ Bv5oGdooTc/_fQQQL$q^z+V+W(q}߲1c0uuu6 dm=4B!M?9s`ƍx!Ο?KKK(..T*g}uuua[lX,D"D"9s3g΄R).]xDGGsո~:tuuQQQ lٲ555 PXXؤ#-- M*2ڴiH$WF^^w^q!!!(--ENNrssQXXUV)ճm6YYYYHKKCnݚ}:k0`>SH$B,^gϞEVV>cJe 1{l;v 1|pTUUa4iuP:tO>ϝd<_]]ɓ'#44&M  0k,TUU)!TnB!hPɓ'pYXX '':|>㲲2t҅;W_}̄x:gll ;w @ IcՅN> z ㏌@ p<ބTnٳ'VXݻw#-- ߐhDGGC("((pvvVb߿wFJJ >}  xyy z͟?&g~B!B!ahhgg͚ d}"%%p];vLdܺu Rcff|ܻwOἮ.Fף yyyضm<ցbٲeM.b |8z(8.!==X뭭Hn;;;III-~G/_FTTD"a@,իWʖテ,--[oB`` QTT~&LB+]ox1cΏ7| ~m888 77K_|9.\;;;cĈ 0|pP₀B7ovT  0X`AǗ&:u*6mڄ%K@GGnnn_:"{֭[!0vX.}Νs-yYn޸}wO3g 22VVVƚ5k>5k sssŮ]P\\}qE#B^e<?j!4r7=ӧ8}4ك+覮OOO! muB+Hnx#KBHөشiN8{B" 00055mnB!튂_B^A;vrB!_B!BB!ӠB!t Arr2fϞѣG#&&FBHgDo3%$$ֶV:?o<̜9;455aaacƌ6O?<[n!##;'J1d 6kC6lXK ż<}Naaaqa۶m_Εww !vA+DD"Ⴙ{b޼yXv->|?BP!͛7q Sa0[[[nǹw*l@"?/^DAARQ^^ν?"c8{,-Zݻcȑ,o~?Ƅ [XXxa{Bi3bǎJKJJ ccc",, r;wO4 aaaܱ#BBB0tPӧOGEE^PPooo jjjUVbָ~:_Hs"++ ""Ha&yo|&]S[[[.533ܹsnߏ>/455!1uTz|>uHxyyaƍ\X,Fee%w^L/[`7cr.\>6f6تUCgyޞ`֖w BxbvY+**b_5>|8SSSeoKLLdUUU/! !Mof؍7sǎcbX!_LMMK_g6mwc=xxc֥KĄ͟?>}ԼPlÆ Y z؋*tdm[tt4V\ +d,]pܻwŕy+kx7{ݽ Q\\ wc077-,, JDM|}kcon=< w"!!={eee*Y^^Ε֭Qǃ vCa\yaܱ@ P_B!6~CCC'fZZZxHOOW q"$$D!PgSSS1x<ܹ333.D `<1󖖖*K,Nœ9s Jo>.ՅN> <{,7LƝ‘#GPQQ.]qAWOOO&ճgOXwFZZ"""o!##шP(DPPRؿ?vލ<}@AAA򂦦 ?>bbbڻB7 BBhkkА;"##l$%%qul޼/^֭[ ƍ |w(..Fii)6o 5 ׯGUUm6.]UffffT鿪e˖5 G^ XC? gŊqQTWW#++{zL&Cqq16l؀Ǐ#22RTVVDll,nd\zee%6GDDo\|QQQDÆ X{Or%%%KKK[8}4x~ (%Jjק=̘1Ÿ8Ԡw۷"..:::pqqATT&O\yxx ְ{ǥرR&&&0``&۷/B!6oެR";;EEEM.gkk0ZZZ KNM6aɒ%сӧ#GĉpttT ܹ &;Ɲ㏡ͽb1GGG|'ƙ3g +++dggc͚5pttD`͚59pq|b׮](..ƾ}0qDBȫn%~[”)SڬMv:4޾ӧg߿_iG7uuuxzz"((044lw:eшh*sN:6_Kijjjpww;6mڄ'N`޽H$ D`` 6B(%cbرB!JW\i.VD/!B]ox#B!-QK!B: ~ !BH!ӧon233```1c %%d&,,,~z B7Zc-!!!m &BˡCD͛7q ӳ^V{żyvZ<|s:~V^^N<2m4w7!RGرc @ RT!o ttt`nnKB&q%%% 1 J3vz [[[;o͕/((7tuuaffwy555\!C`=ݻwv+**addWWW<|PiX|yGn ///lܸ\>/455!1uTQݻagg@=z`۶mׯC$aܹʂH$H$֭[vH$<~sӧO[lBHgR eeeXr%N8ѣGp"''(,,ĪUj\~@FFl 3g΄R).]xDGG+!-- IIIDAAvť}(..T*g}uuuq?(,,| .tS^^478U3g6n܈󰴴[lX,D"D"9sӦMD"Appp+--mB!Kfff"55.WWW%%% P2xyyq\> s2 `)oaaBGGGj8** |P\\ lڴ MT*+//W&_#<~ֽ{TTAA[g:g!BZF'O}ʸ@+}L8탯/x<.\ȭ)Czz:A1x<ܹ333@aa!LLLTG___}ڙl#$$IQٳg>ӧᡔj'90}CC验999sT-"BH@PPmmmr|>bvvvQD+_QQj8::rADݛHnv1;;III*OWWFQUU..555ݻ7qmcHR`ǂ T.ÇCOOW(**R޺>}ȑ#8qt3uTlڴ K,ܐפiW%7| ~m888 77W~o߾ ؼy36tPD"ٳ[nH$رc[WWGzϟ}b4+bBH-!55ϟDGG#""ߑBF%&!u{!B!_B!iPK!B: ~ !BHA/!B4:D;}tJ¸B(ruT mnBKo˖-=?ѣGsi[nUHsppGyyBZNN-ZCWWիf͚ .p/ ###c֬YHIIQe>Cޱ*N5\t ~!>VoKSLYfff{wiӦASS򐑑^{zE!e\7ؔd8{,wbΜ92\v `oo]]].ߞ={or '' Y]] ==۷oǢEaÆuVk.//Ν;wVV/s;wĚ5kȘZ¥K`nnZ[o`` vءt>##...T*UHߵkD"o`cccҥd*066!PYYV/b1qu|~:D"Ν,D"D" UG!A~`hh6?Sիxn`_ӧjgOΜ9ӧsA/$&&ܹs̙3 ő#G}vL4;qHHHJbcc)"czU-[ HLLe V5̤Iە;88E1L~W{=|gfK,Q:k.c,77D"n_ƍc3ooo*=z4 \;%%%L**g3f̨牌d~~~԰c<d666 ֡JHaoop'yyEDD0Jsߑx{{sڵk1ƾK;31ߵkWϨQ ~/--~^l .M__=|PH$bW iѧOikk)SpyO zzzD||Rޠ +;ݘ.]H$xpuuviYԷ?7])?WwX^NίZ޶U… amm vgܹxߐ'Oڵkٳ̙3y&73fpiug_tLϻwF&(..9p ۷EEE }? WWW8pŐd{.<WWW$&&[.((x1h"pߞ>yϟ ͸ʯѣG₽{ݻ055T*Ell,ch7::&&&011Abb".\gffB*ܜ+ceeդ6>ÇC(B$aǎxJe ^^^ܒYfJ)H$jRTFFFWuV!ູq))) kУG#555j={P0`X"(ӧ<== PYu@=s#rrr<޽{ؽ{7uĀ1ƍLyA&A]]񨪪B||<ҟeeeTlܸ;+Vƍ9r$冿[c111˗?uƵ?g^]Xxqkjj~CqqqUHjBCC'&O c(++>X%%%*}L8탯/x<.\4Ƙ_xHOO@ xn[}}}|Wjg=<<#?͂BZ3444P]]ܾ}yyyڵ+ ,|HII˗ch7M,[oǏcǎeozWWWcϞ=---L0Ν;QZZD@aff]O>.]-^uҥK(,,xxx`ĉ'bԨQ8q pVVVXhW&O'OWc0~xAOOIII8<O>EXXWFɓ'!ɸ>.755U|W @(B(B[[1χX,v/ jHEE;w{S444궶"##QVVn]IJe˚\.99nVq޽&O!67˚ |>7{E\3PWWPL!''S\|;nhu„ ޽;!`gg#F4XoRRwqm_g[rL9ضm}];"W^U޻wr? B +ݻ7k׮ܹ={r3uPUn/**رcqhii᧟~R_JLu 8ѶIi53f̀={ᅦ0h CB$aϞ=غu+D"Ǝ ºu#F <<_zzz@ʹ@\\jjjлwox{{M_vvv4ɥaӃ#۷/B!6ov!3ʿZ[[{M]]]sGs=>6m7ЪcܹqDDs_{{{+<ѣ~$(, y桸'O͛1gx<*~g~uv!12SSSxҤI AZK3B!5W^km~Æ SnӦMCzz:~mkڵ+444гgO裏샺: agg7|)))ࢩ.y UJ=z4N?Dvvv/B طo1x`4h"22+HTR___矘8q"LMMajj .\#ٳg1eXZZ=B~tG6:OU 4Ґ=ϟDGG76ߑVо7B!Җ(%B!B!x߰0֛6dرښ>}:OEO!QM1m4?$B!(jיzgPwH??z&vttDHH~a~ xлwowށ.~m|II BCCall CCCpuD"̝;YYYDDغuBn޼CKK Ǐj.;KȀ <<tܿ| V]]ׯCWW-[4$$Ann. j*@~ HebH$H$̙3GÇ#11999qvť͜9FFFJt ʰrJ8q1?(,,l뗖$dff뿪'B^e3e uгgOhjjB,VVVx(--Ǐ7n@OO]VgΜ nܸ(//lj'phiiA(b֬Y8p.\L"99hAꥥ;> wvvOiBi]mFGGcʕjd3gΠw*cddlhhbX]]Y]]2 ///.% 200~@uu5޽{`ܜK6HRx333޽{.Fף yyyضm?X ;;;nD"^o]IJe˚~Ki !B^fzی3t~СDسgn Hc*acc{OvPSS޽{C oV₀B7oV;v@* ?,Xٳ=A[Ovv6TnWNט?!BˊZ6M-ٌ#0eʔҐ {{y#&&шh4k$ݖ=B!/Pss >|Xy0sL866vvvԄ֯_ǃƌܧ~ vx Ҭ2B+%//'Oln4 $yDm9w^̛7k׮Ç@(*y&nܸxzzի\-wEnnR{?3qE)I] ~\BK!v޽;N:wҥ?>wUVbָ~:o ttt`nnKB&qe*ES5EYYܰxb46"@__FFFpuuÇUNk.\piffsnݺ}/^ SNU[n6nȥbTVV"//{垸TA᧟~jR#S ~ !6lΞ=tٳ6l¹m۶a͚5BZZu#\pJ[?oҟ*Sս{0j(xxx`Æ *kl~)|>!JgqϏW%]9t֭[uqy ˑwwwtrr… #..JI>///|ÇP(H$Ž;}*yz7z۪*9rҥ _]tl4K!/vTSSСCn:x{{c̘1/ ~SQQj8::ǫ݊Nvl(]]4_|GoΝCttB444vTeɸu@GGr\svի!1tP iWX?GEuu57d2cÆ 8~8"##Uj_~Aee%QYYJ")) *y^(%TC`6 |>U~n:xxx`a IDATLj#5UE~;#T>sC p3?-- Ç嚻K- ԩSi&,Y:::pssC^^B}A>}p8q*}Axyy;7acTj?1ulhU᭱]4iMҚGZ+[BBB~vKǏW:oc͚5/\{ ҥK[#B^VmvxQ]]1c(-_ǼyHMMM!?}MeN2f&MĶo߮t>##9;;36zh{=.7`FFF͞=UTT0vѣ355e|>ууmٲE>lԩlȐ!^cӦMc?V3zhrJ6a֫W/& ٵkc3@LMMҥKL&S獯444T*x"ڵ++++Sjom:7ĢLυ 8rb5551bbbX߾}Y׮]Ynؒ%KXuuRz~G֣G&ؒ%KX...쭷bFblܸq\[W{{{P=EDD0,::ZI!夦2\0uT 2-{-D"-[ !H H0gΜ&ew9s& Jq%+݉Z46~[[[ 8{1qDqpIxzz6xƌÇ7ޜݍ KKKz0ug*?MGvtQTLY[`* "$``@"嚙JZfuHb悻755K\1T" 4^s.lM_/_/89r<}zzM+@BDDDsrrpܼy%%%ؾ};!>7??+z-B(ܒ-Ж]PF*~E`ddXӧ+Š4*mhhh]Ovv6LLL`oo͵&49Łŕ?<<044ZD"AϞ=Sl6<{#墢")#t -GYYYqommͫ~S~2u733äItDGG+effT~cc6iΝ;ʳ-\EEE(,,Dttʱ&{OKD]gF<K,emmr٬Y>۱c`hh;;;Y@SF:@@sssܹsq K pun[EEB!2rH ?q; EyuW["g͚5hll?7{ n߾{ڲ˨ӣG Xf P\\M6uxvm㏍֭[q(;;;C**L WWWn$4>|XnفZ#bdeeuv7ZJ ܞ={0k,|Ǩ?X7 ?W6dnݻwU>|K.qX6tRHRtN].{Ν; ,_ P <b_}U-&7oFEEI&aټo4L''' 8PxxxxԩSj8y$Ֆ6DDDpssSX7;&#FD"ݻD6#TDtt4`aaxhDfРAܺ~x饗}?|pDFFbĈ:t(Mp%%%0az,\PiVX`888o߾v]D3g֭[H$H$ O5_[-U]] IMM#w[[[,X\yYY`ffKKKx{{w.]ڢ 2`̙?o /B=Bz¼y0n8|g\jkkQ\\={`ʔ)J@xx8_R!D"ݟhh0>AAAlڵm#\]]Ν;i۶m, @eYee%333cEEE= :{|e3u Ypp0fRM0-Z+|2f7n`ׯgNNN ]]]d?-_+gQQQ/Bcǎeb'+++B(fdd0GGG>kxb***BL\}\EEEL"(\y汐V[[عsίr9333kLr12e +,,dcÇg1fbb®]O`Æ c1j*֭[V^ͼ\cR/˗36|||؊+Ԏi?Iҡ.]@^^Ξ=2i,(= h.22*#:]qqqJB0nnnx0i$;ػw/MN]kET鈈6P^^1c~!_q}B [j+kMDߋ/"$$Rd8p \N UUU ۦNPv!@VVW5oɒ%\ݟu o L&H$''6=󑚚 sss!++KK!NH;G\[DW^y˖-ѣUأ. ߈5@"tDD8}4BCC㣏>cE|Yd /_#<<_~%r]^p^^^{.8'|HRr^0n8/edd֭{L6g֬Y KMD"BOI:W!,,]4n;~m.\{_uc;;!hǭ Es ?~<Ο?;v(a.L5666\kEiFSկMhh(ۇP)=ShGe޽@ 9s֜/=Gαr]888 Oƌ3PQQ{r=z gϞ/:gߤz纺::t<1@&!##Cakjj$K.{ 8|"|WtҰsN̞=[)3|#h.I[EF(lzqF/JY6```3g(lѣ 1x˵&"P(Ā1?}z-|8|0d2nݺEoGyy9֮]Ǐc޼y?qjkkQ^^Zbǎ8r\ymm-2?Ig/!tQ"⨋EaΝ޽;<==dL2Ear) 5hxm֭[pB\rnjjO?&MH$>|8"""ʻ\q Mg/o{AqqB  Ap! 4q>8q"رcܶ>&&&?tAT=o}Diʕ+ kzװ~z 11Q;rZAY$%''...Oߧ={ s/ҴѣG n+WkrӦMo]?!/橉;x d20uK.Űa0k,z;R{(b!DgP~uwƎ˂߿+Ғ8cg={dB3{{{a}vuue/2b< R P>vX;ﰉ'0X~7cofǏg"ٓ-X_gޞD"o*_[l„ ԔYXX#F0TTرcƍ՞砠 f=[drurssg̘1, @6~۹s'6l޽;ׯcms%x|e B!mK'x兪*{ 聦UUU(((@QQܹ+VBlذ(,,Daa!f̘Ѣ>8{,^cժUJlڴ +Wĭ[^zbccaii \|WzXSƧ}>㏊Baa!"""T][ (//GEE>+Q]],=T[++"">>>;#~A\?3f>CMM .\޽{hB!M~믿⭷ނP(Bd_х2ͩ7;; d/Ά ն2{\kݼyW\E___x{{7"::[accA!j^TQQ@=EuKB]vTew}F]]O|ݻwOkzMsssÇmV~4?}iQ%%%\!B![l'4=>z>_<͊w}ggg\tĬ#466"((}6JKKd466vX2dQݻ*t(E5eee˗/G@@>W4eoCSvmdT[d4#F@"`HKKD"A@@d/N:'OԸُvލ-[C||<(k`` RSSH_޺_ Wjj*ѽ{wb}26k,.}k H0sLܺu DIܸq>,0~x[-Q]] 62 FMM r9,]U@xx8_BY{R )) D;5_>WVV"::VVV@||hh0>AAAlڵm#\]]Ν;m۶ěupOіԅܾ};w/1DОq lܹ uddd0GGGzzz2WWWVRRn߾x1/^"""XEEswwW5(v7o<jkkYCC;wBHmrfff,&&R)322b/^d˗/g yD"a/";u믿T֡~mW񁁁,88UWW3T&L-ZclժU,(([^y{{`?vDرc1ÇyTdFb>duuuExM#.ҥKܚ˼<={VeҵEFFTeDk"..bzF-22"WiC޽닼_^^1c~a6>mgDZQ:=zBUUUDǙ3g`ggXXX ""BjMtF$:u*֯_߿qG3<<044w|GE"Kfx+((@HHd2D">s899Iݔ!++KK-m'8Dff&QPPPJi?͹ Ztӧ},,,xm|"H A ݺuiӦI߷o_nbW_}gƮ]xխ7"Q^0n8P|qGS (E:W!,,]4{ٶw%A5ggg=!@BB5>&O{"88sQX( &066Vh#b߾} E\\RsŌ1?OqhѣGs/뢮ƒs*ɐM~;0Xf _M7$4-55iOĝҒW\@!O+mq;hM[D\"]^Ph:...+WEHkaHIIAbbw$!t?#1d;v{󲲲sEff&K/'q5L07*++!H˖-Ì3xk:t( P]] www)ĉ󃏏._k׮Ç8z(ٳ011ALL ϫ~M>~:\\\p.˗Ν;0556mlIB!ßFGG#;;}/ɳ… 8t@1#"fϹs={TGU^+++.pw㭭yw|o^nfff4iӑh J4bllL_BCgɒ%:urX [[[ú*''Om޼NNN-3#i>C>}xb,mm%>>S.$77< IDAT֭D"|||2: @4n[PP_4͛7kO_mًyf_5{_rTUU᫯¤IxףG Xf P\\M6).mغu+8rgggHRIrssڢ~BHqq1::9~ի￑T$m!**JeE{AAApuu۷QZZ%KMŐ!CsNM [-r;uͯE<|蚽>>>۷/l2n޼&Mٳg-hd20p@mllSNmɓ lQ!|ٳgcرׯƏϥ"DGG Gmm-ڵkH$9s&nݺDD$hb }ŵkZ_~}JGɶocߟEEEZVTTϾ 5]?Mק`1fkk֭[{|nnn,))d2?2@\\{7~ܿ/>k꿶緾I$/SNKeۚ4]UV n:zj~w{F[ǎc";v1XYY;|quue%%%۬ ԝMʕ+ HBNN^~eDFFTeDk"..b=IQ#<<044mDFr3[ndd$׮X,FHH^ʕkħ\BU رCaȑcƌիQ__K.aȑ#  … k|_[oA(#G5v9OuvQ󫯯3g111@DDK2>ԩS~z@[ƍ%вQ966vvvݻ7|}}×+_exDs=|\~zzzRRJbbb#$'"!0e2EѦe >Drr2  QYYp\SD >meΜ9?>Νm9+WDUUo?~F~PXX+i>۷/>lڴ :{@k>K|#>O,ꫯbصk>2>o^0n8hk|nb``p~Pw~޽ &*=Ve[kp}Mڪ;fHHH4#ϊ 8Dc ž}8'_c ܿ'O޽{ @9s(ٓG7oG^h+o+cƌA}}=~Gn n݊p>|'N&# PQHކ:ڮ5n: 6 )))?1s+++ھ| 0Xf :>/дz^>١@ہ<-{ .ODM:*"6nܸavvv0003g?x2 Ç&Ԣ)"r9www,^Xab Kt=zzz1bV^ &֭&|# icҥk\]]f466ƹsZ7>ׯ/Ku> X`rss!pMl޼nnnt8ŧӧc˖-8qGUhK!]8tDDuV,\W\ᶛO?ŤI '}}ի( 5jB"i+GYYN6mL?rHB :AAA}|" 4cccxm۹s'`aa˗# @)# H{nA" +sZ{򹿴կ\===X,V 5m1>]"Ni?00HJJH$K¥i;|Xk뷤fmBZ999r "^{5_)))HLLԺ?$O'bر?~gw _xFǏ+mOJJʕ+۽}B!OK.qk:pY~B!TYt) YfҲzGyQMAABBB 矷83!Je6Vp/--e&L`‚1IRW^aܜű0ߘ=ٳ' ޞ۳ 6Կ~z6x`֭[7֫W/o2LxOOO󒐐Νȍ;mܸQ8ؚ5kxy(-\Bi;$]orr2B!a``C__+ X1B8pK,۴}>b߾}pvvFqq1y888 11'W]],|jǁB!O.WSv^Ԛh1| ۫Ee6B!'UO~SRR`mm kkk>S5-YSNU[.akkSΔ t ">>>*3wgE4JNN@ @ZZ-((UU͛Y??uO"M]:"{.e"{8wűcZ?gggHRgnn.\]]ՖBoQQQ*;tbdeeuv7ZAAApuu۷QZZ%KQi: 2;w`K Ctשk~e/Җݥ]-,]/^SMk:uJ8O<@ұT>*++CPP`ii ooop۷oD"AHH3<bȐ!Xp!z聤$nÇ#22#FСC1m4,,,Z^v$ fΜ[nq ?t}@SZcϞ=2ek.899A$6mR(q}Ya WϞ=cǎE~6>wX PNm6i uhPgAΛ7ZΝ;o؛o-##988:ϲ_3###V__cՕ0Ly1/^"""XEEswwg˖-S'77WoEEEL"(Om{~XLL q1X}}=H$_dNb}k:,88UWW3T&L-ZԢgdd0GGGeڮ5oժU,(([^y{{`?vDرc1ÇrOOOJJJ۷YYZZR?U]?Fb>duuuExM#KKF{ m#22*#:]qqqŝ3B.tHӿC A>} PUU3uTB!22hQ1fxzz?lѱ"h?SUUkgs8s  DDDwyEDjшoĦSbWK߸q#O666Jr{/xa``CCCxxxpwDD*.==MىDXYYuB!wRѣGnΟ?bɒ%X|9BBBP^^p|嗼_^ ܻ 7haa >Drr2  QYYp\u=?ۗW_ٳk.^ǷoIM?իƍ#Gxxxh쇹9K|߷x#&O?upO!|DGG#$$???nͣ-E|7oGw,͝;wٳg ž}8| ?c #ݻ3gšJmoʀ5k\WKο<"H$ڦkhh:%KMM? oggco-=sвBBtVk _}&MԢ}ݸq#~(gΜQ'b=?XxqЀ  772 7o֢͛zv՝?m"ؤO-[ĉ=zWu|cBF1b$ vލ4H$m___o߾pttIJeZ5+++lݺ .ĕ+WO1i$D"I-9'"4))++kxPVV???b?1_4GD}#ؤ$D"(\O!`>h}4Ciʕ+܋kzװ~z 11Q]wõ& ?!#''...߮h8~$\zD!By,_x+..3tj?<LƅSinҥ6lf͚.!<Ξ9'SN~0qD*ڵ ~! aee+V`-))A||<Ξ=  99 /SVVӧ> cǎqofVVVbܹDcc#^z%B;9Tشi'&@S*w+ڨFVV-9!BiO]r,HRΆb1Oosg掗gg˃)`޼yHLL헝 ۫틋 ~ 0BӠß8x ̙}MM Ъݻ hZH$H$L>uuuJ혙i싹9jjj !BH'|΢.@SvkkknMfg133T*GT cc6KIyruPg$Pg|΢.M= /`͚5Cqq16mڤGSY!JQZZvpuum9"B!S_xSOvMm6oތ X[[ &MٳrmdgN:v|'OVB!?t]UUDWB,wBȓ2=> o$z3u(--ŋ֮]8B!t!]2BOOYYY*C%&&ʪzE!BԡɯUnBgB:-{ B!O zK!^t#EO~ !BSKO~`v92dH/akku c8~$\]&Bj(RwŸ~:yvB&_lҥ6lf͚KKv!BHWҩO~°yf۷oD"AHH222ʽ0{l;^_v 3gĭ[ H HƻT8::{ł P__{_|9Y,.W^YYhXYY񨭭U?G\\>6moB!]\BDDD'''G͛7QRR۷:~С(,,Ć Bbƌ755ž}_ŋؿY}(..Fqq1Hp<22UUU(((@QQܹ+V(Q]],퇿?233yB!q%'|@^^^ɥ իWۼ;w`ʔ)ذa (**‘#G'"Ά 2;!BȓwHR=z{8p zw}LU#33(((P(Dee“ٶ 0eDGG㥗^⶗ƍmkhhP:޽{033؆9jjjv]{L!Ut7::!!!aʔ)[[@ hU|cػw/!0g54o*_ZΜ9mٲƎ;xqa޼yȑ# 9;;C*Tm;puumq!BW7&&=#F@"`HKKD"A@@zP <b_}իWFBBBFSSS|駘4iD" ۶mJKK1x`X[[Ww܉ 8"&Lо <<CMM .\޽{+yyyؾ}Byuu5v lA!B@N:Bzzzā8 IDATHX\zw7nDtt4\ {/ʳabb{{{̮F!B:GO~SRR`mm kkk}:lق'Nhz{Rjͅk%+۰a:|;v,W ... J4`6lzCCC 0ӧOWX6d~@CCCXZZӧOǹs1>>|ʱ{{{+엛G͚5Kj._w}.-"[+{&r@KrׯW(SGSNř3g3h *:t(̙3ׇ@ /en4Eo;vBCCajj SSSDFFrY… x饗ЫW/'Oƅ k^3g333K! رcPC]]n݊+k)ѻ ///ܹsjt ;wRAAA())AII 4ܱc1k,C__G]]===GԩS(//Gzz:Ο?c nj3C7o۷ݺußvɣmBHG뒓_BHM?֭0rH_ @qo{_l޼#ߦL&ݻFFF8q" >MF(b055EUUΞ= Xn sB__x׸'N˗+n߾ hllD||<0w\n tVVʉvK"a/{)x111~ܺu ~-IP(^t 'Okĸo߾GCCQPPF8q"?QFW qzt>*` Ŗ-[VVVxWomm늊 kЧO ZywQ.W__jXYY!5zKi> C]]e͟GTS]]ͫ?_~%4-P1sLD6X7mø>~> /ݻw/V~~>uD=ְᾗ U7ON$9M,-xߣ_umBHGj'B!On+|B3gP__-[񰱱Aqq1ڿq>wطoWb tMmg̘/B h|ZC޽[i"-9 xzzh1m4466Azz:KbJmX @ m󃛛zW cƌQYOzz:366VطnŋOo 7?f޽jǓfc_5-<<\cǎq޼K/1LOO9sIR6`ذRcbXÇ:c} ޽E]\Fe^AH ob.^ 0wEa bJ%˄" 2A!^ǃcs03s>u/|b۽z233DNYY W_ qKKKR|ꫯWs;@pppheDdӧ~РAjw[HH222AK.000@޽1{lZ aooٳgĉX~3j/y *7nXИ>H|0믿cB*bǎԩ 0{loۇ3f@&A*"((wSĔ)Spĉ,$;;;lڴ * :iS1i$:u AAAAqq1ݻ#<<\ܧOJDϭuc~;N\z}mھt8::ϟop/uL~`KDԆ޽[:K_""""0Q䗈: &DDDDap_vf_"PQQaQÙ_""""0[;"j~B:'ׯC!63DDDDa0%"""ڐ{!11{&&&Q_V$$$`޽;O:=x1c/_톇#::}H$H$077N8"}nj#駟rR kk -~™_VPYYo/PZZ z2777?wݻ!?ӧOǸq```Кhvuz*LMMOOOZroVX;w`ɒ%77oҥK4hP]-~j/浪 GP(0aر5j֮] Z˗/wAVcڵ5jJKKcL0 ѣGQUUh:~%=R(,,(/..Fhh(,,, Ξ= 333T<RǏvJ___$$$ݻaooT [[[l۶MksvvƲeuGooolذNBH:Es`DEE/_,~><==1ydo -ZqơW^0a%V䗨 'N^CϞ=/c˖-(**!Cj*\rgΜAtt4zF^3gʕ+Xj "lٲ/2z^{b JKKb >|X<88%%%FNNrJȑ#acc$~||wQ\.3_ >}:\\\PRR~(ol|@cDnn.v,=+q/Q;+Vʐ"ޔtIB"Z"""aP#A{>իWj*q󓨮^{">>x\P`ڴixk׮ 0ӦMqAPZZ*&Mb666H$ȀT*3&;;; : /ҤkP(]g.7SNIR`TӃ Z>k$022XZZB pm68"6k~ZP 7nDNN9y+/_ 111qV7n@LL п,_+,,,0o<9r999ظqֹRJJƐ{}}}^\Vq1={QQQYYYHNN'$$[nܤk4g|'oEEEHIIiR0b/iR5=z_ _P(```ѿq{;whI׮]ǏԩS-uW]PSլhP<|P8x*d2qD"܄?MH$bL&BCC >j^]:믂0j(aܸq˜1c4t޽{W5kЭ[7D0`jQPP ZT*[[[[Ν; &&&u&/ Ʉ3g9Ԭ ƍ?QMXrec}sssDظqxʕ+!C!C~~~”)S5 .\F% ^^^|rК5D Bk%t6m… ykݻwF |||'sUm0yd7K,iPꕞGGG8881\K:w ???={]vE޽qe?~k֬i_6AAA jPEee%R)6n{{1%"z߿ "j6큈: &DDDDa0%"""/k/^tT 777;w JR|^}>S B|8p`u5ox#vǘ4iN{GǏ67oҥK4hP{ł ///n׏8KA#..N۷1i$d2t(//ר{nC*۶mrss1qD o&r,ZƍC^0aTTT\^˖-kҘ󑝝HrHR`ĈbbrF;Xr%|||`gg={… ]vAR Z1l޼ G],oqT*̛7׮]JJ֭[s~O3|}aKY(((@aa!bbb'bܹذaq؈fB׮]QXX~ z`zz:\ڵIp5ilݻwJBdd$oi FII <\R޶m۰zj\v ޽;KZ:c033þ}p=ؿFא2ݽ:-}> }> Z-[jjso=Dԑ1%p]jH$X,裏 ///Ə:9;|0?sP*3g>s>```CCC8;;M*:gcǎAP`̙ Daa! ''ɈR)~077N1)J"##CsKKK2\>iǧV"jc֭uD,^X| Xt)___tÇ6rssѧO:/** ) 1QzzzlRٳ'6mڄׯŋǢET ů̙S*OJJرcT*Rq}R$Ojik|DD7v,44ӦM1dx;T'uoaaD|XYYЭ[ӧf͚%`ccD Hmh7ܽ{SNg}H$,^XkMsMۂ hcjj {{{?~Z4!H B>_D|/Q;&JT*T*all \.ׯoJJ _3h;wۘ3g>|7gRRRT'g/2֮]͛ضmLc1b4霪*DGGŋDff&0|pճވgW,1WTTÆ nR(000cǴ׿bݺu8x *++q5hiQ nBQQQ=?_Dq0% fΜ ''';v,0l0O,?~<6oތHHR8::"''G,Caa!uÇcʔ)Ⲃ攕۷o7N:􄑑T*řnGUU T'ƍ: /@RaϞ=غu+T*^y@= x1|Zmaݺu2e RF|ӧO|7x&&&pss͛7]>0zhP*u~O]M6a…к㙨DUzz:Ϸv8Dp旈: &DDDDa0%"""/u痨z=iڛWvDq旈: C:ڡQę_""""0Q䗈: &DBťѺ3f˟Z###zj&""zJL~_~%pYHVٲewHDDD5u,!!9e| IDATr$ K.aѐJ@aaaklܸaaau) b۶mMQ0%ˑ ooox{{#!!A<00nnn(--Ŋ+p&@ii)Yo^^^HJJjAՃup_5LMM1bܿk֬AII ͑#%%zzzxnc5Ν;cccckg~ڱXt ݺuCbb"/^,PGF.]W_N"ZlGkA&59g3QC8KԎ0|xzzbڴikkkp""zp旨JP*P*066\.oQPPO vj;vLl(++C~~~^xCm+@DD/Q1sL899iKHH7c'O:tg|'prrBxx8FFc`eeggg=z9?teDֱ[j311{ƩSm-'|ӧk %%֭k""g_"]#???VR QG™_"]t iiiH$ZehaKDkkS(s$DDQquL~`KDDDD_"§~ {{{BP`ڵb ZK"((1c@"@"駟ꅇ#::bqwwGjjH^&"/Qw^,X?Q^^m9[oP(AUUF=uVٲew>uDD60% ]j^uL4 zիW1j(t&L@EEXV\\PXXX@.#<<8ݰT*-mVgpss믿qSNׇB@dd$rssqm]R닄6]\\h"7zҊƍVg\ MDD&DXYY`$$&&";;/_Ʈ]IJ` ;;999ʕ+T̝;6l@yy9Μ9>/ׯ3JC.@ZF```񧧧#99ՈNYo^^^HJJL~:RL&kެYP(`cc\|d RQQQؿxG}Pq9Ə~AA^z%=V?&&055Yƹs`ll [[zptttDD|aKԎƢ[n֭xb}ff$p TVVrssPTPT3g}loŀW_X|8wn߾L+]cbb"QTTQ^^6o""j[7v,44ӦMX[[C__8~8<<<ܾ $ 222 J묣P(`;~~~طo?\\*ںJ&:eee022aADD-3DT*RR1r^__n:g{S888 ֹx";FEDD-/Q1sL899i>}:>011n޼s񨪪B߾}!J1qDܸqC,?~<6oތHHR8::"''ζ,,,c8N PTسgn JeVVVpvvѣGsDD vM6a…gǨH$n;wbΝ5JJJR@&mptt"3DD޲-??֯_0&DDox#"B4"""`aa QQscKDjoVB#!"eDDDDa0%"""/uL~:LH$H$ 'NhRJu(`O}~ wwwjի}""z1%"\z/_ƤI_~Eso޼ NGVٲewBdDDԖ0% Wg>w(x{{cÆ bYqq1BCCaaa\pܿ_~:… PT7o]JJ[67",, \]]m6&"/iprrxCpp0JJJaʕ:ZƖ-[`ggZ Zs6Rӳ8sDD>1%" ()) 99111033T*ETT߯S[Ν1lmmmѱΧQ‡\cXb )))o׮]PTEBBV۷1i$d2t(//\p* õkנRRuVv6n܈:cP(puuŶmۚ?QmL~A!!!P |͚5GAA ===VeAVCVcܹm"-- 兤uHL~ݻPՐH$puuq8wakk[oGG:FDDL~ڱXt ݺuCbb"/^,l>.]Ç]v&QTTL`sss7m""xQ; ___iӦL #88X#H Bm5OYYZ;""jcRR`ll \RU 8fddS)))8p lmmabbܹF+++ܺu EEEjeeeϯ7)x"SLDDDb̙prr,..?7o PTسgn JW^yE<7==cǎ @i?zhP*5ʭ쌣G#G0~>@"4]$96m… D .1hv܉;w"99Y* u\ՖGGG8881%6!88u~z1%"g5D&t iiiumu&""j*&Dfy\PΑQ{eDDDDa0%"""/uL~:/ ]tT*Ν;b͘1˗/ߏ3&&&pwwO?u^xx8owwwjիm""z>0%?~I&aСutR<~w㭷Bee%233P(リ*:!!!ZhDTVVKlٲeݻw}""z>0% q,??و\.T*F!)..Fhh(,,, (d,ZƍC^0aTTT.]ѣ!JB:u}}}( DFF"77oڵ * HHH:klܸaaau^BWWWl۶HDD=&DXݡR4oZuQRRl //+WԨddff"77vRX7JC.@ZF```5?"-- 兤zˉ}`KԁرcP(9s&r9ٜ$''#&&fffJ5  ˗/233qy<<<GLL ajj">>:k;wư GG:.GDDC.b!ܹso>>}3^+Vʐ"4vI={ĦMYYY={6-Zݻw#77-Z\011_顲PXXDkkkGZ/X066L&k8FQQQ횛>ac "F򛟟g϶V,wajj'|%44ӦMPw|}YvZ $ 222 Jܿ%A@ii|kՓJ-zd2SVV###&DD\7»{B.$%%im-RTLZ!ˡT**/AXXׯ#..Çވºu ɐ+W`ogg{{{ڵ .ZƱc0jԨp=PVVz/bСk\DD39r$F{B.vu9s&qSN}6<==L &_G߾}QQQR_]~كٳgcdM^@^^ЩS'۷ @JJmXYYGsz""zՙQuL"?Orl߾'Oj߼yԩSw5?`ycXh>:ߒ`ݺu EDD?@DBpp0a XBDDg~Cԩ H"""`aa Q/uuW(s$DDZܲ-[@"@"h81n8l֭eH$(;;K,!C`jj CCCs tҥbڵ+0g8qB+'UOF:Qŋ-z]:6k'=Ҹi__PXg 2.\rTVV";;۷o덥1f̘Frߺu+=z$/++k"v֍=kB[D?ѱQC.NA/wXv?ԓ'Obƌ@bb"N>c֬Ynh۷oǫ*߰a֯__o8p~ǎ:͸ܹSا~*9K}jS{CDDDϮ%/'A|_Խ+իW'NjϐogagΜ`prrԩS}zge &`֬YطocjyJPH5)Ϯ]T? ((pu;vٯKPP8Cr1C&aĈ{ 000@ΝR7oj]Cfff033CppSRΝ;@( !!!zj15D?s#IDmPK~:ppp8VWWVVoV<^jژŋgϞ$͛7GťK_ 'O 1sLps\'` AM>}4{FFF~:mۆ1c~-???|066FYY퍇61zhݻw܁% 駟bԨQHOOy ]f?␐q.سgsΘ>|8N>k׮3g8Ϟ=#G՞ٳ'PUU%`>Ǐ?kkիnܸNj/+WY?99#F :T\sN7uyғbA/=ɺuVZ@|<=zRtuDDD69 h&l6l̞={W^xeg8MMM.짴Tx>f/L_|zzzz7o>""vk5?~~~HJJ™3gYfc_zĎ;0m4DEEe+WD.]}ܹ2e mKCqiDs\$0|p rJdffڵkxw5u;|p>s>|X,/((|%K<&"""mrU}jkHUmCxx8߿h ToV;v`ǎZǗ,Yheee%mȾ}%Z fjj*TGDD4ui r9֭[s_~dž ӧ7>l???ܿ^^^q;{/Z[[O>Ay7YCBBH 4]tzٳgcժUƠ\{{{̞='NhMU{ChhVqУGۗeee5uL&Crr2\\\~ ͛=w߾}1cd2R)pС:$XI&ԩS q= <%"""jmaf,[ K.Żۚqs"((Hz*,ugIII>pX_/Ξ=IZ֜;w˖-C>}₍7֭[ӧG!113f̀M?>sIɯ?48p B߾}1p@055EddXg0`LLL`mmh͛7w^L6M\.Gxx8߿/߾}&ML&C׮]1Xyc|2F T OOOL<˗/9>HHH@@@F/BYo]~p y+K.EEEⱟ~ &&&S ]]{zzbʕgk_ .;Çc͚5PP*x7p)\v }:\/طo^}UtW_Ç:Yf~+++q.]–-[PUU033þ}p=ؿ2]]ryyyOc`` 駟"((H,88%%%FNNrJ|͚5GAA ===ₒ8tP+//Gjj*퍄5oGqMjۍP=k?bٳG#@UUV^aÆaȐ!xqet .quY-+++,\~-_eeeعs'|||P(TkMDDDkz CCCaѣ$ JJJ`aa`R/222OIIS^x { 99/_(DDD'wޅZFvc ?++ ?3<}}}a̘1M믿)Fc͚5())3ǯK ,, ݻwDDDwoٳ  wk@.|w]v(J,YK,Avv6vލ={_~?`ii]B*6Q}.] 5bŊhɓ'@\__ⱚfdggC__h4{H,ccO΢-]o|}}QPP|044ԩ@"aMM/!!H$=z4t邯 3fxu鿱c;w ,Yʂ^~eoP5mۆd̘1Ckz9s@&usN+QCY"744S\3kmm {.N> >>>H$Xx֍;5w ">k?h$e666H$ȨwL&y8Pyc㷴 (((5vvv:|רe b,r}#c_&aʔ)عs'gD_C7ex71rHLӧOG@@uǟ9ك#G xΝ1vX_FΝun. .ӧ[; "h+J􍍍!ˡT*u6l$ 󑐐kS(000c^g[#F_gϞFTT֭[L,\rE1%%ԮIFC卍CڵkgΜ'ĥ7(((9ٳsAEE)~].oߎ3g6mԩS5Ϛ5 ,@aa!~'*ЌtHLLӧ{n|gիzj < €/++_|={СC n;wĉ___.sf5rH&DԮ5醷3gɩI׿_|Nj/U ֭Ô)S Jřڲ4fK||<зo_HRL87n1vXaذa N庌?>>iiix뭷+hl6_BB&O s.G/ƌ=z@RKij4Dee%ѷo_`llsM? ,Yaܸq~-"""`iiY/^sgǏW^||fKDDTB-@Xt@gҤI[;6EmWaҤIy葐"̙3GN: BllPPPq"""BТֈZSxEGwYdggjږ4ceOӃ7>c/DHHLMM1zh{qęb"""zvͲ=lR7n}kf899!++ 7oXߘΝ;>>>x1:uIZ 6ߟi6̙3uMėe_Z""""04f~Ϟ= qu@D.\hHIoYYp"z:k ȑ#///s-)) +W[;"""zFkjj : 9rdDiyDDDv7""""0Q*3|.o [}wwwjի["4"""v3ϑDTVVe˖!&&wmȈMJ~u|0`LLL`mmh]Rӳ兤&];"""D* _+J"###%%zzzXr]|G XYYaqa;w;wRĜ9scb0008;;2չs`ll [[z8::DDDDmI3֭uD,^X| zScBTBR!..>B"ZlGu>}YVTTA4W(ZK+jɪJohŧ"d똛\o""""FP;wΝ'yJݻw1uTɁZFhh(AXZZBm+ׅB@vvveH$ //?{ԕ7@8PDEk ȀEԢ#x*C;e:UGV;V;-:| j:Ӫa)`Qr((k-J>{Fvv:kLkOSJB:r066p5B!W @,C, {>V( =M2;wuu';ϟ׺\ ,7%бkĉ}v<~@ttN}޽{hlltԾr9'7nG"BLtzm޼ysrr¶m`,^s9III*['k*$""{EJJ QUUŖgffvvv3f l2GLL !k}}%{{{ܹs]xYB! &n8,[ &M°a0uTj޽{1b+V@[[ViӾҮ]E"8M!2ȴٳg#002 ׯǙ3g8J@UUjkkaQPYY}… {ӧOc…҂˗/ё-?>lllЀk׮!;;6[n:~KKK=z>ĕ+WI^O@&aaa]އpj}0 s'nBQQVZCCC" -B^^`ii @Tdggkվ'|v:="""rgΜU`bbX ૯###Z///󣣢PRRU|*,,\\\<[rB!9X~=D/??+V\x xppp`899kjj'Ofu+S[a8/T~ P(?77[nEEE|>U|jllP(+++ɓ'066~!BAc򛐐(ŋYfB0dlwttCII A(a.Q9mD"TTT-C]]өojj̙3_bƌxX|9;gXS|W ݶ!ajjJ/!B,b1b1`mm;puu';ϟg;;;cHMML&ܹsyyy~D"8uuooo{wg8x  L8۷oǏQ]] DGGk~w񷶶BP`l}1Ծr9꺌ƍ!BΛ7*Ǐ9III;v,hooM~s%v؁h޽[;b1>cw'""{EJJ QUUŖgffvvv3f l2.~'''l۶ ŋS|Бܹs]xYB!+L'[la0Wfg1SLQ[BxN8`OP_- 0{ as.]9g}3~x}*--wH=nݼyΟ{yBC!ߢ +>>uuujWtعs'!%ڿ?gj\>L oƅ 8Boq.w虁 Е [[[=DE~jjjX~KdeezPkk+!ʠݸqc?)URH$UHH$ fϞ=2u 1{l7x7oW^^X :|>@\\jkku>o###@" 11*qfff' ǩ5)).633Ü9swUyX.Ʋ0773>C#nnn011._zo&`iix]l2Wr<==0d5|( 9r`bbHaƌ Jqq=56m///899б9{̚5 #mڴK.~7?:҂Caɒ%?~o &&/TWW# dǏBPI|`͚5HKKCSS"#D!-ZľONNS0edף1.\Zl޼mK\&={.w8;ok~ . ^^^Xb{ɓڙ2А}Sirm"O={>;3msOt¥SLeee@ @hh_D"ATTּ@\ӧC(hiia݋#FXbw}wwwXXX >>?܌IIIxQ/ @ ܼy LLL0uT(BMM M cʕhooWc׮]HLLTH$B@@J… ]7f8::+9s-G}~P(DHH-Z]Jj{^?njaÆ5vgocs;^fj+{,..N垝:u-{K ]AZs߳g{WPtCgk֬ԻtRKu?:[{.v!=[nV!BCCShjjBee%x<8v @ݨ(pHLLСC! >yIUU򐖖KKK";;'HHH`#۳l(͟?" e8s VZb,X@ aff.cV{!q%̙3hnnÇ1j($%%dFFFXt)^y;v,?#55UB!0n8… 9r$ݫ67Ȁ퍜v+ǃNyCBBJ&Mb~{Kѣ;w.B!̙SNi[o+򂟟жo}SBΙ߿/-ckk3}M… dsiW*2˗/g+++fѢE_ qPPĸ05-ǎ}ήBvݏ>7Xx10k,x<L&cwknn)W>q t$"$$hjj̙3_bƌxX|9gN,vy!cT -0 g PGGGx<@ E"***tnX㡮Z=Bƭar9LMMi6Bzݻwsss 22haa]v9ѣXv-*++0 `_|߄4N{0335얛*ljw0 c F&;pٳ3g䔋D"ɓ'#552 pi6,Xo466"??_-,,0qDl߾?Fuu52229 r\e!nܸ%<GGG׿O? K,o;V! R"x8qg_j߄t;o<?r<___$%%r?~<$ 9C"`ʔ)lyqq1^}UXZZbј={6$mۆPcVa„  $ ֯_)Ď; @4𴷷 ӦMSY;GDD`޽HII@ 7gM233;;;3Xl{{{uC={Ve1!g9::"''P(xn޼={`oHJJ0x {x{{ !>lݺk֬իe}EAVVԎZKRH$@,!#77>}:N8p\)FU\\ oooxyyH/&wǣN;wDbb"%Bze{_7vY FpSJNNVS!B`Bɯ%"#!B(% u@Br B*K!B % Z큐\_w([f7''C+sźuLbݍO>}ZxJJ 6oެSB!MI~I﩮FAA?Bp5k -- MMM!!BH)EffrC  ,, f$$$HJJ£GH$Xhn߾ DDt!_|<==!₌ l޽1b+VS?,, 6l3 ggg ECCq`ȑpssV\ `ڴi=V\v\S_]ڵ jD"8B!d鑑_ƍT*ŦMp)Ny||---|2rKKK=z>ĕ+W6͛qmcСٳg#002 ׯǙ3gt? @kk+ʰo>6?>lllЀk׮!;;l}Mww%PPP. GnnNB!'ϝ޹sׯ_ڵk yyyHKK%RSS]>$$$_#""-QQQ())Qi'..= uj*"44:7|pprrǃT*\.Ǚ3gj*@,cꫯ@c=u aff.V{!B@qt_ ˑ+V.^x͈S9O"S^{I |>|>=\a8D"vj{666B(v{ZZZ\h_B%k߄DEE/^0̚5 A}}=߇+<%%%]lYD"TTT-kjj̙3_bƌxX|9Z[[sC 0dlrLq0C]]{jkkaggU_eS(B.w\.)%B4N{0335>>>ؾ};>}K.c;;;cHMML&1U"//ӏ=ݻF/b8x V( =M2;uۮġC81v'NcTWW###ZD0222v///ru7\Ka~觏%:7o|}}U>|ƻヒ)SpF>vA `ڴi8m#&&㏵+""{EJJ QUUcm`,^\69III;vN5DCC0fDGGcٲeZĎ; @ݻwssuٳg9s !B0\[b͚5Xz5l̍FFFbҤIx{$H72j R%%%znEdd$O'N;>y !o)wxbG !\=իW9?vҿǣN;wDbb"%B4>𦍊 DEEAP@ `׮]쉦q.#<. PPPd!*2$%% iii=^?''WFYYY/ֆw;ק[Ne"%%XvmM!A$퉦Tt\=>]YYY=.82ϵWWW #Gi^z K,M@!]L{ mtEC Hv5qaٲe4i Sr+쮾6[k_-һw}]EG,Ą󇵶[ڑ߭[} !W^wo7xIII]S\\3g΀aСCWnݴM+HBuu5^}U"99G{1c F<022BEE 0{llذӟs[2pB`8!?}wmjkk1k,۷F-aii HMMErr2'vux܄2p* \SSk׮_o,MEHwssӪsss! EmH fBBB^{5[N}Bd3vvvömrm0- !D;IU__ÇASSΝ; &;|+W ;;gO> >@KK ._ GGGv1qDbΝb֭[Cز v ¥KTtxHRTTT ذa[>{lB&a8s 믿qA*bӦM8uNkP(PZZ cTuV|ףiii044!PPP.GnnqBȳeWF@///gPRR–'HHH`.GDDzL0G\\`ll ???r9Μ9UVb ,W_}N0۶mC[[^U[UU<ܺu EEEXj ;wpu]|>~5Çx<RV妦hjjBee%x<`ff飰fffpqq2oooBHO;U__4Gzj>x_X~=D3??+V\xnnn֭[QQQ>fűmϯ~[ĉxamm)777g_BP-flH$ba͐J駟piÆ x555ɓ'ǔ#ɉ}]__C:Ծ6|>|>=z|xwzᣏ>lبvά҂'OІ^Co?}vu~~>󟩝M6QKDEE/^0̚5 &̜9_~%f̘˗sD"TTTtOLL =$&&#ڂ㡮:xzzÇO'OoQsUPRR@!C0 d2677׳ݿZ<4mQ, ؽ{7P >>=W(B.w۞\.)%^Eo?3l0lݺ/"''ƍCtttm=x="!:lRfffkkkb Gfcǎq5,XfBLL &MF\rSLaQH~'AzzVYXX`ĉؾ};>o[Əm۶ԩShkkÇ~krD022wvvɓ;v@(Ν;ҥKQYYW^۷oǟ'\|};5BS}!??pqqaGMLL8xyyA.3ٍ7B7J~˗w[g`̘1GQ3͛_WcNNNضmBCCkkksΉ޽{wB(v9~)f̘ x{{k+33 .LMMobٲelyPPJKK1j(c͚5*#رcP(yfvEÇwށZ[[!9Q9ropB;Çkkkcʔ)eMb,\Rfffx79~~~8w^u={vBc4}E={`ҥHNNfF'??4i@,婔IRH$p )..7PTTphB^y.//?Ew񨫫SΝ;H/!ѴB***B@]vSݭy(@6ɰX!%"66jQN_R{\$q$=B!AjϞ=B!dwQ>%ͶB!fĉն Tsźu>_,ATɁ3/BBBpi)))ؼy""fȐ!~C?Bv5k -- MMMzBJ~ !}&66*@P444pʛ[[[X[[#)) =ҺꗖB"`ѢE}6$ $ c߿ӧOP( cyȑpssV\ vw8M;۵k  &_"J~ !z7{lB&a8s <>>RBmm-6lؠu5j*++o>XpauV|ףiii044d JKKaaaVa߾}hooת>d2 ,,8Ñu܄KD/!Dnݺ"Z  E@@[^UU<֪}}SSS455<033cˇcccNNNxJZBť8F! mrAUX~=@.#??+V\x xppp`899kjj'Of)GDzjB}}=GQ\>>kkkӪ>466B(vZZZN]BL(%DEE/^0̚5 L&cf#x<JJJ G0Ut0qmwG(b:FCCCqkae< ݶ'ajjJ/!dPi^% !affkkk=χ+<==q!@ee%Ο?wvvɓ Ls浒H$.q=466>SݻsssI\.G]]]ܸq>>>ZKC| IDAT!DB̼yrȑ#8x |}}cr>vA `ڴi8XZZbǎ@ `GJC,?֩~wꫯGٳӣsuٳguKc}G?oّ2BHBVVQiT Db=DGBqq1兢"}CHD# :+:ܹB=zBƍ,swwP[O200@AAmakkgBHE/!<_}R{\$q$?ѴB!2hPK!B J~ !BȠA/!ߙ;w.֭[gbvק>}ZxJJ 6oެ!gQKQPP0P(W)[f Ԥ!PK3T9^VVS^SSiӦXr%Æ ÷~+tRql2L4 Æ ԩSڪu{ň#`nnXmmmZ__wrrr0rH+WRRR*c A@@ZZZTڵk! c'_B͞=dX~=Μ9)?>lllЀk׮!;; \tIK.!((}_\\<ܺu 555:]@GÇqdggcZ_& @kk+ʰo>6TuV|ףiii044!PPP.GnnNBHC/!Dnݺ"Z  E@@[.qZ &&&X`+w„ ضmpUN###Zŧcg5///󇣢PRRic066+<<<TU)PYY q(,,\\\[q2PKU鰳?˗oݺx<888u׍`S.ةW*⧟~ӧQ\\ ;;; 6cnnξ644B*~M@nn.^}UbH$dffɓ'1 MWƘ1c,^u n㰲BKKJ]BH(%aaaH$2dL&c477mmmPWW>ħ~8<}| gWʕSSfΜ+W HHH`t}vU. {nqQιBrr9LMMall|BQKUbbffff|ӓ[YYϳ-,,0qDl߾?Fuu5222 000m6L6 uN~E"8}kkk+ F&ǎckw ///rNs7n܀OJ!}_BH7o|}}U9r/0vXNyff&`gg1c ::˖-c˃1j(L>9Ď; @ݻwkտmۆPcz[qq1^}UXZZbј={6bbb8Ν벝g"""%^c4}oK233?Y]hMMM p777|BVVTʤR)$ JJJ Fqq1兢"}CHWuuuz𚚚hg@ҵxʕ+xW8e;wDbb"%OoɯRxx8l٢0/\qq1~; BTlܸ2wwwY,(((Psrr2lmm,B-zO~ǎCOWD} !zB! B!A_B!2hPKEɁ($$OV9͛7!"B\eo>xMgEE~mK1~_ar {իy<acc///,X}J?zsNZZkw x<ѸtRy]v 7nƍ9?3g{?nݺpQqq( Yiii!!~;'mmmg_d2?бM[vKQZZ( TTTXn]( 477@PPyn߿?r}}6.\9M=x)~GGPP󵪧k׮aӦMشiSK~IL@ @XX"##9.[[[X[[#)) =B"`ѢE}6$ $ݹ>OP@Ljȑ#\HIIѪ~gvBbbD"u܄Bt/QF@G)7>|Ȟ91p>} cTŋ;w.Z[[qq_Bvv6ϟ2X qk>;w2?ShuT}[oo1yd$\Rc{B7nR)6mڄSNq!JQQQ*bÆ :~_TVVb߾}puuEee%*++pBߺu+|>Ѐ4 @kk+ʰo>kUaaa]\&~x<?@Lje;e͛9ewQy!''̙3q.GW_|EL:ѣGe8D")ʡCfff3gݻ8|u0zhsFt#66B/2{˗kaС022=fΜ˗/7P$''vUJJJoư9׬K{ds_kׂ#00AAAlyUU򐖖KKK";;b055ESS*++333|066+<<<TU}(,,\\\ۛQ!^/_@4xyyqK~r9[rBV1-_:Fq%-ùsPVV?Q_w#7odo޼ylau#JviCvv6<%%%씆*TUUqxNP(ݻQ^^˗/믿ѣG5SM}Pq\.)HH!&~~~022Б /1cư_~w~:~'A[L;vsLB#GLLL ###̘1 J_1c $$'ihh?Oj߲e &L!CW^kP[[  ̙3all3gbĉ:F 5^.m]v :Slذx0uTL8Gc## !affkkk=χ+|||}v<}.]|ɓ'#552 @TSmCcc1ݻsss{r9lƍ)vB!ɯ;zm|W:b@z*Ξ=bG+Ba !-Z.Քm .;;y$`cc9{uT)GӕkS^yyVn o3~nȑ###9ukkk8p۶ ^VX+V-Ftt4KuWe˖xUʤR)c]'~; pGq<)>(˔S":{T<8vϟ7|Sm7ojN~c$5-Q@[m,ڑA] ,vjgVGݱ,`VPR;# Ϡ@5@`5%@@~s~}>Ͻ~~bŊƶn݊{SSSe˖{^ں{4b(͜9u}?kjjZ멧_~%lF^"ZٯJ˗駟SThnn6ݻDD4l{@{D/** yyy¦gΜ1ڃu)oW_}ꀹKRa@N:%lIr>nIIo~~]dKb`Fbb!X~߿u$E9ěo7|S8w(ٯzš {Epp6^AAAPTC29F2AYn|||F,""GeUx \z?~^@/Z gƾ}PRRo]]]xOOwww <<)))CRFMJJ3pBƍυg1.]?O///̝;YYY ;#l\vjzQ^^_W(++Css3Fӧ K?,G+!! {zwq1L6™9&AzHBff&vaȁTUU!44!!!4/ %""rl%""">DDD7""""r ,~a<ի?59sSNz磤tl߾}8R#"""V|KٳBLLL,ڵ VȌl񛐐>srr0e;~~~ظq#ZDFFB"@TB]?ظ9N8`H$L4 Xcc#,Y777HRlڴ ===ٳ~z,\'NŋB/+W@P@Pf@޽{le2lwww:u oFyy9 D̝;:[lAiiسgqE /hp%F1PTT:466رcӧCVСC ZZFjj?P~N2DGGbbbPXXhѳ#"""6muT*\.G\\uuuDqq1T*%?ظ99$a9T*Ell,{(--EEEF \kذacʕpvvDDDCrg bҤIjskcS8{,6l ^WW(,,ij> \B<ܽ{h ' ~lx'L`0ŗd}V;V ]]]f?ZZZ19hoo7T񛔔 TTT ::/ Z[[|rlڴ PHJJ`7:N&<ظ9d2M@$Y8___D=o,0)zcƌ>ADDDdlH$puuX,FGG&~p}`` 5j.\0{k׮ѣG%-..pu ,Ν;ىׯ#77!Jq oBBB덊o0c lߚ5knt, oJ%͛4̛7蜓'OѣGJJJO'6>X ==hhhh닙3g">>ׯhH,[ AAA8x?X~%Js,^KLDDDdm"@FBff&vahGQQQ1V Bjr+dg*"$$Nf;d]* (//3{n$''MKDDD:N۶mw,((hGݨQPVVHglݺuBVDDDD}*~2l3!"""\@DDDD/9 DDDD0]z5^uϗ3"Μ9S>6())s<==۷oԈ}kׯi8gϢ 111}Ʋk.Z!3"""zl񛐐>kkk DR Fc4ֆ$ )))st}MM  ^~e\r  ĉD"I+566bɒ%pssT*ŦM#Ϟ=ׯ… 1qD,^f矓)S`رƍmV~w^$''L(&""" 6['11s΅NÖ-[PZZj4RjQ__455a֭fӧCVСC ZZFjj_RRTٳx"^z h4t pUUU(**B]]q1wwwǩSpmx(?sNCYY}111(,,4îߺ:TVVbprrRDTT0Ѐ"ڵ H$@AAY}=9rIIIrTX^Gii)6oތѣGC.cڵ8}Q+W...˗͞_R!$$qqq6+?sUTT&MP_#"""z6`˖-zظq#/FH$pM@@scc#`ѢE± ƈ09`t2ҏc ?;99 zbaʕfgxxx x'q]<|DDDDlMJJB\\ -- Xb;4 t:xjkkH$Buu5$I`0v}z縹D&䘏D"!JMMMhoxd IDATmm`ҥDذafxz~zcƌaKDDDCʦ=H$rrK],#00VZ .O0-BFFt:ի(**2G&ZKJqZZZ]G ^ `ΝבxGG&~ WHHz=o3,/"""T{5k <<'OѣGJJ f͚e4~q`ɐH$Xd ]ft;~mC"`߾}]e˖!((r"''H$ ECC0F___̜9X~? oJ%͛4̛7Ϣ{>8""ϟ7sYh0"@FBff&vaȊ󑟟o+jP(P]] \>$UUU!44!!!DDDdlK6Jf;:޽CVu۶mw,((*j䒱QFg׭[+dEDDD:#hyzz<.F8"""r\@DDDD/9 DDDD0]z5^>SRRO#~rkthXGIIIؾ}ADDD4VZeO^~6mڈ=Ξ=.“O>W^yVȎ4&$$P"22JhرcP(i^ /ވB{{0)S`رƍm>CPPܠR?}_mmmHJJ;w 'N@pp0$ &M\ox1zh,^Xcc#,Y777HRlڴ ===}ػw/M(g^""""k?;w.t:lقRUVAV#11Ÿy&4 v'''aN۷Q^^>|(駟Px7_RAբ hjj֭[bϞ=hooŋogϞE}}=._cǎ c/hp%Ft:!::>>Dhnn Fyxx@Kc̘1,|ȦԲD\\WWWxyy b"88XXVq(..Ʒ~ ;v,`tuu!,,L(M4__y&{;X|ѸL&s&LE!##:pU9ׯ]G ^جnnnX`v܉N\~7:/$$zo0c %""")6Uo͚5sɓ8z(ÑYfϙ3 'OÇP( UUUxg0$&&bٲe-x뭷T*1o66999HOOD"Ahh(̎F___̜9X~9R8|qΝ;g1 ggg#++ رc5R|]kV Bjr+dWUUBCCJkCDDDVb_m* &wtؽ{7m%"""eS/9m۶;J5r bԨQ(++3u Y ů Eov?L6™a%""""◈CgΜԩSYV^_n3|9۷['9su zٳgՅ>cYYYصkZ[[!C"""rd6[&$$ //˗/#22O~bԙlkkCRR|||兔ܹsPSSB_~W\BBÇ-ĉD"I+`ʔ);v,qFtww][bҥ Ą PSSOTBјי3g0m4L<SNŦMtF,YnnnJشizzzZb޽HNN69&e\ٳgC7G}d4RjQ__455a֭ӧCVСC ZZFjj󗔔 55{A{{;.^aN۷Q^^unn.oߎ+W Ǐ$&&bܹtزe JKK-z>]]]:::P[[C K/oooh4\t 8pp`|ͥPVV~ωAaaEqcWիW___!1w\<3xCCk.C" ##CÑ#G$^*"66VWT pok\\Yrp'|||PWWJl޼NNNP*(... ԩSHV ^Rl޼G\.ڵkqitzpuuŤI='44㈈M}زe @ף7n|h4DBύE PhllDDDDㅅF}}=b1ڰr>)>zO8`Q~NNNX X,^`0/Ʉ ?TϷvܽ{...ADDDdMIII!::+Va7oB*;!P]] D0a-Z t:J#Jq XڵkqQ|u1BXXPd~f Dpp0;Pոp9 ,Ν;ىׯ#77fodpvv7z4777|3f<M[f ?~8?o]<~8zzz0ydH$,Y׮]3e˖!((r4;X ==hhhpo[oRy!-- ͳO̟?i%WWWL>iXj#??/!"""+|dggC"))کv~V:;;%""";vd߾}B ~~~(//Gaa3#""",~ľ}jpB^/%j駟gAGGx 477s/w}ye28S]^Y_gۋ_""""˰kb|ڰ%"""2;6ʜo/vί2ۋ_""""k,bhp K%""";6F 0w\_^w߾}/ ,~m/Klݺ<ӸxdDDDD[;26~xdgg;^QQ'N ,, /‱ZZZ3)-v~{gUVARرcNȮ7;{iv)v&66SLvDDDDvůƬY]bKDDDD/9 DDDD0X`KDDDD/9 DDDD0X`KDDDD/9 DDDD0X`KDDDD/9 DDDD0X`KDDDD/9 `0 W<pwXhmm?oook9q&Ol4h3xss3¡ףil<}DDDD0>s(Jܽ{W8ގ K,b;nݺ(,,ϥKo=r[o _?G^^V^ D2s~^ٳgqY<| -ZƦMΑW\ 333g֬YwصkWk***LaժUftjpၧz _ K.ҥKx1w\GB@rr2_nѽ!,, ÇUUUBq;XpEg? T˗ŋ9gFjju넸EEEU]] www̙3.\xxDDDD_\hiiRįkO;wL??ᅬ[naܸqh4xO+W= C/r3~-rss3~xnH$2+FTTN>7on‡~(={,G駟F~~>_.z|܈ak'|W_}>,j*444vww#55wE@@p c=/L͛78@ӡmmm2oO6ޚQQQ . --M81c cww7^ytww DAAFe4_uy  Zl۶ }6?b#"""*vs=/عs's{=FMM ƌ7np"bٱc{9qgAaa!}]455ݻhnn/_l7o~cNNNկ~eMMMR˗/_ K.`0K}97oꫯ>p.3gĉ'O|=w}py?=pmMMz[Kb=SB7/Ė-[hQTTRs#""" IJ_ɱ8b1:e˖Ν;g?1{lxyy .nܸ[n8ݍD#"" `0~߿!H,u?Egg'|M¹GttE񈈈w~w܉M6aΜ9񁓓{1̘1ѧ //ċ/ۘ>}:RRRၢ"̞=NNNiӦ!''ao-]9/_.|yܸqg}K wށBQ}aIXz5r9b1ƎpXh( mZLرc!BeePѣ;DDDDDbKDDDD/9 DDDD0X`KDDDD/9awŊ8uO٢~?/!""0ߎϽv DDD4L5k/{'Nr&DDD4\ussp5kpNEBBBPUUѣG[;"""&|ፈ_""""r,~a%""""}~?KDD>D?}~]>DDD>Kq_""G_x#""""◈gΜԩSƀRRRqFc?m$zjr~~~DQRRxzz:o񉈈Ⱦ GݪUbm݃Q[[iӦ=gϞEWWbbbeee'+ooU; 3:VSSB_~W\BBÇ΋֭[tRb„ `ʔ);v,qFtww Ξ=ׯ… 1qD,^w}^xFTTۅcǎAP ..~t=466bɒ%pssT*ŦMcv1'`W[[HH$(Jh4c@[[///Ν;}ۻw/MƐdBnnEs}eӧOZơCZ Z>br 0~x;N:۷o}窪*8v0 X7oB`׮]prrWZZॗ^74 .]8pbN~?/11s΅NÖ-[PZZjvlPTjGCCuVst:oZ477+~-rJ<==^q{\.#..}uvv """peal̘1hmmZH$BTT\]]Gz=JKKyf=rk׮ӧ$Puuu͛* IDATR(ohh@QQvwwwH$ddd輊 bҤI Eyy ٟ/~8___ٳذa{]]E ㅅxg!ˡP(w3vXg'''tuu gffb̙7\?\ZZZ`0'd} H4 D"Qoll,ZHXv>_ekiiǀ<==>bnF$+VQA$`0ej7V,_|.] H 6&<<:JχJ2;9HfHR@SS|}}- ƍ`NӶ6`0HjH$~0)zcƌy_:$"""c#H$puuXT*ō7bQtuu!,,L(Lt5b|jW4777,X;wDgg'_\[A,QVոpBd2M0-BFFt:ի(**2:/$$z7`ƌ{KDDDdGw͚579e˖!((r4+f@@z-(J̛7iii7oEyUUUg;e˖ śBɓ'qa( ܩY/YQ[[iӦ xުUlӻgϞEWWbbbeee'+ooo+dGDDDԗU; s /x{{#** x[[///Ν;ٳ~z,\'Nŋ!"22JƢ/,, * sӱzjs1( 4sMM  ^~e\r  6:ĉD"I ^xڴicf5@ףX| f̘aּDDDD#k~׬Y>ǫaaaHLLIJe˄Ǐ'OD"%Kp5=y$=p`֬Y=X~T*1w\L0SLۿF||<$I9s@Pɓ8|0 y###l2A.Xll,rrrDP444}<T?8Ν3ZkLDDDdm"C;BffEػ0dff_v**??&Z ՐVrBee%BBB JBssvލd)|1 o?m۶~ǂ^jstFBYY֭[+dEDDD??2PkONxzz<.F8"""q9 DDDD0X`k%gΜԩSMXz5^ucGIIIؾ}pFDDDdΞ=.®]j̈^YMHH@^^^׮]!H+ٳ{W!ֆ$ )))sΐ]?L2cǎ6n܈n@MM  ^~e\r  6;>8qH$4irssF,YnnnJشizzz8O6 'OԩSi&!==ݬ_ .ĉxbttt㵵D"RF1y{Err1L(&"""Mv~O 0T*hZףMMMغu]SN(//GAAPN>jB`` j5j5RSS͎_RRTٳx"^z h4t pa 555pssCGGjkkq!@l|UUU"աǎ1w\t:lٲ}Qӡ >܈l_ Ò%K̾EEEصk!HW<~W.#..f_?#G ))IX J +--͛1zhr]O@L:Djfs+W3\\\˗/PYY͛7 JQQQ}4iR!44刈38pl^V\\7 b ?^f2+~cc#`ѢE±ގH\F}}=b1ڰrJb cDDɱ  d2'''X,X,.]lܜ;v|4 D"Q~&cvܽ{...KDDDXMJJB\\ -- XbD J}/iبt3f "ՐH$&sxڊ˗>ҥK!a5 `0,+fM@$Yx~MMM4` ?ƍ`N۶>yxx@Kc̘1,|l#A"@.C.^^^bnnnP*ؽ{7׿b>ܺu }06a,ZtW~с. E~>ׯ7;P@N"""znDT*M{n$''YKDDDa_xw۶mw,((*ʦۓQF "غucȞE_)*N!4y\&p&DDD(""""r#FIIHLE [W_TDDDDD& Vg͸qpwh| ϟo4IӧO ""a0/ B^FDDDD}7;VlkBDDDdW3O~=z4%"""2;v};;;o>%CDDDdgXڑB}`Ϟ=jVΌ>#]^{ 111Xp!Z-DDDDf_;ѻW^+?g'w~ """2;vo< DDDD`b|S]^_7P׷DDDDa /yat}{KDDD48v~m9]^ _eI׷DDDDcFY/A]_HOO7u?3@[[DDDDakQ^^qx lٲe2""""z4kiiABB=ĉ/~cח;vhժUxp1T*kCDDDd79 v{<AˠQ4ET#8Ac MZveUZO0]Ą` 1cTEm V)BC "&\ܿ?`~rޞnʓw޽7>s޽5m4(r| """z0%""""䗈_""""2L~h0%""""䗈_""""2L~h0%""""䗈_""""2L~h0%""""䗈_""""2L~h0%""""䗈_""""22pAd 8a50 \&zk׮_""""2"L~h0%""""䗈_""""2&dZp\xq@|􄹹UUUӚ5ko}.\~&L)Ǝ3f ""_~رcv؁;}}aǎ:u^x:{И=5MDDD#iUt F^^.]{TTT ** 011;LLLPUUY>SE:؈bRٗ_~_GGG̝;cf_ߗ[ԴOIODDD4^FױpB{pС~ ~U^z%|W())A}}'N8xW\8;;Jٽ{ܹϟǧ~ DEEA.?jH?@ cDMDDD#@Ϋ*vyץ퓎'$$H-Z$ohh{\шO?ݐ...R_/^EGGGL8qR?ZGDDHϞ=+>3W:N>-6661cİ0Zcc=&bnnOJ}TVV("N|A-<;vLEQʼn'ѣɓ'111Ҙ՗9 @ g̘U@ܳgTnhܝ\"FEE B433ǎ+gϞչ^'^q EbrrrD= ӟ$gtL֗ r1&&&8q"nݺ>_3gi[n KT p}@EiGLLT Ä tR((**B]]sbܹ<ħ~ kkk( ܹs׮]Czz:N>b3Ơ;֭ï~+9s%%%o!66ڀ>qJ%߿/ommqehj*TVVJ߿z<ѧm:Ǝ WWW={@Gs|T 77W}vQB[[֮] 7n/3}^z%=gΜ۷iz{{UUU_Ԅ6$%%H򳲲؈ꫯbgbXp!lmm?iiizKe˖HԊVQ^^zk׮?6c= @jj*RSS+W6ގX)ݱcܹw+O1cpy={666Z]᭫\{0ŋq 'u2MSrr2/^ zWf/ \\'xhooǭ[ $@RxAjj*Zx1bϞ=G]]|>3 `͛7Ңt@|O< )|g@q=}v ___?;&]/Az~;y"""O~ױuV477#++ qqqy&~Ƕ<Ppm͛7qM:mmmPհ:Oy wWWWv:/xyy!33('NۥUs!$$׸DQRD^^^u ]ȜSO=I&M~w׸Njйt] Ԋ=] zɅbbbGJwHLARO_џǤg^;ׯZiӦ?֖Π ׮](Z =)q׭['}߰aà5Ǫk,a }J~;ox ceMJ...RǏqڵ;{li/SNIeoƛo͛7xCxxxHZϝ;w}\zU~ll,x $''ɓ?Q hpvYXkΝ;ٳgcҤIl`ڵk+W?qO<B ۶mCmm-4  #o^uҪۚ5kDEEaӦM?w)y\~gyf@c%;w"&&<@DDGGGGikċ/vFQpKJJ®]ȨLp5 &LZF}}=`رHII~m{x1n8TUU޽{pww s %"""Ǖ_""""2L~h0%""""䗈_"z.]Ȩq嗈3|ŋ;"""Ǖ_""""2L~h0%""""䗈_""""2|/s~W~h9DKDD4rp嗈_""""2L~h+CFF ҽ(lݺUP(LPǍÖ-[z0[hNrssڊ`$̘1ׯ4qwݮ~W;w.ͱ|r455IeԩSaeeGGGlٲmmmR^|E,Y&Mi_RR___rO'U__hqqqhnn6}VVARѣGuʇ* HOODDDDƬ{~򐛛r"++K*G}wGAZZV+W ??eeejCVc۶m8uTbSThhh@yy9*++qMl߾@DDOVAAA>MDDDd'?RLRa̙:*JiF }YY._W^y&&& qUVV"??{5r9pȑҥKɓ>""""c755۶mh4?~\pww7NjcffV{^^vڅrZXYYIMLLdptt]\\ >j@HHt`ccc_ƍCcc#ZZZ7$P*xg;wUpaX2 7nۓ &@Ej)ש$Q L"r싡X4 ,,,Hgۃ\.BB%lmm簾? 5kd2jjj4777xxxH{+**p9zNNN033)suuEHHV| a$7sLh4tիW5S""""2=}ݧ]\\{n" S8x 9sԱ~;A.cR١Cwwwr?o<lA,]tHsppΜ9m| -[fy;yFLdff]nhh (**BPIIIصk85xSTD}!66/Qn{ """"䗈_""""2J~1ȡt/** [n+ 8::l$^ }999x~,Z'O9i&ܹMDDD4ުPRRiӦ w(=Ыھ7hmmEppNYRRf̘c-"""ի]ꫯ0w\chjj8S زe ڤr???Xd &MӾ DmmmNѰ-lPI&ٳ:Ƿlقg_VVARysqq1Au_C4_ii4AAAxꩧV; 66V<899ѣ{~򐛛r"++K*G}wGJʕ+GYYGDD`PضmN:էT*P^^Jܼy۷o7 p.\ :HTTT ""BӧPQQ ]֠z{~~~hhhk8qBZFAA#88yyyݖ=j'?@Je]_P(T*QTT><<fff}YY._Ǐ78J磴lذo{m`>}xb⥗^_|!%}7x";SSS̟?_K.'Ov OOO+DDDD*TظqL fffhmma…P(hiiJlbb"L&Tb UWWBBB-111A;W~p=,h%ݹsVZÇcŊdظq֞ԞL0(BVKY}}N'=gggd2A.4fW{.? <<ǎӧӁ_EQs|霿۷op-iճF/F zPYP(P(Դ]MMMhmmŬY PSSCZQQssrrN+BBBZ c+@~~A5 ݻ`R;7nm{///ٳ< C̙3hPSSm_W^נGDDD4z矇w:tqqݻ# O}dggF\\̙S\KeB{{;!ׯ< `jjӧ'č7א7oA@vv6 .]U+WĔ)SP(o}O:^ҥKu9s~>,[qF:C Fz K,͛gff"33SxCCA@QQ P]v!99Q x7/PTAaaNپ}ė~P7 Ď;-2ecUP*hmm\.GJJ <<Ј) oEEEpssg}0L8/Ґ9Ekkǘ%%%aƌX~=Ə? <=^:KJJ \@j8pSNeie `ݺu!A@ZZA;Bz_|!#::vvvE\\\.ɓnp,Rj/hjj;v <0SJJ bcc999_뼈]=DDD`PضmN:Unmm>wEaa!9"%ӧOGEEyXvA񑎩T*444y&o.'eee|2^y 00ZuT*fΜ P(P*(**2x C/\x/cӦM:|ݻ֐HHH#G.Xl{CH7d~ҥKɓSۈΞTl۶ hpq馮ϣ2 R>k.78(C=zػw/ /n{{Vj־7* b֬Yo@]]lllzcܸqhllDKK˰M&""")th(J@||<! "j|KܹUVXbd26nܨ'Oo3f 557o3d2 Ҿ7nnnѣG?IZ5 &8lllhzC‚/A.CP@PwSSSYYY ;wNjԄV̚5 2 555:7[ nܸ:ㆶdbb$Ž{ꊐ$$$@VKmbbbpA>}@*DZZ;J%̙3hPSSm W^W&"""!q?ooo8x 9sHe...ؽ{x닕+Wbʔ)P(xԾHXXX୷qY{{;!ׯK-[`ӦMDeeT[yAdgg#-- `ҥZ1EDD={6A*淓|||p̙nO>2  лFLdff]nhh (**BSRRvڅd$&&w8DDDF7~T*}þ}ėG o5 d:e6l0DEDDD4r1}č7Nq''!h""""2L~h0%""""wN\\ji}~cĉdž ~ #h" ?hll47~~~xdL4 ˗/:ί `ݺu!A@ZZ>++ @T}ʕ+!nܸuAl޼0uTXYY[lA[[۠m~ ~))){NNN:o"""2"QgyF|u{yy[[[vqkUgɒ%B/_,(׋(bpp)677ڞ8qB'DQ[nǎʗ-[&XBTբFCCCW^yE*OHHJ,?{\___1 @lii߿/zzziiiRyo'xQqԩ=+|ĉ>m~VYY) ZOokkhhMLLĊn{7ŋx!11Q &''?gfbԨQPTѩ3gx4 N:W^yP(,{w-\e˖*++{r 8rwAEEd2aiiip!affѣG` xs\uS(P*(** 0]t I[[[ܾ}[sAtAE8::JǜP[[+}Ə~#WWWBBBcbbp}Nbb"fϞ R#>>---JlbbVo4 BP@dddy~ct7aׯ666=2n8466ڹȧs[tt4J% >>AAAxg@+ay &tﵻvvvd^r;;;C&r\o߿PVV@,Z*ʠ0EqGO{Z Ɗ+ ɰqF=!~4 ,,,<"""Z:+r  _{hhho0;v,{QUUt1118x N> cWWW !!j7 ??_jq\v \>бƍqmjjBkk+f͚%%nZoOO ~3g΄FAMMM\z^^^}m=},00χ+N_WZc Ë/(/[ M JСChoo;r9BCCquʕ+Xp!1k,DDD`ʕToXr%LB~[*7oA@vv6 .]ji~]\\{n" c 2?=1988gΜ鶟O>Dk3>=k,$&&b͚5+.Dff֯ P(jIIIصkP""" oRPSSCllCO|hd޾gǎݖM2ePn1j(rÆ h8+t`1bңgܸqz;99 q$DDD4p iWODDDDD?$\%""""_VGIENDB`wader-0.5.13/doc/devel/images/sms-persistence/000077500000000000000000000000001257646610200211505ustar00rootroot00000000000000wader-0.5.13/doc/devel/images/sms-persistence/relationships.dia000066400000000000000000000044111257646610200245130ustar00rootroot00000000000000]s6W졇RaI%/MvN;m=g (،mp^I6ᗝ ފ.'}zҳӧEd=4 ݷX} uތpvvvay18I"j8$e0ϓw1S (>~eܹ읥m9MUSU]}unû##PV+S\/U8e;nߦylި;u`[BDe?o*opq`Ow;cf4K Z?Z?'/ XSZ\%8F2fj@|;fx(DJ=Oɱ|&IsXl?֜G7D*߻>=tzf qx$@x qq#$L;E #Ө琛 oHϱ3 |8rl`CB3%wFѽ(:mb%;墫wkս dͩGl7*! <!wKi <rkz%\k_qM#{#z 톶Os8t|淊qFw[-f /.zewc6@8@NTs5Mz+9㠷`,7ۆVj/j`oIi"QiZDִmFy[U^4778ȕnR ;Ykz4ԁ943Is@rDzLSֈ$W=9=s%.eY0c& H^h-a̪| ތ΄Ml 6qPTΚK'gO1XQ!RK\odyx)UgzO4M !Ů"Z bMBwaKՑ[cY~ģFx)y@(D'L*+ZiOe ;BmrmQ[وB#P2BEH)-V7\8q3CmJ5AmZ _ۧHQc#+!y3t߷oG]II#ڭP{x$)\E͐۠A@y[+?+S5)R>sUّ48cO2_z< 6&jުSDpZJʙGGdtQ!Q 9>a>v /{UYݎxe pP^'v/V|y~htE2$-_( OJ3ÁzJr%_i &XC扺JuRI%1Q~Fpa2)0x/+}oז5Okބwader-0.5.13/doc/devel/images/sms-persistence/relationships.png000066400000000000000000000621371257646610200245530ustar00rootroot00000000000000PNG  IHDRqiĀCbKGD IDATx{\?ǕN Eh (gFbɇ q49 31$!Fl(IzsLzoM~z_z_Bi8B!wD:&ӑ;""""+wDDe3FD/(T-i &DDDD5`rKDDDDZ-i &DDDD5`rKDDDDZC#5k@P޽{%ݿ? *!2""""J*4ݹs' E{EJѢE e5]Q5R)ɭ'X*4ԬYS0TJA`ffRva޽ZҥKpwwR5ΝB>~'OFÆ &M`޼yɑ,^ RL+HDDDD <==1qDaXvmXp!~̟?@ߔaΝ5j֮] ̛7G.пo߾hѢ>;w%"={vc:ww`>]v,=""*B%*B7nܨ6n8x{{аuϟt0l0899pBܽ{Rߑ#GSLij~}xDD |}}/sBBWRUgϞ>s޽[z8}tݻ1FYO?4n9994d@HHZCC#"=z-Z@xxZ(VZhݺPx`bb3335 :WNR}bƤJ%M%0ڵ+LLLвeK:uJ=)) ް 뇄pttɓ'Kx6*6##Cw)r޾@|VVbbb{nMӠAzlDDUɑ#G~DFF?ڿ{l۶ III2d}A}N:HHH͛7 &yf3111Xl_^bYaJWaТE $&& iiiuѸqc 6LϡCpDEEqJ"-!lS(jyyy!C?/tרQ5#$":ƍ'բzxx`BH履~+/k=z666|2?R R ƺu`hh>}`ӧXl2YF?rtuuK׋"##q5ck-Z$$$H7j*g4E%EիWoFJuō7 EEEOvڽFDDM__j7 5j&&&]nccl][.;V}勏e/AP}j뙘 11QJn֭[bDTThͭʕ+ҲlZ fffx7\V+]HMMŦM lذWرcѨQ#ܽ{#QUj*ܹsyyyXt)rrrУG ӧOGFF?~ٳg>~ ӧOqF|5jCir[[[Z '|޲eKtƍCbb"&&Bۆ J嗘>>pvv["TX`v;w]vܡQe Շ.^-[W^E߾}ѥK:uJHK1%Js=tgFnn.MӧOiӦrF (\#ʤ?7n+`aa'O իW ["CV KKK9rAAAOդo␚*s$$}}}|gs̙J_~9r$bcc["Piii?~<#xxx 22ܡLxMRaܹu&N]]]lڴ ͚5DC*-UHok֬!k}!C "":u;4` #K,͛71zh5k`ggYf!99YI0%29s Zj;v@R~;1L e 4h#22^^^HOO… agg rH2brKD k׮sڶm .`ȑrFU^PF{ӧGaڴiƍ+w$&DT{{5krss1m4>}͚5;4Bo8|0ZjX5 8pQ%crKDڷoZjXZZȑ# |'*-U^z… ضm4ik׮:u‰'* ["*TZZƏ///>]vEDD  whPI8;;СC +0qD8::bǎ crKTMݺu ;wƒ%KyرchԨܡ200#ҭ[79s?3aÆ]v8r+`rKT mٲmڴaccPFrFZLOO[LǥKa4l/^D^o㯿;<*&DHJJ ooo<}ŋѩS'Cj e tuu1j(ܸqAAA033ꊁƍrH䖨 )UsΡuؾ};T*6l؀;wԴb$? .`#iRiӦ!::3g΄!݋-Z`ر䖨 {|||MpBh۶-.\QFUbT<~۶m+ĪT*_/ wGTZXp!1vXagg???$''!-Qb ܽ{111߻wݻwǬYiӦh֬YJՊ~7aʕHKKSk&/^D.]@GKyvZ\zDFF,Y&M ((6F!x*))) fffWk?p>cLTEI߼yS;==&L'=z޽{ҥKLlR]PfQ-ebKUF#GuָwF ggg۷̷m[*(!!|4|2ڷoիW+VCPn]BjlΝ-wZ355ŬY*56… ؾ};///tԩ@||<֬YSqV3Ln % QQQoѡC\z8{,>3( #^z:ujƏJ) :W^w}KKK={oK.L6 ׯ_knwy̔JW=_4(RRR`kkBەJ%n߾ KKJbbŊ BJJ ttt0tP̟?oF\|ΨU,NpO}:VZUlBBBx R,^a}RjEh; /ŋhӦM 002r@U߀4,Y"w Az%|o^yyyXt)˴ܹsWPdڅ-???,ZH0 틘PiXy*]U:F{1/)SrtRRLMM+$~ϟGv!~brKT>tK i sխN^Ma|Fa۶mrJ3 :toVA*]???aaar ^^൓۴4ԬYADDDDTWYӧ;v,͡RYx1 "##1h ^zRǏ1yd4lhҤ ͛Oll,ƌ#aVZٳ'.\P K.J֘;w.rss KNNƸq`ee]]]Ԯ];wƑ#G^ """" J% <3gn߾Ϟ=+4A۷/FiӦ!11777$&&b񰱱Axx8͛۷ocӦMw6l7nbڵpwwǥKФI@LL ݡRpBԬYk֬} `L2HMMٳgqeU"""":99]` 񰲲R;tP-[p!޽HF ;;;L2ӦMC-гgO>}Xl`HOOGXXaÆɩ@f̘/BZ6nܸWyHüRY¤IwyfC-짟~7ni2d烟RB 33ر#¤_{[066ȑ# v EBBB1U6lPm$AjYYYݻ6YZZ>|… ===BTѣHNNddd/o˾K;w 4+f̘+W³@l̙hܸ1`bb^zڵkuP(|˖-ԩ&M͛COOɓ^%Kj/)>zc%&&}J9&LL'LLLPfMT*( @m;:vCCC?^X%"JmQc6&]5C >/ѷo_8pN™3g&mq oamm5kk֬)݃'pѣv///"99.]Jw~/y2eJ͛}_~Qa~I%G} <7p;=iiiÇacc///=!!O>&///cǎa8q"߿+W`{FUXK*EdB!-Z$$'NÇK+;w.t/>>>jJ899Iu// @ϟ/r)))YXYYW???Y㈊ 4(U߬,1ydѧOW^⯿Gu IDATD|||uw!իW>Ν+:tPvKͪñr-@\~]{ARDffB7xC|RDž. q1iYNĜ9sJ6lի1vX4jwui&L0v*7,Ǐ,,,o">>^={4 AVVN*-_5q~-nYD/cI&prr󑖖Dx>#̚5 999Wjժ .`Ŋƽ~z;VONNF^^vڅҥ z2?/}DڵE&M… <Ѻuk1fc*͛'EJJ~;&ÇeO>IJeˤeBOOO9rDm:VT]U0n8ann.ͅёݻxqѡC1rH-((H 4HQ8NNNje]vę3gBш@DD?ssslyɓ'Ƒ#G`aa v~iK4Xx"SôiӰ`( ԫWVVVo ,[ je>ľ}JM0VBLL 1o+pڵk iJttNIۯdL g5jbرE-{كH\6ֶT/iKOWTceŊB GGGeuD׮]Rm۶j%lB_]<1gQ^=T*EΝŅ 9V5Mu=V^&Pq%-[&n߾-RSSEDDԩݻBgϞ DxxTf.bccEJJ8|WشiBaee%ƍ'&DDTi0%*+KKKeD1%*S+QItuu\dffQaee9䖨x^EDūY&~$* %[""T&&&#!:xdLnR培MKK9v044ӧO3hLnʁQeHq80W_fcrKT(gʆ 䖈*U~rS#!ZGLp`crKDDe De 䖨( BH4[WòarKT8QժU bYB0%"J` J-U*&D&?eYBQ--Qܲ,xLnIvjj̑ic5DeSNѣG̔;䖨 BjԨ-v$''WhlDUEw-_uEcrKTzzz3f )Çc„ 0554GDD._\`yQCbXbEGT4drKTf~~~X|91h s] >͛71sHfvvvٳ'z?C:/e999X`?X4] Lnsrrd?~\4 [2ruuEӦM{Af]]]f͚a:{%V"9T*̝;G;{{{l޼gϞ\\\0{ldggC˓:;p@|Vp9m+WD׮]+-FMڵK;;;7oƣG6l@VVԾwJHS5J  C޽q5ٳ+=F"M:7W_}'OP1!%%&M+"##J'Z3&DUVJעE ߿""L5jCԖ|±ݻM6&k֬f͚gbڴi033È#ٳpttķ~+#SE!XyLJвeBGGȷm6 6"LbCϞ=+)""͗RussÉ'*8gn^^d]UAAA)#n%FDqI(XT1%z +ϏODUVl2eJ>Ν;c̘1QT4hP%EX@ƍkת-kԨn޼)]ND{мyݭWܹ#KD꒒BG 6TrTgn^ٳahhח-K1u'NĖk.&#Gh4SJo<}6? M߭,662GFÇ-k޼9X%*~~~ߓ&MbbKTJ#F`bKTJV2&/[r֭CLL T*i"66W^!UK.ӥח1"ʌUaѢErQ,Y"wڵk G}$w(ݺu; @999PTD˖-)wHUKV >?crKɓ'VC[ze;CCJ $_;v@͚5ѵkJ , $ٳGmmRY?~ɓ'aÆG&M0o D߾}q:u gΜ3ӥSޏQdr۵kW4lǏ/-|\#..ڵ+t_&Mž={p=lSfI}n݊Çcɒ%@NTRD:uƍzpwwٳy777}{{{߿ܶ*"""MLn V\9؍ <NÇ !==999HKKSk3664}ejBBBW^8|0ټys=""""M%PT EC~JH@@9OOOCŋ?ɓ= mB__vڅ1cx>lI}HC}q֯__CngATᅦ1 |y?"""c̘1ؿ?ЦMڵ [nˑ3foۈŋ&M`Rhll,&L?7|#iǠAp $$$`pqq)Sp!dee᭷ºuP^=χS9r$"""иqc7⟨?t‹C"99Y [[['T*h׮3gHMMbҤIEBT ]]]ѴiS(T}"))I1BR):u$BCCE~Z\Ν;wJ̚5Kl߾]W\)PCQT|xPYTeU֭0|E˖--RSS{'ܹsEFF8qׯ_/t͛7M4nbR<{L #DbbڵoݺHNN$)&M$~m!yyyY?^dffWjժOw…2^U~&_ ֭[m۪Ϳ͛}...bǎ&*N/Į]Ԗ:tHm۶-}ik `bbڵkK666ƣGРA@ݺu8( o^m&&&HLLD||<,,,2N}G-"/(ǎ;7oܹs4_~ж]vEjԨ+W &&FKKK$''#;;[Z*SZv[nG}???T*x4[o+WH7 BCCݻK7l؀5k 99o6Ə/"$$ƍm6WGGG\1111b\|W^Uadd!:u !!7oDtt4&LP! iCCC 2?`􄉉Z?///֭[HLLDƍ1lذR浞o333#++ 8taKHZ wA^^.]...prrӑǏc`hhXZl;bܸqHLLK...ŪUoԶ|j}r~z ;;8u#wXT -[ )>>שS-[ıc$? &'O 1a\pA:;ۿX[[cǎ:ڵkcR3ڵ 1 22/_ƲeˠT*ann `Ν(~z=z46n܈}ڲJңGo=z`u߿Ν;K}_gR*Enn.tuusN|׈.ʲOWRl===驶ٳ%?sBmlloz&ׯ ]iӦ8q2ӧO䖴t4_5ʴݻO?~iٲ%ch+Nv*>zarKT ### 6lnJJ ~Rm 3f@ǎѭ[7;ӧO 8my{{#** ϶ Bhh(Zl=z7,SAՄzQB|镦(iDT(D/D"_:/77 4ƍѻwo)ŋc̙âEx|PY) %JlJ={ FeDU9J%l~/%""* ?!G޽{%"q8::;j-kذa|MqttDDD8B||jOJ,R䖨ܽ{鉘cEԣ?֭[◾@eff"88 RDZZnݺ^MMMhjjByy9ZLL >C466bرc0%IR(8~x|)ΫT*X&Mرc1j(:uݾPPP8(JDEE.]iӦA.c7o]{{{kpssԃgLusssڊ_ff[5Rn:ƍCyy9 {{{^~DzTZZs"%%eee())+:WܼyS|BCC1p@S"!!O=ڴKMMRSSQRR ӧ駟P\\ŋ>ׯƍ qZD:kYwm`?Mv!)) j]sضm0p@.]¡C .HZ:T*d2]>H!88b{y\zUl7x'N/cFLnz饗g}Z?̙38{,֯_kkk8997Dff&v޽{#A]X/b޽ozjl߾K.Uk5aÆs7KKKGnr"S˗#((&L… uޣ|޽o~GmmX㩧Ҹ|A_6-,WWWcB ^{Ur8::eJ?~>刈L&qׯǧ~ӧkq䖈^z%}YXZZ";;[cENNHݻ ^buڱB@EEhjjt1~xt/X"""nx ())BR_2#GãK}%%"""ꦴ4k֬ R_(**jSbrKDDDm .Ĝ9s]vR_HOO\.GDDX~dee+d"""nRTm`5iMTN<%%%mٰaC8rKDDDD䖈$ed\%""""`rKDDDD䖈$-I["""" &DDDD$LnH2d0%""""`rKDDDD䖈$-IK&;^cڵzu[""HHH0vDD}[ҙ $8vBBB˗;&L`LJrr2Lnz?544a2"=}6ȑ&;;;XZZ޽{z; 1z,HO7r$Dp-#GBԻ `HL[""2Y &D0777r$-\.䖈z["=W"Rג9"2&Dzr#!2MvvvQbrKDDђrY$&DDd-m˶yDD=-p+0q䖈 -!DcrKD䖈 GCCQ0%"">Dq]#1MLn -["=in'"u疈 -dF42"2&DDd疈 -GnA]]޹J&DdLntвA QG-["b(,,5Xz6i'e'۷ois}l޼;)iЯӸB{{{ӦMŋ5-++Cll, LQ]3,455յp+=V𥉉~,iرcA3ULntꫯ СCh"Q\\ ᅬf,YQ%Og}fffHLLč7u[PP0wCAA^}UcMdr&LDb֭m{N8ӧc5j4=2Ai3`Ϟ=ⱕammF-[k#L"Ə?Att4>3x{nkGDm_5_~/2bcc1w\=3dcrK:thh=<%겟m2d.\sssGFd:;whlxFEԻܽ{CEEE6fff(((a i-Q :듒Re˖ij/_ۼ LlGnm`UT8woȈLG]]L@LL 6olz;w`Ȑ!~z:sssbСFp䖨T*y6寿:[7`^ML&ҥKQccc?EGG3m#D?`Ĉ6F8}4d2##2 ((}X6e|Fw}6J%2 \pJxuS@@~ߊǫVbbKLwyGm ҍmfLl["=Epp0F|cCdpQ@ss3$͛7 aРAF;]rss&RQFid,_! ؾ};|||qF&D]`ooxױxb&-$99+V0v ?5VڵBq}QRq$l"*,22;w4v&-uIBB֬Yc0NVH[\VBBC^-46ZSIKNN6v 888`Ŋ疈-I["""" &DDDD$LnH2d0%""""`rKDDDD䖈$-I["""" &DDDD$LnH2d0%""""`rKDDDD䖈$-I["""" &DDDD$LnIR ߪ+ ݻW~L2Jau X{L['''}-[1c}ܒ,XO4tM\b޼y}6 ..RDTT/b„ /źChjjBSSb 899abwU8;;N,{GzDD̙|?#HKK466bƍj#hܹطo0afd2=JJ娪jWg}==[iR0&$iAAA:uL&qx߶{>- cǎ%&&_GHDD=HIIW_}M6u ¢Ea8;;_5f̘Ѧֳ})))Qk=[i&uiFRYK]P8rKd$yyyto(qaa! £>̙34i`aaaÆ(47n@&C Qۿ? ;;;1lf$2ky Gn3gD]]ٳgmNԓ0e̚5 ;v쀳3.\'Nt?AԤ,[Lzt"Chf5n-[`˖-Z_g}]ԦMg!pHf͚2TWWڵkشi,--/_FEE,Y777( ?~\<>uھ*  ?ӞiD>&DD}?!Cॗ^ݻQZZڥ~:} i4@"SQZZq #F?> Afff8~8zjxyyAT{G-Z$>D{^i-3&&Lg5DD};~m@]]6oތhaԨQZz{^ֲ祫ֱiWSӚ< j}Qő[""€tR̙3~l7ovGW4pwwX0AHLLٳgЀZ[wA`` `ȑ>=ly:QMM ۧ54zlݺW^8pܒtvwwk1%K\|TG}1c~mxyy +WT[ڝw? 0/_̙3accB]v!++ ~}|ѣGpwگ{^n:TTTI'''8;;^^^z"YF $$$;v ǎ;Ʀݶ׮]'\vMA sϞ=BYY p9]عsؗ0vX֭[ 1ch]Y/1k/T*h^+1Ill0o<#<ݻ'( a•+WٳBzz^>8r^"'''~i 4h9rD?MwkkSMD5mعs'T*m۶6mtI/&͘R_O>)ƍdff"88 RDZZ|צ"JuV@LL JJJ0o޼nߊ[k꾊-IwwO8999Axx8q5|GxammR̝;)))(++CII ^y:JARR{ۇbrKǏ˗gL82 O<֭['.I}6_o db/=?>-P>}7oҥKqD'˗#((&L… :}tIH{Pf|BINԤGntv#DDDD$LnH2d0%""""`rK ÇGffxЀXYGӧ!QH޽ OOOlg.&$)* ֭Chh(1n8#99ի5553r9f̘JPPP8(JDEE@455@DDW1%mPUU",, t:$\p0k,աUUUp䖈$-I["""" &DDDD$LnH2d0%""""`rKDDDD䖈$-I["""" &DDDD$LnH2d0%""""03v;]H [A`0L^rr2Qt2vX,_aQvZc@@Lȱ!ɌB\=sIENDB`wader-0.5.13/doc/devel/images/wader-service.dia000066400000000000000000000053031257646610200212440ustar00rootroot00000000000000]Ys~`i^*gklL&VU KB$LR(^;|H@hӊ)'4 'R' (~ۿ~yN~~S@;x'${w?Y N5b#FS0cx* M'oy8l)y*̱GiLvE\y~!MjzsZ/kSuLxU%TR2R,.޿B ͶYjl-!|A*-e,^Z;ƞ/דKKK&8c*$QTOI{:b纥 ]hfIl?>Hܵһ48/5i ص}3P4sFN7T_w j%_") HraԴ^z\Sl?*-9 7x\GwxC|o_(oO '䨄HojSɚ$VXG SnieT|^ep`@p2*}HWMGݎ&ad5#VU>ڽ=1t{@kIb)e+vJSA{7N#IBTHM:rIH9]P V?g [bQǜ$E28HXH ~)kةֆP.u'oqnq<|or*A_ 0TտLjdHb3FCŢ: y.*ҊjUrd(c)QI ۇ0{I6{}<e* $ƾԵ/7mގ~Mh-5Lj0۬"/ߪEujiٿ`oVW>9qZ7=ݜn!֕MS+IESEn ~x :ĻHWuEnJ+ZnȲe ]Cwg5f3U jQiCH>Ru5`22*6ȑ0]&s{dN\[/YG'Fkr'W#DD效Uv- ;.q\׵Ip3u AAAAZ!?*z   w pscȰgwL׺5 йgM4Yڀq6z1 L1#+@P@P@P@P@P@P+<ܺfjhƹb &j=JO $XD`A@@ 8mL,ߨOI081 %-*,,,,A-((x?^CD>"1jMEJ{ʷYp-=%2ZjkPp٩Q3W)4J!cγC p> QFa>(̇P O3YM"hȰtu٨m\%sZf n%.K-xwllAi_-aԓ|^6TDȵ(gEE˹bzyT>율*;CG)ߘu[#y0Z{#c+W! "  "  "  "  "  "F@1)1k3"ΈkMݘDV1c! 8$ɡB7 KfYS誒\.sY:k'zrD9^bQHV5M0۹ .huMz,A,K9$ɸD/|9޺sQz>F#: +6Y}$6OCAWR>)ɳ̈(ݻZ%-AVҶ4HgZ$&a@Blv v]A3}Ҷn5w؛4 yfNFWPӳi܉rU$tdxrxQ#= sRVɼz}77ggl1wader-0.5.13/doc/devel/images/wader-service.png000066400000000000000000001610441257646610200213000ustar00rootroot00000000000000PNG  IHDR<s #bKGD IDATx\TU?/D1$("D E]X4ĨjW7 5"""zld2899!..N&I"""zl0!""ǚ.I"""z쵕0!"""-aCDDDߴLDiђ ]Vk=!vލǏW^LxaChz*6nܨs_EDی352>۷Oro>!׮]{ETT}(,,O>شiFSL BNJ QZZ*h{|tўgĈb񢶶V׋ӧ?:wߕ/]TDGGu|ڄ3>wչ파 L0 J{kSNnΝ;Rd2K899uX3>8p`eeeeBhm9Rb@hܸqdd_sno߾kkkkՇ̙yyy8x LLL0yd-u1+++lڴ @Ì?&MJ%Ցd-~ayy9f͚?dXxkPZk-ZdɁ\.oVޯ_?!PUU+++ kZ%(((h2 W^ʕ+mVK.jk|Zclsoeepl۶ iq,ڋ3C[li1#G_n?>>cc޼yzJ%v܉d(JL6M1aXZZ9sFooo̜9B-[4Yn1qDcĉzCku|۷ \.Gpp0.]$ܹ[nŘ1cQFul=?m BRR,Y\J婩(---E~[㣋^??~<ꊕ+W6k׮[4h^Ǧ+>KH >K;OOOXs}||i EDDDÇ#;;s>h̘1c$ӧaCDDD:uTv+""""DŽ """2xLx8$&&X!CX6n8vbdmUVuuFP޾ov.3o4i$߿%K`͚5j(vu|2Ξ=CjOg_VzkS {N\ Q;mOLL /HKKRDXXXͅRij> .HLNN֨wy=fff>}^:w!3fh̐TTT &&666F\\n߾W|$HLLD]]TٳR-FXz5BCCgggt m J!::ZUmO;vr @JJNr~꿸-[z|ܸqXhLzX`A___қ "V~TΝ;WͶ4m ,/RٳGX-F!EQQx'Err%,Y"jkkw}'LLLʕ+ *Duu˗/9>mt4qi!BT͛7Ku=<<ҥKE]]oO2E( QZZ1߈#EmmӧOu.B3gh΄hٷob߾}B!]&ݫsBv~[?00PDEE۷oBOM6IbĉݻΝ;ã[YY)Zn7zJk- gxǎccc?^#BdffbÆ \.GBBv!q2>* Çа%,, 999i,_FFF!ӧOt k4gGPTHOO׫=>HWaffBX޽[^DDLLL`jjc6~O:^za'N}p ܽ{qqM|Lj{pAgwd2Y<_~BhmDkGGGd2@VCVPi퉯)//ǬYl2BV#&&F1*͊ o<}_+Whׯ^yH _d2\zUvՎ֩FϞ=x4"vѣ|||n:#007n;ᱳCQQ:4>1ׯǽ{p1=zT*wvvԩS %Sta ݑP8r?kزe *wppƱ*66[n4̖dee_ L<ׯǝ;wpe4; >Μ9#FZ>rH˭3!"j'??? !!!(**Hx|||T*sN$''CTbڴimx{{c̙NNNXn1qDcĉܹ[nŘ1cQF5;P?~<ꊕ+WUnii7xشiT$,Yr k;mR Xh^lggcСCo4&/??_c~2sDD=U3f)StҮ xzzbŊ3gSm۶a۶m-UVVBT"'' Bvg̘ 'OJk<Ν;o/w#J* W^mXo&,Xw?EDd Zrpww_{V J!t=~CңG>|?_#""oiu'EDDD"""2xLx1!"""DŽ """2xLx1!"""DŽ """2xLx1!"""DŽ """2xLx1!"""DŽ """2xLx1!"1dȐ.!..]CW񏎎ƪUt?zhl۶C~wuDD=DEEԴàn3,.\R RdNRR\]]ann{{{$&&N*J&-- JaaaسgOk׮!$$VVV۷/|}}q 6>~O>D?qqq:H7nRSS=6mYx{{C.:MGbJ­[MMMQVV&駟`nnׯϣ Q;߿ .[o7npttϟ}?vڅ͛77k'%%k֬ 'xRPYYʕ+XzNAV߇ j5j5.\(ձ_|7oĉصk/dt[䠼u(jDFFXvZذatn_XXX>ß'!)) y>2mǣ *84:t[\|j2dF;wJumۆYfR~iZ@2""B -]~]d2?K򗿈zJޔ)Sğf_tIΝ۷OsbϞ=UK,8#FwyGz'ÇտB,X@KͶXBgϊ{զaÆ~3g赿+⣏>Nj_|QNkVϟĕ+W &+WF%֭['ŠAIIIbܸqB!ꄃ8p?BBB5>ӧO?O?Tj[_066Fmm ={Ԩ~ @UU4׵kפ%%%ҵ4xx8mۆ|Ĵx->bccuVVbܸqXhLc͞Zǿf / sss#11uuuRݳgrZQQqqq}_kvwwwr 0)))RYqq1aaa;;;,[HK_nn.J%}Y\pJJz]/?GY맭OҾ ND tQYTT$ŋ5{{{>Hz/^|E}BB oѣ֭[mL2E( qi!TG%\]]իWEee˗/o K/l8q{s:ܹs믿ެHKKB&WaaP*bR]tRQWW'add$V\)PQUU%EpppeZo>!ž}B\vMݻW* QQQۢPdOk_[V!!! Qy7?k={j2 իWz>|8O>fq`<?ݻwcDDLLL`jjcJl???;v @֭[:@LL 4<4((P]]`033B@lllR u6]/pR8::߿5.3!"HNNwŊBXXxܽ{Y=R?k;;;zs022BmmN7Bĭ[~dggVJ2220a( (JJ_ZZ L{{{M'''uqq1`ԩXܹs1hqA&A&FYqq1~eeeBh[kt94Ĥ.ǧK\LD}]L4 Æ kVfll ={[YYaӦMf41i$T*v}F[@:F%%%tZy&>cDDD`޽8xyy9f͚?dXx_~B VVVFdɁ\.Z-spp@AAAe666dz4W\N״oMϧ:AӉ IDATgxک6mŠ+Z,,\xo3)fff?|G()){a֬Y:ߞ{[ ƍ_55584]wwwj59";;;cԩHHH@UU ??v|غu+<aV'++ `aaɓ'cs._vGį-Fvvv(**BYYYm888DcluNvXDDDԇ~WWW%M+8q'jgggc„ '"##1shX_'''(JRJ%v܉d(JL6󃱱1"֭[L8ٹs'n݊1c ..F(߾};1h rҥK:ߖ $%%aɒ%@aaTR Xh7ooo̜9Bhx7\.8BE[ן.ǧ 6ED5oI]]~BBBfhH3f@FFPY(JARIPRTT{~wtꖘ6Mfx"iOaQ'e"""2xLx1!"""DŽ!11SNOOǐ!C:ǟ'=RO> ıQϞ=:ֈq}\hZCHLx:HTT^_hh:ϟ?s!$$5wڅZdggٳxW:%ݯbC/]lʔ)RYrrFd2Q]]!̘1C;ׯaaaaÆ!22_u9sHc*-- JaaaEh4n8,ZSLA1}tYQmͅRij> .Hlz 7{ ` t9k׮!$$VVV۷/|}}q GO !!SN[oQޣGCP`ұ5io|IIIpuu9푘::;v쀻;r9 _m{IXZZj{BP޽{mƧK ۷oKgϞ7r9[|pꣀ QǏ^76ñcǤ ÇC{L8׿PZZzܸqC|lDEEAV#22:D^^ubC777jpqqZZ… 3%%k֬ h׮] ccc6l73f N8bYQQ233>f㳴_|7oĉصkF°~,\onܸǏnW5 Ȑm߾sE=ڌOU**++QPPB\rW###1~xTUUW_ŁtCԄQQQ^zr?7oJ&C!UjjjЫW/[wY'Oƽ{/m>y=}_Ftt4H W011;ΝӧCgϞ(//Z*++55A_ӧ+ !''G &&WDEEa={6n߾t6Ĺs`ii HHH?_yyy8}4`ddG gxd0{%8ӦMΟ?_U m>aooATb-ޞ1x`#FW_}jNBdd$`jj {{{DEE:rfϞ +++9p VXooo`޼yOaÆN{ҥR?7d2r9#44(..#ajj KKKh5և)`ddڇRl3[b}">>^JDuu54ٳBaĈz=ꢽedd`„ P(P*HMMؿԹ=}T*ݻꫯ OOOӦSJbccq@ii)d2}::t[VPPr㶖=jjjP(гgO\x)))í[}Ν;8}4qfݻ믿_~(--ŧ~ѣG#;;c]puۥ  ظq#;{3<#m߾]z}ٳgzkp߾}=z4mۆ˗/pv(d~3F'*VVVشiΝ;Ǐ믿_|;v cƌi1a蘆$mO{+//ǬYl2BV#&&F\988@k;o[\\\0bٳ۷op]ֿ#d2rrr]!ƘVTTPTTfe_{i/m&L6PRR7|o(޽{Cyy9RSSakk+ݶkk|_MM jkk LW6[X[Ҩ"** ػw-,]ֿ3N)Gff&d]ZV5fZ9r$^~:!!֭[D...ŋݻ۷o~izKKBRٳg ???UBM0A#!CHe}988)SHall,jkkBDFFJmⱞ:uJDEE n'|R!DJB&AIsy8ƍ1kSiiiPT6mJqƉ\.,--ŀԩSroooGIŋ/׋hѷo_$_~E 6L 6L̜9S7kʔ)wi_^8;; agg'^z%Q__Sϟ΁2eo5ꘙIuz-ѣGƧ=5?p@1a&Ν+,XQg۶mM GGGQ5BQRR"ŧr1|akk+ͅزeTO;V=ZL2E+W6J̛7`!!! C/cJuz-@?yΝ;ؼyTĉGDnj/B^k?lgg'VTBƦY?iq2p;mСx1w\i;>} <<{n;R0o޼f[g϶YGXp1!jAӄҥKͶi^^^4///sfK:$?vXE^^^Gwޭ]%%%xwtRq9r$c8~8>i}vJot@ç}|ȑbcǎW_Eii)tJDd#mܦrL&˥kPvBTVV kkk@1vXiB_nff&*E^4ni pq}ѯ_?ѯ_?ooҤIZoiȾhX3п> :-f5*oi"^{3 kkk} O_eeeL7FFFC"))ž.\d޽{B`˖-,.\R RdzjC""t"߿ .ݻO777jcŊ8{l믿QYYuDD"}A`` AAAzÇӡ1QxK͛akk [[[|Xx>//P\\/R6H?!0@||<O(((ڎL&kۛ} LOku P(P(ի b֭8x YYY١eeeLLLpȑi`Ctya̘1Ͷ!)) K,\. 5x{{c̙SDEEARIyDdxK """2xLx1!"""OiQﺡ|]u0 ukX:5t}FD>cCDDD <& .@TBT"99Y?)) 077=QWWG@@V^PmU`ɒ%\.Ghh(F)]QQqqq}N[n5kWR(,,ĕ+Wzj5j!۾};Ν=~DEEAV#22ۿ?.\z 7nW|UUU8|0Z=OqLxtֳgOCVC&zyJÇ Ӭ^DDTO>im###9r7oR@aa!233aXZZB.#!!vҹ(l߾pm#**J#݊CPP^:u z€Z'N9."ؼy3lmmakk//VX///o߾ݻwu###&LBRDjjj+:ggg{x"Μ9WbѢEbԩSasT*ݻꫯ OOO+..[-%2XYYiO>q^熈 -AXX >>x+++lڴ L4 *JjG&5k>sB&akbњڿ1|_Lrֿ F={^;@t ZFϞ=;u4=~8C@.CP@PW^7]/fff١eeekjjP[[ OOOd2\zՅڴ~}}=q"//03uT$$$ LBrr2ݫ"66[n4deeQ]]Wϙ3g0bb#"DŽ>Ø1cmƄ `ii OOODFFb̙u1sL < [l899aݺuĉ'[k׮]C@@z쉱cBPH3R@"z 4rt^GFF~W[_>>>P*عs'T*1m4<((IIIXd r9<< S0!gsonk5>ܓȐ0!ܹ#=sΜ9򂫫+`ƍ]Q`C eddݻ|TFDd(uC6m9shٻwoFDu3nYa-~Z nfϞ=L&CDDFY,w}۷owExDD Q70vX<e3>HOOu& BgϞ] Q'`CB`Νe>/bWFDiuCs&B#"Lx3fq9L<]QcC ᩧ,og! ] ޽{ 777 zBuu5:<"n*00zBnn.VZ bCD Q7ellS222v.":ɓ'hHܹ=w&|xWA](ΆGWe8CDDDϸ"!zp0!"""DŽ """2xLxcǎ=cccXXX`ذa_Q~k^{ 'NԸf_-)(yTTGǿOZfYi\@q h\4 F(!/f23Q 5" (DGF!PZAll~p moh>pnU{_ߺ޻xC ;Ş` à@1nBP(r%ٳg챚"77>}:+q1r3ud*/,,Daa!JJJ֡6t|Gl0 aeeŋ*}1wtO}JwyG͛nB6n;ׯ˗q%c…nӋAFgse,\?_~Xx1 Fmt颻;{=>,??:_/HXǏ@ Pji&AjS(]󩻻;@ϵ\J!Ϗۗxp $&&n Pb…GG.Ejkkabbrɘ;w._ryg׭4wˀP(DNN***`9#Gq_p%A(wE\\Μ9\G_HHۚϞ=|>&&&(//GBB~7\rٶ֭S;9c[o=C$%%8rf͚2FPPۧ 99/ _[Fjj*3o߾(++q jRex׿*ɶmIMM !D}QGD-}DgJ2i/V:B{F'9~81ce}V4C!D{ܹsP%`llLb1wRݻwmk~G˾njj"111l߬wٙ;wWrҥ.xK]#ǿ{Wܭ@HIbb"իJoÇёӧ6KS899ɉB!}!H@@Wy)hcժUpvvTWW… *VD 066fWDƍӧOJMM/:2 ȑ#5>i7$$x'Z`Νí[pyE޽W_pQӧHNNҥK4%K ??Bpp0_isG,_ H`mm1qDqoܸq(((ի!H?|\l۶dSXXuB/Ç?m4mXUϟkب_&oCP$srrb///W+ xqc&Z---ř3gؿ'OhZ}Qוq㐒xҏ>Vb)O"[[[;HOOG||<{zwJc())aW:"999h„ ŬͱP^n߾ WWW͟!C`Ȑ!uv x1^^k׮!%%@"ׯ;?Zݻ1]r мFqGٽ{s J &Ng >>^ejΎ=V\\Ԧ5^إmb1f(x?7oﯶbE8IC-|/F܊fKwt5-9s&Ǟ@1aO{|Ѷi͸2=uˀByǏ1o<9uR6VPp deׯgڴmLũS wjwݺupss0㊌_W\pسg ;;;ѣıc GGGٳ8tfΜ;w@ *^^^z_aW19 vqAܹs[nm֎;(&LPݻwAѣG9y1&{qf_XBcҚUq[_\ռ蹣P^n޼?AXX  RLJر{0ѣaii 0sLr>6]@s,\666ydii)0`\z ޥ3gs={gx<ܹ<ϟ?Ghh(LMMϟ+6:.]vwިÔ)S`gg;;;QQQ(**j:/]T_¼y֭[6L+"E cǎxZOK.]`̞=5-㺻.\Cؾ};~71wJwgӦM1c`mm Æ w+kD"|677GZZF ?!CKcE.>}:oP޽{C(~q> |>ӧ&(H( 2}A޽ѿ,Yضf¥K0g<-qV]t]3f+W0|8::ѣGx Hzl#/mt=xXZZΘ1SSS|?!8qݛ=nkkKlmmo|4vImBzvQ\\```@z-B_^-%pwwǒ%KpElٲEk}Kĭ][]d 0`0`g{w; B0h3N(ÇJ9r${رc3g0vXd{2FVVڧR^ ߃W|:|\~~>t=􄇇ڜN祿Kuܽ{>>>055-#@ ۻB 8pj3(^KZ#?x) ˗_ň#D BP 8::BP( P( BЀBP(J^ҢP(=ӚLJg&t 4CP( fx芄Bi?9>-\;M]BP(+$''cZDFFb՝dQǡiEBs՘ȑ#add7|lΝ4hLMMaooիWQ@@֭[Yfܺu L&c?=WƟ'`mm KKKDFFӜ%''cȐ!0`DEEqѣGcʕűL>vvva?[KDɾ\D"۸}6D"D"|߾}D BRR 3f̀9燚=>>>Xv׆Bi/HLL<7!\]]1x`899aTUUq/$${!|||8;wpM|x;v,;/Z基t.m𤧧cҥغu+jjjpU8::ŋ 2 7n@bb"bccq늨7tPHRڵ JJXt)_"@*"44ToÆ x(//L&͛a``GiiAVt\kQ(fbccacc?~Vb_,,,{uJJ ƏPHx<{LEH$R9&0 cNNN O>Ezz:acc~[II 00]^ˬ)87xydpBRWWGHɶmXҸ˖-#.gԩSIDD"rm>Bt155%2LA>/--HRB!%%%D(Ç&k֬h&?OB!Ą|'Jxxx$++Mt!Χ3Ȗ-[ڌ6E6KкiyѢE5jiӦaΝχ'Xy||lb8p B!V6fD"O?/jDvv6<==ၬ6i]Z BmOwaܹ;wnWAI BPz<4CP(z>sҝjP(  B3t2;شզt 4CP( <} @Zܺu vb/ËbTUUEEE(--źuTƉqmdggo߾333>|OsH$"`<`F/;::a_"jS0LUVVbΜ9AQQR)Tle&9DJKKl9+ի8y$>S?lmmAAuu5{ѣG#??:6n.,mllG)ۓ1֖mi"laakkkHF@fH$HHH!99}/\2ڙ011F;<==oz3P x|>B!B!LLL`iiɾVñg9s@sԩS?&M¦MP__b!88X/;;;#00ڝ;wƶf;;;֢ܿ^^^l`.ͪ~Mr.Dž娪7|6N:w_&H.WWW+`T .jP^-d́PeffJӞ-łEړn~rbB.ĉpvv':4e|Ѯ`}҄jjj ѺgѢE5jiӦaΝχR >>2 666Fpp0V\7Ԅcwgg! 7߰1c@$ؽ{7D"N)999aƍDŽ l2L0A6uu1vX8;;cРAO8aff///b*:ܹVĞ={0j(DFFbĈJr]o[>?.-:d́BP(g<O)j!U_uڛnKʕ+6lЪ=E?9rV;r9( B򗕕n{:VU@y6lIHHj3.- !tرc'|Bjkkٿ:Voٲewe_/YXYY$2uTB!|ׄBLMMIii)!SRUUE!*w%N<رCb0 {.!RD"""]tL26-*++#_~%122"7o_;ȸq㈉ ),,Twm>O222!d2YZZJ滭B!9|20 cΝW .- Bt82&&&쟫+q׮]k׮]cFX##EpIddd(]?44/_ʥf|uѶΝ;qϞ=Km' W$tf'gi 8bi#^^^裏06ldee8)>|Ji-{޽{H$BNNBau(zxxt--h x^233 BQbl޼׮]ȑ#d[lADDD x( Brzϫb 1C BPsppdKz&t2BP(+pjՇ_,\Ug Bz%--H$<6S( E_hhzlII O@;;;İnɹTd꽺?]hΨ^ >>>Xv-gܺu W)ڞ\P lM" L2/^]dx=zζ2\ĉHOOW9׷Y'MtU]x1 p $&&"66Vi ]uUf꽺?mhΪ^+Jhh(Ǝj|Pz1(F~~>0c _ջD>8v<<B!B!LLL`iiɾpٳgΜМ8u/Ii&ףqqq֋ Dtt4wQ?>vvv>***ZSNݻ)222RjZ444ˋ ԥ52 ?Uݝ$J[ur:P rv1%* \'NY.eLOJJB@@/'NН%ץkƸSMXXXUt˅=<Lx{{#88+WԛѺ^WW܎Ku]MwVb9*++kĞ={0j(DFFbĈJV/Y!33ξ~j+WݖW"ϻ+ $$$$@"FmS]qI$&&|>r]d*h%ץ_WZA.ؘ> C v<<<զPmӷz|J Oȗ_~I͛76SN%B۷Ijj8;v ƍ#&&&POMM%|>dddBd$--bhhH=zĶ?pӧy)!wwwBHaa!111!| ^\BHYYaBٹs'( deeãM^'-S(Jg)cnnnpssɓ'tIK544/_J樽SmTj̙ӧOН%忮@%n;wNٳgݞ=+41ldeez(*]ic޽ػwUUUDɁP(:lxzzYYYnd< Bb16oތk׮aȑJ-[ ""G;P( W^8d2s}vDDD988Oi(ݏ6<ǎ_V7DhfwСCX|9 ׵bڼy3*++;L?5j]ƾŨBaa!PZZuFGGG`|zH$J U/==K.֭[QSSWё)**µkùCCrss!P[[[na׮]lPxbXYYA&ƍHLLDll,gu رcQ]]O?*~UWWP7L2EJU̜9_XyBBy׉)ׯ!%...֖xzyΑglxVM#00*iRm+CB*b׮]puuT*T*ҥKV$\"z333>|Or]+}GWLWXǏǪUjr= H$H$Bxx8b8qꢤF.@sѫWbҥóg8700|YQQQBJKL&0J;99CEEh555G6-8q;>еIII! !9ֵ"P|)ϋpY)8ɹ:r]1Q^|>B!B!LLL`iiɾVd3زe  pvvF`` sΝ;wT*~K$޽J >.cϞ=8s SZ_֮]/O>mu4i6mڄz#..ץlZ**-xxx@.m~gEr\鲖at}BBRD_YY9s &&EEEJ SѥIrOQQ4O?D+>c__UcѢElE憓'O"##Ci& 0|>ӧOǽ{H$R3"ݻ!0uTV>m4ܹQQQT:pDccc|m"d7rJAٳFBdd$Fb|||p9v={VW2(C6٥CCC#؁H>}؍瓌 B!2)s%bjjJd2 F6BȎ;ȸq㈉҆b0 {.!RD"""8:u* UUUBn߾MRSSY9y$y)(( g'O& .$uuuߟl۶*eȻY˾G!eeea͂/sNQޝ)hߴLyMNV#bnnN:٪A7-kA݊$)) JUggΜ 8}4+ٳ1p@B|7L׊;99aƍ -[ &M c|055رcQ\\ʹHڃ.:z]1Q( X(=2@-[#Oғ`м"C)޽{w^󫪪 RDCPaiO è?,--u OOOxxx ++ۍZZ.(˄ڛ-}C(]H^pyvUܒ+Vں P( <<<^l& x(]J˽`-qppdK(.P(] EP( C3< '^>A3< BPzTy_{9t/_/555{?~ lՖ]7oFeeeP(JAk3w\ǫ%%%!$$#FѣGU;|>\\\ͅH$o۷o0[fLH ]pu= Pϟv܉AXz59`mm KKKDFF8t[hl 2 @(MYYf̘sssXYY555o}̘1puuł XyII O@;;;Ġ=+Wɓѯ_?JK|ែ۷#""B8|k7BPz6m == TJOOҥKuVիptt :Rv킫+[sҥlDTP#F#RRRc X`zjv Ɠ'Op5$&&?\bTUUEEE(--źu EBBߏ+ @Zܺu vb7lrd2l޼ ffggcĉŰL&Í7X6HKKCAAJJJj\W_UWW8SLQQ( Ҧɓ>|8Nϣ c/%&D"a/)!99YbX,f/ Nc!-- 7o|>nl5+ff '''0 Α1*++!J0 `bbڧyQFF֬Y###Bȑ#JBBBлwoq IDATyyy\W_eff...uyzz}3BPz*Oll,lll`ccǏcժU낂͗0 |}}ѧO8qZ- \'N^^^<%%ǏP(H$B||<={il~@rLxx8Xbl c).}GFPPl2>E|^۳ ɔڙ*I|_EEƠQjjj86( 0-[v=qI@%$,\@TaaV ô6lpQNee%̙~f0XjM0 rrrڱsN`](㫯'NX,@ ;~'amm ah.\gcc]p#_}gnn\\.1CP(*>PPXZZy<Μ9:uuuؿ?ؠ"<<{3g4O:GEEE H$ؽ{7RSS!㵵hhhénc& Dtt4esڊmԩS{.e*X/hhh۷Kx&M¦MP__b!88X/q#_}\hloaذaBP(/'Z,ZFR:@H3g>}0m4ܹQQQTug! 7߰1c@$ؽ{7D"N?44/_7D"{ 7n?&Le˖a„ *iӟ& 0|>ӧOǽ{MUaff///*ݞ`ر|LMM1vXxd2XrG>>>8w6gϞ2 Bt_D<݋{UUUA$!''G/8Dvv6  B>X,ƃމeDDD:R( EZ}:&V>zh\'OF~o*K˾}`툈P94߅MP(W5jb1PXX"bݺu Ɠ'Op5$&&*`cҥغu+jjjpU8:: T]vRRK.eK$HR:b8::"%%= W^:_r٘8q\x1 p $&&"66VMvv6PPP۷\ӑ/qyh)S( ByuK#PUU(**BZZ6o 333|DGG#11m/ B!'' c/UD"aq!99Y.EANsssr9222fA("<<GQj޽{>>>$t˿LE.OOOOYP(ʫX|N:n<|Ar9^RR d-/W@JJ 6l؀Bx<>>K+b?X>m_1OE***@={2L:ӑ/***4 ,,,PSSgϞu˽\ Bmu/~ *rkkk0 PZZ 6Z}gnn\\.1 v( zIC(B(kQcc#˱e#::@DGGY;wkkk///ōسgΜ9y5)6vvv>***D"ݻ Xb6?_~%RSSЀ۷o&MM6ňCppp|.u?r>B(++SX puuH$_͗M0bصkb1O}dd$.\OOOb~&ۿy_߆ &Nr<==2 DXXVXq uuk}?~<Μ9tӧOk/B!!B~ѩv(,,H$X<<+((qG-Z XQQQI> /~*!7- :ʪ"(!Er$Bt]"BGx,@!hB!zO@B!D!B%<B{kZBs4C!G !B%<B{B!DQC!G !B%<B{B!DQC!G !B%<B{B!DQC!G !B%<B{B!DQC!G !B%<B{B! q<<<;PC!G !B%<B{B!DQC!G !B%<B{B!DQC!G !B%<B{B!DQC!G !B%<B{B!DQC!G !B%<B{cwB!}Fx!(!BޣB!zB!=Jx!(!BޣB!zB!=Jx!(!BޣB!zB!=Jx!(!Bwq\@y2220gΜ婢B!܌yoP*!c!ݾ}7o<3#=4C!cgfFx!gܫp>PhByF=K#=B!ϰg%顄By= I%<BB!龜B!5顄Byܾ}6mRP(Ĕ)Spi̝;NɓR}B!aff5k֠Nƍӧ҂"Jx!20XXX`ƍ믿b}CoZ&BޣB!zBZwEXX{9c/vsA(bĉr _ qLLL0ydO|H$=8v 11O,X`no7?''#GTOkt!DX<~)S#&&5kfϞCgΜAkkB|֯_wbժU ۷ahhr_v̟? }u}__BJ8r֮] 5 Bii)aii PK @ 񨨨@uuF1x{{#==}ٲeHHH/))B!0qb";;WXSbذa9s&5;v`Ĉ011=ܬPÇ#--M:DGG Çj̚5 :t(|||p}_\\?d2}}@@@6l؀H$8;;_jEEEXd nܸX X]viԾpqqȑ#zj">>_G]re)ѣG!?:&>_( 1;;;b#??KKKh%y裏pqC*"22RiQQQh333|7x.],/'N 66[n?Q(ףeeeĆ 7B 2 [lꏌĉ!˱~z& e_upQ444ȑ#pvvƘ1c4^/eBJ000@qq1lmm]R / b ˅B—ڟbuzZK (|rlllCMM _uu5$I7[3ٳqa8\R!Atpp@ii:T;::8*$/6"ɓ'#**JmW.4zKoP;uw|2Qc_u$ Fl|w4CQAAAظq#q5۷Gvpuӳ[qppΞ=۩ϟ|{t߼y3Z[[qE~ݧMMM3f 8CUUUC-Z{ԩSZ;vLrgggL6 .;v n𿑊kT~D777T1eMΝ;jl_M̟?vѣGDWBZw!0i$,\111ڞF@@1~xD"~DASfffOPup%aҥS6##|ӧ+?abdffb׮]Zdϯf̘;v >>B(++Ӹ<##---pqqP(DPPn߾͗`ҤI033Ø1cp՟{bܸqرcG}Pwh puuH$_͗}_p<==;]S&2`ի݊pLct$}SbժU !˱}v"..! x/_)))s'! !zhjjP(DJJ ;,B2͙3s0!:.iB!DQC!G !B%:Qf޼ylӦM0;pz*c2&Yjj*[jknnfOfg̘BBB\.g ,((; :u*D~L&ߚǏ3PȎ?c=z/ dg>deeeg_|cl&HأGor6x`ܬ\]1Ǐ٣G۵kFmӤ~uLJ1Ʈ]<`1ssswӓdz&uggg#FOuBCCÇYKK ;<ߴVرcو#XUUg&L>;s \];y1ΗЇ> POW_}fϞcllӦM 2ݺumc/ 'Mݾ}`%%%|ǙD"Qsԩ쭷ѿ5|ҩMGݎtŃL!d پ};YJ___$%%N8@X[[cذa\lܸCDD@&81+**ӦM=y9ݓ"jǏﲬ1.ż}ۦ&ڦIO_qu;ǿ'4韵k>@hh(jjj/ Jbmmmq]פTt"!.777:N"ͧڏMM _uu5$ ͏?q=X777_|~:1ydDEEi~::]OOM˄ 0a$''#((MMM3f vv6D"8J8{,_iӦ!!!rpMu;֗^z k֬|ѢEػw/N:cǎLMM1el޼=Byy9wEw>u;>`ddpl4ٿrDѣGchmmŋqy\okk;wVI;v n𿑶k~سgjjj믿b={Fk?v=?}B !D-___5 f;w .] > IDAT???333w^7111;vByFFZZZP ܾ}q޼yS/v3f;Pd'°bŊn_mկ}>SA(#_]yFFaii>ӧOWE{yy!<<D믕wT7 0i$a̘1Dxx_~prrX,5_];X?quB%<}SbժUN9y$^uܹsC-_۷oGjj*;^B!}=&%%%8w9*sgm}nZGk׮ŦMWϝ;_~...O-Fe:VVVuŗ=6b,+--Ehh(  777*-suuƎk׮u}kimmݻww!77G;,BH?3g̙'uJHB@@TPPիZtIkXf {|pttp:y7?~܌իWK<_B>JxM6!$$!!!KΝ qN>HXXX /zuk|rqY8+z{&C{{{3~~~ HӸڥ=z49`~r!C@hh(G!''1tZW n:/?۷SL'1cqFjg,Zի Fwԝ"hG72 %#Gt9b`OBTTr9{9dddt2*\rgϞҥK066֨ jnng8*;E!D?xw! ! K/---C'q9qɯ _}2CCNOqTaa!23???7|(**V}/}!D%q2O^~j /(e 8 `mm={3fP&quY5"2pPyL@=+){u?oﵵXf RRR\_;WIoLnדh$8wghGG/S\xQ+u;99?O(OQ]],XVhBr<H###D">|ǏWXo[4iB!7$oA999R+qB?4[:޽ T?ygFKK &N_>[:C鮁6ݴL[na011 r9Jo&BEGGG+@yy9~7`ٲecwBtё~ c4C!G !B%<B{B!DM\Bٳg;BS:B!d`qwwP/։g u!M~~~ !)B!zB!=Jx!r $%%;$BQk%.uS_qq1~d2~;Moo% >|r:tjگk޽{ؽ{7䄕+W_ |ƾ}f|(++Ù3gkkka1b^z%$''֭[!#?~\(ףeeeĆ :#-- IIIq `ggPIwh?⋌ĉ!˱~z>>HIIBbSGgϞN˽֭[-Zc}Wlٌ1&O6mĚؐ!Cح[ƓcEFF/1Ƙ1d]nϖ/_߶mcbbm UUUc1vmǏgDΩSzKm{_~a%bVVVĄ1B6O믬M4oOow?6k,VUUŞyÚv5+G||d}7o344T/̶o޽۫}BtT~ǎCbb"… pqqXXX @@RRoĉ 5 Q @@/k7uuuPDm_SSFc:\QQ6m}IbX?OD"@zz:ϗi?d82'''gMooiaڴi]C!D_TPҥK#KP777?]~\sssP|{;@t~ <1uTC^^-[3f 22 28u<hoKKKa߾}x4BG(B$A$aȐ!omQ&L@rr2?\+ 466 cƌqBvvֶH$=z46oތV\xϟ˝1m4$$$@.n޼n套^š5k*D777T0#G vԩS܌QQQEdd$;ڊ|,_1cQ__OOOlڴ  :T>!mѩ^u EEE5jZZZn:%##ѰO0z ...hllH$[oܼyNdff7={`nnc*:oWΏC"&&111K?t:cc13wCݏ!`L:VP>yh\\RSSWVVC!337+K$DFFbܹpwwWknn.9Yf(^B)E˗/@;WΝ;~:?oׯ/ &NidB^ ~2WWWDEE=`(--Ehh(  77]8?$ }]Ou9s`Μ9Qt~BH%-B!=wB?!#!BH8xxxw(щz,XzwWD! B!DQC!G !B'< ,̶֯m`ggkkkxODW[NNF}ju.%%/2~ --[&2pl£|!!!8+WDcc#1r _]:wttq(,,P(vյr9iB1Z]keePUU[[[@ee%WiڿhhhPCC)!gN# ! 0dXZZ 466 cƌX% pT*UQ]:wvvƴiӐ\y&4_]ϟܽ{ǏTwAmmFqiSL͛##-- aaa݊_Y|;PUU4k׮a=n+!E^u7Na???,]@ػw/ƍ;[>##---pqqP(DPPn߾Qݚoݺutܺ,n|OP/B puuH$_ݭ>P%==2 DXXVXѭUŧIb8s8O>3fhńB̝/M]X,Faa!D"Q?D;'i Fnn.9Yf!}pwwիW;tvވBUU.]ԩ>ŋdB%:ur*<2길8XYYCTB %<[$ !F!(!BޣKZB6wwY`ADDfVM`ȑ=޾;۾v۶mx4鿧?=&O'NtZ^O!D{|]0\rr2v L۷kgԄNe֭Ö-[p޽~BHWt63g;-/..B!;MLYWWhXYY111xU}QQb1,Y7n@,C,c׮]ZoX;w`ɒ%Xj_c1&&&Gbb"rooo~_l4]D@@6l؀H$8;;@AAA055-V^0yRRRx.wppҺnB!}Gge"##1qDr_'OT(B}}=JKKQVVJlذAUm?j(HRܹRRZoۗ T >|R[n7t(Ih*-- IIIq `ggXp! L~ YYY!X[O.#??JnBHP qU;044?|||2a˖-033P(DBB4m_ Eaa?-|BCCN\i]]BHЩRSS~z@CC;D  qm+**ӦMuܡNo\lܸCDDDxbqe`)􏃃C~=i\e]>?~Ai%>B!=S Ott4BCCK.E@@^}U bA._8uuu8 JqҎOugt{elll>eݻٳg qXr%ur9Q{bWZYY8TUUPYY kknߛgnn744ؘB:uIK(B$A$aȐ!H$pssÁRgϞwvvƴiӐ\yf`ddmwŝ;wP[[ۣVillDSSƌ' 븺wQOobʔ)ؼy3=zr!,,[UUUJcvFB.Jx:z1nܸN333w^7111;vByFFZZZP ܾ}[a333| P(_|ѭWWWD"|^mO'''$''~~~Xt)Yn.]77.JOOL&5<==+Vt+?[[[?gΜQӧ1c -B6p_!.G!QXXHinX|94~ڭ#G`֬Y}/ByΎˢPUUX}/^!_#HgeN8XlݺǏ?GGG|…:t(d2~'deeuzlAApuTTTsW@vv6R)lllpaHRlݺ/?>R)"##o޼yF}}=>.rDZZp @ QZZ2TVVbÆ ׫.~u7337|ҥK®]ruGr9ո݄Bã2c IDATCȀKxvލhR-?gQCCN:>TUU)ڵk=z6G!DT믿quZ>c ر @YY_Lkkkxzz",, +VZ\&LX,Fff&vXӧχ%>L>]afݺutܰtRukhii B!pmů??.Ww|4ǏǙ3gri BkڵM?+d2i ]^^TM.{7ٲe<[X?9>7k,gw76}.꘹9+++{QTf͚#G۲f~?Xݻ|U_ c`/_:7oLjuW\\RSS{\\.gdseZի swwZ}IgGx˗/ܹs]<.ϰxbD~x@+**o>=k}nTVV믿̙3accs8pcZ@A 9s&RRRT**1hSY\\>~<:'FFFl0pٳgbccuܹ'1_rpppA`oo_~QoOv|Z4 ]=ڛO>HXXX{nnx)ih=URRM6a„ DXl҂_~9Ο?C1τ^3pI]FѣGOfff~m1XZ\|{QYWw #ԡL&d|c U9N7ߟ>E,wl޼@ιs0dZ YYY8{,pܞm7VVVvڦrф` 6Lxzw{[wx83л};Ds%9Wu^te۶mɓ'ĉ#))K._ƍí[w}v[###;޽{ eBII ƏoooB;wqssfoڞdJfVvK,WoboVu3nwxn<Ϫ;xl28::;@lcǎENN~7V]W^ʾ3h<99wL&H #֭Ö-[:}YW:k6l 33u_gΜGt.^˗/wZ_(/amdҤI044Ą p-P͘==={b[A,cɒ%qb1b1vڥq\X,Fhhhibw%K N;v`Ĉ011=2t8x  1|pi>MTWWc֬Y077СCk?d{U(%%/ QB7,--=N ///F$;766b̘15j|}}.tԞȴhKxo:g{U:&<ڎyΝ022Ç[[[ oT-Tzڌ ~077op5qZ雞8^^^Op5cƍÇ7xvvvGJJ Ri2l£̼yz|G梊B}}=JKKQVVJlذ0j(HRܹRRJJΆT* > T[fffo\t YYY '[ԵO7n@ @MM d2lCCCGUljr9';Z>>u=<ٞ={ݸq`UUU2{1}6JJJǏ3DPOvvwhj8ɏHL^O y7Y>o<ܹs~SYYnܸQ\XXy(͛e˖ׯ_W3pР*2bӧO=G7Uө=駟j,]tL[KL6-Zlph/\m7o<ПҎE}477JWWZy[[)))^}7oݎ餰]v1cƌIoo/םBuMRRR5ksΥ.fϞMMM8g_DDEEE>LLSZZn`Æ dffvZϟ`@UUN6Ν;l6bbb0 466(83޽/Kzj o<^4^\۾Fv 7 ==][g|=T_o|wEXXtvODLL /.]֭[Gff&6mg(M9<`XX,} fĶmŋ?^KVVUUUrq8&۷o{vINN`0>l9pNG;}#q8ܼyYf (z/\5lEӡ]FRR !#ѣܺu M__ٽ{tv& jݺuګC@TTdgg{P__O?qqq(BnnuRSS'>>޽{udV+JvvO?4ӟ_dÆ [g*++QDz7+W|r"""HNNhG7d"%%3glӧn!SxN " WZر#@K5}ɤe!&@LZ&//_455q9Ӊɭv.]4lǎaX!3!фGss3vۍ(ԐkoݺgY||ډh?ԴihhhT|EE+bT&]'r: daCwl6q&bio%!&gN}'IHK!b&| B< T]B_7{noĊё!Bqo_B! 2#B'!B=!"MI7@Eux!""hɛbuxGFxB#B'!B=!D=zg}6 ;k U^^oۀ_w49y㕕{㐑 dҲBL!%%%̘1cgnV\9쭷^cΜ9㐝f2#R ؿMMM( ytuuQZZJtt4QQQիWZ|7XVV+uuu~?##_|ݻ1, :tՊnȑ#^s裏HHH@Q,X}tsVh42g2f3iiiq!/ /N8Q^\\Lww7ʹӟ>l6---+_TTҥKa˖-|?k/3mZZZ(**ɓ+/?餣۷233}އ+WzC@bLa0o/y eҥ,[L+omml߾E?8 ƍ|lڴ222HKKUѣ?T{d2?,,w҂` --p_},Xg\tIwB%sxA-[rp8 .с``޼yZ?X~ϟ?_;O*~qq1ƒ;6RRR|yf8N ٵk|NFyDFFu>BcQRv;6l 33k0| t:1L@LL FE1́Xx1< .h"VGْ%KZ>|:V+ZyNN{졲EQHLLUw+W|r"""HNN|L&)))9s}8}6wH@2"<<ίG=f26m ^ ::zStx ȵGLE^1DL%HK!AO:l38d,Rzz:'Ov{ǟdҲB6aܺuׯ>v{]୷^cΜ9O4HFxSJAAZɻիlrU`poR>nSS(Bff&yyy;ZCZv92,;wXjF9s搖Foo/W^jꫯ7`ZZi}z|G$$$( ,`߾}ZY[[̞=~; F_x^uVX~#~_p=󗿿+P21f3iiiU*AP}zկ~oZb b_եvtt999ի՞rM-R+++Uۭ_ROL}7RvקϟWys?}8qBUE=qKzرcZʕ+ՒOmmmUyuΝZyjj/W511Q>z5/vww!!!jKK׿?ϴ*#! DGGپ};(BUU10_~oMhh(K.eٲeݻwiii`0Fxx>=>CJKKG1&Ir_i&fΜba|'bO>3fBSSQ~W`B-BZl qv8 .0 ++K;6q`00o<Пؼy3vN'ڵK| }z⵬UU?vl6qރ]BBBpݣ[z?@NF㈱"##2+#zv6l@ff&k׮# x}K'&&@cc# +;w.t:1Lͦ;GΝ;q{l;a0n/?l6k{=,::@{{zjT1|s5Me]WyhrXr&'@i !(X,, DEEiCC%++*zzzXvl$%%m6x"ϟU7o?2sLsL&oߦsT9pNG+g?9۶m~֭[۷5k֌*7lf={u|.\E{{s]FRRҨ+bJYn/uzCQrss<loK,jraZIDATdggkW\aDDDLQQ9O||<{qg*++QDZ[[SO=ŢEXf b>ď3k֬AQmDMOy d"%%3g<(XBL2CM dest=:1.189 reply_serial=2 array [ object path "/org/freedesktop/ModemManager/Devices/0" object path "/org/freedesktop/ModemManager/Devices/1" ] Wader has found two devices present in the system. The ``EnumerateDevices`` method call returns an array of object paths. The next operation should **always** be ``org.freedesktop.ModemManager.Modem.Enable``. This method receives a boolean argument indicating whether the device should be enabled or not:: dbus-send --system --dest=org.freedesktop.ModemManager --print-reply \ /org/freedesktop/ModemManager/Devices/1 \ org.freedesktop.ModemManager.Modem.Enable boolean:true Error org.freedesktop.ModemManager.Modem.Gsm: SimPinRequired: \ org.freedesktop.ModemManager.Error.PIN: org.freedesktop.ModemManager.Error.PIN: In this case, the ``Enable`` operation has raised an exception, PIN/PUK is needed. The ``Enable`` machinery, and its state, will be resumed if we send the correct PIN/PUK. Had we been previously authenticated or PIN/PUK were not enabled, the method would not have returned anything:: dbus-send --system --dest=org.freedesktop.ModemManager --print-reply \ /org/freedesktop/ModemManager/Devices/1 \ org.freedesktop.ModemManager.Modem.Gsm.Card.SendPin string:0000 method return sender=:1.193 -> dest=:1.191 reply_serial=2 After successfully entering the PIN, the device is given about fifteen seconds to settle and perform its internal setup. After this point you can interact with the device as you please. .. note:: There are plans to create a `device-specific interface`_ that will ask the device when its ready rather than just giving 15 seconds and hoping that it will suffice. .. _device-specific interface: http://public.warp.es/wader/ticket/77 And how do you obtain the object paths of the devices then? +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Wader internally interacts with :term:`udev` and queries several subsystems in order to get possible devices in the system. The internal machinery will filter out all the uninteresting devices, and will set up and initialize the rest. This process includes finding all the serial ports that a device registers, identifying it (via vendor/product id match, or by its model name). Once all this is in place, Wader will build a ``DevicePlugin`` out of it. Wader will emit a ``DeviceAdded`` :term:`DBus` signal when a new 3G device has been added, and a ``DeviceRemoved`` signal when a 3G device has been removed. As mentioned above, the method ``org.freedesktop.ModemManager.EnumerateDevices`` returns an array of object paths, one for each device found in the system:: dbus-send --system --print-reply --dest=org.freedesktop.ModemManager \ /org/freedesktop/ModemManager org.freedesktop.ModemManager.EnumerateDevices method return sender=:1.156 -> dest=:1.155 reply_serial=2 array [ object path "/org/freedesktop/ModemManager/Devices/0" object path "/org/freedesktop/ModemManager/Devices/1" object path "/org/freedesktop/ModemManager/Devices/2" ] This is the response of ``EnumerateDevices`` with a Huawei E172, an E870 and an Option GlobeTrotter 3G+ (Nozomi). Wader is able to detect, configure and use the three at the same time with no interference between them whatsoever. Operations you might want to do on a device =========================================== Once your device is set up, you will probably want to register with a given operator, or perhaps letting Wader to choose itself? Perhaps you want to do a high-level operation such as configuring the band and connection mode? We are going to provide examples for every one of them: Registering to a network ++++++++++++++++++++++++ Registering to a network will not be necessary most of the time as the devices themselves will register to its home network. Manually specifying a :term:`MNC` to connect to an arbitrary network is possible, though:: dbus-send --type=method_call --print-reply --dest=org.freedesktop.ModemManager \ /org/freedesktop/ModemManager/Devices/0 \ org.freedesktop.ModemManager.Modem.Gsm.Network.Register string:21401 method return sender=:1.193 -> dest=:1.193 reply_serial=2 It is also possible to pass an empty string, and that will register to the home network:: dbus-send --system --dest=org.freedesktop.ModemManager --print-reply \ /org/freedesktop/ModemManager/Devices/0 \ org.freedesktop.ModemManager.Modem.Gsm.Network.Register string: method return sender=:1.193 -> dest=:1.195 reply_serial=2 dbus-send --system --dest=org.freedesktop.ModemManager --print-reply \ /org/freedesktop/ModemManager/Devices/0 \ org.freedesktop.ModemManager.Modem.Gsm.Network.GetRegistrationInfo method return sender=:1.193 -> dest=:1.196 reply_serial=2 struct { uint32 1 string "21401" string "vodafone ES" } The :term:`MNC` ``21401`` is Vodafone Spain's MNC, my current network provider. If I try to connect to Telefonica's :term:`MNC` ``21407``, the operation will probably horribly fail:: dbus-send --type=method_call --print-reply --dest=org.freedesktop.ModemManager \ /org/freedesktop/ModemManager/Devices/0 \ org.freedesktop.ModemManager.Modem.Gsm.Network.Register string:21407 method return sender=:1.193 -> dest=:1.197 reply_serial=2 Oops, it didn't fail :). Although if I try to connect to Internet it will fail for sure as the APN is completely different. Configuring connection settings +++++++++++++++++++++++++++++++ You might be interested on changing the connection mode from 2G to 3G. Or perhaps you are interested on changing from `GSM1900` to `GSM850` if you are roaming. Whatever your needs are, you are looking for the ``org.freedesktop.ModemManager.Modem.Gsm.Network.SetBand`` and ``org.freedesktop.ModemManager.Modem.Gsm.Network.SetNetworkMode`` methods. This methods and their parameters are thoroughly described in :term:`ModemManager`'s excellent API. Sending a SMS +++++++++++++ Sending a SMS can not be any easier:: from wader.common.sms import Message ... def sms_cb(indexes): print "SMS sent spanning", indexes def sms_eb(e): print "Error sending SMS", e sms = Message("+34612345678", "hey dude") device.Send(sms.to_dict(), dbus_interface=consts.SMS_INTFACE, reply_handler=sms_cb, error_handler=sms_eb) And sending an UCS2 encoded SMS can't get any easier either:: from wader.common.sms import Message ... def sms_cb(indexes): print "SMS sent spanning", indexes def sms_eb(e): print "Error sending SMS", e sms = Message("+34612345678", "àèìòù") device.Send(sms.to_dict(), dbus_interface=consts.SMS_INTFACE, reply_handler=sms_cb, error_handler=sms_eb) Adding/Reading a Contact ++++++++++++++++++++++++ Adding a contact to the SIM and getting the index where it was stored:: dbus-send --system --print-reply --dest=org.freedesktop.ModemManager \ /org/freedesktop/ModemManager/Devices/0 \ org.freedesktop.ModemManager.Modem.Gsm.Contacts.Add string:Pablo string:+34545665655 method return sender=:1.54 -> dest=:1.57 reply_serial=2 uint32 1 And reading it again:: dbus-send --system --print-reply --dest=org.freedesktop.ModemManager \ /org/freedesktop/ModemManager/Devices/0 \ org.freedesktop.ModemManager.Modem.Gsm.Contacts.Get uint32:1 method return sender=:1.54 -> dest=:1.58 reply_serial=2 struct { uint32 1 string "Paul" string "+34545665655" } Now lets add another contact and read all the contacts in the SIM card:: dbus-send --system --print-reply --dest=org.freedesktop.ModemManager \ /org/freedesktop/ModemManager/Devices/0 \ org.freedesktop.ModemManager.Modem.Gsm.Contacts.Add string:John string:+33546657784 method return sender=:1.54 -> dest=:1.60 reply_serial=2 uint32 2 dbus-send --system --print-reply --dest=org.freedesktop.ModemManager \ /org/freedesktop/ModemManager/Devices/0 \ org.freedesktop.ModemManager.Modem.Gsm.Contacts.List method return sender=:1.54 -> dest=:1.61 reply_serial=2 array [ struct { uint32 1 string "Paul" string "+34545665655" } struct { uint32 2 string "John" string "+33546657784" } ] Data calls ========== Connecting to the Internet just requires knowing the object path of a device with dialup capabilities and a profile. While the former is obtained through a ``EnumerateDevices``, the latter requires to create it explicitly. Be it through Wader (plain backend) or :term:`NetworkManager` (NM backend), a profile is required to connect. Profile creation ++++++++++++++++ Profiles in Wader, known as connections in :term:`NetworkManager` lingo, are stored using the `GConf`_ configuration system (whilst using the NM backend) or serialized to the hard drive (with the plain backend). When a new profile is written, a new profile -or connection- is created and exported on :term:`DBus`. When the profile is ready to be used, a ``NewConnection`` signal is emitted with the profile object path as its only argument. .. _GConf: http://projects.gnome.org/gconf/ Connecting ++++++++++ Armed with the object paths of the profile and device to use, we just need to pass this two arguments to :meth:`~wader.common.dialer.DialerManager.ActivateConnection`. Under the hood this method will perform the following: - Get a :class:`~wader.common.dialer.Dialer` instance for this device. If :term:`NetworkManager` 0.8+ is present, it will use the NM backend and thus it will use :class:`~wader.common.dialers.nm_dialer.NMDialer`. If we are using the plain backend, then depending on the device, Wader will use either :class:`~wader.common.dialers.hsolink.HSODialer`, or :class:`~wader.common.dialers.wvdial.WVDialDialer`. - The dialer will obtain from the profile the needed settings to connect: apn, username, whether DNS should be static or not, etc. Obtaining the password associated with a profile is a different story though. With the NM backend, passwords (secrets in NM lingo) are stored in `gnome-keyring-daemon`. If the plain backend is in use, then it will use its own keyring backend (encrypted with AES). Every profile has an :term:`UUID` that identifies it uniquely. The connection secrets are associated by the UUID. If ``ActivateConnection`` succeeds, it will return the object path of the connection, connections are identified by it and its required to save somewhere this object path to stop the connection later on. Disconnecting +++++++++++++ Disconnecting could not be easier, you just need to pass the object path returned by ``ActivateConnection`` to :meth:`~wader.common.dialer.DialerManager.DeactivateConnection`. This will deallocate all the resources allocated by ``ActivateConnection``. Interact via wader.common.modemmanager.ModemManager =================================================== The :class:`wader.common.modemmanager.ModemManager` provides an easy to use interface to access the ModemManager DBus service with minimal fuss:: from wader.common.modemmanager import ModemManager manager = ModemManager() device = manager.get_devices()[0] device.Enable(True) print device.GetRegistrationInfo() Sending a SMS +++++++++++++ Sending a SMS can not be any easier:: from wader.common.consts import SMS_INTFACE from wader.common.modemmanager import ModemManager manager = ModemManager() device = manager.get_devices()[0] device.Enable(True) device.Send({'sender': '+34612345678', 'text': "hey dude"}, dbus_interface=SMS_INTFACE) And sending an UCS2 encoded SMS can't get any easier either:: from wader.common.consts import SMS_INTFACE from wader.common.modemmanager import ModemManager manager = ModemManager() device = manager.get_devices()[0] device.Enable(True) device.Send({'sender': '+34612345678', 'text': "àèìòù"}, dbus_interface=SMS_INTFACE) Adding/Reading a Contact ++++++++++++++++++++++++ Adding a contact to the SIM and getting the index where it was stored:: from wader.common.consts import CTS_INTFACE from wader.common.modemmanager import ModemManager manager = ModemManager() device = manager.get_devices()[0] device.Enable(True) # returns 1 print device.Add("Paul", "+34545665655", dbus_interface=CTS_INTFACE) And reading it again:: from wader.common.consts import CTS_INTFACE from wader.common.modemmanager import ModemManager manager = ModemManager() device = manager.get_devices()[0] device.Enable(True) print device.Get(1, dbus_interface=CTS_INTFACE) Now lets add another contact and read all the contacts in the SIM card:: from wader.common.consts import CTS_INTFACE from wader.common.modemmanager import ModemManager manager = ModemManager() device = manager.get_devices()[0] device.Enable(True) # returns 2 print device.Add("John", "+33546657784", dbus_interface=CTS_INTFACE) print device.List(dbus_interface=CTS_INTFACE) Troubleshooting =============== Operation X failed on my device +++++++++++++++++++++++++++++++ Every device its a world on its own, sometimes they are shipped with a buggy firmware, sometimes a device will reply to a command on a slightly different way that will break the parsing of the reply. Wader ships with a test suite that might yield some clues about what went wrong. Instructions to execute it:: trial -r gtk2 wader.test Do not forget the :option:`-r gtk2` switch, it will pick the `gtk2` reactor to run the tests, otherwise all the glib-dependent tests, like the :meth:`DBus` ones will fail. wader-0.5.13/doc/glossary.rst000066400000000000000000000026321257646610200160600ustar00rootroot00000000000000.. _glossary: ======== Glossary ======== .. glossary:: :sorted: NetworkManager This tool has managed to be the de-facto `network configuration`__ tool in the last years on the Linux desktop. Almost all modern Linux distros rely on it. __ http://projects.gnome.org/NetworkManager/ ModemManager `An API defined`__ between the NetworkManager and Wader project to communicate with mobile broadband (GSM, CDMA, UMTS, ...) cards. It implements a loadable plugin interface to add work-arounds for non standard devices. __ http://trac.warp.es/wader/wiki/WhatsModemManager MNC `Mobile Network Code`__ uniquely identifies an operator. __ http://en.wikipedia.org/wiki/Mobile_Network_Code DBus `A message bus system`__, a simple way for applications to talk to one another. It has replaced similar technologies such as Bonobo (Gnome) or DCOP (KDE). __ http://www.freedesktop.org/wiki/Software/dbus udev udev is the device manager for the Linux 2.6 kernel series. It is the successor of devfs and hotplug, which means that it handles the /dev directory and all user space actions when adding/removing devices, including firmware load. UUID `Universally unique identifier`__. __ http://en.wikipedia.org/wiki/Universally_Unique_Identifier wader-0.5.13/doc/modules/000077500000000000000000000000001257646610200151305ustar00rootroot00000000000000wader-0.5.13/doc/modules/core/000077500000000000000000000000001257646610200160605ustar00rootroot00000000000000wader-0.5.13/doc/modules/core/backends/000077500000000000000000000000001257646610200176325ustar00rootroot00000000000000wader-0.5.13/doc/modules/core/backends/nm.rst000066400000000000000000000004011257646610200207710ustar00rootroot00000000000000:mod:`core.backends.nm` =============================== .. automodule:: core.backends.nm Classes -------- .. autoclass:: NMDialer :members: :show-inheritance: :undoc-members: .. autoclass:: NetworkManagerBackend :members: :undoc-members: wader-0.5.13/doc/modules/core/backends/plain.rst000066400000000000000000000010611257646610200214650ustar00rootroot00000000000000:mod:`core.backends.plain` ================================== .. automodule:: core.backends.plain Classes -------- .. autoclass:: WVDialDialer :members: :show-inheritance: :undoc-members: .. autoclass:: WVDialProtocol :members: :show-inheritance: :undoc-members: .. autoclass:: HSODialer :members: :show-inheritance: :undoc-members: .. autoclass:: PlainBackend :members: :undoc-members: Functions --------- .. autofunction:: get_wvdial_conf_file(conf, serial_port) .. autofunction:: _generate_wvdial_conf(conf, sport) wader-0.5.13/doc/modules/core/command.rst000066400000000000000000000003661257646610200202350ustar00rootroot00000000000000:mod:`core.command` =========================== .. automodule:: core.command Classes ------- .. autoclass:: ATCmd :members: Functions --------- .. autofunction:: get_cmd_dict_copy() .. autofunction:: build_cmd_dict(extract, end, error) wader-0.5.13/doc/modules/core/contact.rst000066400000000000000000000003051257646610200202430ustar00rootroot00000000000000:mod:`core.contact` =========================== .. automodule:: core.contact Classes -------- .. autoclass:: Contact :show-inheritance: :members: :inherited-members: :undoc-members: wader-0.5.13/doc/modules/core/daemon.rst000066400000000000000000000006161257646610200200600ustar00rootroot00000000000000:mod:`core.daemon` ========================== .. automodule:: core.daemon Classes -------- .. autoclass:: WaderDaemon :members: .. autoclass:: SignalQualityDaemon :show-inheritance: :members: .. autoclass:: NetworkRegistrationDaemon :show-inheritance: :members: .. autoclass:: WaderDaemonCollection :members: Functions --------- .. autofunction:: build_daemon_collection wader-0.5.13/doc/modules/core/dialer.rst000066400000000000000000000004721257646610200200550ustar00rootroot00000000000000:mod:`core.dialer` ========================== .. automodule:: core.dialer Classes -------- .. autoclass:: DialerConf :members: :undoc-members: .. autoclass:: Dialer :members: :show-inheritance: :undoc-members: .. autoclass:: DialerManager :members: :show-inheritance: :undoc-members: wader-0.5.13/doc/modules/core/exported.rst000066400000000000000000000010521257646610200204420ustar00rootroot00000000000000:mod:`core.exported` ============================ .. automodule:: core.exported Functions --------- .. autofunction:: to_a(_list, signature='u') Classes -------- .. autoclass:: ModemExporter :members: .. autoclass:: SimpleExporter :members: .. autoclass:: CardExporter :members: .. autoclass:: NetworkExporter :members: .. autoclass:: MmsExporter :members: .. autoclass:: SmsExporter :members: .. autoclass:: ContactsExporter :members: .. autoclass:: WCDMAExporter :members: .. autoclass:: HSOExporter :members: wader-0.5.13/doc/modules/core/hardware/000077500000000000000000000000001257646610200176555ustar00rootroot00000000000000wader-0.5.13/doc/modules/core/hardware/base.rst000066400000000000000000000004721257646610200213240ustar00rootroot00000000000000:mod:`core.hardware.base` ================================= .. automodule:: core.hardware.base Classes -------- .. autoclass:: WCDMACustomizer :members: :undoc-members: Functions --------- .. autofunction:: identify_device(port) .. autofunction:: probe_port(port) .. autofunction:: probe_ports(ports) wader-0.5.13/doc/modules/core/hardware/huawei.rst000066400000000000000000000007161257646610200216750ustar00rootroot00000000000000:mod:`core.hardware.huawei` =================================== .. automodule:: core.hardware.huawei Classes -------- .. autoclass:: HuaweiWCDMACustomizer :members: :undoc-members: .. autoclass:: HuaweiWCDMAWrapper :members: :undoc-members: .. autoclass:: HuaweiSIMClass :members: :undoc-members: .. autoclass:: HuaweiWCDMACustomizer :members: :undoc-members: .. autoclass:: HuaweiWCDMADevicePlugin :members: :undoc-members: wader-0.5.13/doc/modules/core/hardware/novatel.rst000066400000000000000000000004131257646610200220550ustar00rootroot00000000000000:mod:`core.hardware.novatel` ==================================== .. automodule:: core.hardware.novatel Classes -------- .. autoclass:: NovatelWCDMACustomizer :members: :undoc-members: .. autoclass:: NovatelWCDMADevicePlugin :members: :undoc-members: wader-0.5.13/doc/modules/core/hardware/option.rst000066400000000000000000000010271257646610200217170ustar00rootroot00000000000000:mod:`core.hardware.option` =================================== .. automodule:: core.hardware.option Classes -------- .. autoclass:: OptionWCDMACustomizer :members: :undoc-members: .. autoclass:: OptionHSOWCDMACustomizer :members: :undoc-members: .. autoclass:: OptionWrapper :members: :undoc-members: .. autoclass:: OptionSIMClass :members: :undoc-members: .. autoclass:: OptionWCDMADevicePlugin :members: :undoc-members: .. autoclass:: OptionHSOWCDMADevicePlugin :members: :undoc-members: wader-0.5.13/doc/modules/core/hardware/sierra.rst000066400000000000000000000004161257646610200216750ustar00rootroot00000000000000:mod:`core.hardware.sierra` =================================== .. automodule:: core.hardware.sierra Classes -------- .. autoclass:: SierraWirelessWCDMACustomizer :members: :undoc-members: .. autoclass:: SierraWCDMADevicePlugin :members: :undoc-members: wader-0.5.13/doc/modules/core/hardware/sonyericsson.rst000066400000000000000000000003221257646610200231420ustar00rootroot00000000000000:mod:`core.hardware.sonyericsson` ========================================== .. automodule:: core.hardware.sonyericsson Classes -------- .. autoclass:: SonyEricssonCustomizer :members: :undoc-members: wader-0.5.13/doc/modules/core/hardware/zte.rst000066400000000000000000000003671257646610200212170ustar00rootroot00000000000000:mod:`core.hardware.zte` ================================ .. automodule:: core.hardware.zte Classes -------- .. autoclass:: ZTEWCDMACustomizer :members: :undoc-members: .. autoclass:: ZTEWCDMADevicePlugin :members: :undoc-members: wader-0.5.13/doc/modules/core/mal.rst000066400000000000000000000002111257646610200173550ustar00rootroot00000000000000:mod:`core.mal` ========================== .. automodule:: core.mal Classes -------- .. autoclass:: MessageAssemblyLayer :members: wader-0.5.13/doc/modules/core/middleware.rst000066400000000000000000000002231257646610200207240ustar00rootroot00000000000000:mod:`core.middleware` ============================== .. automodule:: core.middleware Classes -------- .. autoclass:: WCDMAWrapper :members: wader-0.5.13/doc/modules/core/modemmanager.rst000066400000000000000000000002541257646610200212470ustar00rootroot00000000000000:mod:`core.modemmanager` ================================ .. automodule:: core.modemmanager Classes -------- .. autoclass:: ModemManager :members: :undoc-members: wader-0.5.13/doc/modules/core/oal.rst000066400000000000000000000002001257646610200173550ustar00rootroot00000000000000:mod:`core.oal` ============================ .. automodule:: core.oal Functions --------- .. autofunction:: get_os_object() wader-0.5.13/doc/modules/core/oses/000077500000000000000000000000001257646610200170315ustar00rootroot00000000000000wader-0.5.13/doc/modules/core/oses/linux.rst000066400000000000000000000003731257646610200207250ustar00rootroot00000000000000:mod:`core.oses.linux` ============================== .. automodule:: core.oses.linux Classes -------- .. autoclass:: HardwareManager :members: :undoc-members: .. autoclass:: LinuxPlugin :members: :show-inheritance: :undoc-members: wader-0.5.13/doc/modules/core/oses/osx.rst000066400000000000000000000003641257646610200203770ustar00rootroot00000000000000:mod:`core.oses.osx` ============================ .. automodule:: core.oses.osx Classes -------- .. autoclass:: HardwareManager :members: :undoc-members: .. autoclass:: OSXPlugin :members: :show-inheritance: :undoc-members: wader-0.5.13/doc/modules/core/plugin.rst000066400000000000000000000005101257646610200201040ustar00rootroot00000000000000:mod:`core.plugin` ========================== .. automodule:: core.plugin Classes -------- .. autoclass:: DevicePlugin :show-inheritance: :members: :inherited-members: :undoc-members: .. autoclass:: RemoteDevicePlugin :members: .. autoclass:: OSPlugin :members: .. autoclass:: PluginManager :members: wader-0.5.13/doc/modules/core/protocol.rst000066400000000000000000000003601257646610200204520ustar00rootroot00000000000000:mod:`core.protocol` ============================== .. automodule:: core.protocol Classes -------- .. autoclass:: BufferingStateMachine :members: .. autoclass:: SerialProtocol :members: .. autoclass:: WCDMAProtocol :members: wader-0.5.13/doc/modules/core/serialport.rst000066400000000000000000000003541257646610200210000ustar00rootroot00000000000000:mod:`core.serialport` ============================== .. automodule:: core.serialport Classes -------- .. autoclass:: Port :members: .. autoclass:: Ports :members: .. autoclass:: SerialPort :show-inheritance: :members: wader-0.5.13/doc/modules/core/shell.rst000066400000000000000000000002341257646610200177200ustar00rootroot00000000000000:mod:`core.shell` ========================= .. automodule:: core.shell Functions --------- .. autofunction:: get_manhole_factory(namespace, **paswords) wader-0.5.13/doc/modules/core/sim.rst000066400000000000000000000003011257646610200173740ustar00rootroot00000000000000:mod:`core.sim` ========================== .. automodule:: core.sim Classes -------- .. autoclass:: SIMBaseClass :show-inheritance: :members: :inherited-members: :undoc-members: wader-0.5.13/doc/modules/core/startup.rst000066400000000000000000000006551257646610200203220ustar00rootroot00000000000000:mod:`core.startup` =========================== .. automodule:: core.startup Classes -------- .. autoclass:: WaderService :members: .. autoclass:: StartupController :members: :show-inheritance: Functions --------- .. autofunction:: get_wader_application() .. autofunction:: attach_to_serial_port(device) .. autofunction:: setup_and_export_device(device) .. autofunction:: create_skeleton_and_do_initial_setup() wader-0.5.13/doc/modules/core/statem/000077500000000000000000000000001257646610200173555ustar00rootroot00000000000000wader-0.5.13/doc/modules/core/statem/auth.rst000066400000000000000000000002321257646610200210450ustar00rootroot00000000000000:mod:`core.statem.auth` =============================== .. automodule:: core.statem.auth Classes -------- .. autoclass:: AuthStateMachine :members: wader-0.5.13/doc/modules/core/statem/netreg.rst000066400000000000000000000002731257646610200213750ustar00rootroot00000000000000:mod:`core.statem.networkreg` ===================================== .. automodule:: core.statem.networkreg Classes -------- .. autoclass:: NetworkRegistrationStateMachine :members: wader-0.5.13/doc/modules/core/statem/simple.rst000066400000000000000000000002421257646610200213760ustar00rootroot00000000000000:mod:`core.statem.simple` ================================= .. automodule:: core.statem.simple Classes -------- .. autoclass:: SimpleStateMachine :members: wader-0.5.13/doc/modules/wader/000077500000000000000000000000001257646610200162325ustar00rootroot00000000000000wader-0.5.13/doc/modules/wader/common/000077500000000000000000000000001257646610200175225ustar00rootroot00000000000000wader-0.5.13/doc/modules/wader/common/_dbus.rst000066400000000000000000000003501257646610200213460ustar00rootroot00000000000000:mod:`wader.common._dbus` ========================= .. automodule:: wader.common._dbus Classes -------- .. autoclass:: DBusExporterHelper :members: .. autoclass:: DelayableDBusObject :members: .. autofunction:: delayable wader-0.5.13/doc/modules/wader/common/aterrors.rst000066400000000000000000000057641257646610200221310ustar00rootroot00000000000000:mod:`wader.common.aterrors` ============================ .. automodule:: wader.common.aterrors Functions --------- .. autofunction:: error_to_human .. autofunction:: extract_error Exceptions ---------- .. autoexception:: General .. autoexception:: InputValueError .. autoexception:: SerialResponseTimeout .. autoexception:: PhoneFailure .. autoexception:: NoConnection .. autoexception:: LinkReserved .. autoexception:: OperationNotAllowed .. autoexception:: OperationNotSupported .. autoexception:: PhSimPinRequired .. autoexception:: PhFSimPinRequired .. autoexception:: PhFPukRequired .. autoexception:: SimNotInserted .. autoexception:: SimPinRequired .. autoexception:: SimPukRequired .. autoexception:: SimFailure .. autoexception:: SimBusy .. autoexception:: SimWrong .. autoexception:: SimNotStarted .. autoexception:: IncorrectPassword .. autoexception:: SimPin2Required .. autoexception:: SimPuk2Required .. autoexception:: MemoryFull .. autoexception:: InvalidIndex .. autoexception:: NotFound .. autoexception:: MemoryFailure .. autoexception:: TextTooLong .. autoexception:: InvalidChars .. autoexception:: DialStringTooLong .. autoexception:: InvalidDialString .. autoexception:: NoNetwork .. autoexception:: NetworkTimeout .. autoexception:: NetworkNotAllowed .. autoexception:: NetworkPinRequired .. autoexception:: NetworkPukRequired .. autoexception:: NetworkSubsetPinRequired .. autoexception:: NetworkSubsetPukRequired .. autoexception:: ServicePinRequired .. autoexception:: ServicePukRequired .. autoexception:: CharsetError .. autoexception:: CorporatePinRequired .. autoexception:: CorporatePukRequired .. autoexception:: HiddenKeyRequired .. autoexception:: EapMethodNotSupported .. autoexception:: IncorrectParams .. autoexception:: Unknown .. autoexception:: GprsIllegalMs .. autoexception:: GprsIllegalMe .. autoexception:: GprsServiceNotAllowed .. autoexception:: GprsPlmnNotAllowed .. autoexception:: GprsLocationNotAllowed .. autoexception:: GprsRoamingNotAllowed .. autoexception:: GprsOptionNotSupported .. autoexception:: GprsNotSubscribed .. autoexception:: GprsOutOfOrder .. autoexception:: GprsPdpAuthFailure .. autoexception:: GprsUnspecified .. autoexception:: GprsInvalidClass .. autoexception:: ServiceTemporarilyOutOfOrder .. autoexception:: UnknownSubscriber .. autoexception:: ServiceNotInUse .. autoexception:: UnknownNetworkMessage .. autoexception:: CallIndexError .. autoexception:: CallStateError .. autoexception:: CMSError300 .. autoexception:: CMSError301 .. autoexception:: CMSError302 .. autoexception:: CMSError303 .. autoexception:: CMSError304 .. autoexception:: CMSError305 .. autoexception:: CMSError310 .. autoexception:: CMSError311 .. autoexception:: CMSError313 .. autoexception:: CMSError314 .. autoexception:: CMSError315 .. autoexception:: CMSError320 .. autoexception:: CMSError321 .. autoexception:: CMSError322 .. autoexception:: CMSError330 .. autoexception:: CMSError331 .. autoexception:: CMSError500 wader-0.5.13/doc/modules/wader/common/backends/000077500000000000000000000000001257646610200212745ustar00rootroot00000000000000wader-0.5.13/doc/modules/wader/common/backends/nm.rst000066400000000000000000000006461257646610200224460ustar00rootroot00000000000000:mod:`wader.common.backends.nm` =============================== .. automodule:: wader.common.backends.nm Classes -------- .. autoclass:: GnomeKeyring :members: :undoc-members: .. autoclass:: NMProfile :members: :show-inheritance: :undoc-members: .. autoclass:: NMProfileManager :members: :show-inheritance: :undoc-members: .. autoclass:: NetworkManagerBackend :members: :undoc-members: wader-0.5.13/doc/modules/wader/common/backends/plain.rst000066400000000000000000000006541257646610200231360ustar00rootroot00000000000000:mod:`wader.common.backends.plain` ================================== .. automodule:: wader.common.backends.plain Classes -------- .. autoclass:: PlainProfile :members: :show-inheritance: :undoc-members: .. autoclass:: PlainProfileManager :members: :show-inheritance: :undoc-members: .. autoclass:: PlainKeyring :members: :undoc-members: .. autoclass:: PlainBackend :members: :undoc-members: wader-0.5.13/doc/modules/wader/common/config.rst000066400000000000000000000002271257646610200215220ustar00rootroot00000000000000:mod:`wader.common.config` =========================== .. automodule:: wader.common.config Classes ------- .. autoclass:: WaderConfig :members: wader-0.5.13/doc/modules/wader/common/encoding.rst000066400000000000000000000005021257646610200220370ustar00rootroot00000000000000:mod:`wader.common.encoding` ============================ .. automodule:: wader.common.encoding Functions --------- .. autofunction:: pack_ucs2_bytes(s) .. autofunction:: unpack_ucs2_bytes(s) .. autofunction:: check_if_ucs2(s) .. autofunction:: from_u(s) .. autofunction:: from_ucs2(s) .. autofunction:: to_u(s) wader-0.5.13/doc/modules/wader/common/exceptions.rst000066400000000000000000000005731257646610200224420ustar00rootroot00000000000000:mod:`wader.common.exceptions` ============================== .. automodule:: wader.common.exceptions Exceptions ---------- .. autoexception:: DeviceLockedError .. autoexception:: LimitedServiceNetworkError .. autoexception:: MalformedSMSError .. autoexception:: NetworkRegistrationError .. autoexception:: ProfileNotFoundError .. autoexception:: UnknownPluginNameError wader-0.5.13/doc/modules/wader/common/interfaces.rst000066400000000000000000000044621257646610200224050ustar00rootroot00000000000000:mod:`wader.common.interfaces` ============================== .. automodule:: wader.common.interfaces Classes -------- .. autoclass:: IContact :show-inheritance: .. method:: to_csv() Returns a csv string with the contact info .. autoclass:: IMessage :show-inheritance: .. autoclass:: IDialer :show-inheritance: .. method:: configure(config, device) Configures the dialer with `config` for `device` :rtype: `Deferred` .. method:: connect() Connects to Internet :rtype: `Deferred` .. method:: disconnect() Disconnects from Internet :rtype: `Deferred` .. method:: stop() Stops an ongoing connection attempt :rtype: `Deferred` .. autoclass:: IWaderPlugin :show-inheritance: .. method:: initialize() Initializes the plugin :rtype: `Deferred` .. method:: close() Closes the plugin :rtype: `Deferred` .. autoclass:: IDevicePlugin :show-inheritance: .. autoclass:: IRemoteDevicePlugin :show-inheritance: .. autoclass:: IOSPlugin :show-inheritance: .. method:: is_valid() Returns True if we are in the given OS/distro :rtype: bool .. method:: add_default_route(iface) Sets ``iface`` as the default route .. method:: delete_default_route(iface) Unsets ``iface`` as the default route .. method:: add_dns_info(dnsinfo, iface=None) Sets up DNS ``dnsinfo`` for ``iface`` .. method:: delete_dns_info(dnsinfo, iface=None) Deletes ``dnsinfo`` from ``iface`` .. method:: configure_iface(iface, ip='', action='up') Configures `iface` with `ip` and `action` :param action: can be either 'up' or 'down' :param ip: only used when ``action`` == 'up' .. method:: get_timezone() Returns the timezone of the OS :rtype: str .. method:: get_tzinfo(dnsinfo, iface=None) Returns a :class:`pytz.timezone` out the timezone .. autoclass:: IHardwareManager :show-inheritance: .. method:: get_devices() Returns a list with all the devices present in the system :rtype: `Deferred` .. method:: register_controller(controller) Registers ``controller`` as the driver class of this HW manager This reference will be used to emit Device{Add,Remov}ed signals upon hotplugging events. wader-0.5.13/doc/modules/wader/common/profile.rst000066400000000000000000000003761257646610200217220ustar00rootroot00000000000000:mod:`wader.common.profile` =========================== .. automodule:: wader.common.profile Classes -------- .. autoclass:: Profile :members: :show-inheritance: :undoc-members: .. autoclass:: ProfileManager :members: :undoc-members: wader-0.5.13/doc/modules/wader/common/provider.rst000066400000000000000000000006711257646610200221120ustar00rootroot00000000000000:mod:`wader.common.provider` ============================ .. automodule:: wader.common.provider Exceptions ---------- .. autoexception:: DBError Classes -------- .. autoclass:: Message :members: :show-inheritance: :undoc-members: .. autoclass:: Thread :members: :undoc-members: .. autoclass:: DBProvider :members: :undoc-members: .. autoclass:: SmsProvider :members: :show-inheritance: :undoc-members: wader-0.5.13/doc/modules/wader/common/secrets.rst000066400000000000000000000002341257646610200217230ustar00rootroot00000000000000:mod:`wader.common.secrets` =========================== .. automodule:: wader.common.secrets Classes -------- .. autoclass:: ProfileSecrets :members: wader-0.5.13/doc/modules/wader/common/sms.rst000066400000000000000000000002141257646610200210530ustar00rootroot00000000000000:mod:`wader.common.sms` ========================== .. automodule:: wader.common.sms Classes -------- .. autoclass:: Message :members: wader-0.5.13/doc/modules/wader/common/utils.rst000066400000000000000000000012031257646610200214100ustar00rootroot00000000000000:mod:`wader.common.utils` ========================= .. automodule:: wader.common.utils Functions --------- .. autofunction:: get_bands(bitwised_band) .. autofunction:: rssi_to_percentage(rssi) .. autofunction:: convert_ip_to_int(ip) .. autofunction:: convert_int_to_ip(i) .. autofunction:: convert_int_to_uint32(i) .. autofunction:: convert_uint32_to_int(i) .. autofunction:: patch_list_signature(props, signature='au') .. autofunction:: flatten_list(x) .. autofunction:: revert_dict(d) .. autofunction:: natsort(l) .. autofunction:: get_file_data(path) .. autofunction:: save_file(path, data) .. autofunction:: is_bogus_ip(ip) wader-0.5.13/doc/user/000077500000000000000000000000001257646610200144365ustar00rootroot00000000000000wader-0.5.13/doc/user/images/000077500000000000000000000000001257646610200157035ustar00rootroot00000000000000wader-0.5.13/doc/user/images/add-contact.png000066400000000000000000000315341257646610200206000ustar00rootroot00000000000000PNG  IHDRasBIT|dtEXtCREATORgnome-panel-screenshot7w IDATxw|e̖dw$$!tvE,]6<{9Yy O B 2H! l;3&3(xprIW2cO,{#6$Ij~^[T޹~@hE/-@磗7K’$^Lޱ}fF*&}~m/Wq6QuQo1oٯĩ,Yl$w.c`^xELߧKR0=xqfLeɪr|$9/ E{x=v𭦣m/zYFk8skZKQ"DLFcmZH.vٳ1AJFj.\R6]Wwm/?c~]pd{>,YXF08) 7bN{G`|VHNŗva7Os<&I]02躎;ۍh;f]}-1A1yF(bnMaw{DιU1x7۾Y#´ˣP_ކs?)&ZbN+:J+j̫xꫢVHteYw_GBB 8ێmt gI;8q7ݽWY!Z\01 t:`aͲv $D1d !-Y}6;#J! XUJ; #U pj/EQM% P̽pny ekk\KlE؛G~ \.79EǶ9ܸ\}kkU3ne5(uV)8J-.,B7v FPU'~Vo^HG1FcPX`$fՙPϯ#CSBD6`SU,*CöQj,Q?E#;X{J4D_U9AŨtNQA@ (  ]"?3 N QmUUhnzy&n1F fFhպ-Ͽ!y)vy2a€:jzݶ}-ݖNZ>{>нT(dנԭ ey;.x! !UQGQ']ip0J(~gյSy:zEW:Nŏ#{ ѷٛ( XM~Z9lZ}2w%~~cc#QUGA75ڹQTÀ^A;i^;NQq ٛagrvBaumCk]œ|l؎ց!.錄b1lEvn?`Rr1 lW̥y7{6cy> # U|Uy;;U9'hH`hm9|b寏Rkk[پо*p `Ƅ\P^XfLR_olj-?pp su?bTcvۋY;9b 5^bB؀d[͏wG\>i92s_! x'/I8ugXW6e#j]?Dg)/f\ sd?oY;;Y$竟DߟǬ3Q$?:52IEZr*(-tJαlc3w4$oXQ[{)~৮C{&YڞXwbk)}%F*`ЉQ"*'FZu11 gĄO)qh;`NF.c}ˎ,ĦZ#q/Hz}2*vƖ>eq'nPF:*x@QK Io}KnWʡ0[t~HF tOwNr&cqMvL1!)ék|0lDKZqUpm`K?/wQz|sE`IӐ uhϰ~-mCODžǨ'6WN6zrR_CPxQl4b1뜓Ub$%X(wbS(଀_cLR%̄_͟qAL4kk{3JxG79.Q+\A^G9Q[@η9T(8gN5$GZ~K%"MCgPQl&:T![%⸛\W$GP 8އrDߤ` .ЁD4(:s4;&\rk}RuS!UMʄB8!Xة QU (t7fMiE[|DYգʂ #91ۜԏ^]:f@@!Aojadh-+ f%xLA!FXNذ\[fk9h'J'=1hU z7QIjq3dC؛V}k}qJԫ)nzǒ,X}b_wVvWC8)V\v쫑Ԫ%%%ȯ$i- J$dD肍e7w.$QǏ3j=;Y$d$gd$gd$gd$gd$gd$gd$gd$gd$gd$go((†ޖ$5\;/8nT%EF' @1!!BЉB:SkGU=Q%EGhdž!("t)KR. B)rU9fRANYt$eQèJZ䂢*BtaF$$]QCQ2ʊ Qcj>(Q(JeoAdaЏBGm Yp$tS1_(٨j q@8`QzUd!DB逢@f~:Âc/ U)%(P㠂PAHhg~v졠!D& ;LxcDej0a0a0uEQ4 ݕV\n QPpYJR[լ9_G  (b;AP1\e+5){Kpiyt OΖլ珝|%*6BRx\P13G@6(i гAdUJxA!cI:]Zp ?S1om66PD*y("L~ 7.GLN5kvx^ڶ9!yBN'naǤ?Z8)))!66hƎ aҥ BO^租ӈÝHɸE/z/ܢK[O@gKWNE #'7aU),*tRPXbǥ Rmgܹ 02T+L7xǹK@nIh-bSQ*nι#u2 %̸b65o6d]bSTC JH&>a*6bhqkׯpga  H^]qX[pt]wSi︾w2 e9y{TqHUCgO. vwQ4cww QX>tbe`OJLL ~~~uvdmM|1qZ_%Mhߥ>KDdZJNA\$:zC';g?ݺG;v*۪Z-ff?Pitڏ6;.6~,&¾M_B(hl!ۜw'mL||<7q!LwP ~=*[wDnzy5ŴrQ4&^ZV*[4 _O1 5{;굟1, N A^b@+*V666ēn n=z8ֽ{wvdsާ-2(~UZv~9&t;lڶyu!xJg[ƙh5!*r<'{߹KXo哵[ݿu{vJ\f̛bq;qԈqLV5ǚ|sXk `(o{s^`C%B#n;Тn7FAU(]ۿaf bHztXyNqqǸUW]OLLO\n[ 0 49+Ho`,'$qDڌtZ<0 Sg.~ʇO+ټa)Ja;?o:] 2PG>_CHhE>O22POq8ay`[2n֑_zm-tџGЉ샔]!E fR\"»xWh*{z꺚,Qd`b# 32~OVvuĹr9H_cPU4dhd_ZȊ?2@)<4qp #=L5r0r㪺q`4 BVG~\*87] >vNtj۩[:NxDICRW8<\.MUV\IJWq[tbB!؈ L ئ&6{6K"jrk7m}Zn k(fo BԦ:9rK Rtʧ!֕_;"plޭ?O l_c{*f3/ԣ+۝yϚ֔߳)x,3EKO ",X̒L6RCIh: k ?}_j{r^8rvdl~zrŅe9LF/~j! Kjc[W~Z-fSqKJ̓~Qmymum8JJïe R?foL}<7fڀmj6Cn&QAI1|nų#gB`f< l$3?a󁀮u\w/l-ٛx~\g(o2ou%nCkݔ#57?| r vׅ֝6ҹsg~mt_Ij93Zq55d]c۷$Péة_|E̞=K/3g2sBf͚U?O/2/+ wٳB0g̙SgΜ9!xך6ōpNwiOmN$kQ#UV$j+4~vUߥ^"& *^rssuFaa!AAAbQӉjvcXuEQ(**fr4 UU+?#U\\j4MՊra41LbaܹL>UUҥ4"'+gddpA zfa6 Çc8pdeea4ud ::H\.[nEjeĈ[.]C^^ 49_IjT={BO݁W ƍe' nwWEQ`0Tn:qqq!֭[X{nW1*>}z<%Ijf)8QRRbGNm2 ._#nSuYWTT;iB~~AKڳf)8NTDK.e„ ک:h˗^" $f)8*aa:|ǔV$GX4j$I]R9WҵpN.W٧.W=˸L&~f3Yk~9$I lVՊ3ЅQlhZ_-#FhT9$IҩkwffIj䄄$I># $I># $I># $I># $I># $I># $I># $I># $I># $I># $I># $I>ޔNv7wR;RqqJgL&defiZs$AHez$y-,8Lწ^Ι@q,IDATS ]8z kV.ϟq^+ N+~J:" ܩHm\LL,,z_sWbIV&3b; Jf1œ)<|`0zuPI|FI|y N JH&>!oYsT/lZ%l7w"T}Ysh HԚtoD1\xbsߝ\6JH&YLg[KTR7njge^Cz,ֽ9LrdU&FZ:E)_yE1s=,I+FFU/~I RP g$Tf.8\SǬUٞG9teKVYxR~"Z~~ƆNAA: N%h?;JzFϳ,bWn7S6Kr-n\2wrnm1 NbGV1 /Ĕڵf.8*F?aSg.~.DoXʮҪ&sE=J@l+/L&*q] Y.t''>0WOaÏ<{v| lW_۾7` J`# (i` [_u+â~QSjך^JXH'wy45 '$C=bX~#yY͝s^|i3%::SډΝ?֨Ǐ`-g#IR' $I># N+c0u`סȂʄGEshZs!G <{2#˚f_N4M~Ijf_N|gZ\ƭLL8{+̌tyI #=\}:ȂMfz;^hTvrvycͲ2K9I|FI|FI|FI|FI|FKʸ\.nyŸ;FX^"d"}F~=rAGj txʂʤ{sF'30X>M9r4=J䕸2[θ1#NEj @0p ?.z땸c+s`'Q͝NDEs`ɂhn<|Ơ^'$L+8m0J5O铉&{5bu_k((H\4ˏ*YWƤ((pM~yds7ek_S_U/Ót]fΩq_ ܛ8ٗ_JIwW\:{]@:}}?]v9[xyJnh&; 7LܡDZolF'JDbƄ4͏]=bBc~ "BBщ!"dУbsZOS=<άkPѥѫshyڶî#~:|D~_?}-LqO|&G<QQb1&dXyuMĈqj^X(Fu}qq5 O#&oqQ.F1zp#x}pc֪G9/^J{?cÇSB o Sy[tfuؽ_\?s;vzFϳ,bWn7SyYy*:"-aIVkKX߷8w2*FCpg_Ka{4sQ0n]w=Z6S H4+.ITb_ya2QCrU>oN-_KpY \2) v1T+\=P~)sXgg />;1Qc2s[,g=Wo,o)94iR4)+y̬m(T~P~O`U)lKcPyGug~'$}#lOz ]U ̏ծ3ir1Ōp ScC- xΛ~IWS660==v q۞ax!wث1$c We?O!`7ΜLf]h$4d_S8tv*9UŚJo\w0:NEjzW^67*#6}FHɂ#IȂF4]~D M1=}2]I?iHDLB&{-VFb;E/㒚Dwc͌%ŕJĐgnj$ F#]r^u1$@ ͝N]N.ktdie×$_$,8$,8$,8$,8$,8$,8$,8$,8$,8$,8$,8$ԦuWr<#ͭ5w:h 22QgMb^^+ f?oGjӄn^t;O 'f3l+mXYߘHmG8dBh{Ilz9‘zqW {}K" Ԧy/t) Nz6ݒz׹>uHŐGjۼ8SÁ.a)6qf-,8H1ѝ=et! n=Gj4# Gjd4E%GjZڧ9Yp6`4vU7As1 ^'Rˑ#׭Si9HTT׊,8R5q,|/L׸n^KinSYbƞ?ꕸHmV\x?5+^dm[g0fg)╸HmZb^tSiu F#l6bʂ#y6ͫ;tI $,8$,8$,8$,8$,8$,8$,8$,8$,8$ ,-i<$IjԵ6)L5 s$ >|6W՜9I߯\\~%ec/.l$Ij[Q]+(P*=S<#Mf#OIڀ;@eZ.>zpU$5 N/_גIENDB`wader-0.5.13/doc/user/images/contacts-main.png000066400000000000000000000534161257646610200211620ustar00rootroot00000000000000PNG  IHDR0u<sBIT|dtEXtCREATORgnome-panel-screenshot7w IDATxw|U9aFq[WjYZkZgDQ 8P=%!;dq$&Ą}/3sNnysVwB!۳}\hoNoFM^7}B!h6uB(;K/;z}gaE1UX=Ra!BFqxмTn඗68 bFc%qB!Ym/>v~[*'ɕ' }P,L!< <4 屿Y'EGoˊUw,Nb?_ז)…Bi1r`oj}GiuYapi(2m2NR~d558ιwS俷ܔTG?HZZ_neQBqxϢD}Icw'<qd~4U҅3gIç艦f%¯]6%{Bt1۶0 =.CǶZ[dQaY&zTEaia ]qxjMf4܎$Tmf9#CI?Q_QckTK)A|n&>}x+]{f䴣8a 89=:qNŔ;ىsw:&֒v|?K%wdF{;XRx0\[<"/ީ.\?b1J*l01B=Gf-VEI0Z⼴z INS:llj($F3x`%Ƨ:O7ϸa@')^f~ o ?v?px>i[AM=O#gT UKtʥװ%IYgua'лT;}#\_yD6c>3s(z"ny{֧HˈVn\EϩپGV"QWa ;pqv+03*6Fz#f_Eg1"c9.7ֱ?c9u3y$D>kGs.7е8_#!oϚNt*\x0V;"fq)ՐTtVcꊝֳ#eM WW:|PN0?HxM]H6-t xPU:DF$0Ķm곿m.fe1'=e1 _\> 7XElzCA%L/cSSl >O75 76z{JPz4>[$3y#RϓU ~OϘ%S~`é~l.{/x=:_r& 'jmAtC_Y=C1rJ$QSmt>o"v[vu^s~ gl~v]& ˨%SkXt4B¬.ʀLIբ 8/۟uW18Cɱ\x9mz2EhF ^U_p`OJ rsb[ _\ʶJr?ZU.w|k?6jBR9<]C l[`LZ9qBvuo|qN[EYcҹ5͖p`NѲ,hDuĻ%d["!Ium¡0JSFE f+Ou0ƞ0臿B'GmP%w_5 I<{))>!D%&}{I ꡒP$ca{N$3޷OW3@dkQ;M#TR~>o_q'8$р:fŪ򿨅d2§rT>&v]Z%Df 6+#1;̀Lt3jΝe%ܴ ˨ p5\6"RJ%݉[Y0 i ,&6O3ό0xW05UFSIoQ\CI>|'a[ ,|`v R) \VVR#C ^ ϩ) _e QSI7%5vyl@ף\u)eܖvu.Z5I`(=HL԰#:ΠJ{g_􅤧/dɝ[!#fǠU}v+o/J(3Ϥ6vƬTS# 8T\*lpH?}q"6&6QddipT U(xzx$2%(Zpw2AacՄ>ygb0*ueq^X"[I7&6QBFF*]F>d&&FvBL~6AyOz`6 [_^{C$ފTNPHPf8j=4{ƍ?A'k/Z!c`8Zy0J;vYh{mY.+w7hncZ#_O`TeTT6x7復&~UP]MDqMsx mDž݇M,0Aw,4|MI㯌($Iy7黊Fym6#X_fP-s2>'JYU:Qm`_(zMIdLHH,RJׅplGϑg4g.bo󒓫mCp咔W+&/H(#ks"Ae$1KEiB N:Ԇz2rpeކ,nsGrf=mlqA69uWd68c8Lㅼ8䙌sCg 8DהckhrB46WTuMe%l6H&Ŋ^ۗg0ar\d&L:«-%f?+j̧`z&f>4G w9p4m)u`n㥌淌y2=dFT}խj Nܹl56G&sY_Q/v#5䩷ӯc _*+ίOL=3q>asG3I6憇6>-W& Ã+ ҧ]Jff$loe979v0QiV=+ Ų9,+";Ӟ`Spo?Sَ3m4i'5vbi~ ZQI op}\C10ik-־+츖%L]]Dc`}*_; J LTm cs@#MpX;8n򭊪 E4'ZMl#WNZNOS5s 1q+IR]? vhLlYƇ`6z@G/\Ȧߟ2`|ZĖ;ٜ5G *c.p_I P|,:GY̝ulgùi`@kS7'_,n_37?w%`xɾTZkF Wap˙t HRn-^OGSEU=ck 3}TEs,bk'%?g1* uk).|QKx{%%ˆbg2:[>`ce1ʠtI` ](pͱ;K}wq-[o,븱;ZYѸ|PeM[`6Z`6^6Sl AY6D7]~WBUp%v6V|=_.ᇺx J n!_I(juwy7uQj˳:u&QPURNr{zB[}/{)Q]?7-hB36ܵN݊ۙUpqT=λWmE1*b618̶ngs^_y)PINv2$ׇϩ#; ]-\n9~Ɓ"6ͯ{KYC i!uG˲E -ZO­ɰ4#GjTǨwՎT~uCM3TM%/0v4{,$kU-u3vɰ8yW;ZslO֩.:@b)I`{KJƧJhM[ﯖ˓LMxv ^&$VlƘ]]-ݑeco{֓"ľ.LM|%BY p:I3>.ڏa] >.b}AkZ;Cٯy.5?Tob[2x7elWnCr&~~CC+gZj΄8оRO6[WWGMun+\!p{B!'e.qa|F\!B=\H{rϳB!=MG#BE+Ol5E!{!BshF!{6Z` rǜ=yΟvAW| sxf ?io!9B!Zw!*簁&5;bɐ!Bt6cwULkZY+/d :#/T/K^򒗼%/y ɷQ(+kn^}ゅwe\~ ޾k*2B!:Q,דF5Y0'i0i/qܕon |B!Hċ wOWM!H Lشy :zشes<B!DN0Mʪ*b΀(t[pu 7 !?$0P QP ]INN&=#É*>ꨪ]HiiɊM,bN= {B}T$0[-Zܮm뺣 ]nk~p>}zxm˲0MÁacYmEqp8? bY&5l޲DrzӇ&Bs-vͺuF#sԬF)-+CUU mۘ٘x^4Mc TU(Jmm-AD~?ŔgP!t`p010 b: Ctr1λwg5~ҲrrrrmEQPUӉeYB!Ux<LӤ1y1-USHlBoƶl,;Zm0M"RRR8ēyYuEO\c[,tILL ))D^٘4e6ԍ~Gut=i@| Mee52W!nL`,"H| IX/.$rܛ=m\_L2 UvKOK7Nbb"bZ=~OØٸ(P[¶-S%B!:Щ(2M\nab_%aVV7`y&_zwUl&77my >\.'@44Mk:~u('$$PYYK!'u[c&iu-KHX7eQQ2Pcn,Uay^ĸ\.RRR0 PBTVVbVa-2UU4 USm`0H$AӴ={0BA {4 hu]wjZ&mSQ2q] eo*Ԕfc|*Vi jr"A__ IDATC$Ux^Lӌqh`6nӉ4-jkk!ľtuF\Ғ1}Bt-MH7`puki lh(N8YC ? sB-(G;{YUΩEZY4́j8,BU1?!}[Ly 9}PUiI]ϲLm§gNV4)2L~aXab0M3F6HaEYpK`6 !ľlѧqȑ'0v¤=]ٳ=W9[ݦdgi ILNrV@"JRvM jx\^סǮ)AIA $ jj$GiM$ɌbPZTHlVto? \Cg;;'Ң6XL':B@ID|(J!(/zEyYvAЙyU\ +aR[3Jbe6˿?-6uJ 3މ [8؊[UvX/#kH^"1TEEQT((OmhQ߫֬0 ln6!b?3'wB`K `[br=@Q KJ@#Gh!m;F @Q4۶hMsjIˍeYXA !C-,.%4Nw2O4I^bwyLG߁cy3n<>pw&y[ksqe>:Cvt3sn=w=5!k9z|}1yjy-6Bt=ۊP$'\ST]udP6+p=6?0Ok䉐 .b<1Oo΁d/t ՒL9ؠğyDp8hh4 Fshzơ9m I,t4ۏM09k=O<K&_(=r\5.eW7T@ͧ\}.|E߯`7oq[5$$ukϦlXG|{ܼj2|ՋЧ oZͿV9KY'zIEv֕q}\UV|aXI4#D0MG=xs(idWt.{p:a-_'~/ogKz^:z$6ID `R/0U&k9Ijqo'.3ڸ(h҃;n&PnL(mE_yno@ BU'f SW/-|" .˥b 0 "(Hy,ljoUOp8pzp8cc4ǎNq^s`Kw&;+qxs8yDzЅZ:zy䦍dr|.qL4ݯ`84_6E΀"T2SSBAg_INANCBB˲6+IQ&4YEBbn' ; f!g?emaP:^b+~?U'l|^趥Yv!q7DV{d*&I8k^ =h!V~epԲ"3tFsx:U$0ZxwRM!? g/GI'r_.` +gLfmAy Xӱ܂:xI^FInF9QJ+/-G-pGÆǠ UUɰQe%)$jk%TO9t*ˊB!'s]tU> )pCo1LȈXЧ>G=f[+ٲ\t#ݩߴnnӓ>{~-ce3/t|I`Q9}*rVE4,KUKϸ#_>iFIZ33'Yk|o<(d75._}UzGxRRpjȋQQnb;!؋gfQ`OWC l&=ʮƗ+~ɾLgNκcN89 ۾?tQ3Bԃyoa*LS4 6]ś'S>ыSoã7>W@+WXw#w91/{|c=+{\;ȅ; N{5~ItC{Y=#]^sc+m(s!K7Tw| a]Goذ̠aHh- ik[}A%ki ;۩#b7Ӈ;Ғf U4M#=#3gN6qf^/ojc6}B޽.m;Y Fɢ;8OeK?#::vG҂mYhIYn%mR~ kz3 (=Vnļ{Դm#gN?x21BTI|.I9.БtUcztupS5W_Sg'?MQy?;翧,~iLa;_Pq!|Q9#[*cE5KU7^B쩓#T{/KoYmktLlⳫ}h[Bbg!vn_Ƒ=?yb9]d. ;~?|(sO_oE,DZ.ThbGk( mmӑm۹I`Bxï]SXy?17~5=blTgΫsޣ,K]A,eD I&Sl99n>{\B!Э(qͥA^z#J xꦧdcQ=LyB!ءy <jBl~ sad2-'NrM#9|JfUX/+P=S{gttB#o2yY7@=hi PYwmNv/'߿FMuwBϑ(n;$#DjV U<^#VWM$j`+n)~ -2;/ωBm3B %;Jm8 mQWNՏl!B@7B!!b# B!9!b# B!9!b# B!9>feB0tVQS[!hH]폖ɓxE3IB^ԅBl޲zcy֭IJJB!{ 20p i}6bk=]M!B[QHLJ00 41 *n/+˾]N0)uOWC!D7|O>*Tuu(*嫶EQ ҒnB! N`8bV'1SUU&4jjjPUmĢ.B!B:̳4ټyi;myiuJ2/Bd59s/W+"%0j%17WN.UUU;MZ(Nff&@R_|ѡ2c[Ǒrət T^??^6KF`oU#f>g̣O>b|W-]:̘1K2ILHb{E,Քx0`|0G&55޽{ (--%Hh /xek~`o8?8_նdQiww8y;pμymY59s9(/NwV_2eh}G̟H2bwyLG߁cy?ֻ֘:Zos;;pyuMqѳ8|t}+NF=c .28buuu+نtBL7]/n&reͧ\}.|E߯`7oq[5|K}?3窳,^ _ sxu1(=r\5.eW7Tu\zuk$V-+'=/YF7Ulʽ~ˆ5uBqT`{1שml,"O.׮& QWWG4EW.-[eYO^\8uӜ7!'Q'745~SHx&p uw8~GMd|>>(m!o=%pr9ҋj"Fpof/1@ߙ'27 /A@N_ E*^y[ܛ/`jO?pd}&]\ho<\r/1 n:GݞߟΘ4&!؋ux* pi/rwd,BS5vŷ 7MxKKII O^:,~Yhw̹".:=0a#G&];?\q W)3X8}#{68%~r|hLI6 _8=8^MxW-y<شe5vۡp20ٸ+} u_SiI$U{!^C] Xfn bdff6&4(FQVi;}$5<6օ$0($MϾNBi8q.wcOfcq[e<LJ"=B-}Sf"I} jm 2kJ;(+g]IzJS&(8q8~+ !C-]-0 ={cab )(.-[Xz9ee۷>KO-h5ahyaϜL_om=smغ;r]G쇔ă /= [lzۖ,ԉ`1W2!ɿ/ߍ߭%&eO-q?!G~]IaQ!@"mFQzf$ ao.^$'&PZZм!h%:P+[];˾]C|OWE!D7hs83]vutUBD#$$$&QUUFIajB5mBq`#wkL3mөij&j-9>bz ۶ٺu+X!B}gX4JBB"P۶PUŦMTH$B$[?3MwoY&>Ug!9ߚ=8n׳qzTEUe{oƶ-beZ >` 'cu}OW݆QV\!4|{<# LZZ*cƌ˽ZFp=$$q:y!XT B!dI`i9٫}F!āK[.-ƞB#Gُ86L٭.>_!hg!bzc[.._~%߯XP5 '̖12B!hU kWma0|9N9TzmuB!ľM-0k׬FBЇ RY]i̙^x!ҁYBۣc`~ >~D>/U5A(TF(ڶՄJ%!-EBYs!? _z,m(CSqh((B'DO-F`R[_C[coUuOx!V۶vˡ9 p8BUu %TׄDx(rI˟ǟHb"5۫Įc[wr\|n/./ƞz oomiı<8Ň|ulxn~1?7g? -p{qko?uBt6fw˫Onj"&U5oTUא@Bb_Δ8Bq~?el7YQOS3=Ɯ.:?]KPQ׼Š- X,1ᅫx`xz{5ޝGGU}d2 aWQ*TҖR"uhj]UDЊb-"NJuc.eS"UAeSdɬ&LB_sKITy羠.~J"rkQ(pKaOx~ۺ~[Ԯ#$WN6W(IQ lŕ%9;-GY=:B)=~)V''k~qU|wuf}*^q>[K _ߋ}ԬҬkG_޴kfkUM`:wS=aՎ_J< }uo3E>Sm#4yi׏Q߭K1m9?Rcfݨ矡sN>Kܡֿu\[ ou}850㵥5:*u2IDATO*vKN<"'R\P:9(.u{*5w*Yu:n\ MvfNzTN II˴6U:ㇿפS'3`r.YSJg ޯޠLg ͭKNL5?*{yܦ#EцrPMY"\ ;yLg _2ʏ>֒_D6|Ӌ]enx'c+USQsFkgh]Y[c?Yޣw~QZzMۏT>\ l~gүݨ!=2cNן'ɏ֐YPu7mx{#coqoDvzy$Q4q9V>KjP|fo_CK^PNP$);%oj:r۷$F@W*)R_(\"優=VznXC^uOw5}6\#C7FbR4(q ;*juJn_Յ^-IӊM.Z;@ #1[ImiuV jE:}Z5T*)T:8{|t&\Wּ7E͋s<~hU=R4cMڲlMZe0B8^?;J{\K ./V~_yD V+#8a9![ GPR+Py4vBj/-_:wأrp 4sq 8t50S&AO!`9 00h^< mIOQϮm'WӰn?ּ}As=:8]^_3q' ԕy˛q^z*4_?>]6}~y3#hp_W[?@0ػ¡ZUo[i>QӴ&?E7ފ-:ׯj]I*7tOiCDR2=fx_\5ڋim%'8?9õ(T% /ןhD [N}u޸կh6տRzuV}wDWkD=9B ŊhJuz[q< ͜ƞw]:- H>Sm#4yi׏Q߭K1ؒhD@{ `B?ꮃ+M=ƏOUŪ.Oi-<{ wpjxNs*ڧ4Zf,}&۴ϳp<'o%ԫCt:')[n3Nt.gv6^s7wT>M\{_iM<{W>Kj_wjC;FIi͞[d_w`w wP ʵ\)-9 ܤ Tkf}9?<kQ֧zX7N *Z4Qg]4WCyYZ (PF &кp+o>^?#.D=YU}{~\$\{|f;mxq-ܵO&B;$)]!Wй/yC'=F|[^oi@;"`Ou?hژ EjUMfQ%)%K]RKzK`ק]':`j:N}$:Ԋ.T*]8O~uG`uшj >ҋS֚1I~H[6׬4q쬾?}Re]Y2N֯c;ɗBs,פUVP "_<9UťStU^0:dd+؟ѢzӒgTh>ןhD \y9KŌlhf5CQo}Yrw`= 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs$xJevIR{n?O{_Go<#C{]$ `!,c!,LbIR1BJRq&I1BXF$)FH!%)FH˸!,#`#$e#$e܁IR0I2FHI2$)FH$ `#$ `w`#$eLbRb;0I2&I1BX)I1BX$ `!,c!,LbIR1BJRq&I1BXF$)FH!%)FH˸!,#`#$e#$e܁IR0I2FHI2$)FH$ `#$ `w`#$eLbRb;0Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`[usIҰOu/xC&mnѢw] #h$ҞЬHnwKҢw]He V˅4h*4۴|T?Bi:;IKkhp_KRCH{Ny9j~Wk|IENDB`wader-0.5.13/doc/user/images/delete-contacts.png000066400000000000000000000642621257646610200215010ustar00rootroot00000000000000PNG  IHDR0u<sBIT|dtEXtCREATORgnome-panel-screenshot7w IDATxw|ul/ H4;vˉN~?NlNS#PDEPH۾3c`!ńP/}f}ΌB3d[.Bqʞ,)Cά?^iGѬ.;!BzL=/7=vό\y.I^F=PhB!@s&Rm#7?_ T= BҞ~yžm[_)L>?(0t%R!W03ԻkaF&m+\^ۮX|/.%\U ! ݠNTc>t==ңQn9ŕ!`:zٍTT[&sq !owI\ǎW>Y5g$#)!6,o^ K1{@k-:4a͞JRq83:i^NxwTzʂ_fz4B$lضh4>׶kK/aDn"K(v-]dOn'quL=/a*߬M.Rxy(SHLaDpz&xq4|~|"zvIrԓdk1:c'5"vsuL3^YGI]*^o"V ?7_Bg8cuRm~mt~ߊ֌-aF =0 u9}B_֊ͥ4n<7'Qmz _86ר=>h$JjtNЏ >4t~E8ܨft ᆰs齸U$0x /q%ȗ:"F>rI.cu_Kv0&0^>Ps0f46""F501!~f;]*h93h7=k2-蘒?cpLajfw^HqВqk c=/`h&~mQh_^x?|y6e׵u9<q=ۻVlYMLm>R(Yl`d8(FaJׁ;'U}")SHIQTuLOOF^NF^TˏMv ξp'_cǼש5 ((P\8]Nְ8 f`#U+aГo* `|2GNT|(W|J41!.';tST`sGH&Ԙ[+(&N3KGKq4Zw4vԣ %}}p6]T+]]tNPBa6*n Qq8*6R)v,_nptO&<]{m;9|2d77Rcl_F>c+8L;C?Pn#iP`uӖ4*޾ln[7O_Ymu+`f0 ;vBXhz{0tL-c\|`*sP@y0YS51p9zW^̻;{qUp^ϵZYtL[:O6LQe/& Gyqy k?>FeS71=o7t.7V!$&~I{}5;>η}_jX n NL3kSQt~MbՑ5t\u|U=%{'w? mռ[3q%'qsM[ݓi!.L>|onYFB3Pjl8dQ;W5"RU[T|f߸ns~lJ[ӆfr}Kmnt[zIlc4ql<TB4GF0biNOuU7.cUA'/_݀ ulzSKIxv}>G{q%yl#\( eOp_ϓ:F:KOSUm1X@}i4t蠽]<6JZ7bbDgj64-b H̴dF1H7!5vs4v=tϞ3w͙t̆2Zhp^s]0'}3Rr;jm.ekNfċ${1^\Q;CsA .`csViT&kt#rn}ﯭFF8eXݞ{ۭuWm4(Oɬma{- >y"O۬ &z|%}M܎z̥K\MCG;̄ V/5 )Lfk+_+#T;tRI׾:|<!K{RSIG=\Ǚb|BOnNJ㫐ADɤp1|&6c;⦄N_y| ¹%$-S!]cgaEElX6RIҞݙςP薩"iq =sKc؛SQE%G&y~ x{p@L6r_ -^le?9lrWc U *LBU>TR#B$RVjPH$TY|hݿ{>֞q\бس-`^(( PpB#1Q$`+M%& +%_xV>dxq운u$7Zߕ|S:f֮zQQTD46W\i78C8N*靬V嫄CQDp3n>-/ǛVvSph>HĂk=;*8MKålשQDמbDzCq[)nO65EA *`SRN&*d;;ʀg`@щ}ab%%T%mpBSc ='[d8l9$%obտ )jO^6;TE5fױSEݣ@)et"aCECx8T:п %cq~$ v}Wqi,琼q^0&mIsyߘ^agy#_m\{|WN)T=+ִߤ ľqz`BiFHp蚡0839il a˜T /uޏ*jңQpYIĦ?g߸Fr >~6A3G{Zy;_f5Q6a†Apy=ӱ!K6}nCgLx)_欩 $T Bx۾'`4t߰YTwh82~譇;]%Ŏ]Ѣ)3Ǩn*-=/.9'H~985 ZJ<]ֿy%#{nI*ױӫy_pb ^WLt,cRjq0ĀNӻ~p~/*Fz/Ne(~B[>fcORt)`Cq{EKh_zngV,w]['r=܍n @&i촀i5um$h>Ěh5X8EO:C7T[ ZĨևP؜P,=6#ɫ3na*up zo_3hQ2[EonTlPFn[gM@fxձ~ִcd mOl!ɲ0`E ]~_Y#LU%~ȭnln%fѬy8B_'dJJNehkշa*kyVHt{=_q_#T6.JƼ>烫a&جut1dG?—^~M Tqjص}ڡ84y f򃥭^EǞm=09/>2 V:{ LK`s0FeyWXO_Lj)j*y@TAWܦƱ =8m޻dƟ9fŰϢ(6ʘi{Y6R8C%5NaA0JpŪn'/Eb656vX -s|G }TSE)ӪBtP? ;)5D 3uHQp8-[iUH죒#Hͤ]U%~)јׁ~="ĎӖ!2z;>`|o(E!3v=MEq[IxsЯI&ݥFRw}q+*k}mRf_CH\mӨFש0Xbw$+^ܦ17PuI$ Bv{fFJK M8==v671 8=6^z9N  :%EI׋gG য়VϧdA\tozBCN0ws~Y~=P>㏝icw:УQ?HGN.8-^j' UV2uqTVUPUU4hݺwɼyL8wQ!8$@# D,V+Qk}qBqi90ab:o4c@ Jvѫg/N<4>;5ij,ZPYfH$nHNI!--4ÄBA>>phU~!V-`z $&&iXmVX`III >gcZ3f+.)&5% R<D#D"5FhͰa訪YP@06!P֪ndK9 C$@ H(w3`n7Iݚi5YKQ !]h n%bD"X,a"aKl)jCF0u磸Ғ2B wO!8ZQJjiH z(\.x?otɓy׸‹'HJJtt84 4u7&L0W]MEe%;)Y@T+KqS B! ` j1 U lvQTb߼+me&no6TpV6c;pqc4d0vO0@,zp(D0" D#ax=^Eeլ[p8c!3!`֊L' CױmD:EQT_Mm`j,7cE]ǵVf3g^[o͡sۗܜ\RRRp8.7HOYY%%%y&vcX?`.Yֻ%BZ1tCILL9ۖ5-m IDAT$,{kzLM 1ˋ@eGU1xP1ii7jo%KC#aEb`p8vtào߾n݊hB&m4b`Z?ZꆎaD7tvѨ%tZBM3cgjJ6ӈ~&Fqjr"f$R u虽$I׮_$55ͪ+Dk׎VdRRRYv!8YV̷]:%q4v 5.}˝VBңQ<ϲlh(ѺXYb=}[PV7s9nfrxD$$$ lEi8B O;xgwFUNI Cgk!vٝ׺SZVq]DQ<ԮYtī4Q3g*Ukl*`EAQ_xςE !`?d֩\hss/NJ fr-_&(Dl̪"Ԝ؎%+DYUIԂej bip!8oF: q9^"]zaT|%gժxQ ))ihB{ۉc=jVOcڜ(f4|F/DRJI4MCQhR,'J#B.eӘl]&hԍzSliB(7֊XJ$;cR›2t"śa!q`k$ZͷEݽwEWԞiTgS]!HӼi׮`!g3Y>g_RbMbb" $&z&5O4ȼW}Q}'D= {ׁ(hZc&ˮn90X6NTc [։#/ˏ ~ZXǰ)ѽߘ&jcFsw}M,]݈Y9;+PR`F%%HT0V D"}u?f촨={Vj{UTUSzNYsEE%.BOx|A^|;u '2kݔ2,zT;45ˆc ,_՟ɿZ;f>W6NdJ2H fY>,ˏ̎1F]& #TWWS[VVVfGUݵkZRUnLmonNV0z=B›x኿وxt_ /pw3I2C}yGn1x{BV svWHhnw)`p8Z8v\N'~SPP+ CaBp6͎&=-cѹS6. ӉrɅ8b(S{qw$:eqKqzb?r_z|6^jۜ>#&*cMQˑ8 WEtas6p1;*߉aP\&4Mz ÄAߧ/hh$Dh6׊,, 56۱;"đz7ss6Sf /.퀭7›ed_ƋKFDU\.^BhRǃ,uw^=%… 3<_ooc+L.^ϩҗO%COމq}~kypfة)ݙ̼Gqvt*ɸ yGޅ4 |u3o^'Hc}C˹^||` ʸGE ?dSx/*,?ҽXp̉t|+6N%õǒ}3|YB6fiT" ĿIWLUsePU­CE!& !M~n17o"5-}=/Hr2X-Z<UmBT-Vlظ9TTTh BDP\\Lnn,YImӆrb{q TaQOm!V&Og*s*pS5=)6X~0`y !h&0&Lh"0t(YRݺu㨣bҩS'|>x< o4GϼKoC+L5X|[R(p8Lfff]A+nE;)sƟ>PA!i5}|sT>9 s|ݼ6mƮŁ珯c >ԃt mNnӛ&)+^7u6 %1kH*nvDH.@=@i`j4,G&TT>:u脁5+(^@B wp ,ݶl»}|8xvBekyY9\:0k]Y 9 )# /=0-f@Tu*.d 8zsI>P$@4PU5NbES54U#  p{_ YI8)܂:[j/P/B!đMa5:h*6MSQEQȲWb, ^E#M6GKq@rT1=U>ĶNF!.zo͇ىD  URY# Bˡ]fLl4R:1;Xۣ*0TT c0gewbswd^~h6{a\0+C>^s19L;ٝo~BъڴIԏʠIEeť;Q\BiN**H !1 +kJwݱx;Xl${EqMHA@+ WDP X/A""x v(MTP:J%˖yHX)ds>w33캛<3EC˦ຶja=`$҃1cez}.w{`фwjE=s2C=) U>ijZ6=P;NVo5*S׻cTf+gu_`/u?\ALQHPO'OV8?%C.DKd **,RK ;R/7OC㶩ѨZR_w,|.kw[\,#ef^e gkKG{}Z]W!_I=&~Wc7qf7SOyjF^UGVkoFd IDATÜ_iԸD 5Lm;iWحU)"PW]{_^i[!ϻ~_jü/+~߶~]t츎Z@=!U2\vL93<-3C.쒜֧3N]J#hh**`Sj}[_m[Wt[u׋_ig+~*!KٱWɫ/ZAc+p-O6$é+4ej޼wPǁӵ=.#㐾{}:nVwi{W{viwiR_d~=wN}/N ΃g~QzoSKRihGV!3WQ`7g6&hs/w5":kA'VGjwV-lB_z;XBDaؿRv %UYZ{Lkb;LJLM,GQv˞GΔrf$LM?Շ˩s(Y?>;oo_gj[r~n}FA\띗j,M4m mMu_3KTZ'm#^JN02y:_4^~._90.W~L$E2-*_C MӎSr:eO<]J){~IOWf֯3MA*j?m&D?q TPk{TO^;Ҍ?u5djZ>XeԬ睪I3 w084uXmYE,TRLS՝++ÐK ~׽O,[D-2xўTraTP&Aj|aEq$PZY5X1Z)/B#- R!_.'[+NTrlZf)-/}Sz;{V(s|f|1EסCKU__hD|- /}=`t%\NCNtlQPE)u~Ev?T2NZ#DsuZdK/ZePTzN;r 9'&?.;`9uj93˼- V݃?kw}Zo<0ҵ{CjoӮtm|S*ɩ,tJ!ݐ=Plb-%?$1\]W[ ϢY7w(/|߶jS9V<%0U NYmY'*!JY9͢ޗUS|%ESJ!ѧg՞aQ;´9Zy,mJ#蚤#kt_iPRjd)WSJsؕrl換jC C9!ȕ^]Mcs7RP)Km|X_h Uar8$]E'n#ҳ\:5[ujvD=Aa)$klRmuͱzgA%3uz֥͛2k /g\26kJ9W^6`Er: c7SfikZNZ=&K'~_)#q{ZG{jعiO2\w ã4Co{mTL=ET;5K굶waq N׌;thݡ7b{/vaaI^_5U t'J{͍u]:gc _P+Ү5ku:{]ۮh_/yOGNmq\Ys zRe/@5T 2vvE+/]^ȞtʌLY\k+*iĉz{%)\uoz!) @/5Ϫ`[RO}tşR"3(^`wYx 0M[w*qy^O0 _ `10  0 Kf=fƹ%yq~L6r'W@O]Q'v?ѨըY5hqn9Sˏen+V y^%JO^Qfm7B\ gy%YUÇ^SPbP톔I3??wƟfjog[v$]2A%׿]mUj~P(/^\)7åkǕ!?kKύWI)կz71,6VnՠWEXF3jڏ^NrFHs<ϵtˣ}]/NU kWjQZK5CU#_ Ru{vg_K s>+7&;+eyf %S*+~T+3Z/oƓb孞6R^}uf,26YurԦ:Q>ޕ _}#;fP~WL؞ZG6.E(_k n ASpJڕqZzMjjjjSӈIwjU8>ewudnuٔjʾT Gu/z|ETЯ1\> .9_BJS{KyJYg6[(!/<ےKrHY7=uyDSMQsuIǽj;4L;?g|ݧګonWg'%#+W=]{=$#R9a)qP:{ @%$@6MyE_XCWd_>.əMP%ðH\)qJqz,!,Wj(f1q cz9E1o~?QN;_ÍdcF\Azݮ/{U]Gnv/8nM#4ugV_|_\E ,9RvnWâPZq|6[[ӪfXQ 7 ]_N~_8۽")\)D{+uU@Coٿ,;{|/sz/PCJ`u&erzc JL6 ):IuBve_Y>[4Z7JknQ SWwL&M{g%ާ^w隷Quu{ӵto ?)D隶=$.>30ﳴk}rZ;Q+-Q۱GoR1Zpը]FFDZk+x-`*|G@6> #_(2Kԫl 3u /g} g]XP _A֑зTjڛ*;%Un2lU[ N~=@P-^`Vwd"Gв#:-|xWR~8եgic>TI h'P(oXB[^oeתCu4d-ERj뭱<9&Ҙ`:BS<楬3"'뺇kD)VW=9 =$LIF_詡z;XEt)E'YRHciVS;wW\:bEQ> #-l$=|h; 6vj/Ti`5VJ6g_5fƥ6HRVm=FZ}4enzq~_XB[qOR*l< juá~7!vHOVBe . Pimti_+|+ j]xrԸSIKbu$AtxS rn&?zlF=&Cq۵譑z|@)fj;Niߑ(&zVVҙ{^\] $iNRr6UōAt?f/+|;\ݺY|3083)&(lC>HѾLIV)Z/ޫcG-hҪzJ;N[;ㅍc_{}uPJy׎P(/,RVǗуqP?;K;uV@Is[zWm*qxZ~H)U^|_7N{Pڪ9Ͽ}ySx+o|Knx" ٱm7s,FU{u Af =L |~>߾a}>x~MDo2^o \5_3o=)_ҨzMYЁԒ+n'C׵ُ- U:sL1-2\[S퉏!)#y^Mbx9FsC}zS$:-QoJ'w%i^uTk?6SVDWF6|A9b/LFRF xc7'ߣ]ZԪ{߸G-+H[_ \[q]w OK%;\2䒑ӮAAu.ߍ5Zi]GM4uY7˳U撑ux T4Mj=u=P32G9ӎ1_{ԟRz / e4L3I^> |n^|LU|m6CV>dH1dqEAu.G mk~FX_NQ;qTm oK(FL~vB=&b^Ԫ̸Wh=ʓ^I++p9d]rf`_oFm vok j=yZ^pF59T- Hc}uAuB/s9_9 @NVL!m#oM}e| 6jҴjyW.W>U {nLY,pz}jI)ڸ󨆽sܭ eURL%b6 >7'u·$T[zڝd(b?D n >4Xo,\> 84MJzz}`}4uۛդ{kL) J״W'v92G#ϥ;e7߳BdXuQǝҺ;1S])z-j5j\@Q1ċ\ L ߏg+#+Meԡ]%'*9)EV}ldӓU^^=6r:uZ~|V= sjdۥ,7}a PfשIhş8+tijm^^N{RujV9lCnC׆ʯc飠QuTog}K۷wL*KZ""($YrʫîԴNNҟV)+=K{i/K C ˞T0OizYpܗ_E_08_ھpIܳ&MUВ%KTʲVJؔew()թ-Qҥ?6lԮݻ5^r#={^էMT5bNk]mS?Sw@ftv spsunmIuQV@E3Y|nӷdP5أO ԴIc%&-[/@ 1TB%wmݲEKGJI>30.} XT%ަӯW>Mn~Tmw|%m@紑߱te/aTP, c n5VZ]S^OϗdyQsGzrd:RYdѣ3:cE?It.zϯڜVO7zq!Ǻo1Q@z˨3!iN=H:rf`ZtMչs'-^DGkǟsMm۸A6P-#K .9(%B&$:~L3xL/{ֵQÙsۈ6.3Ts[}UrقTJ%mjiq? +Ah9Oa"V> lÙ+$I.Z^Q@*wFhDKrI)m|". S"̺b-.rbUST*,LV?w彝쒽cN.RhѼy6=C}6TySF`E cjѾdeu쯍r^k Pm=!י1~Qn 30*`F\.\9%nȞeIDATC~30.)yjDρnH#.I~j͚% T[=CԳ'(X]!t=%K-}#V@f`΁HKUFFZ5n uА}Gv[R_׽}y6;XCy@oup@2BkrWXՠA::B)l `.WMSȱuMNTVתj*rYi5ڹk&߫[>WP墟i`S!>*om*6V~>t3$IeKfp ZS-V~{9}gΥӀ`_7zjװ5,ԇҀ k\N,BNp9eXu XGWl+%gv= (]_Kq|A-!O#>{0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0L j:ED3 Pݺu3/x$:qhD%$`J(fR ̘)XBb `f,!P,!̌%$`J(fR ̘)XBb `f,!P,!̌%$`J(fR ̘)XBb `f,!P,!̌%$`J(fR ̘)XBb `f,!P,!̌%$`J(fR ̘)XBb `f,!P,!̌%$`J(fR ̘)XBb `f,!P,!̌%$`J(fR ̘)XBb `f,!P,!̌%$`J(fR ̘CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCc@Y$sƷWG4hH-]^Br9&<33*IK}gO%Wzjr #J:F>WZ%3:wloH҄F?'۰QOK;H3ڵ(1k/++cMBIENDB`wader-0.5.13/doc/user/images/delete-sms.png000066400000000000000000000663161257646610200204670ustar00rootroot00000000000000PNG  IHDR0u<sBIT|dtEXtCREATORgnome-panel-screenshot7w IDATxu|߬K6 )^R ~k]hKUP*H)ŋ;]MB &39ٝ}̙0ƝM7_!p{%ʁLUzOemqڭ(F᫝B!D#F^jQe"Sø_1.S-:"B!PLV =]yE1&%/OJTo=I\Bw-5v>n čgtabp54M%X'ɋBgGOJWϠtw>z}ۨ6Vt')s(;ĞP UR#_j0@mؖ`]*?ǏײFp'32?ֻ",'Xh =c9頫ĉJU ec4g8=$$* -XKo9-j92mP ǀfٛ,/jA9cY MFvZĎ1c>oGUE|:Oo;a>n)mqʕfX> ,Xi-=h:T ?zN38nhF0`MP?UGs*!mqY+bK~xGǴZAAZ(.ĥ$pƐNt,È?\|N;q=#pT|()Df'0w*{FQ67qI12ϒNTjKoyc,?w-o6tOյr}R*'1$ƠQnzT0IrOUL2ѹOca׌M IcTߠh#q'KC&Ǎ[74M T>QIEeWKYK7,K{q8O~ݸg +(rJMuA-Vh8@=߽ߧMo,-DRFs_O}q]U#2s--1x}~қ.r۾eϬ(-S+8@q`t-=c+融\KMͥ$3M-x+7UPFEӹ$~r x|>_mǠls-ML*%-V[Ì -n| =ֲkoZ*ل96gyY<.[ub>|;I#Rq-ëy:7?N5>Z@AN?_g'\2ÚBW}Fr)?[#Cs@|Wl&gOcEZ>VD=`\J*{6Ch5]SA)+cQqՋAͮ_un|+Yg .3A]W sU<~S*EĖ0{^lQIo$fmIE8."|nn4ߡ7"Nv =;uV1M#Bd OϔBt\חB^aV?ZwF@IS_;ңF~ضUBq"y?l qd|A J}  *?UV/X>ey5ŁexUEEWDE x]EGu%N&4MSȽx~#!7O.:~e?WW˭G/RݣǏ3+lj$ X `6#xuTt $E@b4VP0W 7\nGE*Dy X [(5 Ŕ6Çr2F޻5ńK H:Av.Bb4'`WpV] ;(.H[K7X^嵣7$H)$P'9T=ڴ7ԋg$%R!>H所հ߃k1_KvEo׍=.sWNx07~iN"5}V_[T־V5ӁeWRW-E6[5}؜vJri4W g<7{)[߹rtl5 1v3sn/=62:ikPwEkQgJ[Ơ(DY *)$dz:`CNՈ(0W@tRH wu݀҅fW*DF&%68`.8mdJư8~!N;iU4c63ؾl JRņ\EQ0%v@1PRZ]={^ïP(.\A_6Ҳtp!O#^KN`QNmM~qSd&E!|lJ?x>w_Jtq\S_ux.̇|U7*|_}(q6Ԫ8 q!~MBMnᓘ L奣.٧BGcOQ<]CL&:\(f'VO^:7QpUv_* aLim\Ufoi:X1}ƌn\_〃(KW W8m0X&pkIhl PS 5Fa`ӂ޲Ē0_Fzz ^Ǟn׶hl6`le%0R6>{3EڴsmJ;בcL[XJB  {9Kۣx |UkF)jb%ABũŰ" rꡟ%Tݶ,?Zj,봬},e%VouY=0J5=01v tVql ҨQ阣`xMOU -%Jъg/vmPn" 9nHs~9 n=CG`],X[׻~#S)1HR[LPQpEiNѮ,w]?}|2  31NzE0ɟFS cLD8 ҾDozE{Ahc$ Uݏũ1MzwrU|:`02FiA5w$Oqy: tPX3[n{ cXׇ1@W'yh3ta犇;5LSs+ L (D5,6Fv6R|U-3iHm%:3 d&Auc͢թm9{$.ŝ4מ ƱU3ɤ(F,5ec[|fj$*B1̾-tok .Jv7H"HrX&mTb)4L*lQ6t0 .6(OP0hJ@la@@C_DzBT{ 5HlbSVׁNQBuA1`u@blр4c2.vN-Xv{t屭#,D;TXF\x- =0՝BhnZi$Q .z& ,?&5CbWG<ěSVFa20NM#tҵ Qa!3RJpF`mG%ptuUswX{Q T:p~׫ "N,6Vþ㟡TKc䬡>ˏײ bomX*ɷq]4޻[8'=bce<4BԊrc6{Wnx-}-/&]Jɦ?`!=:{=#/@4b >NVe!D]˖F},O_m89+d@\AgBO;)] UnJ[B! =B!53:{ޝOaʙ' !ByUgfLYyB!ı'@z`Bqs̰ /B!8ZNZz`Bq̩F!Bi%?sb[ w5@Wz17G! +)rpdOoԻ2쏓B%CjBѐ^d6O%a ڙ6{3XRֿ,dI&d/n]}}|_ܿ4v|X9B!_F]ReF&Ϻ"@`Yç?xM6-9+yoYGoG=k/BR񢃫°*KNu{ٺk#ѓQzRեG! jSgS8=l~p2W!uQU_g[ޤ='B|">!BZ/&|L|c<:1d(tM{r!Bԍ2_׹)]9u* !Bcgώ-\==J]aYW+'BQ[zΗ@ !p}`B!|'^!BMz`Bq dox!BV]61htWF!UoC!8<&0fB!p+ BQW!# B!9!# B!9!NβwǙ3Xot]}v6#]5!#=0 `ƍ0ee^{Y<,_HWO!8HL=x^WKQq kh"$**)rHjDؿ?VHBqLVYǟ{۷F&::HZ46o̒%KP :tt)G5F5ٳl!B/5 AoEBBFLNNs!::^lF ژUܼ\bc1(nw`@ @@0@0,?mdDT F x==b#B5Q5& ~~x|^+tOfГZurHnLbbFj&`1L~~SSЂjUѵPA2rs+<һ'Bs-Q yCii)V ]0i׮p8~|%0F࣏?dWRZZJN bnc1躎1~^eSX<&3 #B!Ѩ Njl4!tbmҡ4떹X7qN&Fۍg\cUF _}<董= Ŋ`4P ~χ UMSqEPWfu~F =:O !GFL`B=hj!T bcqFBh)P˿_gD;ƪfiĉ>aڷТE :vHzZ:8Nvv@ @YY)nf˖-,d2ѹK\HBqLjFSU4U%**e\:cr IDAT((щ( s7?b(ٍ6"g~.\?GQL& ÉnE4:vHΝm#;BQ' Gb205V:{Q5B0Bi]bD[!6S1!& =EٱPv;Z&5-wN'^I I&rf!hg6پuTU=U!H|bz@?rQO! :-s15f lǐ.|<ƎQe(#л_Xխ(`Xp8DDD`XB=1Rs0!8eo/f0`)`0*㐦ܾ_gjij5nUN`s*V@/y؍`l?cQh;W7NQ( $0B_g3xd}"sM6#1 ӿ/":(MT ETL4&ÁpDDD`HQZa9z i=KrPSQ"@ÎUz %0T:Bqݽ&)Mt5qdx4ğ_=߲f-ݽm&XSR\ ""tuxhD%a8 I;ѡۚ(&;ΧcسuEPZ'P>9[$0B㈪rH4e_fOҮ $2̻+nQR8Fċ}#S0nG/MYǐBtAzP BW$ݬZ'FE b4;T$i!xK}h['s,ŕa.`Yƪj˿}({WU+*z`L`+BhVz&Mbfӷs*n[)FqF8"22(z!PH`fG]5h -/[@( F =0c2Vln{!Y~93KtEĉh)Ȟ̊oa($o)khөb5JWx.sTS+7F] "8ڮ5Z^,=虣C8ez貨{V*zU C U׳mߋpV>[q <ˇl`j"0N]0T)- !@хt/J̈ osFK`WrePv,EP̠l'kEެ+ZG&v~RJJ+cX1 k_]qj`0T譱ZPncZ*i.W!{n(fޞp'?~LKfo 8sLVԖ>Zw+zmMHLTv PVC:Ed(m|0J^2GkjUS)*.DD`0lnV+@EzDQ|[݊kIF 5/U6?Lǀb؄7X\/po㆓&6=1f),v#OLdxL3Bt=x };d9qSX%o=2IoI3eyimhPeך=fwv)LlY4[VXG$POu>b4g4YOg ^9EGEvt+"۶(yٱs;QQDDؓd"\ BĒOdd:*Kqo&?c Ws<>ck1y@ɯ|4ܿW~7<;w;Ľ7ɟqTF<,{_ݗ'/bͼW8Wy/['E] oˋM=J405Ƕ'[h_܊eo)捧ۣE9=gC~zj^X,-f,V #cS 7~;ܑ>Bn"Z5r [ fg}\Cx!U1b5L9k&뽣e-]3L\ lf1ve%={1=S^]F/껖VhZ+!* GPZ&)\sN~g0“A`Zqi݊K/yLR&tԉ-IHHjbqED).)ϞvŎ))-uV\~$5i$""EB,@&_ˌwr?VlݵO̳U7RzS/x&Ϭy,/S)*^vXqAk߅يI f E׼XuVȶmϳwv/.!fa6Y8vplݾ;v}~|~ Ŋ$!>Ңy*ݎpȍ8a(Xqڄ+:+9ə ״Y}<- . /8gGz1ǐ(`MSˑ8W]ђ!l F p/6FkaLZ0\.\.>KBB;t$  QUܦ^1F Ɉ:eZl"ĉtw .Q=R෷a]PR={ߋjx|Bw7 \Z8!a$$%gg8AغOW6x) 2~̤s{ Dūټn9+g xY6{ҷBQ[^ =&YH½HBMg#ꤷjkhZ6m"䩆B!ß،nt4]E42u ?ܝ9uB!%UH֩MbjkbPZZJ0$ *`"'M[Wxǿ˖㊎;8H[ӫGV |$7ovi~za~?Vؤ *uB!ı!}`tc់.U"`;x<=/M%%%L4?Рu9&!WSH7>XNetI)`0H &IMٶ'qMU,v%X!QDGquJ`>sn,l$uF{|>rss $%%Ann.;uc:|)z} ^ =qg_wUR8GA lteަijӷi\`UU2m2I?56"*ڥrB5e{iGc> `Ȑ!$a׫?MWiڒa{YfѺuk D׮]y攕K-k_j`+6_RgtWsc%}T4 oٔh:Ny{vu+R[,*ǯ?p 㣿2B4 qx9RdI?M"{l^~y7Xyz`z L$;EK5CAUE!󑐐@nׯ͛7d2zTU%%%˅Vrۙ3+.D+&K$=OΧn|GwXY9U鉧C} VekGole  |~ߏ#>>4veZ+i3oN e{ oAc;sOubmA L~+m+_<V}'>hE+^VXק\ ͣHku_5^ᬲ_YgG_O7'&#Dm2<|Ofݓ.ᢋxBf'i<|siNmx N}YZy|r[9;JuJ39^C 7Sycx{V(}WƺzK~}K߹Y9?:b4 ũ s_aZî_ㄎiHcͺ5v|SL[n%&&MV)KQEpŧ4μu~/_'of8~6Kq&g[KqN,iN+OMFތYgpgK{/g [bw{ _sz|KON9kHк5|<ĥ\fN1sM4 Q?(| bzn=N$9:1Q }ؙsk?֊V\h2bap6&%U_MzY3QzS|~?ͷe˖أ`^g!U&/Kn]q>b_WV]MRbRM$%%ߓkR*X[1r܅|ty/ PBqz-oq@2\8u(Xqڄ+:+F̆ NA-Mg}nCo, )DI9ə ״ zcHu&)/I!N\2F9qU]:(5_(u?ןbb07?/~]Tӱc`u}gm $WC O4M{h^52#̷LmIpY0~9 tLݽ{7SF>Ve[>NW!s<нtϯ)7m5+ٱ{{ޓ,_<ۦWeV ||[glHbCni͍_besy=Q>|__ffHNX[E?{"><Wgq_NzɴP=`ʽSeSU3yGW qg]O?2$\]޺'<%Lp3ɭZv]8P{2xhί.~;ˀz"=s1J.xl\vE,3ӀԤ$??N q Cn*?W$#`a#۳/ݻtK.tЅt[6(u(מɥ,xEQxLy,3Uٍ+Ǹؙv=塥]xt.|M]Kp,pUǥ8p~y8+^ŧQ3n zǹ'm9V]\eH#%ɡ>!]ժuZ9@I3*R/Y-˦gߦ>֒7n*_ ko~0_tu7.%g#Rn)ޝzZqȿ9ڽ*Ho_'nӳg&׺w0 tcfrؗQ .Knqv0|Z~kR6M6M|e80ի6+T Cwx^̥O@f_?eXK'ML5nZ+0|ٞ x GA6m;8K+'/[V{£tM tӂ Csx۔J9 0 }Kݭy7>&Ġ'Yti󚯔 %:2|Wxx'Wo޳[)>ܡ!]ZRpBǸUe}{T׆9b=SO44"-=L$9VsU/̡аh46}`ۍ-zg5q(42Y#Yhׂ;tFrQR 5v/:=HRL8{M}-{I#>ԔGW+'4MWۮ \HI*49Cy[OUc{Ɏ8Mo|zDw3߭kjU( r?`v٢|Gye\NhO-K٫i)%e(pеG-dn׵OLYZJMbK?l~7pj4en{Wvܣwtuzz\i{8GKNӇTBJZ1UkKKkt/uݤO'I9~^\_Y;>Х/1\+uJRG( $9Ѥպsv,>ªL9dn?-ytչ d~1d(G>C-.DkUS?^C;"?j/ _*W%Ö!wKbb-Z]JhfaH\mRxd-o^$C>S'G8tNU򺔚Fr鍅ѺԯQ~usZW}%)=1Q$E775vWg>s-z|{PM i&.ޡh[s_,v9B koS Фy8\]4YUf;]w_x@m; Ӈ>,]}x4]ԧ['g*Qq:}w@9y%ŐWOᎢl*j[xMYBµ7;Zl>4TbAmQJ >Wb 337Qg6 +]֨rW)dF1+W J]nY}kܥ;+ɛsFapȞ۷ш{^1ʝqϔ2 .CbԤa~QפdHq}5G>~s*a8.C!_>tK|)7)׭w CzBC!O Zٶْ BGj~fыtgyk[Ww>eHGo}HDiҘ! JhG˶g],ճ>s ow. {ޠVuPe8h|j;]5z} .BP*/ሖgUQ*, =XZp *|VޓnWA;)Z_X*}N}aglk[C 7+ӝ?S7ޮNIjl͚:K+)o =5eBF֤hշi܃OQǪDڔsfOGK&Dtsݚ}jK= $$#PYS0I=e/u1Xg2T{G6tΘdZ緋 8uv`zvְmUBP(E# CHNr.Lw*"+= tv&tFe%ãBW.E//X^0CgMuSzi<tו>1Ӵ'ݭLSKׂG)&*s%h4}ΉOoK}R69R^SzL~̓ګ}HI?tոkʈunMj/9#GYO}ۯ! wwueߥW]D%gbe*DrǮrmaߧl*HW02^4##S/zv]#YܲX=X,ڔG!(--kH,Y,VEɺvzyB*nSqy5 d|ώr<_J=n|Z+Ej'4=)k¤遉3uܱI9y9 &[(;WYdXXJtk]*e^>y }*ze"\e;ڗ A$YHvRY˒{@a Y])+R.Reۯtʉ Wsv\5(ݾ8R)+$$D-[]*;۩tߟ4\.(>>^ WlLG]g \VŵwMm"kWk[+!K'=L l} ?ە0czZ!M[Z 8OSo\}C$#_/{W^%gkU~#u-WjPCW:|Taaa_?L'UVnNSF2$EwS fjP!o~tMuzZe>P^3"W||C Mh[ \+4u')ɧ+IV֗5 >OWpp_o~ ?t` ts&XfA]H5Kۢ+#t %.Fr>P:$17*^AW|LYM׷W:-1^^$)m#v>ِk[t]Cs\+4npܚ3Ը;TQ_ UH@E]+8~s([=fݫl1| ZTz-\[ ƨ޲!ϐ/Р*ruy`<9Q}_k7sh^Cm]oRQe8uwo FQ̳ϵ=b5E#Kz4$h"Fhdx%Ó-]Iݮ̈ցcj6Y m ]TF>>5J۱EVG#uH;/U3W @'Vpx r6<ϴ-d;Gs֤˝No>>OÔ(?^CMϗ;}~r?EG7)@;~K]M!kЩUϩuߡѳ\ ]sje]mPףܴZ[jv$U"z`ps868 Cs 9tQd~pjedDiҌh:cfX<א\6Ywxj1tfNj_ެ\+4n=۟.\I}{-9UϢ9j: +ңj?t^mnɞ6GGSSVtX|5O{ WM۶lbo -7GOO;锃W)|5CH\ \E@70L?.9㴲tAu)>m#uw s8 W|Oã dm'kϪ[&,Ѝ*rtJ@Y\ƍSU ɽKhFol+ܹX$=FG&jԸMr}}Nj_k|z}W?kr^ -~`P8Z gܢo\G.e {yem]M+tdž~߲?O5% M{JIs룧Wm_Q3tC?+Lֆ5e.=?w˲|uͯYn^ -`X[+ suKճPu9B]rכZ^(ڲp,u;TFݡg~Ȕ/Pm^Q:izkMo}l؇Q#Ficnn8lAM>V%ܲRWZ풤pNb9=́Athè<&BQ\n|W/NwD6G_Sµ˙vK-D[KDy;͚|ۘdhzco-}]*)tUhR+%VWm~nH>G,ٯö =-5t$E Jeެ.gzdH@Fsvʐ.9m, é=.kI2d(gr͜2Z} S~gIToin>L](ʵR[oYMwԄaWkSكKsuhs2]%EB'\wv[g1mIT=JMڝxO=]1O׭3ҢXԸ]yzWK>ΓM J(ؿYU|ϊNϽ֗O?)] -`PF%D 2SRW=;tU_GC%l_ᅵcHt=_e볯/^-C]za4u} \fTl[jZxcxMSݨ*~U'\ޱP^[Fֿٯu;}ܞ<߰POLWKi}|W{U9/VۈFtB_-vPQF;/ -`p!CɃO 3uye|N.j!$9N#9!{X{NI_]nI='*[Dc }BWu|?oZV%)|:')<.2dȧ_#?3U:@a((sG?ej78Hnp6耷vx+z~a]"<1]ҧU>Z=L>Bb>_}|KtS%9zpZMɵ/[ Gܠ~?z{Dwmi0@6j#gҳZxkbE+QCRzz >L/_ngH˪WC(9*٧~m/IښVEu@1QRu#k|(Yn[}x4jܤu{w$K[SUZ)&lVQnzڔ5>|zeبwX*9O}>ֿye3kB{?SR6-ͩϳ29\9juA&jZ}4>j rAMR/KחM!jؽ_贮Ee-!pӤ'&hTGS",ZRԱjQ (?$a?Qy~~'E7װQrOlL#]'_}y4uʒXVo:; Վs~*(0 `,2XiJF `Pr]巵Ǹ%J^4_',EvlGEĦ ǩSv*ʮ0i*=0(+{0(47B-~U|FQeEJI;k!RB7H QAAAS?cxղG7ʯ[YP4WTxݲX,]nݼG7W] j\?6[yY,{ 8 V!k7јS4vh]~Xڱ{2zf͞mw[]@j|{_~cnv>xUgfhtڱ}#5|Yӽu먗Ҹ 'R1x`!T>݄:e{Jf܃[/) WԿ2ӳvj1Ͽf~mLJ9b0f={%YV7l+GDԯ kݯ…rK=S̢&뤿7< íg=h?~BΕߎou+_GF%Հd}j O95o0:5mH練P^OE>xP|\z^|Hz`JD(W[,gF6IIaz.p-9^~5Dyn`Zf> H2t :X_,^S;׆u_ԲM[m\F۵=^TW!C{LaVWFAܷW/7YRY^*Sl[r)B #EjaT9#<4\M4NPw_6:Qm۵)*6FuU_2J"IO;'UrO*pE:㹩ڤuI>U귵X$Wxj)!ho)aXb*,̮͛($Ħب(Ym[忞R4c^>\i7u֬|]y};5xe5^=-T'O{_P߸Qݶ.m؛/_I_Զjz`$W|Yr y T㓜[ui}NMNԘvY $:_3^/s6fr4>EwuuxeTNФ:;ud5:'}p`ZVg.K;:} EF6UbyfkBCǗoWFDk}ukIUVv8Q{&,> =0mU܉M>6[k:ɉ:m5mD>oN-[Bܢo[UHwW۔èGԲ۷]}n}v=GiYE|I WFѺax Һsxur|!$GԶJ[Vv. ߴ@<0@-3vY58Ša282z,Q?yv+ ݔ5D!LԲtXu E tN"䬋 PgQt0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0t0AUT&HuY~S/PB&H103CH3&H103Lb `f !)fFLb `f B R !̌ 0A!$1B=0A!$`CH3c)H103z`CH3#)fRb `f)fF R !̌!  R !̌BCHA!$B&H103CH3&H103Lb `f !)fFLb `f B R !̌ 0A!$1B=0A!$`CH3c)H103z`CH3#)fRb `f)fF R !̌! CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCc@,4ӌO?[\WvWznM&-zt d*qU|rѨ<g@OzneHnӒY!CO?͐GK!PnvI* /RS^1j@[}SIENDB`wader-0.5.13/doc/user/images/edit-contact.png000066400000000000000000000522411257646610200207730ustar00rootroot00000000000000PNG  IHDR0u<sBIT|dtEXtCREATORgnome-panel-screenshot7w IDATxw|ul/ z#Hc'gSr~ӳ`AETDzF )$~yL6~ߙQcƱx˅B!>Q{x)xi=&=q$%&v?rG1fcQ}b}fL3u.mXg,Όh=>٩j @ף±IF(D#QRۥsKs:p5αO/gP%%A,|C(A,A4Yg!1e;~7XOxZ,B_c1Ccc G_PGgqtv0weϠMrYi_+&~!{|6S-j`}m#vS[6 CTѣEžңnyi~~ް}ِmQ 㬣bdj{s׫ز?|PPA4pFQ(F#Ôv~0+O,+3DRR-_^O#qP똞$Ψz ueǂOM ӌn >q'_?`רU ((P\8^Fۡp8F*V,޿/LEyd;_(ve߯H9=hb=2Bx]NupR!$}HC^I>QMwruz_'>v8jͱӔSb.@TY< tRtr)AC +U7D pH'ٹ|-IyF?ҪVTغMɸHٷc]YN"3HlwH)\E'0|C-z4iO[z4Rxʳm>} FU( yHcQtE1ӷr9+ׁCI;dNc`ͽ}$h;OwdNn,vz x}%N{o&헙+!Wotܸ[*vt+?/i]&`cm1boZWl:K5d Y4zNʻodwث}k&B~l6nzIbҫ[2#N=>?Ayӹj-m?*Jq֙BGn6c/M;5#N.9-}=u>4,]Qn|!wQkg>.1L>{x dGy熛yWl%s1oOdT]uٓ))2g9 ϗa)DؾàP9|,vYN _?ғ/ȣ^D mxO.}u톑ۯ'}{Dq7ѡ=>|h#Q:bK]ǥ>wLF ʮOo[1As2I#Ũ=WìLIfctRi3.]/L堃xrnO6g 1s(MãYMzչg!È֋җO`c'm:n/j,za>?E 58a{vR! =0QY =x<^zptn:YNCxUBAM-dG"`|_9tAX fp%ۿ*g齹[tDB/mҧ? (NDEqsսq;j3`ޯAEwKNN{7mT2p[xB~nH='?!m|5RеJ YڑJ?>Dum'V~C%=UFظc툛wmKbk .NLt*W=-!akLHG'I{"vV)&4BL L#yfQW[LPcozNEa@g֋I9Rc;ဎm*E&@[*v0d];Vc-LB>vTR#B$RRjPH$TY|hͿ{?q^Vױػ-`V(( pL#1Q$`J %{& _镾,2MvOZ:Hwt\vx =0*~Us{l;`Q`3w :MtLvT2=V#ۥ]ť8U(CA ̳~+t C'`w) bG>(y 'MdŲVm1"[gN=T@晷w7 :&C* n9vZVP oݟnk-[-+ࡏ6fCmԱO{^*`Tee*Gq |>"rz%U*!Pl26\[c f>]\){'2R61y'G{7Ugߩtui4:52hScSlI+N`ŕÆ"t(h#qYAѭӭ> Ύ2 `xkaT" "CBB%(E Da;c@#6'Y*ڿ[6Iɛp>%ˆBt(Xڎ}T@Q*PJ(v].Ŀ>IPQj(^Oi>eCA:) ]]3OSAYPN}? Ɲ:m\{B\by>Y{y#rl{NaMz=+8]}\E~YW[~WTh,^>.vR>F> _Vla]R:ۖC: vEY=U$ цn(E6 .-ZkE^QM-0R^%c"nTykrOE˱: N;^o*]Qj>q<m`Y;C։д @FKwd:JȰ%0(>j xFA囷o@pck{SFR1j}#1RH}sS4\ +cMq|y/\Fw<'M}숽=D?%ǵgwo"u`A·d-}ml|!4KUR9֛)yg)p[N狒|s<`Mw1䱇* mP|s,#/}NCI޸^?&oƉmsz_^agy=_m\s\N*T9 jAm}cQtӌv%CaPV'TGÄ1)<c?)#^pUTդ{/ǹ ʣHZ/Mo}}6A;W{Zy;_^Θf%Q>#a†A䗇&~=ұ!}>~~UI! t߻g&/bs۩ ]T Bp\O@0KEu߲YT;T3~େ9Ah|UF^]bǮ`KcvDo{l-r| >d_ ﺂ׾K$U<䯸xZUԺ e+}<&_Pq0'_HvN󻘽p.{zAg2?-'Xoh .RN0GŸ" \{%mw?Pi{[wM[' r ۺZFGL$iӤK3H |5jα6q,bý}%to ;Q>meP؜Pl^qP7Î0:8̋6JQ(?ommPF[ [?gM@fXձ~Ѱcd m;Beay}hw.wԹvu%TX4-EѰyTFwHKtoܳjHL7*iv+ Qj,VvrST,f#Q}cNj4WQq$:[%=5JIČ2*JWP e@J. S#@0c_BNv^*;TMUU2離yX#=!®H8mY"㠨ؽRHR2cTd'^\};mDb]l${"*~g({`5T9IJ6a$A9Ylʶk?q nO$IVv}{;>#%Eåņ&;ޛ[3i1'v|/dTR#PUR[%ij˓Nrvz< N&ζVu8UC-Q,?\P$|דo ~t ) V Yn5نW0Bԋbc>}WlpmCHʠ^ͷgG/6yB>&(BlDQIp2m!DC㲏!?z TPHJu3&53!83ΑZ{梨5.Bq ,o~UK!BQowRbJv,W!վμ|69Y!Oс!P*q )_Bqw!G0B!8t "W|9_₹1B!/``*aWļsy~\0Y&Đ!B4CHu&?ޕ[bw,R#B{TyE|zEЏn3\o|[#ZΜpE|b !ԸI?DOŕyf{s]lپOõ(C4#BqxͪLs˩K"Ym{2闸oIB!h]Zwwdžg1"FżRt#<!B[O&~LrjcwqϹX_fbo?3/>.B!D(cG0ZҢ5 CA $Bݱlyd5@~._F''BQ_K.@ !`hu`B!Z_W!`!B:q -vB!qu570C:ٓBEP!1 !PtB! %B!Z)`BH#BVG !B:R!Չ%^`۶ɜXb%iҫgONHNNNK&Bv ߸Ѓ[Fy'XbEK'Bv e ?ӳg.fvn&~{3vhv{KBѪI֬7ߢd.Fc!))͢1M6?Qrs{1adl; EEE,^g&59s~w={h]B!Zf)`^.׺HI/įy[BuAD|>_|9p8LT),* = ^|~?=zvW\??~һ'B:Vu}]gBA>S;fjsd̛;BF#a>ٝ:p8Xx!6P0XVVZ)R^Q7KFQTUetօfgs2eĖE!UiI=G4%F#XV9D>L}:g̸UQ0Lłb%5%`0 ڬtԉΝ;өS'ڷkK0`/a q8sT>i B401L؇iE; ٣''|*~cҜ4ZEe%-vVfy$A7$aaB @Jp8U~!f-`z HLL$==Mڬ0|4l8s>I3F+,*$5% R<D#D"UFhհa訪YP@0!5kF7 2ڤ! $ {߿?n} G7gZ RP_iۦ-h n%bD"X,a"aKl)jCF0u磰B wO!hu(E`Bee%v4PUh$BzF={\.fɼ\xETVVRP3ɉI$%%t:q:hi^& ૬]* j*V%˿)f!PԌA0b I_l6;I(+o^h1719ܩ͕bM0.?pL9aC4͂G BC!BpP(L4!EQTVZͺLph !f,`b=%%n#1(ԕouAl#HV`]9#/2Ҭ709 z f>:uO>d琒tD"|~JJJ(**"??͛7Bnɒ-!Uju ]'117ٶ,&af,b IDATSXcn$ew,܈YZ*(?A؁1CIKMc1TTT~,YB(WYA8!( ݆ᴣ}!6GKB4HDhK! jT7t ӤxcۈFu,竊P PSF71 P1#AeCuH '8NtJVv ɸnV\A$M6$''B gE!ġju/|hE)Ðiea1t[4yg˧x6DCIm,Dm3oZZc/,܂ɘntEUl6. C$%!!`0f(Ju _1]V'TUka0tmW>nw>SuKJ0:nH4g0vcx&jF7^SzOض*|u FQ%.XB~5qSNb-8̵o߁6|yqi[ x^l8].œYQ=WwvC0+ PP<`RmX0X,u#Mi׾>7_̓X;xCl wlꁩ(/'αoLGbq+)vu[Dw"5k,ҼJATU+/Nj+0 &ӧ{(ڶ`[0k]du/ $%%!Mht+f+\ڶK1mNp3@_>FѯDRJA4MCQhR,='J#B,fƈqӘl ꋳMhԍZSliB(ֲ^H%g楄7}eiD~( 7sBAÀJAogV>ӨfB!đOѦM{l|Fb޲u݋ ls`7$$$ś6>Jb4"sB_FzUhZ~k_bCHjUiX,Ucp:0BR1gNEK'"D.?yxR5>Nn}G7(Vt={+snoBF̲? %5} f$QT? f1 H$Beg݌wJu5s`^tt:k~.++qܥ[q o~]^~ߔl2,zT{45cJ ,_۶w]5[3f}>l,e̒Dzdkg(`v1]8J_eͲb6;]uwВ5s`{kv;T~v:xoύ ooڝ ugs,}c=-wsb?kIx>'26jh !elĆ40 0-,+X{LF vC@0iQtCR,V h4jtb. QS@ucp8"(hf-,eMG=Σk/|>51s6?eFՓMs{$rӭ/çŜUӸLKNed\rr'0( YΏ_DZCrK)Wn ѤL#5p }dG~Cؼ~6xq[Xrc;Ҳfl~'z?)=LӬy$%&va˯1LmJbb;h׮-Rة0)iBv?.K-'???| [=?[ʚ]q(3$]4 W`s{|\ :i4~׾do[WOfcu}kxcun?'_^%)b ׅ3/B0P3´\Ahppb>su}l66+6&^dܸ3K0$ P5 Պ?ৢLp8Evu֮]֭[9ēj[IBl9\<5'D?Go64{BF]9KY:0iꃔўN}S{od7fE+bزd_ R1Å_dB*E4 CU J)o5h]6;M{!LKO!CayW?؋eesZ~<"̶ܖi#Dޖͤg{jQu]S aOM떳bSyT O?~k !&՜ٸn5z.hJeռ0 \釐,8\By^1f,IWd~Gް!wWB>L8ؚ,$KiW˙pu'tKm1{Kgq o&Kvd2?3pD+v%{ Y@B!Px3giwu} f<糶1 0vw)l3XJm_|@~t8py>}ݷ4B!j4P%q^QX}}GO4_RF(pNUZ #&CI0tb2yuPR}Jx0+8IȻ:MuۃpjH|!̧hdpƚoTc*f51v櫔<3v,d@;=qcaJ֘1m4*ʗp\r I\«0ЍRkc]gfҹ{`_!8ɽBjT I.0"BVG !B:R!ՑF!0B!huB!D#B!Z)`BĽUvBju5S?bANCLҸwo!F-Zx/!PsH0~[6ӾCGL0bj޺u+Yړ !PpL-.*zb`HT|li !pH0ŘBbR*DQ(F)++nw*yyy-8D,i9ޤԖNC!D3| / Å)^~TVV(*NaaA"B֡SD"U` ,^F^6¡P"B֣ACHӧӤE͛HMKoKG$ |]!3B#C 7|Ɋ 7!+-PB"x< ܕ%K4u9ٽd ?]gQOn!V&Gg*#b.ӻslPny !0Ǐo"0t(Ub]v娣bұcG|>x< o4Gθ9nNY\a|})ZcJ9ܸq:>)@{CGk[!D|RS1 ( HP(DAAPtȨQر#P(D0$:ڵbF l`ޖ\pauc<֣Lʜ9rհadwfͺ5|>~?PH$R{i˖-$''cF[~EN,3hKS{[<\˸q;j"ki^2<#\66œ/9V?Ns'Օ.OG.SOs j^@[/dt{7t=V8ol.|f8pd [1X/gwfx&l[ ̟?38^{qGM0 MHOK?ѭsv=V4TnTn$T3ﺄKL"wt^ɒ ׍W^ʷ {1:X?ᇢ_Oy?> W~Fg۬RNNeu͠:Iq>IF$6Ŗx~vp*9eyC}-Z-(c,Id%RwBCV-xaw zj232dff4(FQ웂=Z2 \g^÷G%s!Nø=f\Ϡi3v/=׿bRwҽ6·9m{ ošDǶ\Z7V5J(bא^QBp1 #;\W;-R~UiKY!P n適~tlkV#gKYNQq999{lـwy\~xf B%kX9^:3k]Y 9 )#r/>0-f@Tu*-2_#p#<}|KHUsx;Yt7ʎ#kS߶+9f2?ȋ^?Trͷ_BqjPd H4f):ӿwϘacp'),)c!ys9̽i*uKWې^jidp5nǺO^sY}"=܍oϕrU:k$={ bW)y&o?yr8i32Ggv8"7#/濉WnB#2v󫅋)-QН&+^t]gx=>h4vmPQv**ɉ+'5I^G _yit,i9cMEiqK"{ߙKf^S90MUT3 $Ih}QjR*P _^f!BYZl@ džRSR#TTT U+Nv'.p$ilݺpQWB!D+"lVTHHHa)XLuhjAnLut\t0tLVB~E ΝeTEQ5dπY+ciQ ݠOx=؍N8&t+7-BfR>p8~\0ii 8 l6{ $$xZz!GpխBC!Sx<]QHDKӸsfw#0pvm !D3{7j=ֱ/A w]{_U}q}V2" DZ:bŅ-*CX֪T(jQYb2d02GB 冄&!~w7C|Nj$G9ɗD)Ҡ|-+?kguYECkU`n3L jS+{*bqS{vrh5`FtD E?6s 0ܚE+5]:1p>ygn̙Woӥh}!)BxZ<͗[R7L7咖-&/~Yq6/cܩjz`+Ir[1J5UuhYǎT G#wTVsiǦig˝}4{u6{6+[jzjCtN|]:Ru5[xh>5KIikS5ZvcŵonuZ{Lpcz*XǯP *vݓUk6-R̻MH\EsE⎚g(Z]n4$I '%CGDzf-D+U}2q&g>OLP*\ކ_W~ur7@By}r9x հ#וZ7ʕW$ F/V_}v,JSL'\7fIWkIkWi֝okM;kSty`;98k>?VoXuY7t)N֥,ɕ[x*][5/WwRn<р6ti?]R&-N9)~QTkrυKuSq_Ч.($eд)ةiuVҤ/t/R;\nq\n隇ch~ =Vןr܊W)U|~eWսjAL$T xLrW19rݸKfWA{UY3N*)+Kӎl-ؘ[-\Ugy 51-y }eoJ ҁhɕ(7u#nٵ*G\?yE'µT|f \Զ@}l;}&8^wtinҾSu= &~2Nw_Ze=Q W)޽M{2rM!3{jڲy%.̓АNPhI|f:Wkjsv-_>r+/oӭwkั9K_-hk*ϯ:~ _*H8\,WJ @;*mBwR ʣ5.j௯glҰ[Wma }.> ~RZXf@ ڟD<+;OnQC?ćgm8/'wK։ܚeǓ ҨɃ4*ֶ%1SտHuKh* 4zI^? j`UGH} K'Q/R6يߨ|YNڤ}׏/P7_|J}2])V3ҰWSgۥIPn%ϏW~u_aUZpܿ )՟H$.Jў+LP\7]K}et5ɊkꀣPУ]>tPT'W}o>OϏ|Lܥ7cOugq(_9 wV/?5S=UtWna酊wMWnRvYU8(-=SYiVh<Ъ9i'>6`uIߡy/7ܨ3Z\{zN-ه*^Cg3">RIJS?cL3_:c3UU3:ʮ4~ݱ=,yēW~ IE*ZϯAIc4@cx=YR開*e}|QIgKT~G~eVռjӫt`β7[]~ڶ_Dw8,}j_''Ǜ"eݚiE-5jj}>On,x,>556U\X[A?7[a' g@;ez$7^7ND;@3?zC|?ߖЊ)#tNjVsdEtsj+eVAݵeZג-z8kz+=6Ts/3u@1SQ6yn\ڴ?5׬R^(Eh=tO7.)>[^ nMԶcwmݤ[n8`6V7r0@{_q)_ڛq{;,qVx"` 9/s$/߯ qb}g<,#q G([rlףtew۠SmZcVSQN }KĕzFrG^G7&XNt"2x p]W]RuQ˖'(ڿ(R0Tff~d++3S: ή9'56-[|ѭ1]{^P[E5BĿ,(i 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00&I-^\9Mx $ue%| |uԍ}x=3~]mc$ `!,c!, LbIR1BJRq&I1BXF$)FH!%)FH8!,#`#$e#$eIR0I2FHI2$)FH$ `#$ `g`#$eLbRb30I2&I1BX)I1BX$ `!,c!, LbIR1BJRq&I1BXF$)FH!%)FH8!,#`#$e#$eIR0I2FHI2$)FH$ `#$ `g`#$eLbRb30Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`!`9 00Cs`GC@9taQ0PޮmwR4=T(Izo"_y ۵M{9R v%GFʗWjjZ]̰GHRYH0y{pT?^/H)E39IENDB`wader-0.5.13/doc/user/images/new-profile.png000066400000000000000000001302431257646610200206430ustar00rootroot00000000000000PNG  IHDRrRsBIT|dtEXtCREATORgnome-panel-screenshot7w IDATxwx߽-H$BU[P+{Pl &-B5 5ғ$[rSHpчls;󑨆i#:.`RmT1wvB+;@ `2(* MnNr>uWE"ǴM/>t^.K1a,ͻ& @ )՞;+KwU`N^=}<40%M?Poe?2Am銲qicLZ#F}aH^@pŚC s+#y{c7cIFb`|i3Sf-Ȯs@ hx#!)L\eGO^ z=/EfƤ0 h/h;l$OZ\xeu࿁ -&?әYFU smg(NYN uaDM $qa<Ǖ5]1:t:/[a2+rFCrCqY2Si1 J 6]$'v%5؛~8)(\Ihp%Uo˷nG$Q#st |c҆={PjOu&pc9"vAh4CRq1ʒ5[Ndsyto+-'Nm}.["yRuz|3E1P࿄̈xH7ao+7~4qx?VK;0R$Wꊯ L}n{bˏ3޵&D˒x0%VܳCyeȔkc4̽"m'0^WV~ylk&Wm~|av2o+끕w|PbB&j=QOQ "`Bwc$`T ;fL 2wҼ0\=8yqq(PuV+8?OsC[5K6[wlfrƢ_ M )p[V/Ndg2Ⱦx#pPGO}qVˠyp@ N{ 0~ք˘7g0+ !Ze*=DL {8ƒUmH`7<]/A 0 }=ߧ$ s@B<6[qxiaf;w|Bi(/;V;OIx\=ڐSbdO' #w^~:#1O QA iMhs4TkºSc]%7y Fvѐ2,v[x|¬\ԆmicfT SpjT5I (/UCR*5ksfڑWVL\oM=ZY-<`X!DQ 32"?st8>.R~0a4*7GFc:)Z&#&C?oi xvl͹ExЪUmFSJIq&q,q'CcK ӁDIQ RQ>%J1!R&c*oĭų藭 hQi2/__|)U1%tS{v(1](z=~.\{#[-bEgCa7nJ$~"Jk+}YN濳{"7bҝi UK4hՒgg]69Kth.@~q>^j4"3߈jI" یeCBi()fxjta"{j T[)IHęgc„T]6/`7S 0_86) F{i.J fM]vʹ'M߯EWuǹaR_~ o#lޖ;2 OH'{wRNK}e(%0"_[VK 0qQ+'SYs9Lڠ'fUb$+=Cty?TfcEJ16jU˶}DZ1 ;Um]We L$v+O'DzNFЩ58)OWLPCCL:_iy~{ dƍ3BQfr &0f$UehN2H-k [bBCJv"J5I銓%n&"C@`+=(I 2qR_28qя=Io9A}Bo8KaR.&"KAd wd 0|{'UK-2 W/'IF\}ɯeec:CNIb{c}\(Zpۤt+=dw )m܊%._vz*KT؞ʒXlq $!10tx>@"U9@b-& n +H/sIsd;-R,.4&#XB&ͧG? tm!}|o.CWBG7߫3˖s3`:#d,\|Y\UyÒ_vcBwpd^2(Nºy)\TqUw~m7vǚAp}aXIHZ:yÚF]e n[=0B[m[1!7Rd4t@`]GYBh=04Oz0sGGJti\Oel}i!jXbĀӑ/g {U;ro%> V2[KpQnE +~wL^g؛%_JA[?7b]%<9r[H|s^BMa:lsQ6W k] =~TUz`T_ /(quR݈\@8ʙard/rsahDbZ$Q*әbcJ'9*|2ܛ{0*ZKNbϽ:9hje9F(\ҙd29Gnۮo:e+FCr={I%`, @ # @".BvW٬vDbeN sz҉!l_v壦O ϯE.o#+ֳJ6˽7 ;&q؃b~'IбtWE:zʼѐ2h4z@ 4 MkwS-lUC@ ~6aKVV\\LA~^(VQ2@ ' W_r_k_@ Ʌ3'{ƣbλ@ &" I"@ $" I"@ $" I"@ $" 9ˌApH!SY{v=]T!tZ<+q&Jc='h2fڳXЉɀaaKK 3W%=m C=Ns=O[Pz#6Н^c^`33>Y=ewP mG)L}}0KӒ.S?  *H-Dꂪ$ z`0孷T yt@[ymyq3H0yz eTGMڠ=5UӞ[Թ t~26DeJWf?JqDWqMWAI-xWn 8vK]{<~/9G9Dm&hԈf4~^t`tO6o~{{<%yޟ\&lcŝ ^&Jy];+6qz#W6:{>|ORYv_k,rƿ=\& 8QNxݿK|q˘0g~ )ޤ=@նpx4xqc&ӱ5֪Fhh>{)O,OF~bTdX1-G[`e7IFskI*q4JD> J_rG(A3zOL'~y:Ɣ=t8LG۠rJls78+W:*ڒv< PW*oγr! 9lq/\8~U,MbCiSg(nyQ y\,4$AsJu9HѼ[tum_;KY╿~)2`ʯzR-A E2_8&#&}q%z|/\Wν8D"]֞w՚xX£9g+'/_}3\qn p +&U $?i)gフ.Ė$]~Gg5NZWqz}2#Ӛy@V60uD:rD/W64 DsFtsGm{nnfsm}`[V/niATu>|# `'ܠV=dVFŭXz:ŖErЍa"HWܾYG>͆dS>ˑqLmʯ=N_$XnW9}2ᑮF%F)-NiK*Kmbq& K#h>&"jv-^so@izOߠկoWH{lHeMB'X3KƄQf{_ SD~mr`M Z3;KL,xkv^,E>b[ӕ&H`6P}̬%|p:E?|Jo*B[Ȋ7f߫!hZLw\:{+"_MCX0&b\bzdi9c,BFa1&Ʌ|4IF>YQ1r ?NsONE[N|k:}KX`":`OjE3t~s^B{ӏKNU9ƾzt /ng|ʂM^2wg‡<ƀoۄ"SIA7>#>E?q}?"aΜ>,\o^;Ϯ ?o) @p=7{?NUFΙ˜>buu ZntA]0v$r 3b rQOF#ZR 77zI&)T]Q J)XE'0 u@P:}L&JJJ8wJ: FVCVA#ͫ7^~xnoN^]Z} B乷@s uF%0?СCo>w!.6Xx ]B O5xp%]'* Dtz=6CfNcǎVNTCll,78_B{ޞ DۅT]U M:I` rӏYx19s)-Jɓ9~{bwjL: u2^^t SGrHغm۶Gqq1h4c]\\pvu';;wf5I^sfdpE/J1C 7$) << jْpt!kJ B$F#%%%I,,,ԣI^D{+.j Qg]IP(₇;FǏRd2QZZnnnH2%%%(Je; CVZ,zΗ$ LZ&ߟ|4 2Vٻ0\NpYFt$yBASLz[ ʖm \ͭis7z+999"h֬YwbV$rD-~ʗR*С;w… $$l#?,[#cƎ#00baJJ ЦMr9Æ bV6^^H$@ B!'e ~g5k7߃3|\\\0LR\\Yh˗/3GFrIt:yu"y &'0Z ?G^.P&;vӧOS\\` 33sq-ZĊe*i&9M?"y *)u\b4IJJbɒ%T*0 L&RRRHKKC&Ezz:7m0+˹ۇ}[$/@ XV蝝 jիWo>@VSZZJ~~>zOOOZnͰiΑGiFKo*?jh$KǩbHtSY[&׎LZYm@KQͳZ%0BAb:uǓm[pQ)..&++l.^Htt4Uyyo`}{^[Ϋʼj5I^/,{ s4u ClGܨi,uXhʔl'R0괩C]Yj$ gggZnE/Ǐc;O>m6J 1a"˛t{̕MW(Ob-7'*2KAA>Jl sM\l r#ض6oq7  H^k/s Iq&ygb %+1.dr)py|wV8C{2H.6]./JiC 8' Ǝ嵷g0$6లrgY4DEdO^+P5"= )_bL7wu]]FǹqDfF;.bUK$̽Q3eZj ֜8| IDAT;HsWTN.bxX<3N!C0wg]עTwcHQC:w9ը25=yճZlaҜ`˓Ap CnY{ٌݒm׋{Z,S'KIIR˓mбcG:FEARzNDx8Qw#]0m($Gg/^c@w^h9nhq^ Yczxt=I]9(o6hʶ_\7ι,غ#[1*}~sEAf {`C]+[ynz(ũޛwXk88>^H["yrf,WȳtG%CR /EM,ɼrL6ꪂ=1}p秈9mcQ?t{I>޷afdlnO9cxW [w`@mkHצ{B dڎPBl8]먭Vi+anXmˑo3i`UmDQjڢϋI=y]$'|ͤ]aɶdןt-DIPTx{{8s v?OO+$W" mT 44 '@{.O1I-4نn{_~kW05ꀮd&h,X=}QQe&i-i嚡Pf<3~*s&79wx7[с1O+- >s&M!,s''5@I$)Q+ ;, L=\<1Sm>{*8tٓqPc]ԁU\ّ)[6}"DrܐM0`ڪrJYAʹ61u7m;Zڢ4e+䊓w8cxn5mO- %ٱ_Fx...dI)\/`ʔ)DDDϨwb.h32XsPɮcx8Ul~5J k$P`C&$ViϤ޾ydꯪ8) s:$m {fKxpEcׅ, GK^u8y(.dR`I[} Q.bRw'$^#~`IvԣʗR?*|Ϛcv;sXu4'Vx>g12|'N[jVr6q ſ֐d=`Mmęb_TW'۶qYjNt`Dwͭb^gR*Wnn.6mB.c:~ O݀NӉ6>>L+oK_1dN G @I:ˏfT-0?(ez4 $Rq~R85b]Iҋ-"xfGMi3&}M$ǻd^_ă{J_=}Ib٤e.y@<ԁlՑٙmOF\P,#SXj G dYA]^ /Z\&idZ4=Pz%޵U׋k̚?ՕW%T*5kFHh''Ou8)"y߿{);v9G5:={3l}2gӎ6$8;Tø!_Y®L ,| #\ 8 &S8cK^Ö&%z.n%4gm9Iu|1 6C+!Z= j#&+~RϕZ~\9h F||Mđ|Ҡ˒׿xMD[1SGrz v=8_wޯktX[qֶlQ׿:[R,zu!' 5=ؚzmt|l=[R +"ln[ט5P/ L$T*j5*b^KK.Оgt 9gnn댫=u`+{{"K>(QҪO| -ҫ}؊me]<#{I PkECܓ51O.Э<6 adkB`3vEWsؼc}`o%7|ά֒R!cd8mZT!%9Cͣ !2.6 ٱ?ǎt &ӓ!r Ju'={O<18_kw<=Y *\8sfho Ly}(A纼't7yU^ɫ.RI͉$x hйb(.?`Tүv"gi_Ršc>{$L }G8u&i A?'bMW9& Fɓط{7>%EI BC L={8LNi)jwJzn.q?Ehd -@ 49ue@hݺq~aN <<8vBxFGTrB#eB|}9Zy#yM*|}V9!E'z77:vB(ڸsr2?$GMWo ~iUNyO,(%1c '\)4ؽ'U*"ۛNNKL9/#k>Tnĵkm^^iSg e*'֦Mkd2g=ĉ7"L̡Ctw';"Aic2qxŏ޻WOO}QL&L#|uLk"'0k,j)'z>8FޔT0E\xAo L& ԛ%9*XD)`AŠ|xbG3Ap8=-sWd171矬kY5+J,ȠoW]T?$`;%!I\جK\E;!c'V]lȵXӖʕ4=ɘrl^W%f[yx>FFQf0HJNx ϬYرcG$r*'8 frh(RZ*%i*/fJ%F U*9XDU vH,enoY$^͛IJD>ê׾%M `k+bU~;ꢲ$!uVSh$g~/bUbƪ qXT,j1غޯjgBofY,9(-kVyd[~: '0DAAwӿbbbsڱcG%r*?va`QnK`ϜG~ NNNt'02zc*RHT.N̘9vnGK>n>v`k+bM~{–a=J\E;#bCJ~+Xa5N+*Uבl1eغޫ5 ms{3m#sؼ_ZLEʏR[~:"'0Nϖhڊl=ױ`{ oq [~:"ec8qt%'mlڴ g'ZՋzr$_&cohOQI 2h Bp@&(is~H@C&% n2_WH+1?)RJ6.crɼn,7=F@a+YI%?puUr/+dP6c_]Tߦ$άqhrqH(*d?s+O*ڊIuqVX=c}sY53;UنrL}V%8yPY6ۥnpH?y R;w.jg"UN$֥g"| י͛T5c&A`|kVb @R+dPlw.쑄֞E;8"Sʟ8"aGUi’16+ykJ/~D)Zgk ' 8F#Zvbl߾A-ӉVIuVuπ~݋Qhs,'uH*ߚEX:nԊU[ $!I\ԲΪ_tOej(␴qV{Wu Kغk/nԸf%%ǡz=999;NRJ*JVA-iӦ5~~jr=ɄdHXuu(p=cKjBHQ\Ccl $<==I2||DT4B$՛ 9/#9?_~IP( LR݄FĐL?cG{A@ +LY @$녺X)16|st)DGl:@` UUcŞIf܃ \`X +󥱠,e 2מHKr8QPO2[MN2C2d$讀W]ӝ\|yRgޝ!C3j!;Qٗ:K5)±Ve:.bnVGl$%2j$VvKXA_Ӧ6dT@$0+ԫJ9NU9sqҵY:E2D=HWXHJʡ&eX>ĚCve@,ʓؠFP\^Ik2(g^kA="UN"Fv$ga 9$M=}7d=2 $FMpD"CMʰvފO-e@&9*b=2(ְK^O p f) ,ؐI!'R " ćW쭹|5v6zNêx~uϙvsEšJnԴ2GDAs%J9Ԥ ǖ>eeo@_߲>)>GIyerVN앴'~k_]ן@"YTʑ<;3"s~[}7K~૬XfyS*+WXv:u.]aَeX7 %{j'BԯE씯qxu4;dPW:A="m r*rz v=8_wޯ[u#W٬:-ֱdj+Rk[ۊ)B$0[4h@(iէ nCuQF]H$Ԥ V+P[_ԥ|M5؊f|@PGmrK1ADh'R S@ 4&$+9@ hfiI@ X@ ! I"@ $" I"@ $" I"@ $" I"@ $" I"@ $" I"@ $" I"@ $" I"@ $" I"@ $" I̸nD2g3@A Mo@ h 7U<+ٗQ,d%CaKK 3W%=m C=j_l`)!D%h3|8e{wRԂ FWh@3:9fiZcjR@ A%?=L#(I%TK+.G35ҼH`e .ʨ}[?{;A6<7cًCbs&;,XĸlqeeX^gA؀{xgK6YS1~x-թ=AQk~{{<it>gǾM,cç0y^,1=s/$+YBK?M6Ɗ;L,mu?a],y Y?~Ů";ʬ% z={vMӧpHP¾韓 {7pb"MM`&m;^>HԕCi&7YӁ0LAkM& 8bGJt4oO6s>tt'@w8\]K<֟dֽ&0`΃݅V 3L-P̊U@E NBk@ 4yx #`΋ë򠹇KNMGs<.!G+Nhޭ/;l\̘ٓNsXx]^=e(*st8 IKO%Jϖx$tU akh ^r ׃}yQ$m}`[V/niAvTB 9"ǒS9k^h ߽)FW{ʬ$daқ^-:%&--F ùmt bq& K#eU{IvԩH黫Hբ)<ϡ_RX92XYTw@#,S3+iݡ¹uξ:̢ lRk@ 4oCM̓ hJ{L?$FGE} Y1&{1wC x՜#Xt&nB †ćlg.1?~4syV&/xc\([^zA@۫i:}̟ss̒ӯ-M gNpߌGsLERv+F[t}Uf} AӦ&q u$cVܸWؗcBٖ>wsW\"]o܈~k@ +7@ $" I"@ $" I"@ $bGӡ[{B@T֯z-] h(JN<΄-de^`0>I \_@3{]z'lZ7}H`6dr' c48}LVm՞H`m7_kW랖-h_W0ᎻՖ!<f-6m<D$0u`ÆA"+]H`@ he7"*bإ6YgZ/H7 If9EQ=ը} IJ`Ap}~ߓDzabNBjmZ-K,'6`^P:YDPgM]}Ai<{[h~Kvᚼ+럦O$g7pwI*=/1d|^Ҡ+>֏oFZ1;ߵ'A% d8ycw=ر FfIS 8'b$%2K垝,k^FCɫz/\n䰇@aYoHqZޚ6C:}⛬|~h|=""dz'e 8&EkК&30\y ( UY05?$#nTz 0xH#l Z{i1V՛Ʋ?$󰔩e{y'& IDAT+˳s&#!Ina7ˣ ڪ%$ɍopµ1E- hʽ |39^/8C ڏ~Sys٧ ̼ Ï@$yWO%CB YB=Y:jh׮4&q3MgâŤT8{κKYP"v1w΍`=:Qs{(r=ӣWDQMsㅤ.w/gR|%ê׾QxW5V2[kڰ~G5d-@ _Ur>vۧLKv ϲ4HW8U8Uz >^D߸_ $uKRK062ӊՓq^evMvQX~"EIX8O๹w*ۯe32;_̏2텩Qt&3I%5[IJn銓G^_Q'qiʷsF*Kղ6p[[vgY;y=ç@x%v+Ҏ# KkTa?#-٫.S{iY)H]KrLOk+;xǧXRN@oc6=?'T%g.uqJ}}lX5uoOlV#Ã\epv>RJNJ}oDvԶfc؞z˔!; ` yRSIS3$ oϪ23m+:Tߗ Wȴ>mydon(JX(rE}Ш򺻡S;SiҳeY9{3y锡ƴ)tų=a9x1",4gnV^oVf~fkx TfوԃlQ^f͹[RYҫ'dZ&>=rcYyX&)cJޙ;0\o׺Ώz;1"&父s\uxl}ڸX>^_QƳbJEEgٌUķ}ZOsoÀ|7;\Au~Agq >ŤGkFcZ1Vu56?ǸYyiJm}Ʋ0p"h#avmJnq @m~3/R^w6v8*^W9k[4I)[.GO f36q7|.CAO)_x/ `Q3'+q#ހQwӝaK&D|!n6+W)?aG0_ݲZds,jǚ:3yfmA-_~>92qj[BwO=/,mwlyWwv(B}VH%^AHB'BKBKBKBKBKBKBKC{xQYҽ7%b}%Y$[X8uI`݂vpQAJLʆ4Asaul:t_BQf/ {D0 %幪YRQ/cUGN3vwe5[_!2?g>O^akC4KfuqF~3'N~ -8f:,?IY.ӄ aL9I+uAQ4e~mgiCVɪ%55"uk7 hNfB?M`PP[T$7q.h0CDʷÊW{Sy8q u'-.ux(ow[} [͠oBI-I\m^ guE4)ߞjClNOw Ê7q<4'T='Ϧƙ˺wd݉//v5F6}hOWߜ9cKy; !.&0C,wq9竀o7։V\G?v#_-;S *LcZ!({ LUJ@LwE? =.eTUM3W.sjTV)y|(:ʼ1/brOpk*P;IeÛU6.(̛ljҵ .ǡhaD˲T- ܏vTb#(W@:E,ȶey^L+dR싾ǥh=}$fuz\-[%%Q^=;+ugX6-$/}e}./r~L|5tEcu6 !4!*7i"~jG''I9ԢE~XyQXk}xlFE GrvL>S X2}6Z55)=nnSyY\y}^c?`Ӆ$*n_+o(XȭB[fU>|"jY΋ԭ݀&/j4*5',74z ھk _['tmRZ _f T?&)uj^viՐZ&˩#*g8Zًs>a)鬘PO$.x;پ&=}u!0KKKn_јψ !HTT&-ZS&J[7ٳc+;KU7,,:!D$'}#;`mmkU7' LZm6iVءQbVɩo>/CZQTrq-0(qU'*2HhaC!@T5eI`B!%I`BCFڗkƘTN7S2BvпQr|!J̙v[aHEOHh%0!J#}b]iS.|47L8qf0> iǸpgFi1rbP_mOqJ: )>36=,EФ5\I26pevr331e>ةCGaE67Ob#lTMVdIM&b/dƟu3oᷛ@>R^x|Ѓ#[ _^Aड़M{c>q?µ1ߌxwdiW?4a2C\.q.9c%ρ9$ IZO|!G B/),QFs#ZJ3O-(PƴrźLMzፄhSW{0 2V^ibMWQu};lbDw[)QUḅl#Qu "5p 45ykD;‹,uyO+O wa[Jv\G S̊ K[l]4?s*Hff[GMjTН߂706JTe<9e8E, `̅hS+ ~G݈\,II ttǺlm̚U Q'q(P$cxF# LZ4gocK.Xq]c6#.MUY}bvTϸFO3rR>!2֦emT)rϾ#w|ڍ& X7_e(VCe8l^ڥ.N oOx#]+wޛ?#Q0r\#3We rP!{J*9)}Ms|SۚiU'F(Ju*K=Mzly]I;J5앛9r${fzcH,RٛOs5,$c ]2TJx:,/fSpy,>XLC܍a,Zsk"*G5(Fte[*@U7hEdo"Β^= Ẍ́=ТO6aټr/7Q!%a22r(QVSעOG}˞\ס5|ö{ɠL6> xYwScϕtz-Q4q1&jX5 e(&XR8!3QAcM8*weaEhF O=۔āH-|9f҆Mzz lpU's$9ri;?S\N@ dgll)!;i){cjfxzգAʊ ֥h=|t9k~&ݷ||i-P?/;~!2qj[BwO=/L!>Zi7MhvR鼎.4.=Y:?Q{ZY v}:igvgj`eEcu=έܨx+N2#6Vv+;DѦesyYرQΝɰ\ءQ̘@ĩfik [B&X&J-RIrW.D~3 (0$0QjU̝۷ ; !JרXז%R_nFC !rd4Ɵ_WĞ!Dq_7F/ Q)J*:UMNVu3{D§^#|5*P(q $ +>I`*La>r L!D$ L!D$ L!D$ L!D$ L!D$w!RKc00Jy0kBauW,sR*8:UIԨe$R֍pJ/R ?|T,9ݹkk?C(I.=mҬCpuS% }1k_r("#ZaQTqNTd&J-(ÆBR*kʒBK0/מ1yĕoc@ae R7g[\a"̙v[aHEOHh%=}jH~Sa&裸J+rB]J`e‘LotqI~IѣQ#Éf&&)4K}qPp 3LY<e tkćn*(c-CPcٞ|^()C9 /@P`*f鮱~\(vxˁߐBY`]3$7t$^Ɯݣ.ƶ;iu.5͸UgKΡL{1?4%;×b, N>LY3U|=qlA?q!1"nd:5+eДx5^7;0aEMݡn`$`"fBkZW?1F~O >#e487VW6x)gӆc>q?µ1ߌxwdiW?4a2C\.q.9c%ρ9$ IZO|!G %HN`&^e\Ѐ*I{o}sg ;2u@ >.|47L8͡,/6ֱ} Wh>v]xX'p|ѕ%Np~_o !-P_mOqJ:'rbHEd)-B~!4mVXy2` ۿJT2/:b `VeO`XڸbؠS @B06=ڸhRߧ:m ]pQ*A),bIȥ?;_f.EjXi\{=FH fOJM=ek3`$\wX^%# &o,Gv,Ĉ6RNq F#@aZe$6":U?}"ms*ݭw#pܣDy8Gn^u-?򧉓Sc|4 hSV{0 R6?T3x?MlRJuB۲"G*zst4b 0w5T\OHڥ.N ׹o wcՍD Q YT~}"<epvȸbRJN&9նFڟ]ܺM%k`!\ ;gS#-!GjQMPgҢTg>C;7u$r.ݕqM5x^pm3G"/f նC Gm>P% IDATTeZ6ӓ X-S13ɾ#뎯y7;s7h9ίYDX@^rVl8K}(jWEs."BmRYҫ'dZ&>ˍejTkaIL)y(J=a9x1",4g6C@Ir3GΜdLv9)l^E.Vi@Wi +H#pQ'4*Jw7{jg2,"SjOFQT)ؙ1 K`m}Ʋ0p"h#avmJnq @m~3Kig&=Z6v8*^W9qnFӐw'1ħlx>|RB 'h hW+>ƪ_6|ö{ɠL 9)ӼtrP*DIlp܂ٟ! LZm6iVءQbVɩo>/CZQTrq-0(qU'*2HhaC!@T5eI`B!%I`Bu1i_=cR9++N"QMr !H{gzWP(zGcIHw:P]@ë\]F#PSEL|_Oo=[}O\HL+u.6͸UgAQ=p_o=Lso G%22 KY'u t~Ocɀ.lb+Q@r[؎눁xY=AcibcFcrN% ,lKhI-<\eQfwߣn^.kS'%&벵0kWe,Yn- ] *R9 k8%\0'<ޑVgT%' WVjTI)#h®cZm!?ۻu˭m *o/y8`U~*XKށڗXb"}҇h|j f¡xXejVccnGa QibMWQueG4qRvj̐޵-)G*#܈֡ inG[A&1 {ElxkMS,+nۺlƢ58fa-yY rTlDFgFy*]4"2>ғ"Β^=[@j <, ɔ1%蠟óWRh~Hmas8XxBJ`hvs`*+Ue ɹ{ICg4!\ ;gS≦T.=m\|FvTtji*Yrjᶶ<3;y⨍PgҢTg>C;7u;#J;z6&;lhI;[4Wu|5ryt4k!{wtLTE% >]cY}Z`BbS6|p8q R6r?_%mc|RnFӐw'1ħlx>,;;  EkL`2~!S!r;~፻GmN8Lѵl|mkګgJ個>Q)Iw7{jgʪk8+7sI`/8z=BŃ]+f/ـZ8XYQD [}_7kڣ܃u]M"ph9^6"EJof:zhиd,Gi=wOg Q-ŐkOʭܨx+N2#6Vv+;I}R߾ ዗Kd.)R_d/~vJec{ϛt)3n w'gMWE񌘹ߤ.gY6cm?zuՏQ*mqOh#:I9KAv?0t=VT5~O3P{1 z =l[u'*:YGo3L3k#y.ߴiy\>S?AY T (#y(i46lS_URD^gM׎a|ſxejJcY61vu,jO-Ft:fNPak,ސrB'EM{atda"DZ8w&~sa"D3cb;71,`l: )JB7рRiIVܹ}Ĺ~?! LZ:wf.`4>]B'.h(HUЙFY!J2RIEʴi תnfO(TVkOF%AAoއIVA`Bk`B!%I`B!%I`B!%I`B!%I`B!%I`B!%^jγoN܍h/2 <*%*9گ=|ޟ$0Qj_ʶJᄌR%h0pr k5nڟ !Rky jzyK"(U*jzyZA7 Cy"{("#nR&CDRՍȈckkk~$Rh0T)I`B<LÆ JY ה%RMN`2}B(݊* & Lj&>")/;Zxo|F.Ȳ& Lnx\{ch4|₋)6z=7LS=o=WBA<Ɣ՟I˕_nƸUk _:=o22ZkNB.0vvcs!B7X)^IgdtwƏ^FxX=C{\we+dߑu;vynf׆dJ7SsůF*sb5~=WM݉ެ7Ni7S6aUxIX ,i;d^~fxS W&~?yNl),E 1XAC> )G z2knxzt-Ӻ7t1)}8sqȥ>q)MeJȚq/Sڝ_x9|5¤ƦhQw ^|əښ*޸{ğhʈa/.O)g>]F7Wna]-f}Gq(}3m'+vsg6#?Wg|0<(ב790y񰌑SVgQ&D)PSSXQa,S'15G!7%PCߍh[R>[; Sid 4fGCcY UKѦPJ)^ݶ ^O'Զ[61ƇSE tx@n/V˺X[n9q14N/vC8:RlO<=Um_˲TsoO ww'X?ϵW[W¢P6<)*(]6+eY\KeH*\ ( ڙ*Jn(H.y;?朡ŠQl_E:}o2z胓*'OҦ"S9VU_b2ns_^rwT-OO͙M@y̦gFoEX0e=M=WbDFYχگڋ%rjNDEd'˸]E.zVM[ʾ[$= 8V񬘹cQ:tQY6cm_V~|>u)6- xMqflҧ=VH: i$Kue׃ne޷wM͙F+LGYȦ\鈟nkeD:P+4yl_&v"~}Я:ɥ-+g`Nf *JJO[Ŗ,)b.Z+Wx SiOM K6{ : j;N[c,&7ӯK)05[vo|ES=Ώm؟OBg9,±‚t`ݱ/eyKߺs9KB<\rTʑM榽0:c@}2m2S.0(2OAz֬aTRٝ0$ ,,(¢XDF(V$0Q)@ED~sA&J-JAG@d4P&'{(*;WkT^CDy+P(:tzkWV@(Ɍ#¯et[ƬI[uwtޝ96FC? ȕRR%gZ٥ ju~^'OˏZ*U, Ljr%DxDVV!̭e0I`T3LIL2|;|4W^]q;ȟmq(&d)}"Ǝ7qyk̊VAoӧ:49Ɠ\رɔߣSLoVf3 ~i~V^V΋=1N?w?e1k{?+׮euYkX P)ߞjyNmcQYBHRa&J1,Ԋ֦-=xƕv5|gRG.~#WtuWX"&M}2T !;vcرr! @K`44;I9;፻G]mw )^]8S|ċ2uō.h)c~*ﻌ/?$/bHjrS&Uˋ;>݉W-l)E8Q{c?5fT26pevr331e.SH}fl:3{X#Ikȉ#ݓOr~קд/'GӃì !fbJMl5`ң5X`^@wf|Ap^^.Ws"Z.$?YƵ&Jſ+]DY._HB~r ZiwYnl3~O_Z̛3}gޜ̡%+z ,5[3hGch|8-ӻQקuzL`ÎmibsA-Pz,#δIf`#,2ͷ(CxOO,r h3~d(Nd_֯o}~}__>UoM{atdt(DQɴL>>ue[jyw/:GI͚>a#ߣRJ&wn^cXآ} L+#E!ԳlV|] I`T(zB bOHB{%!J ¢XD(BkI(*%Jv!`@2칢Ԫ\7QzEkTjCI`E6=^[9b$3\ –MӡKWl5' LZnҵ{wn :B<JIJδkK\+$RͳU݈/P(*jlm#I`Գ-M7քBL$ !($ !($ !($ !($ !($ !($=7=7~H&޸{x϶ŽI!J>&H`ݣ۾ʐq!z]Rғ3>b'sH9h5ɞ4Asaul:t/X)3|{<w61ꥥs䬚11|_mc^{\r!BU41˦օCĘ2W@]7o]YXf.O%Vv4heM: ݣ 7{E5[RvS!DJxbހ^EisAHHu~F0dc9sΜy眙QX'kge'лmsCixo_|XkVqU`tX :oY6# rnIkOկ3}=UHl\CF@``(+,v<Br8vGvHdFxՇ^m7 ´F{dC,b#^2Qcjݧ?a_;XֽΰGHϷN-?fN. Zǹl=oֱݑ֌מ tPTkœSzڸbׅQ!,\ܺWLJ rgivg iZ6v5EY20ŝV:R2m"֟4ߴ-;f2hȥB A#?}O@#žrﭫ7ɤ]l鼩7G(>uj4Uޜ_5 }^nI G-|.M4hӃ#>#F?-w;G}Ʒƅ <7a6],Z13I^=| :|Ҕ,!p Sv{IRm)Y Kɗ]pQc'ȭB8' 0!N\.w(YQޭBQʜn6bBBƩ.1|tqy7C!D9rʋ8$ ruABʩLI !*+^lݾțxK'w̗Eqx&2$|DBn1V|ɋolzTW{s0GrYBTV?{bur ֮i_4?XǁIsdOHw2T # Ŕi[5ޢL?홷'|LԝD-ݭutu>˔Y}9,rLQYE 09k/YHз-Am1z/LfϮ,c:PEΣ=GS?b3Qzrn'rgS#>!{i5qZ"Y.tQi<%v͚[=r9XaطX;& ,[A_u]j8"/g%BUOwӥR/Lnn! iaOu1 s)_.rŎ2ta2O_!xpOּ3'2BT^«D#0ckFdkւ0믖pxܛ1lOaPU0&MBiޤy }2y%Lŋ!C8tG:g_Ԝ&]֮*ZIFI<3١X~"#0!DЛ2e:&rww^cѪe2o3 !*rN#-- ^ ...憻;wwwRRRj |W !%z'tbԩJJ@(J69X$#0!Deޭ+͛r[ʦ( 5yMI!*+_OiղEK2BT6NDf!),Bds90QyY,Vky7C :^_uօȢI|t$)E_#p>ӛzAVYI HATUW$OUUR8}(: >eZpkZ!D`0um:i]rpԤx^ !xx_H U6(CsK !pJ`B!R - % giMo?d2==ߏ]5rkJĖK8mW6/me؜6ƳhY*t!D%W KEu|es|Wӡ,^~Dؖ!C0P06ԢKޟlk0$NԞu8Pܓ.W,Wt,AI~̙7B KF,[Ҫ/o|v|~cNZf֝M!s|5n %3|u9W$$X&=t>/y\ބ^Dž>`J!C*9\lĘ1= Շ1}L:yF !bNamE׬"6#=̙SO25e?'?jU(3 /ʈO}Qn`V.LB|u4^$_S[iWBeJn>KAk}<q|WOA*^%P|ߣL#)'%} NHj.7М>J1#t'OÄc\2]Yt Gz~f2 ˃pn9SR9 wƯã `'-|ƛFvHt `I }ԖSxwjEUn"q!3z;>~[m)["bsFU(mIə[Kx@lNXO[WIu't1->By˸E)6-̤pO3F%]!.oV}{i-av0Q=VƼtyTخw8ҮJIcҶЙFns!xK|51[Wp O/ !D%V*T V n<Lj"*>fCye -Ёax_(v, y|bw uŻ/{杭\>)cbhn,bزl~auE΁ !*7(0&M ovjCCl<^t9}]ՔWVE\ u.j}Q+03OK>xGx'MK֌wweXQf|8.„ҩC;u$^)8 +W=ȳ/~ZK B=vh4wS{OtٯL}YFP=n7ﯲ1&^BQ*JqÍ^9QB!&VR6*[4U8 QU$S!^>$w3˧0 G">:v qW*q׉8L&e^L! qHsyZ±4*4 io'&Bwv@j7,!j1c}[%C8?f!D"B8% 0!NIL!SB$B8% Q8bjw3`!z=I񱜏$5%Q,]HQ0zzS/ 5˼> 078B݀ <(eO!cJJROEw)qH :AF!רUMB! Q'hݦS%qHMUB8Ie^p]iC!* 8<B$O$oa=Cٜ\ BʧVKXH(io<6%2_u!d/ͻk-:pڜlB-*VcX>m ƱvןDɗړnR˻q̫ KEyc[J8C)d_/n[nҿBH 09\lĘ1= Շ1}L:ydco`JlB5ȩh4n [|մSaAϤ8uODzݛ9iy>9r3 ԴS~/C hڃN\NRIú9i;d.ZonU?KVY#f\QbwPu\Vŷ 6s={: x) JҿB$*V֧},`'a.p 6qQў7gXK=$?홷'|LԝD1̿܋%qb"gz"Z2sNG6qh^8 b|#!2pN&*#>acDY2m ɷ1aDg#hLٛ8=G {|=OOn}svM7_u })a,Y\9=!X% 5%8-=F/bŬCqF_XۈWcyq45t $_nQtc'~/H?P{{ype M8Ieŭ:mx9g,VwM}ݲkVYϮ,c:PEΣ=GS?b3QY1v@ZWsAa#rrt dݥ`'-|ƛFvH,‰T;qE)6-̤pO3Fx]>͚^-XҖd`B/ܰ5KUw=ATK;~7Cѻ[2'YC?}~~qcߧlU%%g ur\,zϱD#xͷĿUXQ Ozjcj5[|n)=dKXB5C7>#/sBL1Q7>5qZ!# 7㈼~=ӵHnפJIb\Wچx[r^2_zvRfKk[̶92杧]^M5ߟ}lj#?p

ҭ x+.`EU}͆^[..T+QX*C&)*x1ܓ5lz9W΁ !H 0ckFdkւ0믖p8]\ig,ojOH0:]C\4%Lŋ!C8AeOЊlR b{ڷ" @1mCu!4o҂f} 6QQMbsoUDK`tX(Aiu8o¨kHᙧFI<3!Zl !P:uhܽ+ݖJU2K֒wU.%n{OtٯL}YFPF`=n7ﯲ1&wmx !DYpW!V[6}q_1~>z[!6`AFEw+FQPU]UhI2(CjJRy7C)|ʼ 0 w)cw%w4 lRpo_4(N9 !KQBxy}`!tzj7vn3VKgUL8#cBT.rL!SB$B8% 0!NIL!SB$B8% 0!NIL!SB$B8% 0!NIL!SB$B8% 0!NIL!SB$B8% 0!NIL!SB$B8% 0!NIW B3'OkV^j+8 NMIDATA!MJTFI_N6oG5FhutI٬VbDۯpus~"חs%%SB8ndh*ۤhʀO׍ݾ.)pbW._nUn}\@JJ FI_s%뒒‰٬64:rHsD糤uII 8NNfGbQ՜¹SjAPPUTvT'FJέb$XFF6 3RTp77kI njNIx6l6[Ϫ͆d2S=T6vF#HLHXұMXl=ĄD%N@%%#F7+дv;?F{8범n@hUE|I JxpnH< e ״6w咊T*d-E!3T*N;˘|YXR9iCjЮz?Z!{QPZѤa{{~DuU%y fcrI˔^_bZlLz^BhŎ;*œɩjы9-gn]ytRs|t~~9PcL&NsbJZs/oo2&Ԯ@{cI߮^J7-.b$[!!!4b%shn]~AB]/Fb~?i3 ᗤURO`}s)-`B8^oϜAAGwE(Ԙ5 c׮x93%5sεm;I{g0x!Zcx䘥ɻ?X>TC4FBh@Ś^;mamo_9=&"0[q'{jwmUe 0!\ b "9WRs޼Hټ5knάVTvme&N{4Օ*:1)3_dg7N y|6X3'oyk hѐ:Yu3bAԐ:oϪtg=IԩߔΣp ^0,eZYl gh]vՄ^Z/^+` ,,g p^pr ~|Wsj^cY9T%&+n&¬ RVk 1 :l2ef.?7Ϗl(q15ma&muT}"99MvkGo쟏p^>w&I5>˴X/6Nfpd0G8};^0|s#ўm 4hXXo6 hU 6LX& fn_k0bk Fm5w[ K!6f[tM_-礡zLJY"L%|F+\(De5z}a +yVMݶ?dʹ*Yli0-Nn|s?6F"3zQ Дg[*7L=u1o}k”,XX'.eۥBL'gJ/V9sryEM냂0ap͖'k׺YYk*o$ar),lsW /+mOIDv]"hldbJT:zY1[n:2їG( s!.{۪Kz*R<׷j| u?VՄbEQQbYfŮZPQ dW)#=o;p16ΰ^ s*&[vfLpðC_X* 0!*XtFCٳxୢ\j&pN<3grb ` *tw1? DXO =?4x %k u qfKjgkYXrɪ(Ou(kŪfd~KPPQqeIa%b'cQSiT3V55k}.5..͞X 9وݻK Hp'wYO<i|Ŀ[zVLmBPU9ZHLwT\i `%ݦ艧ӕC|{Nڟ{Ō쾜91>`<§o|ƎI\‡7mb)d6njjuw*^ǷjUƍ:1zh2i)!jYU򄘢 3t =k ٕ༂V9P|@ٰ_@`{a*~m!ݸz'o{)9SV<$Bb`֝priBq+7^$)yqT+Y;c : nN!"'^/uNuPBbg_p!1bGIENDB`wader-0.5.13/doc/user/images/pin-required.png000066400000000000000000000313111257646610200210140ustar00rootroot00000000000000PNG  IHDRE仕sBIT|dtEXtCREATORgnome-panel-screenshot7w IDATxw|ْM!B - BxAP,*^`W.b]r-T+ "MtB BfwؒMI6=!g33yOL^ J|4Tv˝zg￑0I1>4ah. #ܪ5>q}01kE/C6@PH&+)EΑ<5k/j4[_y&\'م g]q$Is3 ȩj !Q91ԡmߺ[9DUȩ`'x N?Y AM5Im(عmwMh1s&Cp:5$ENBL\P3,hq$BhǗܰUr8_>Mփpez{޴ seQ9?p U 0 Mur׹m0tY()Љk jAq⒬,_bkSr݂Ӂa86|Y)Ù01z%{.Ue#d& vruZ$IѼE@rW{!ТPBl2}77`BGO?_BǏtwwyo: "4&O!iRC ^qYS]S>YzĬTT=g(j5EcN+HVaUEC Qҋg*4bcl.${e mE*) igHQh6N]I6w~nk~~ foaWӋhyX%s}KQrFs 0в>@iw fX }ћ6l^H6,uOgO;xRE¸WCP'x5/ݜ)cy@i12&[w{EЫ|66jV,!Ȳ UL^^!QBE02e`fivocS&1_I}iZ`+kOUэ2Y{0yQ9e˘/chnA~}~ w}6#Hgg3*M{wԧ7HjBB\~}'!Ȗ".0tʁĦ\ٳ?͖".N|F֡ @iExd}z-w[3 45P}C|q_ʎ98 i!&,#961\}0|ކ̘vsE{|#2T\\='UlV“lBlϣSF,(68>Fk9VXC?̡-Yϥ$(zZ%ExAoAzƑNzD| EvD~]#ؕ"#IgjJN4=0,?_4UDk&IUq".ԇN$ou]=U*{|+aCVe 'iM%V G9{V\V^-ث,rNiD:Ie6˧};hHDDF'[IyY7<ΚcNU[_"C}-\}'_G,f s SI@ylUEЮo9'5! &f_ܳDsE"eց,TXAC'j&1BǪF%*9}U$}xP,ѦKU}Dp.r84*E::j*DXh(Q! ,2m!$ dpηٓS9dLXTty*.W%:IՇ)֋e}?ݓ#Ŋ.Z=FbgR7IӢLi~+}:XK+tҽsQ6F{XC%$-lIc3+"#8-Ȟs}tMi>@K0X|R)'\+Ȝ$ͻgT޼P9+|vJ@`XBTyw/!EXR)|vOU->3nLZiM"GEEE yuo@ 81EWshC" Ʊ{Yx}b@ #DQ (~Q?( B!@E@ C@ hD1߮W[2~wN ytbϢv/_n]AuM]_\Z3Br*zasKIВo"+3.)vg]~f!&OUKiG.gY^V+c΋R?35vI]{L_AIQ:t18du:sE1+=3p߸ґPPqC|U?qcd_fF-l_5Oēw}PdiDˬDEm|/Ie5 *^A4?zĩ\/ڍSֳ0 ciϰo zryNhԓsou'e. HKr_{EǼ[G5Cz#3خW5u:.Ō~)ϼxú{"w2o5{.)@qXaE~֦NؗV\6k kE;LN]F>ۋ O`yjnUcsv|qOQr?CjvW6z-IJ}CIiC2'\u5xs^6k1΁)^{ZtN]9Lxg9{Jύ `IF1UdBL ?gq8F*ΜL~#vv6~ 6a/3t?\_}wGM-ٹ{J[cSsBܔy$<5ld7m<؃]] Ŷn<vlZ‡dßY~euYӟ޹uoV#eVEi*VuRվtvg>K BxV^:2c7yUn|~P˝C{eS+Q*}OvD;oaΟ/Md` ۗeKɰS E{v^>q)<ǀm_tAD2A؄t"(_cH!ocz#g ّK&MŪlCx7uP;XLm9$n[b@2c3iǁl1ډSOBx^}=)YQ~ؒ+R2?kR'K/rg7Y/ ľ5 OxkM(7퇬-(dw[i`✵롪}biǍȜ/~$㼉YpK%qd-}/Bf\ .ۙunaP01\z s#PgO|^nm%.T֌`ыWo8z17qѵ(uM_sbߑ]|>+K4N|=)EH(Ѐn<歼+8}'"zpݿ^汋Ti(ElIf+&n j)utxUgM$})Rs|g#[IB5& +QXk|2M b2hnx%Eank~4YX+BTO,-qA܁AǯnC@q /N{izLy?*uE tT5CE1g'g]Si9+㴊buK a{*^oa s4mZ1/h.O^I˰d",Yps= bz\˳_˳z1?ǸMg_:~xvgٗKIU4ɂISV'R&O_K'TE-,'EO| NZm0)'eka$]#WeW,8\7kjY. Ko Q("F.SG.7}H1frPS4-CO3IV-\Aq8)Ք1!W_S͒=8\v.ay`2!D$ɓ)Xˢ84}|{P*u96~B 9#Oc &MYjK 3>g*DZp|ǬrZO~B٣H-}ї7veO Z/n'ZKE5|[yvR;d=J=ypd<1.~h{1)Ѻ.w4N8xza.k?lL"G}EISX'(4 O| Op>.q&n@̭uKg˜ޭkinwҷsW:_4;!:uփ.`Kޕ>WԆnL{F:ًmt{=&/cҰ^ agls#~?НԳ`E$bK7^h ל2HN:`1,\wqStqCˍ/46Tr7⦅t#p^nwTWQa ܟu wTuي6ֈٿ'$7Q V%C̊ʻ襅r\f.^ol_!3m+zL^FDoYkb4"i,03fE0X 9(ENϰ'rv ~ɭ&q~`3W%\f^H2A F\?}biiŬ]$hcž-<*P/bt9;?1nͽWQ Ԏ:ŊqǡJ br KOtj=ZxޞsA{e!Y9 ~x;Sv+v6aŎp^g?9!'mKlmݟ[#Sw @ 6u)/B,=\A^6c9TPnؓ]*I.mF%ºeooNUmGDZ*fǡelٽ8US+$6 훔ɢrMglٗm-1}g3 (NKiA<0*4ʊ/x]FsqVuDKwt jŋ䓩+l6st N؉&R hT(&1v 1vj-:6a=\}N2ߑd²ޭ0)%~l:I8*,%cwT:i-&Gwq:௷~hGY4wD5SkQxaPي[/Hl?WiTWǔ~ `[@6LYf}^Wb`Yufm G یx٩(F17w\ يۆ'l?WaT(b|j$"ZeT/mbtY:qk"lw͇t@P4oG5޾NڀqV^؛Wfud9<:A.*V),vQ`W)(vmM tnäAcOj򶦪,(\>:+w\#g+n›G~_!wqrO IZeq3[A4 㫳Wh,&;G2y^2jjU 1B'nA|y&D@P =X"Y-w_Fj(fnk: :%F:n2 I_ c'RZFål_aCd){ߴ0vN˻ AuJⴤq5w8lfSs %lM2mWAuO AԝW`;btZEUWX]ΉMx_\{$Tj@ ԄzE]4 Uӈ Sx~|:^ł5e&]օsUnA|a|bTMC4t /u.^A4 ]PU0Nf>at:+ u =1׾\W&*'O!6ԩ(޿EtbM<16Wgd&BZQ%8#&nBRyyMi@P\ wxW.u& S.o+V'bb1-qQVmKdO>|% @P]tWMWzMs.HN&2St` cH>Aa^f|c:eqGeuRJ mI0Lh$1]GD}cM<5.^quvV/WԸFY|QW;j<"T@ TNѢ( i,{GeՆf.?; ÀdM<>-!LJIy^bLh\q:Vo+V%O1@QTdž2et+\OlDՠs݊污XWI)q<]&m8z,%7@PSˢI۴o_+`\EẄ́ &DH'5vj7*x@p[;_&;Պx<'}nʲ?"{6?L4iBKELdtE$uQC"@u9Q]m+D1HjSAt B!@E@ C@ !DQ (~Q?( B!@E@ C<B4۷l;~Mڝ3ŤЬY 1x(ɩfʟ8| rh$,,呐ЊjdC (dт|,:$q74U%sN|mU>s rUCF$Y/a ]7"cz27]ն!ς\qյtLIXM)i\qյ +MwR{4^b5eb?Um#i: ]9ci:GRPP@XXXH"cOg="cC#9~8qV (4UC6Xsd)XIV%5($=]n8( F.Bՠ,(`#B$0 ]]u" sohHu2DQJjNrR, DQ<DhMLS}}RQpxe;ó\Nlӎ6F3->OWi`dzc`2pjt%QQ](V)>Moߖxg(~]IL,[RBt3gQG ˘Hx5L͚!I͟z!C1+ERa!G϶Nފi\:z{6glc[6+,UM鴵/ap,8d#,HRRHNmaSDvɰ+&0Vq߶-6dƦ :^JM(*D]p|:p L|MZO -"KHUдZ?N_H)o2DYDw>F00)#\3JLטYpTuo1G9k{Z%'i?ytOzq&k,}Ӫ?5kܞV<=N:=Qɜu-aH>0* ^/&{agӥ}\V$8;EK`h[r}>E}YkEATuzG=f*ysrKY3B,X#܃a7j]قcQ w>&?d 27/NKyх nc֖<=#࿗c0(b70e@^i#;e'ؖFvmC?嶩{lS'j]aOx(#ٿ=2s ̃=y Y)H(Nh޺ F)N00Ec;3C%/e?V69'"Fu'i$ J%-uFw>:"N:B^ֈsx9%߭m9 (NS<0:&(@}oMVi\PVVf}0ÛGK ĨnsjLLpxQ3 çMcƲn7| +%  NDhoI Aֲp*!Hziy(ơ5ËEA8?Ju9^7,c۷\zsr2kZ)!L()c-<@¨C>]-TA!PbL8Mb8G_Y*{#9OKR_ GMYuj, j p_|ɑaG_W54N,i8\.T3bIfIBWU0E)}kKDq5X( omJ2O?MYZ$:t(ݻ}(ږAm0l^G9Yo|,D\9kcF|hZf㸓QH|͉NhH:aanj%Q_XG74aFFI@Fth!G,+SGqu$\p_rLDv<Ư_'݈%тƍ$UFID +y,|9dyں5Pp_OV>xλ\? o.KEޕ|ˬ*&AMqt>;? }g-1tڇ~Ď3w'M?$q?ém1o><+ +]躎i@Nq\; )`/t:p\h껽]TϿc/!$6R9tSٹؤjv|dI!i'8~G Oj( F56D֭8 ݷ9s=v,'zFsBݺ!6?2Qi]XzuEdgŤ$О<`2]zmSls1)a^.aJ~?:.LMB"yAtҗ!wT+V_AlKxxbEBtCw B8\N4]e$P<됩B`IsZ4-eoِCds>GmSFs4#8:/sM o,}9'؈SѩOWF46]u-a~Gu$ڡJyUtI@Ӊi[zrfIƩ8YHd\|&ǹ{i֬Yi.!Yar((,$'/YA% D;џ;(ݍh.ئX9J(h##0+VLqI)RȡDG`?~ Mf?QgeX4IHzz;[O) HǤ:36\;_ :d v8Ӊ_@̙={|bNf$#Qq>S*?RB5V갩-IfpO$2 005K'=.:Y1s/e`=f_i ֽP! UL&_6^;_ iPJ={𞚧Zr|Kܓ7E$f3wi:&EɆI У7\el6363Nmđ-N"DQP(BO?fuW$=g_ ~n@ (+yhAddDG$ YVP9fEAL&[%Ku#|(ylRTkb!sr1$m~!V+!!!EAH|Id|znN4$f"ԅ!1Zs_rs !M2/UũAYQu}s#hH( G0)|}aPnEA)/E$L&!bR\.qMU=s+5tC9` CתNq yV<6ڕ3Ѽy*/,zQ9DF79M(F+8g+v@u EuڕܯAVZm;s۟B]B7 Negs۟D6il/,"REbv?|K^ėG5kQL ͚Ci Viv]\-D Dnr ^5<^4i"DQP$veD ڕ3dBnH?Y?~=aZ">f(aaaA؂W[PsD@ !DQ (~Q?( 2F lh_A8ywM~ߖ,y@r5O@ ť~^*]yhsNb/ots!rNrϤYR K8d`5.wM~'P  /!|e9 4c9ZIENDB`wader-0.5.13/doc/user/images/search-contacts.png000066400000000000000000000637111257646610200215020ustar00rootroot00000000000000PNG  IHDR0u<sBIT|dtEXtCREATORgnome-panel-screenshot7w IDATxw|U^@ zG:kk_w-AEPU ~sqB: <|dsLn<33 uV]B!W>U=V+Nf fp^E !B`aA%.8fug`Vc0+ڥB!͉bs)^o-nA$yy.ƨ("# BڞdYžmY||q\}b_~Ul!4K$yB!-Ogpyvֻ\rLPa^Jyv̛'\O$ B!ɀ)]ms&e%8h, s+ lĺ֩8x8ru8sggṂ$$aPl;j,S+&*2,m?Qr69X-=FM/c()gC[:M!8I4($3+>x̲L =L8Xz2kYN +[QY4W ,V,\a]c2 mġ0 ),Z>j<ģ/KKkxj79ڧJ{; XEZ:w`أ9qʉl):6G6!vw4 ,+!z[HO}xoLǏExcG[wRq[ 4uP蝺i'zkޚ&Ŝ6^|S>mʯ3^U=/=IeNQY(2׬<e&zX'>5'g@T{Ѡ#67 97w> T!UT0G&jk0ѕ\ONXcs-yqvr1G_8&><4}Ĩ&&eF??RAKi;y`BfY"t*ٳ7>1)jbYa>xk=ta`k&m[4LӈTZ?1pe =m`Qo=S֒u9e}'ηwmRYy-0UHaGI"ɭ:f aap #YR0䌤E\"潾NGQ 0|vIPkCϩYI DOp71EiQY@Ńe ˭`S| ΁r %!|Qx]Pf|Cp1a#=: ~<${TQ`SSF(%ؔۊᏢVr&_7~2p\niJU珅e5%A})LY9vyCl *Ytq*SY/[ax=r|qek^8q<zb=E$gMiVP$iDw ;h,lCc5u(MW_=[Zmp[,hQ=$"u EbXUrC+߅K?l"o.լtf( n´igQ޸{>ٛW=9j: ,G7˗XA/&]=V#P { tRNO)v2W c<9WeR9o^oκ[Hp9Ru+tKnڲn"7 R}&N'#}@bGȚ;.M:*kIfۏ7HMs3nfoI<9mNUXgX:q"'Ls-q0V$tNP+( 8ꊭ 7G83g%m( "XRFG%^.<$Aʂ:V syCeﵗ7%eՎ[{^cǢvY 4#րuE0vJ>!cׄyrϞN$&[ ɏbנU]tY߮v]n`EM4V¨oWQ# T*8,H<8ۃ*>HJH(T*nPPC?Fn]IayP_QNF2(S9K1F!LjnY{`ډ3"~^XdUR I^>vz2зR\~lqj-QHV萬 /"IgVהPiE jF)$$k(tNn۵zzsV vN.X.c`91Kv4)±[YoQmzY.㟮c㟮c7OڕT&0*GƦ" :nIY~awMɎbxU(!(vn27Rn;ͯûvPiN.$md5o~EFG/Tw)hx4UQvGGgF~i*{XgP-y;SB^Q"=TQ&,@:fo B@_# 2&S]#IKWq#8҉ȼ((Ie@/?rM]*`KةJP (\\D2U-4SΣ#23IrW7B4v%'ӲRPgTӔPE~qGbvZPwlA?}=7ft=v&_ú:vRz+QW8tͱN+$jʪYĩ(_wO^96rRվ>z9;;]Vdf]VY:gA &ۋtgXb^;#2:ؽ8N;A5v_B|\ۛtb;GEzvv% _ 7#Oԣqk zdZ1p#o5 hN8M[L z1_{dWI\ƒ8'?Dq]Z1!}6~/68RƓ<,_ºBYyE{ֵefbePWJւ5N>=t>qҮcG=fIYݝoǑ]wH>ıINĂe9XhtlPjWBB) Ӕ`n%krF|Þz{ F lXEF#bodK#!u|spBE**_u}oKmWu.oBY %L]vDche*]? E&hXV(I CҺ¡@"E೙ TA"k9kR+NVc=}#y߄pyf2~"E[@WU^o 3YtV֒[dLȂi8?dMYOsXF"6„ϷE-3lzeR0d3R0z_3BE(4vX J;6^ul㿏S>v//&m_u _#kTF$YcXW^Dp3=us3dș /X355up( !kV;g շeKYRe?dW+CJ6,f RO L"mˢ{n#LL {`}m^ȺRPQNFGշY;i^FĨ]߳wmFbڈV9l.C8lt+5ݍk/xWr-@UI袴mսM2~zA^hlծg0dKSהׇ&1)>A Qa'lZG쵯ӬxxKr X;=xN?SR46~._w=7kJ=jZ`ݿTWzfo/( xvM Kp8jFqqWMXOQnXTSLT2]A/ns8^2CRVR< /`iMAQ4>14]l+7YdwjD:7^* NJM*tlvD'q*6Ǩ능:WQqEW%1^ lE.V$*R(fF09V㐢rh'կW%ip]U%qׁٔQav#i I}܍Ew1JRΎxT|utѿAN"\ w}q+&+Wֲ@S_RStQ*iVt#zhq)Lֳ9OqunH̅$Fv._Ϧ"]n AMnLƕFui17%vTg+_ :TB_c+Ĺ#JK}c܌q^^g(7ã^/]cp#&~jeWK[䬑kY*Mz)BT0s_6W6 {g7B4|}ꡗ\~Ů.$eHn{3ΥdâV/XE˷ QT Iqv!hb7!(R@7b^?cVfB!!ex?|IKJ֮ul^'牚aᔚiB!؋AVzU|jl&-7;Y B!Dsjί+?;_grXkrC!Z~5uTϧ:N\4|>8Y8;7#-0B!heìo|Oa^N`Ey;VI!bk>۷n˯m٣zZU~nqB!Es\lq!B!Z~!BvwB!פF!:u=B!ԣϠ=ՙ tWF!5oC!7L`Q!3+ B\!# B!8!# B!8!SxEUTTGqI XHLHqwՄBr6nLTT4}gϞ.6nIhBѪ àb*B!t=L=|(fn7fv:}š7 !{Cf(-+#+kpX(" ҅_XB! پ};v|!!& w ݂MZn-l"ХKg\.7ea&a`u|LIJ,B G 6 0||~LӠM7MZkB!xu֮]K0XGOijܼM pnm6.NjzzOo|[UΆ HLL$::UU1HˋU9Χj̏۸(PZZe$K#B4W&0a=LA~ap:(*?DmLj,}9sEXǵU5̲,q{~b@q m'|7YNFbIL~ 9*4ȊCyt!FU eY pXGuEAٰ,vU74[$al;G h1MABPp3 seGۘ3K(f\ؾt9I_s#Iמ9>ll.˴AS9rP&]3"CVp:Q}3y< !9fX&]{f+y~Y)-d9/.;o(,c͹R|1L_2WSs⌫S.>:dXf٫LlԵ;9Ya$æifm>0NysI$c8 㔇1N~]T>FYY)Q(l@ J7l64F0r4MaY&vP(n*hbqvaT:(&ǿ{ߞp-߽" (kEϳ#[nK^UBRmÃ_.eWK}(b1>e߲&P~*Y9f-fo7jyz'a\O8yJ<2| /qƤ.~1}9"ADzc0MWOgsޕ}lWҥ_}_x㱆5,UU1HJbB"[n%.>8l6[u i˫U0 aUA#djզie)--PUUH+=0Oq 7<n)P?8.g~XOُ&ֈsX[1w/. 'TW6}urzCȲӇo;eY{l^FwGs/)6r#; t<;ur{m)29S/fSqyca0B;(-\KR.~}b)KQ;ϩJZCǎ)(ڼ\N|>?*`ˌu4CTTTPQQa:u|V=NFqqGq9~&nǻ{x'1xFHJ ^3^g<5V%bjw.ۅiUcfg|o~1N?CLƿv$X7^$qw\KU;BOROg0Ե3)=zi!} `<|m%U֊i:_7mS UdQ}%\&lU;9l6NMtt4蘚BZt܉iHҙ.9z^:$%pp%yPP[.'o.@~0Lp#s_z5CHs>uBo{o6 ]17R U\~Bpb^Q44՛3 /]V9-ƝqQ92͐;g_qt.>IޤKoFιm6~_tOl֯NvXբ2TUݣZq#Pd|2`. #O]D~WŽgbMaLH+2oUwG+MΔi'B ۳>;r&rh$FK;>GYnOY.J|b/)+*ƕd:MvhŃPV I^nG]uI_΅t:v;VpCxI{IwIwZc x&G|$0B!vc 𙄊ݫwqߤQgݷ+;QB!̲=]Qx葷u~*&Ga97*o⧒_r8%lcYƟǍG5領BZ`B4',<񄋷wX7TeY ܽ(xw}⡿sCy1ս\4]q4, yw/ށE{ ! B&3Jp*g8278nv +Qӣg2"s_Kf*0֮ա+\uH/v^WL`֭X!}w2k0xG*[Pj- y&XX -0Mj/:/ULnz!DוL8sd+{;v7,w0+.=ˮF\:d4tNگ26mDN,ӌ2lBZŵs V ZcXvqg襧™2S.>n ^L;UǓ@!&٦2š·9zbyEC't41 4q()m`P!R0ewU!ja}`xJKKu]1 ])**t*[nmĒķw58X%_JCMBo0~޿k)..EtUԜZaBNp8DnZ.B!ԛ w}Eɓ0gĄaJ#T'+u%0UIak6q8V!F'0B6lWM6HyyyuQCS8$~ b_)˴3+"DqٴL2J`}VKb֭_GtLZ a|ҵ[~f12Oȳ냵Y@GLv{fݶ\|S q6%s3=˺P @~FLJ`&N*IiDGPVbq\у3h ܹ3eeeE5f~ 9)+ė<j̙TBj^3 f9xR͌w`4@>thLMT%1~:oFX(at]gΝ|>RRRիV_ wۡTXW3o'/Q;26>YfL9nⳇn7rmtE nEsջd.&dhf,|z;|:zLz{*mhJ2m;_#ظhsGOj*C72'r3y˛DdAbB"e25j}%**P(D   : $$$MpwgB ul($Tf>t} YűW&ZiN+Bn{D’oY\< ~__?嶹U=$g?1Df]s?Ȝcֽ6'.+GqŬ?\yw/Ǻz7E #y_YN{??H| 8\zI^A-J`͛ǓO>ɜ9_xZTiwIg՚UQ^^N0$WO6oLll,i6=ypdp{r`x >W OnZs"I6g˾˸q:j4c[EnDh&JCKgKrk+*+-GI'Ӄ/HgSN[rƹ_Ę^Q=8KIY!+\hj\\v OrJE"n~{qMg08ܠI!cB7o^uU4M4U#1!˗ҳ[ϺW۱c. I^i 7{ 7aY]%gĐ9:zUdKpUq2@cFy-mf!Gr̽y ]bGאٜV֊sH>FNU g765wG>w|~ޖwyx f9}07)u[z;r02F@׾r=9B0ޣ;?I2dOPdQPEQR*bq)⊨(XѺ|EЪh]XֽR@E, h,*$eHH z{{fBrߜϙ{ijƄrRQ彐"vPrr,˒?W P&x_J-RAQ622$IvT+..NPRFV n~F˴rC]ux{F_Ai6 ӥݷ>]A?nu}#GٍU\\P(t~=G# IDAT(+=K*V駁|NT}NJ^i=WLg~˧5}>"QVfBJKK +R+IPPe)77WQ]PrfoǎQڴ2 -MKg:^VE+[Ye5g*#4 zh N_뮻/UlҒUZ3FPDtșZ]?hzfiiRSxdYQR 8{q|r''+Q2_H$HĜGY3f){M{Jkr.TDIi僃uWh735WoXW;]_4P2gwnI]R^=XJllk{DUOoXroۛZ^}OS)|,`ߨ6mWqMKѪA$bVƒdYᰢ:uꨔx8`PPQcN;M{,%9{?f=ѿQ$*fn;VZ8$uа빾jgVn2oz5uHۧѬ^W<4z5uV9fkz0căKR׮TZZ*Y}D(!Ұ+nG$>ԂP6~]`*Hnmd&>lIMUL|Nάr )כĹ_ 5ᄰtIJNNV23''P0A30 w֔?ո1T w5$<j c|'hm^@C2CL`Jj .frs_jTZTVaRgpyHG=YOvhڏ_'Wut:o oܸiwt.(ѥcрR73cUotG(_/[$I8xAb-^e79)_uq S,tQt}5cطjO#HRzN~[|.l1QgKՇ3UoACZNRuh^J=QĴe1/dkGqFM AU 0{/dt/kO yeKP|bmr%sB`,(NMܖQ8_ůLC&*#q 0Csf`Rjl)5ŭx))Q$R/M#vTwP`6|$wR2NrTpXOV\*UErd\^DY a쉊HP\]6M6MM%rnԔ$)JeJh{1Ơ9ZU.,8B|>KJsWJJ=rVjܤ\iMpgNU+膗q5VFgh`eZ"iIKR^)ja/?~P]ofzOw{DXiI=O3WeC q?7G?H\pFm_2Js5}UڟaI/yGuucݾײ.PJzV}Tok`|6lT_EmnrJv|^uKV4$+P_$R4 Iѐȑ3Z/.U35wvyEw]=$?zG{G:].&-ӈR?Ԓ%Eoݺ׺=RҗHꢧ?~Jn]>W'Mz& h'ISĻi7ܫ)C5Xt\}/Ӡ랡~Z7hѴ&BJ$V>8X~{f~S=Ss؋uŸKh1 T$ќ #5wuoy D?k3Pu{9h>gm5I߃v}6_ ԩGcz8k?2wj: k`cg6ۼT!k2ds|mL=>YR~VhBE?*RH$"#;omn|W6A ɛQOVYsݨw=KsO^ziuJ=xV)wKV8C_>\xi=0SF$k3wj3&"w|ŻJ/-RLaW%jդ&>z:^b-GbFkPH]VdddÕ~{'%J|LS{a_'^_✥ $Ii69ʔʦ'K.Wv+(TE Wx@ } d?F*'*m߻A.OkSg;ȵ=Z'~W;2bumR3K݆ UۂooUtiǯ}N낒k/5un?_4a|Cƪ9~OPħBER4b)"]6%l/kwWmd<<_>XVf(=]pTP?\4Ίܖ}2}xN\~HY!E*Ɓ]DzEw\5DOg:zn-Ybmmv|1_byd2VQC/R n#)xI;kw%dd0S6{ťOo~!PilOinv[?F$&-{ajik/tG9k5Rmpb:v##)` (˒") Y -Vi *k9vZ&fE | go'IhnEc'Xw+w"W{ٷͦQϻF+ekޢz]T?.E͒1>ϲ)ź间`{_Y7\ҴQE <\=<}\").E &׻o!rum%sZfv|~LߟRj/ Uhx}wn_8ON;RSk?b_NKPTv%+!)TiW ;jߥjG v[2oҖҔ_R7^6LrzH/,ߪP@{~W{N_z-YS4l^}-,_J<>_m]Vgޝmq.e&<{ mrJNɗ^{bx+$cy^ԂO_yџg<4GX$4R ,jݷRTZuO#T,9ZTxJ>S#z١NUK4զ&kXJjҽKUkuo$g5QhN~t_)k2Vٛ_]_k![szXKОʍ7=ׇQ(`h\vQT3n_q%fEݛWjBY2ʈK[Amx鋟g]wj#]c鹅?D)j?#tjM*Ly[y\^6WNJl/9?zVER.>Kרcr_isת@?+;*?WISwlUKit֪mǘݩp}ͺ̦k@KJ5*n=>,{\X6M Q| dwffWFJ o_H$$+V8#!N'4ihڭh`Nn)v D+˕xSS5xݾM[m]ɲ"aE^def(e) Ȳ"YB]+QHTq$z];IkSZӦ|ngS8vdlҲH6}; 4U^Hp8p8FAkq ʒd]rƕ/~Mpɳۭ>'TP[Ym~{*.ή_D`)Feg+dEdսM'e6Qr zqv5o+I#y<^XE{<|rT(1#>^TeUdf^hpN@M 0]9xQ`@="`q08vi`cN {Z< t|$oF蹕%y5VJrpnC`^Kg@V]{IRTE_Q%Ghċߨ$z`zzgl?5OtɑR}Ǿc%G0}TZH2.9P1_?Go)[^O*}~)/lVqi͎"}tD׿Pj޷q 2G)7{T#)m| |C}cEaռW7> jȦ3FuʷZe5cd5MtɑLgeU#)NmS]r85zf.)rPGeWk-ϯi]4٩V}uSշ&'sVtySזYs%4B]9L%Kg#WkD%6k&߬ߍX;p|P Xij]_ KJlŚ;Ton*PѦ5t<3UVJj~P{6y{>WYCzֲgjsAXT5./}~߸u-%:yDl@-"t|j_64ծSꊓ7Y7ذ٪>&ϵ3hG/08=KuIN ̕Fi7CgfnUnFlTOjqOVIJjS=9JI?THhs i+*^%O>O.{NY|X{(oF}Tp+RtgK6ITLTk߷ݕ`.B ӌG6j7]~#^ժʤ`c믉XZ 𪬁oѧ6?xICEfcgӝM;)ǛpGculhQ<[?.͹LKMiQ+oZ0>ǯ}N?]/CW^We5how)-&=<\YW=<)X>}鴤?C8h`PcV`!t)}Dό}F|-cUWkH"'_zvs*j>ܑ%sZfv|~Lߟ2P&I*D㮟 hBMAZ]5hRwpμf?8INyٶHS&|#e2KEKE:;K5in|}=~n|5w~uF3> JřBpY9QR{;2Si-fh^J>ߺmZ Yg8%Jw*ԋd%j )[/^AjcyXx5hx1C漢ߵPjR|1b5k_k2WWN~9gFVFZv3u2~?QWkcݝui_iLFJh=[ޜgWw^f|P ._=yݩ(~ u7jQVͺ`q08` 0C!`q08` y;p08ytQRjY=`yォwk%;]r[Ga^ry5VJrpnC`ޟW7j?C^zӧ`ҟkZt}VJWh\uڣuG?4cHRgfu(RYgKOti!()m| |C}c_Ս_mc˙^;FWhbs`yjÙ>EVVjx͙rH;MZHykuUxlf졦.9鬑3(|WJ=W>YRr5:C:lP뼒Ru:%3Q(߾L?jpZn|j?CԐzoSZC%ɧ֭;UTJ~QW,I*ӊ{y5ws~*ݖќ ꥵEjPZMXrM*nW'~ҘKQlSbe/掃}ײ}u~mW.p۳T*f=ѿQ$*fn;VZ8$uа빾jgVn2oz5uHۧѬ^WUS4zYf UyT'Tc66O7ϻN*9i$9#+T#ΩmW_-c^"mZ,$ 1;=%rm*H VO:6qTvlIMUəUΖ"zձs69\k8檋Ks.ӒvŦa/[z-w - x-^ Gi]cՍ_-cw֔J*NIT w;j m*P5ڕ\5ػri|l|8Z ]{w!)lOinv[?ƚaՍ?UPYY7\ҴQE <\=<}\"}Tύ^+}_Mӏ;yOU虱hyOzz̳*J^hx}wn_8ONt)o8p HmW7v|!~\z|L)j *HRzN~[f [d*=8[2IDAT}r>S{;2Si-fh^J.դ{pBHkќ|FӳC+ݝS/֓h[7M+SU)Qն}unmW>t )ȫNd=1umeC!`q08` 0C!`q08` 0C!`q08` 0C!`q08` 0C!`q08` 0C!`q08`L/s0袋bP?Jv<Gdd(JH1@QB@QBRE `2f`(JH`(JHQBj(!L LE `2LE `2JH %$ɘi(!LFi(!LF 030 %$0 %$(!5P&c00%d4PP&@QBdd(JH1@QB@QBRE `2f`(JH`(JHQBj(!L LE `2LE `2JH %$ɘi(!LFi(!LF 030 %$0 %$(!5P&c00%d`q08` 0C!`q08` 0C!`q08` 0C!`q08` 0C!`q08` 0C!`q08` 0C!`q08` 0C!`q08` 0C!`q08` 0C!`q08` 0C!`q08` 0C!`q08` 0C!`q08` 0C!`q08` 0C!`q08` 0C!`q08` 0C!`q08`$@$i>[VR0nm-^j,!C!lE.:D{ b)߮=}]ZxM(!5p@Ksaa\U0ƈyon9wë,jQD- +7 _Axy ~csϲL?֛{DAƄ{Vwq%)S]iLc!?zܮa\U` 9m1cƴ/zݿ \?kOTq#ZU;_3w2(<\5tY >eRC |FTU"`G95JU=j5`zBAr.@ kw9QSSdWdݿzĬ\>3z>OUey}Rt̙ݯӛz+Vcl8ھ+ m Sg~uglaEu*WI1VkTE>#7=E*# *:>_OnAf9vĤDVYkscͬK==2|0'Uo1 )ê"Z7 UUUU-\Qsf Ξ3i׮=NtHv>UU$#`_М8FTEQK@+]iYa1MG?wGswZu~ç~ܳJ|11Le1E\ݜFZ=+3x]_O77X5'$©3bшɧU=BUe׾˲"*9-L~̴UPgAeis149D(r Aț5f4u0R*5ґmc>K {T%{љZ񡿭B""{KMz"ڷvo~ߥ O~M5 DDܝkϿcUCL=}㾢5ۋ=,x)|ȵW}4Œ?[ϖ'kb&Nt5f DrɆϷfMZ#e[75I9'o{ƚzW_u}GEwXّˏq5sιjN;'ijC?C2ia'~]=f- T'r+p-\je!X옭)zyg,h-yQ_ܩH1dYercN)V݂6ICBӣqx)$WV.R2KdbeC _ %DtE4ܾC_oSa wwYUdEVY^H*]MQhaD"愇n܄qL8ӇjxTdug2+R 6{ >^u_6^p݀Ȥ=߿q5y{(mm/8vߎUS6ıJg?:hO3Y\y[Z"ܱqKF?ޞa l;SpG1ƈ=kpHVq8_מxl?8h'q =6>ɮ‰-$"xm,.-XirDQǡߞqh[O bƈ'c5 BNq WUP[Y5sxưRgܛPW6Cm_rӋG#۟_ȸY]9.^8G7NWQ_6&~Fʹyod+"3C W|}c4Cۿ}YvySMW-r GκW^S/z﬽쉃"_|W0FtA{LlŽ-HD_~Q/waօ11ɬFGG*-gqq (@D09,BLD"T+sKN44Ekw(0ߓ^;Ref+W cL7Ť%H/1{Ts/3gk|OʵC2SXD"?(RdLL}(~}fL%S=Z(ˎNl?KR?MGEP C':]mrI+~Γ}U>g~Q4q٧{H&qik~953 X/vg!*sڹ _#<~v.Lsv* BȘ _W-&| /0O^$=V|q;k 8 -4#T9Ws镼Q$=2gmYbpДGbSWAuqcE1g½>Yե69iyRv0&p_]k@b&anvBpؔG,d/1bMZ| :/ $秞9*3䷪ q_'+5ʹ't:*sDl|`]4sSi_lĨ9O5azϜ~ t:&b¿Zxq3vY+[1i1v8:_l?t9MFca|[ ZV7w D\_~y'!~_=//MLҐ$t`Kg9NaTJjCTo#Y&ݱb`(ܲMˏLJ9Ժ[5w5R-ͪM&C6D{79,m8Ǿ#dM:pX5;9g9:?$-rA"GX{CՅ &Fa"'FF-iAk\s3K( 'vK$+ۧU o |S2R83iCm&T+;>7^:-s 1Kv`_w%Ov DҦxB?Uo-eb:MRXᢔ[3w7'?~v-o7^: FG93M>5\رo#lq2>nTGEtjB,F=VhFIp}n[UZ\jݣ2_Um ՍI""GGъqQpAÈ)ibdȥ?KՋ!apfpB 3QrUN$QLPkxsƳ8渻_ }mNԿAb]#ӁcȑgN{Uvf $"GB_|ׯoRqkE'Q!x%@K+q紧Ʈ#G_ݿ`+Nv~)9۝ֲϗ]f߿ Vl(hg$.rwGHG SRZAsnm\.VvCzOB#(shlV E@CDTz`M#mAkSZă@o B@wS!tUUG3gP<ovG ;Ǝ?vNqZ@w&1`@+󋊊 ""DQ,*:Կ?v$!@&IEv7Ƙpc5B""UUsed {ЈxKa69眷Bub 0Yj4\U:0 ۃJBN s.B]@BeJ&A^xh9xTU`d6WI6ܟ,|=Gh.~2'6X^xh sH\(,lO99NpUUEQE[&aѽg~hTׁO\yZ̔A&\R5r?jbffʈs]篝<83eXQH#]9uHfʠѳn|}[ ߨkXgk^iC2Sýͮ}#~ɛN~6_,TUTVwUh,@{t5g7%Ô_^eˮ p@DLkjaƺUI!"`q?S_M+-g ODAQ_EYe" RP|q9STYeEUU%V{Mѫs1wEEYddtӋv'iZ!.;Gg_Y;Ǔw~]Ң,:h@hot]D {r)6$2\P-L!"`h'*$ˊDc.ðn\ɲ!]=7.UjVVl{ٻ 3]R}I^6HAFh2VetN=+g}3ryưÇ [z5nϲ 'e =&]0ܹ Kc~Czd3[A tی8eYfk.1bLUUIR,!zAmlPvly#)6nV쭙zݳK,v"jy x hEX^ ( PQYAfV{Ol:xPbb_v\v caA|6@Y7\@_:(O(Ti`;çj >DX,nn^^͉ԮNAz|*(=TUzhf?yAƕ]3z=?}>$I=]gfV%n0d-.OAUUZ:HAn%A.dBDʉ69ׯv=BkEkψF'_랼[\*W(bAyhehg_3>,PPPGDwZVD"ڷ7:]XX(n\W2f}S @q;oYz-]|hovq]7`pb#Cx8٪z¢n :*|؉M._;nX*s,* /&>tþ!ARJ9 Кu^ &^{K\;DO#|Cw83Mh1]}OJ/|im{^C ~{KnXym'Rsw%$GEU]ϩO0t&$.Nٵe1#ΤdgYaO1Qr{ÆF}yn55[#G [+۵LUTAdepy_.41*o\N1. R;ar* :2䡣3lMh.AD'l9[k.~p3W\qgtG:[FfF%ۇ Mfmw~q7U{8i IGbЀI#6&2,Ł u[?lsu2UHɈ=Ĥj&4%T2&$Srpja)#_F}b^ < YMĦLXOƴsߑN~ݲal?&ܺt-~݅}BZ}F-X[_Mi FLZ@U9" !GϘi0DF\\67 -޼i&8vȬ !qEx읱;ldd$ D\Fx yH 8sѰ=kv=4 go|ۜj,ޱkXo#-Qڊ].h݁}n]e.YrѧN7])xFh]R9[~[l\92kEVc)ɠJ.ZQRS]S½*~rk(V;6{eUg-*W6{UZYt ]Ubjkng!GׯMqό~Ȍ _<"蛯 znlZѺ1WM+PspxϔΧ7 l᚟=*˿[:6J_KE_u^tOh@Zzw')6nV'B۴<oZodm{]AB!oW0Ag8x$ NA @ ||z{~CiY"+'VAԈ11qS1SI$2bLjBXsλ0%u9\_~`LJNiTZ6TQ^(  bdt̸IS LG?|ݢ.I0N𡛢FKY_f6[3Ցƒ֭:k~|b '*E~X%&!+)>W\o(!1henaƜFpRO.2;)""q~W֛ +$EQlU~ !)E#^'zVQix7'!qiBz}SG8 9q~ŕW5;?-qd!z=379}r}߸vs-njZ@F!kW;/=sXrJBӗR^$l0- z^sk("9-_x׷#O]oιm.M@TW^=`GyrO[7-.F$W""Gvz+tEe"pׯv2iIDNO ,1u7 F1 7VVH*c\5R?-A#{!ƈ 7/|wV|=njs2L2 y9H[|MA$LKnSo:f>rLpzuW_::$x[gce:v#4#i9c^#cl@?u!z=(O_%9'P?-G3{9i4_QUUAtEEO=CB$jDY8oU,期xUι,I ظҢ#ظ4룦Oq$Pl*hJ,v"jy#7_9O9TV/XxAdDDk?Rxp ]:Hqd sW}gKLjaLF́FzSOOuNrҀ6yJ9&S+ꗘ4V}Q^Vgg@DQ6S5;%%̙wFk?.)RL#Q#M~J\|hlJ<|c:,dɇ hFZ~I]G`0͝΄ l6)] @ B!A @"QB#c}J(EA @ B@ B!A @ B!A @ B!A @ B!A @ B!A @ B!A @  @ B!A @ B!A @ B!A @ B!A @ B!A @ B!A @  @ B!A @ B!AгhX+J!A @ wB/㏝h>eB8ЧحByI@_k @ B!A @ B!<.4 ڸ3時в#qwBdIB#@ $=B֬pvkiGk@}AwY15'{Q6+ _nάDi:ހ6zqcjXУD4}$44uA!)\#!AзfFUy/??%-":@DTS]+m(A`{eUTٮB"S=CDŽEF"3ٟtbi%4iv8ЙpjRU.0"* zP@B :(MBO w$I1N@aǕIRff:t awNBޱ?ckJX)zxk~]?}OjK1"B˚&!Cإ?`E dY--?@5ʶR1q_l5$q""VĮs"N`8:}9,5h~ܵ~8ch3қG*WUڜ locb6}-G0j6?A]AV䌌F@"&BBbQÐ43JvS0cbU3ɒ\7m|mQA) =wbʒꮾ91v*ˍg=ff œb6:H\nEk!+qAV9W ?@B6pV+&&&ƒAoYIϱU;. )LD>sN @BHOhlA`DLaC2[Sv5u!?SFGE9rMI^G`H4?AҁaZTY*H{*~/Pt`,Œ IfG`DL`8ОRciQ٦(TS] ac'G)5n):mzfqrr1MP؄ #$vxe7_ɧ#bDZ #F G}*L眵QO׿(>!<DjY+:j<@Bcwjq`j*Am I&8&@T }ʹ!k*jO@v<ӀAswTW\՟Ue;b%M&l}@($,"5}Xܽ;j/ 0sPHLBl4ADDqs ++j4Fl6c2 A @ qag1-wܝq3WdIBSI%y=hAf݆ -[+N;NR_fm]km@k@bww͙5>VвЁnn!oM2 m !@ςA @ B!@\[%,LTIENDB`wader-0.5.13/doc/user/images/send-sms.png000066400000000000000000000302111257646610200201370ustar00rootroot00000000000000PNG  IHDRhqlsBIT|dtEXtCREATORgnome-panel-screenshot7w IDATxw|UϹ&7 # ApкVqTmu[VV}h[V}Խ@Q !@7ws?CMH߷+/ pvCHxEFcxߴ0#-33_=eڽˌ4[2-, ˲زjƏϓ1ۼB 8~KI8Fo(Y!%[wx'r7ep2u-[g{iF''+QmaF„m^7ƶ:XۊuCYeX{ KCXIي zX6b&iع7\@'+#n'5n6?y/Y^11)lQv!fzڐibaJROaV36#&WKK~to#;bES+v0}p,H$B,6, k>]UgOm-Yl~ NQ` KC ZƔ{=7DL 4D"m&ɝ)K\˟ǫۃ䮻X|ܽ1!|X[QfܤL^岰(5lj+FUhQ\YάL&֟; F%_o#{5#0|QG&]}p,bj6#BH۞fwDlI[WonkyGpքy箩~,TmUW9j`|6\IV W"H2:t!?2ǥo}/\DD$9~A%.ؾ+]&B'ҵyt}dGغd-H_KʾYNo퓗()M)K\RrՏp,~g=\Aב&1JA_p՟ob[*ي~إN;{_Xʐ!>|F#'2 .5}If@~^m)r֬7/P[UykKgKښ:31.sU~1kmߘ M) 7 )SVZp8LFFwƈO*Y`}Ҟ"W?YG0`hBoK,6/""x.W_<ҟ_X~6ĎFaDȿ(3vdov4nx}7}*ŧ:PbD",tض ~/xCKYZaxH8_X7஬;AޟL.lGЦUUMUf$̴QYu͖eMjl i~Ɗ:~L`9ZrOZZq8"ځ@˲m۶)))!55H*#m,{/a3}/~ǎߡGrGR;r:^akb7>'DvIl?|.fT?&`601~ >ArV:;_R: A$TVBY ?7Om`Ĕc ~ϋa0p``c&@}8AJvۼn0B8Ζ* Fei~kn_o .U 'd5x[uK4ѷ<=5\}Rf~[5Y. H1We1kV?6m9˅iM';.c\B&$%Ut 0a۸1idtOuYP. YèeWqC&:"0 <㈏ W݀ypr>|~v0A|Gw]|N~L["N6eT~4l j^ *_U[UWCemֻ5'UQkNImUYS:F#]XiVvH%93p@y;cV6h}ҎI$YQ~5aˏ7sO<wauРwfYȐkCg.ҽ)E>}?Ex`Vt7\PfrțdC M9{!eVeYWeٖ3laƵ6>Bnq'V\Itw7={4_w&^㈶eTNC5Dhluyl~ u[3N)A<^E!F,PV1aZB!l%s6E%EaOOh!럹'2;/&Q)$ܩ91FKCp{1^[Bqj}U|T!޷.eub`bӏ !VӅQ.9oQ_j Z} |OCIvu쵪G\{rhlͲrJ[}3m2f|ʈfn U{m41.*H4Mll=WIJH" 1#B&~QH[a"yx K-L0'е#R\t!?|qS<d{ 1)q/1ߖ@wx[Y-Dq (˶oN>xbTZ}CϦ#QDj*89uDsMݬrJ[o"hQ-7G%ۆ`$ĎmXU} ˶*۶+صvuJ;s{9NXGj=1n2bх\&5ǖ2^1n;xᄡnBU)*+K1qNvS\ \QlKiC}Ԓ} mr]j:Wֶ=ntÀD/̤(lbGq۠g/L~ K2 [ %9,!Y>S=ONclJ{s϶TO1i;ʩ:&ֶ\DyraE$ 5 5ͫ^:nVmQ3a~(^U7,">[nut!VYϯgH`R\Q\G]8w1.""w {U&+޸e?ҩm+UZ ӝEy< .EXƽmn.mx^m۸\("Ҙ} ^t7oY/cP@,("~9. .;D^#V5PH0\C4ܕ_Wb XA\v916e[ԛx{ܲj1]е"mdBS89]ۄP!v:@$OmqC1yyy "ri$,+&M"rU|mf9vivVl. v*GnGYA)7::}>_oiY9Hy rl]6bo.߁TsUR`n;߁*qOD` h] `}4FuvPWMT@q1`б]uE`KqOD`.mmtk gW{F6H|NwKD@.Sv֊ AmR JOs%dge͇33i QZ1U᪜pyjBbod[6]1NHLw>O[BBk׮!\ȪG֛uaT\b2Rڹ"r;0YԱc+r~iZDġ.GBZ52+/~ݦrr;Hn8aظ|y; .9䧚Z5u<}M/}KPS,,m*!Ea)98jT/;6pyt#'Z>n`~n/.u4=ǝjoO/|En(_}^ĈdWtń1žy]B\qzhBUwe+xs[)va9e&G* DΰAttQVČIa]y,eKY o2,ަ!$U=#6X5vħ$(Ec3&s5=& 7 ?>| \6?߾_)Zt9//]]Ȋ?))0#H|[qnλ"eM͔k/%Y, T?n;QY~t8|Æ X+J> x9WÏ$|KEQ^WOgO鸃e·;H|'2Ē"md|b'cSA#N)/ ֛M}hU |M\y"_5»ِ'寵 GR\5MN -](&[ցbǦ%-vakj._'fsTmKI>;1Ex,<;;įL$|tʷP,Y_tqF$"JBN9y75bR9 Ͼ̥c,ܩrw*? )Co=ۆavonƌ@l&-YDEB| V+q\b3JuC *_GPL;=:yIKҒ}"\-(-Wˋֲ3&XOzi# rΩaq>[[H0 GfPYH{!h\r ͮq6VPcC~+XDq۵Ovm%@}emY9eR@8ZDġ""q(C)EDW/]!"r=o0DDMq8ZDġ""q(C)EDU^|)_|6m۷bF DqQ&ѷVD"m/xi9Mii#֯cigѣWܞn*|eb}qtѪzbbbشa-1Ͷ!""-MjzEZ\:8ӳw;m&M~9[or[\O |ɇuIdun>D˲Ll#koq@oL9.]sؚV9)ud;ND$z]HO^s/lQ1qݴgBߚGFVvHDt!k^˵jvMԴ8iձsv>uv]snK>(X?`j]rT&1iߍ= Q. c kʯ~Ojtb #&Ny关dQ\GQ]."-ҪnX ؼ_I͓K*%NH8p/uX3@pB:|6Meu2:†G_9+y.k5[_g]\5 . bl%|5"gI<V~(Ķ>!gAQu9UOC1tlkt /{5WKn̪Ѷ߲l~7K ,~.BH#..|>?:}V9w;\tzxl˦CMUbS~UW<zFo&oE*F^XE8PJx;R^NR6_\J3z= զjJƧ2-YSc[ &9a9_9¶1Sd nps#(ۏZ3u"/*#{0*$U;BIJLiKU`M͂9LMnp+$+߆٬k6,^=.?b5m_DVfȫ~R,";%GO%s8爋9\{_xmE”lf^sNWUĘ 186e;\{[SA7_y\.u83GǺ ײ%⣷c$<ɋo^Ih搋?㦛?ezX"FVf4Kws7W]."-b;ڞ;o;FUo;inK.}/3dƭdffCDDZojms3Zww9}κUZJ IDATSTjU@׿S܆5+ۤ3""Ru#h5v,=JD4#o'&7q5޿"0nO9m[r[]^D-mް-?&م͛cV6oZO٭~ 0Xr{a֬\ƬWc܄cZ\ xq'2W3b~Zsd;97kw&s<۷"_n;3~q-~`,"'3wGu^=;>MFF&&L"3 qq-D'3pm7 [vOV!?)))iM&=|V=ͻJkvӴf~BTDDDDa hR@8ZDġ""q(C)EDJ-"P hR@8Tőڹ!"a`HEnZDımp:LbVZЍCɉ}Ɓhtb~|J&aq=NȡB#hqy$.{u5%YxGFv1F~0s̉<]N26{k,&% ?0rcPb72O >eZa5V93~ ^C|Cj}f0Xg3a$|SDE-JV oF eVn~Wjn%?#HuTFLg>khԨjkdsұ<ҟl)=tIA:O"wŭ rUQ5[O^>`څ_?Ϸ}.eJN()^“?skK)}ʔSL;2M&L۞f4wB@\~>>caG/"?dsmq\9w 3/|!];ߤ}6" >߼]Ý> u'sӦ3ycs %_̍7q¢gsjInlۊs`œу|zLӂeye7W=ؔAy/Mb/+(KKO"_xsW|_\;5e%0̑l[ #ۘp l{SG !ysYOůsI[n,}XgMGf0>ˏ?k3;OG}g(\;wK{mMqt֫M&T.`Ɵ}Plu{sPh8h6~+d/.ĥǧ7ogesf&d="ͷӠ֔%lW .`bvgT;䚟87fci3c׍BzƊ}Mf{o> "rPj)N6Vꆸ5WZWgN= yE/ab'B[)ܡؑН( \EpZw4Fi~^aqvV{ثba}#k:Gklz:͟ϒ`-?FGW@rNQgԀ=>SkR9}@7gx{yo.RW1b(8a>Z*l#3 揢w"|l,:+i` & OiS'`T,)G4M}v@؅/!dƹjµ~7}E<|Ճ| 9^0E3yWd\m,c¸R47ض "MhqCǦ&봿KXJLO~umŅG&5^U`.y"OuYGb4Grʼn0?.W?($]B3'ߍb~‰d]r\讛O=̒+toW j~7}ĄbfKLɊ'>k*/e@?W>G_88/]W#.aXcM~F4mYf;ڞ;oATׇݽ8"۱LY~HZpr{nG>loq^r&km^%Wh͒ڊw=br;#O4&y*svO߽c}{%a,&3Y{c\;f6P>9dtTDk4ݧZ~4!݋CDġ""q(C)EDJ-"P hR@8ZDġ""q(C)EDJ-"P hR@8ZDġ""q(C)EDJ-"P hR@8ZDġ""q(C)EDJ-"P hR@8ZDġ""q(C)EDJ-"P hR@8ZDġ""q(C)EDJ-"P hR@8ZDġ""q(C)EDJ-"P hR@8ZDġ""q(C)EDJ-"P hR@8ZDġ""q(C)EDJ-"P hR@8ZDġ""q(C)EDJ-"P hR@8ZDġ""q(C)EDJ-"P hR@8ZDġ""q(C)EDJ-"P hR@8ZDġ""q(C)EDJ-"P hR@8ZDġ""q(C)EDJ-"P hd[۫""RF""q(C)EDJ-"P-:c?W?DD - GOjqgDDFqΨmQ@b;q(Ch"w2n"$rFOax-Nm_Nhs\4g~]c֭̚u׋`s+) ~?ߟBuU>̧/A]H Kr}qd2&y|92&e/U5ѰIgOfI/Ru#ci?fç>|8anCҦhBve# R|X6Ĥ0ӹIl= {qV_M /\m;^gea'Ϯ˗=ԂElC"rj _.`Ǒ<#KKr 6.'e6E?- ?gѵ5͝K^oqH ޚ o12ogq|7 LdѡhνtU]/:Isd}n ~s21bÔjM""#[>'w>cn3T1Pt5sק2&4%VX%gVMoN~-縎qĎ>4\ӵ42Vw;nnA'Z'2K"%+z||V%ڋYqGʳ=N>K$1ǥso ^}{Ҏ=t?uX/-x yzAoz.&%ġ}!3h9$!V_N) 7d4{>t|U\0 I_3tcGs-w b]s;CǶaӃ >oVD@{q8ZDġ\/SNL|B!o\>Q= gDDy`y>=8_P\@i~阈ȡl\ vrP㏙`ϝDDd\y 4|+cGp""#g"IENDB`wader-0.5.13/doc/user/images/sms-received.png000066400000000000000000000307121257646610200210020ustar00rootroot00000000000000PNG  IHDR?iL$sRGBbKGD pHYs  tIMEEO0 IDATxwE4y fb0pwOOQyI8@ ݙG.Y>顷{;V[lD".|=tE+c/q1'k._ @Q#5pmzqxԥ LA~ɏD"dĈVkzkIqm \p]EQp8e*Mpʉ'߾L1N]0#`@( (?DҔ#F4zk)_ ױIR]J(َsx-}J:oKVcO1h Tų-]FJUuuZOE(Ex{O/.9B wس`NV.o^7]Kvij+5Xzٯ3d) kܷ[A4wNU2gOt S|.AsmM[Ym9m7x57 G4+k]+VQئs:K../SWEe_Y+k=uS齂U 8W9tW,:''佾uܸ?؁CWxzd[_g#r$L=Do{?ϜD~B~sߜa=e/8` F۶tꫯ޵ðxsrn>C]z1>!.ond9/;>=Э~?Z{~}sGyi5mPsyZݷz1QZK7̠^}5ջ[}/Z'~X?uA[~nr):[#8iT|.grhPljɽ_8LOd]*1;νw8sxm#~͘OnFXp&(BXE]톝Jct) 5CҶm[{q{qnJƇSb;]v-H7#Yvf\}4>(o~%68yS_+;jv`<^wMϳ⹑e3u$_LsYXgR܇>ɒs㛸LX\:*_\7@1ySx캲-~=ϻA_?K-ocKTϧsy뚏 FZt&,dᄗX +R[C)ˆa_~Cvۻ`LJWǧ+IV#R\Z~snگ-^H$g8^WsKN項׳K9μ\jjMe /f|m4$n)a"Ar^/_8[5$8es>]t:,gJYdTEH@kL. )ZL,eRz 뷬9"w(׌#Slaޜzɋ?äU~杇1@ QW2M#ρH r U| װ.g/GS >Nano}/?|{j XMm:z]9{ >7[^5z79\z%\{5}YyjgtƴiS AE0޷[ [c#w G]gPk+~sw\2^]q\[7ûٛ%*c>0v.f~7M-~*폻eQl 3 Oq{I\s`

b%|3K%/7r"yD9BB&6H6/ ^DŽ[¿3wzbkޅG4F6ؠ5s{ӧMI_3,^DH@cPJi׳.u QKqmo?kn8v]s%kRqϜKn"~9W"\oyEӟIۮq+:N޹g1{,:tĨQ?9gǾC .$1o?k58#CxG :x1]kyU{iO=?ԥ>g_cxBҩ/x׼ ]7^nos37>`)V,_W\j$)( xc5Hu4r'oJוl<8mc[8IHZ%>ƴSE͓MJ$-<"Xܓp>({rZjM#nRt;=w^DD>OV"#T݇ B"|Xz-a/_Y]b'qVju(s p#tG# *")H$ǶUDW&^>pp]]){" w(t)~d{St&כגgwI"vDČV7P,|6H$-XCȻ#Hv[;0;'~BwG"U߈I$ɯ)~ת~ /y7v1j'3<4PFޫYװXciݖJv9 .'~e x9mt/W:>8 -)iJg~"3:M4v?>gް^@ߔdm?Ͻ~We]2 f3ڷ5S ?b[UN'g('3Ǯcy@{uYgyXݛL{?}>o&^_|9Ou\ ,$ yùsmF};*p|sзMXKҪ=F q ?R=ŗs:(嚽3C 4%椿Κ8:2PZiU{2r3nٔU|7_88Wl➘||] xw%Y<}.wqL˹2|NFNs}o dBG̭]` s!ǜ$c+R͸6=ꊮԼ<Z~ow(oq?-?fckk^K޽p)ު&#{ߤlU;fַ|#jǡ\:LEœˉ1l$wZQ_︝_YBew~Ӗ[9$Ҋq'33'3ba鋼sɇ{oR`]G#K^WO<.uKkLkRN~l~{9퀮ޕ9~ Nc>1;ݏо˨긇Q_|mרEu:հZړ΀ *۷kgs+;ǾO܎M-ͪs}sg$}ˀN z2̪yo pU଱rҊM;G2^}fQ/M (ѺPzz(ݕ<ÜZ%gp{s]u[W&f.=n<81tmfL}=*Zm^`Sғs]+aAӎcpoM{"TXQeCUUN6е(o6iY~r-J+ y0}obqTܬvD7<]G%W ]oxǖm^cMWf14iֳd*mh&nP|UOr,+ۤ΍,KK,;>*d]{c|^se=PXͣ׾÷k/ӊc;&ZM1׍`ӍsFԸVՓzmA;Z dcp6֤X7TBGcs۱qozN00(iR[ kص~xmSIgGYuf,mǐNAJ9(Di{_F)B]ܥ.PC4!H'04ANX!/, ‚ ?" ) R%ɶЧu\H2*XR*XZaZť},*HiMuacaVUYUcE%kuUYZa*eR,U9']lg5v2;4HƎod(>GkۣMɯKoKPY'XQs+TDUjb(i 7uױ5q ˲h2NIGy A.hys\Kr%< ;>>yHv巍:<& UXVguMX:3Ba Cɉ B!4=+¤XNg*ymY[O8;bId)ox}ڌ ާj{g~#ϯ2Ek~̞M :3ʓW:M_'^75YkeԶPF"lMo@uT0o`B U\\D/,MAH߇iX el>r"A s(nOtXD05vfY:6YSL67ONՏ}t<>~9K3;ZɇaߦSIVFN eI$R?mK>!( KJUJk $uE(ՒҗtqO!@QT!P7Q`e Xҥ2-LE$/PĖud~f/fU.~kúhrh9brձ'+H,^mZ O\sHsmoLX~%2h["|u ++4L׏hy=I!O@1Lf`.6~N4 zVSZ[͡uWд-8\ֱ ڪae<3 Rhs5P#Vڌ=8M殕4˖H$dn,]Y\O$'D& i:b;&Uo,!l*~W,u(h*c,q].~(Ql-nK8G۔ϱ7|2f mZ>5YŶI$MKkKUL'@$ 7Hȏi icNe6Mhd !pPlA$Mnk0o^jS` xo0qAI3q*J>4D.0[2~p%@[9+2rxDq7Z2`yBi C>)JD(Q20TD|.OA S(q\,CAT- 5.e5^M]>o^͇NA(뭌s5%,]ѓ nzrr2pFZ6DZG"[VaBxB?`Vi .#dJŧ*A׶!B";&+TR Xi1u]Җ$hK}"κJ IлJO,鲤,|;l*S>9%V5GOD_◙[fn2 . 8^@CcɦɸPffrA\<Llpy9aI$S Pf{1 ҷ9C]QڸFχ8i>IDATwSA֐EێK4n) 릭s"6֓ijc86eQJ .A%fZV'I"ٝ-?D B ]0O40h ͚8UꥺJv؉#e^rqqņlۛ Lu\G)ݓ[ Xt69NZ,A;̈(@(Hm9dw,h5,17M-=[KweeCSo=kzEvJ$I3j)\touҳ+SnCG&w_Fi='+P/Nndm#oޯOU$IsFtxhP \²ltU\Sf%+H_M8gEf,xcT!moH[WdO"m-t&\¶l4UV >/'=^0ۛ 7LY? *- '$6DҼ9C]v- MC2g\4%-MgMˈ-~nzK3F&) "04H$hX~& :cI1 (>PQUP,VfEM2>u7ie5 V`xtMA&"Hv kY_ضhtierm*淁Bc@CSc["'nD"&n/!wmdACUT3=IO^M[|"yS=lQ3HUOTE`;)5,"Aȏ(tiI$M-vj*]B~2IRX+ o񡆴8 ۚ=ϲ/V3̆iRI@4P ˴, G܂[\M"=/(~~"6aL&H$R7OWה%'S7XrbŊ24dLZ T R$f*AnТm.!pNgWO7ɢ^aA(L 3q s;?Ɩ_n~-_z (JQKЧӼe'մVB7jC7{6N"Wv/ ]!(f*J0FuHuD3i۔/gQOqOE(A%KtM!S µ Jr[O/&梷د2_%L'`HB#h=<>L.) IcvG58xF,MY-_;pnoB)?}x?%tqwh{s= !_{P:iӒ݅VO&>ߡmMPOQK!'ob% .) ?d4ԛ=J hkY§zs:>]PO l:*W3?0Y7,H_TaPHx {ٛG&IUًJ]槀).UKqu4xњ[9$wyXl]oQ(}ѷ%JXc0=G &r 3_ɯͶ]:ش 01b$& 6]!Wgf,¤iWZVVIޔ9L  pLX4eS1Ʀ 7[Q=)p!V5_Y͑ú7gRfŘa5Fu#*ᶇ`_x"?NDq7[mW=|"㲇''̊:g^W-%zpbcG?īcodv{q]/s"09a?AAuz]e<_u|-YJ0nAe'gMޗn:+Tzwފ*段8){)%oR,|鯌ˍCCM݁|jHv7k U1M ˲)zݾVEkֱzmT& oFȧyw^爡)tktrC:NP$kKYzhab'b$R)RIT$θn s{<-pOןF{?E1'$L?"7XO]7)Q˜ɍsH٬MXDWē׼ȌV_nҹaG1X7swsrd7Ӿ?um˝MH8E:"Z%,'Y_N]m-hd"mQq0k`ڕ|P@A86$LFI+P "Z E( ˱k6ccvz)͌?߂ȡt9"t4:9v!#(l9Ϝ/} w.9~C=9px#at;AIh7tS΀Fٚ{pN$ m}i,n}eS~^P^*!$ ]ضiZM*eLZ$SqKULP^S P!~χ00 n*{'ۛJ٤,zKR$))t)PS)@AJ0|>N #a^ݛnCd~edagiw6?'~88C)6~P,Taa13)k8[7Rjڝv0f¶SfN!$>$K>7NqIU|>O04 C0tt]M D"ٜ_E:Ei2y6YMĨOMLZ:c`:gq~"g [|6aCn"D3 ~]VkmoD"zI <640 `&bZM]ܡ6nRLRojS:qS!akϧ ->a99AAȧiiSt~—3V˞Y5l8?#mi)Y9XmۤLP`B2bZ.)ŴRXrxhPutMG>]3 eqmj@D"ٴ᷅_zZeso(;Xݰm'ض'^:M,JO̔i٢_QiI$om%Vm3' Hv rGD"O"HI$nsmdĵEY~ 9k]"쾘 ^#X"D"m.Mo4?j/wH"v-I| s}O 9(m/ %I$kuUDW!^t.+IENDB`wader-0.5.13/doc/user/index.rst000066400000000000000000000002741257646610200163020ustar00rootroot00000000000000.. _user-index: ======================= Documentation for users ======================= .. rubric:: Everything you need to know about Wader .. toctree:: :maxdepth: 1 tutorial wader-0.5.13/doc/user/tutorial.rst000066400000000000000000000325351257646610200170430ustar00rootroot00000000000000============== Wader tutorial ============== :Author: Pablo Martí :Date: 2009/01/14 :Version: 1.0 Introduction ============ What is Wader? -------------- Wader_ provides a DBus interface to manage mobile broadband devices. It implements a loadable plugin architecture to add workarounds for non-standard cards. Wader is the first project, apart from :term:`ModemManager` itself, that implements the :term:`ModemManager` API making it an excellent choice to base your application on. .. _Wader: http://www.wader-project.org Wader history ------------- Wader started off as a fork of `Vodafone Mobile Connect Card driver for Linux`_ (you might know this application from our previous `how to`_). Wader took its most juicy bits and exports them over DBus. Wader 0.1 and 0.2.X releases were funded by Elonex_ and orchestrated by the `The Open Learning Centre`_. Wader provided dialup capabilities on the Webbook_ for some of the main mobile broadband providers in the UK: Orange, T-Mobile, O2 and 3. The 0.3.X series have been entirely funded by `Warp Networks`_. .. _Vodafone Mobile Connect Card driver for Linux: http://forge.vodafonebetavine.net/projects/vodafonemobilec .. _how to: http://www.howtoforge.com/vodafone_mobile_connect_card_driver_linux .. _Elonex: http://www.elonex.co.uk .. _The Open Learning Centre: http://www.theopenlearningcentre.com .. _Webbook: http://www.theopensourcerer.com/2008/07/24/say-hello .. _Warp Networks: http://www.warp.es Wader features -------------- - Easy to use UI in GTK+ decoupled from core * Brandeable (OEMs) - Data call handling (Connect/Disconnect from Internet) - Manage your SMS and Contacts - Send and receive multiparted SMS - Handle your contacts from several sources with the contact backends - Profile management - Usage statistics with a configurable threshold - Preliminary multiplatform support (Linux, OSX...) - Extendable architecture via plugins, just drop a plugin in a well known location and: * Add support for a new device * Add support for a new OS/distro * Add support for a new contact source (ModemManager, SQLite and `ZYB`_ implemented) - Dialup on heterogeneous environments: * No NetworkManager or NetworkManager <= 0.6.X (via wvdial) * NetworkManager 0.7.X (via NetworkManager itself) - Implements 100% of the ModemManager API .. _ZYB: http://zyb.com/ How does Wader stack against its competitors -------------------------------------------- One of the main advantages of using free software is the overwhelming amount of choice that users have. The field of mobile broadband is not by any means different. We hope that the following table will help you to make an informed choice whether you are a prospective user, developer or OEM representative. +---------------------+----------------+----------------+--------------------+ | Feature | NM0.7+ | VMC2 | Wader 0.3.6 | +---------------------+----------------+----------------+--------------------+ | Multiplatform | No | No | Yes (Linux, OSX) | +---------------------+----------------+----------------+--------------------+ | Extendable arch. | Yes (C/Glib) | Yes (Python) | Yes (Python) | | via plugins | | | | +---------------------+----------------+----------------+--------------------+ | Default branding | No | Yes (VF) | No | +---------------------+----------------+----------------+--------------------+ | Brandeable UI | No | No | Yes | +---------------------+----------------+----------------+--------------------+ | SMS/Contacts | No | Yes | Yes | | support | | | | +---------------------+----------------+----------------+--------------------+ | Graphic Toolkit | Yes | No | Yes | | Agnostic | | | | +---------------------+----------------+----------------+--------------------+ | UI decoupled | Yes | No | Yes | | from core | | | | +---------------------+----------------+----------------+--------------------+ | Desktop integration | N/A | No | Yes | | with NM | | | | +---------------------+----------------+----------------+--------------------+ Supported devices ----------------- Wader has been thoroughly tested on the following devices: * Ericsson: MD300 * Huawei: E169, E170, E172, E220, E270, E272, E620, E870, EM730V, K3520 * Novatel: MC950D * Option: Nozomi, Etna, Icon 225, Icon 401, K3760 In addition, the community contributed the following plugins: * Dell: D5520, D5530 * Ericsson: F3507g * Huawei: B970, E3735, E510, E660, E660aA, EM770, K2540, K3565, K3715 * Novatel: U630, U740, EU740, XU870, MC990D, MiFi2352, S720, XU870 * Onda: MSA405HS * Option: Colt [1]_ , GlobeSurfer Icon, GT Fusion, GT Fusion Quad Lite, GT M 378, GT Max 3.6, E3730 * SierraWireless: 850, 875, MC8755 * ZTE: MF620, MF632, K2525, K3520, K3565, K3765, K4505 Installing Wader ================ Wader 0.3.6 includes support for Ubuntu, OpenSUSE and Fedora by default. Installing Wader on Ubuntu -------------------------- If you want to add the Wader PPA to your :file:`/etc/apt/sources.list` append the following lines at the end of it:: deb http://ppa.launchpad.net/wader/ubuntu jaunty main deb-src http://ppa.launchpad.net/wader/ubuntu jaunty main Now run:: sudo apt-get update sudo apt-get install wader-gtk Installing Wader on OpenSUSE ---------------------------- On OpenSUSE, installing Wader is really easy thanks to their "1-click" install infrastructure. It is recommended to upgrade OpenSUSE 11.1 before installing Wader, as Wader depends on a more recent version of ``NetworkManager-gnome`` for running. We'll need to manually delete first a conflicting file:: sudo rpm -e --nodeps ModemManager To install Wader `on OpenSUSE 11.1+ click here `_. In order to use Wader, your user must be member of the ``uucp`` group:: sudo /usr/sbin/usermod username -G uucp And reboot the machine:: sudo reboot Installing Wader on Fedora 10 ------------------------------ To install Wader on Fedora 10, run as root:: wget http://download.opensuse.org/repositories/home:/pablomarti/Fedora_10/home:pablomarti.repo \ -O /etc/yum.repos.d/home:pablomarti.repo yum update yum install wader-gtk In order to use Wader, your user must be member of the ``uucp`` group:: sudo /usr/sbin/usermod username -G uucp and you will also need to disable SELinux while we write a SELinux policy for Wader. To do so, go to :menuselection:`System --> Administration --> SELinux Management` and set the `System Default Enforcing Mode` to Disabled. Check the `Relabel on next reboot` and reboot the machine:: sudo reboot Getting started =============== To start the application, go to :menuselection:`Applications --> Internet` and launch "Mobile Broadband" from there. Alternatively, you can also start the application from the console with:: wader-gtk Initial device configuration ---------------------------- Once you device is detected its authentication status will be checked and you may need to enter your PIN number if its enabled. .. image:: images/pin-required.png :alt: Ask PIN dialog :align: center Once the authentication is completed, the device will be initialized and you will be presented with a window to create a connection profile. In Wader, you need a connection profile if you want to connect to the Internet. Wader itself comes with a small database with tested settings contributed by users. If your operator happens to be present in that database, you will only need to select a connection mode (and optionally) a band mode. If your operator is not present in the database, the profile creation window will be empty and you will need to fill in the relevant details. If you have doubts about what you should fill in, you can contact your customer support service and they will provide you the details. .. image:: images/new-profile.png :alt: New profile window :align: center Connecting to Internet ---------------------- Once you have an active connection profile, you are set to connect to Internet. You just need to press the big "Connect" button in the main window. A small popup window will appear showing the progress of the connection. Once its established, the popup will disappear and the big button will read "Disconnect". Press it again and you will be disconnected from Internet. Messages -------- Wader provides a CRUD [2]_ interface for managing your SMS. Click on :menuselection:`View -> Manage SMS/Contacts` to show its window. By default, it starts on SMS mode. Sending a SMS ~~~~~~~~~~~~~ Start by clicking on the "New" button or by pressing "Ctrl+N". In the bottom entry you will type the text to send, and in the middle one the destination numbers. There is a "To" button to the left of the contacts entry and by clicking it you will be presented with a list showing all the contacts in the SIM card. Double-clicking on a contact will add it to the list of recipients unless is already there. You can of course directly type a number. .. image:: images/select-number.png :alt: Select contact to send SMS to :align: center When you start typing the text, a status bar in the bottom will show you the number of characters left. There are two main SMS encodings: 7-bit and UCS2. While the former allows to send texts of up to 140 characters, with the latter is only 70 characters. .. image:: images/send-sms.png :alt: Sending a SMS :align: center Once your text is ready, you just need to press the "Send" button. Receiving SMS ~~~~~~~~~~~~~ When a SMS is received, a notification window will be shown displaying the sender's name (if known), and the SMS text. .. image:: images/sms-received.png :alt: SMS received from Bob :align: center Deleting SMS ~~~~~~~~~~~~ Deleting a SMS its as easy as selecting it from the list and clicking on the delete button. Alternatively you can press the "Del" key or right clicking on a selection and selecting "Delete" from the popup menu. .. image:: images/delete-sms.png :alt: Deleting a SMS :align: center Contacts -------- Wader provides a CRUD [2]_ interface for managing your contacts. Once you have started the application, click on the contacts row in the left treeview to switch to contacts mode. .. image:: images/contacts-main.png :alt: Main contacts window :align: center Adding a contact ~~~~~~~~~~~~~~~~ New contacts can be added by clicking on the "New" button or pressing "Ctrl + N" while in contacts mode. You just need to provide a name, a valid number and press the "Add" button. .. image:: images/add-contact.png :alt: Adding a contact :align: center Deleting a contact ~~~~~~~~~~~~~~~~~~ Contacts can be deleted by clicking on the "Delete" button or pressing the "Del" key while in contacts mode. You can also select an arbitrary number of contacts, right click in the selection and clicking on "Delete". .. image:: images/delete-contacts.png :alt: Deleting some contacts :align: center Editing a contact ~~~~~~~~~~~~~~~~~ Any contact details can be changed by clicking on its row and directly start editing its name or number. Once you are done, press "Enter" and that is it. .. image:: images/edit-contact.png :alt: Editing a contact :align: center Search for contact name ~~~~~~~~~~~~~~~~~~~~~~~ You can search for contacts using the search entry in the top left corner of the contacts application. It will provide suggestions for what you are typing if it matches with any contact name. .. image:: images/search-contacts.png :alt: Searching for a contact pattern :align: center Troubleshooting --------------- When something goes wrong or does not work, you might have found a bug in Wader. There are two main log files that will yield clues about what went wrong: /var/log/wader.log This is wader-core log file and if an AT command has failed, or an exception has occurred while executing a core process -i.e. network registration- it will be reflected here. A quick way to access this file is clicking on "View -> Log". /tmp/wader-gtk-username.log This is wader-gtk log file and if a GUI-related operation has failed, it will be shown here. Armed with this information, you can contact the developers by: Asking a question in the `Wader forums `_. Perhaps your question was already answered here, or perhaps is a new bug. Either way feel free to contact us with whatever bug, suggestion or crazy idea you might have about Wader. Sending a mail to the `devel list `_: This is a developer-oriented list, and depending on the question it might be more appropriate to ask it in the forums. If you want to add a new device, OS/distro, or a new feature to Wader, we will happily answer any question you might have. .. [1] It has a rather buggy firmware, disable PIN authentication .. [2] `CRUD `_. wader-0.5.13/doc/wader-core-ctl.1000066400000000000000000000025651257646610200163620ustar00rootroot00000000000000.\" This manpage is copyright (C) 2011 Alex Chiang .\" .\" This is free software; you may 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, .\" or (at your option) any later version. .\" .\" This 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 APT; if not, write to the Free Software .\" Foundation, Inc., .\" 02111-1307 USA .TH WADER-CORE-CTL 1 "Nov. 8, 2011" .SH NAME wader-core-ctl \- control the wader-core daemon .SH SYNOPSIS .B wader-core-ctl [\fIOPTION\fR]... .SH DESCRIPTION wader-core-ctl lets you control the wader-core daemon, which is a drop-in replacement for ModemManager. .SH OPTIONS .TP \fB\-v\fR, \fB\-\-version\fR Show program's version number and exit. .TP \fB\-r\fR, \fB\-\-restart\fR Restart wader-core. .TP \fB\-\-s\fR, \fB\-\-start\fR Start wader-core (only to be called by D-Bus service). .TP \fB\-\-t\fR, \fB\-\-stop\fR Stop wader-core. .SH AUTHOR This manual page was written by Alex Chiang for the Debian project (but may be used by others). wader-0.5.13/packaging/000077500000000000000000000000001257646610200146375ustar00rootroot00000000000000wader-0.5.13/packaging/debian/000077500000000000000000000000001257646610200160615ustar00rootroot00000000000000wader-0.5.13/packaging/debian/generic/000077500000000000000000000000001257646610200174755ustar00rootroot00000000000000wader-0.5.13/packaging/debian/generic/debian/000077500000000000000000000000001257646610200207175ustar00rootroot00000000000000wader-0.5.13/packaging/debian/generic/debian/changelog000066400000000000000000000067131257646610200226000ustar00rootroot00000000000000wader (0.5.13-1) trusty; urgency=low * New upstream version -- Andrew Bird Thu, 17 Sep 2015 08:27:40 +0100 wader (0.5.12-1) lucid; urgency=low * New upstream version -- Andrew Bird Tue, 22 May 2012 18:14:03 +0100 wader (0.5.11-1) lucid; urgency=low * New upstream version -- Andrew Bird Thu, 19 Apr 2012 17:22:26 +0100 wader (0.5.10-1) lucid; urgency=low * New upstream version -- Andrew Bird Mon, 23 Jan 2012 18:27:39 +0000 wader (0.5.9-1) lucid; urgency=low * New upstream version -- Andrew Bird Fri, 02 Dec 2011 12:49:31 +0000 wader-core (0.5.8-1) lucid; urgency=low * New upstream version -- Andrew Bird Mon, 14 Nov 2011 17:58:46 +0000 wader-core (0.5.7-1) lucid; urgency=low * New upstream version -- Andrew Bird Sun, 11 Sep 2011 21:30:25 +0100 wader-core (0.5.6-1) lucid; urgency=low * New upstream version -- Andrew Bird Wed, 02 Mar 2011 14:28:56 +0000 wader-core (0.5.5-1) lucid; urgency=low * New upstream version -- Andrew Bird Mon, 06 Sep 2010 12:00:41 +0100 wader-core (0.5.4) lucid; urgency=low * New upstream version -- Andrew Bird Fri, 16 Jul 2010 13:42:56 +0100 wader-core (0.5.3) lucid; urgency=low * New upstream version -- Andrew Bird Fri, 18 Jun 2010 14:19:00 +0100 wader-core (0.5.2) lucid; urgency=low * New upstream version -- Andrew Bird Tue, 08 Jun 2010 11:16:19 +0100 wader-core (0.5.1) lucid; urgency=low * New upstream version -- Andrew Bird Fri, 21 May 2010 16:54:01 +0100 wader-core (0.5.0) karmic; urgency=low * New upstream version -- David Francos Cuartero Thu, 10 Sep 2009 17:12:40 +0100 wader-core (0.3.6-1) jaunty; urgency=low * New upstream version -- Pablo Martí Gamboa Tue, 05 May 2009 15:50:31 +0200 wader (0.3.5) intrepid; urgency=low * New upstream version -- Pablo Martí Gamboa Mon, 15 Apr 2009 16:09:11 +0100 wader (0.3.4) intrepid; urgency=low * New upstream version -- Pablo Martí Gamboa Tue, 3 Mar 2009 16:52:21 +0100 wader (0.3.3) intrepid; urgency=low * New upstream version -- Pablo Martí Gamboa Mon, 23 Feb 2009 12:11:29 +0100 wader (0.3.2) intrepid; urgency=low * New upstream version -- Pablo Martí Gamboa Thu, 13 Feb 2009 12:01:11 +0100 wader (0.3.1) hardy; urgency=low * New upstream version -- Pablo Martí Gamboa Mon, 02 Feb 2009 16:59:13 +0100 wader (0.3.0) hardy; urgency=low * New upstream version -- Pablo Martí Gamboa Mon, 1 Dec 2008 12:36:58 +0100 wader (0.2.2-1) hardy; urgency=low * New upstream version -- Pablo Martí Gamboa Thu, 17 Jul 2008 18:12:21 +0200 wader (0.2.1-1) hardy; urgency=low * New upstream version * Do not include restart-wader-core twice -- Pablo Martí Gamboa Thu, 17 Jul 2008 17:17:48 +0200 wader (0.2.0-1) hardy; urgency=low * New upstream version -- Pablo Martí Gamboa Thu, 17 Jul 2008 15:31:48 +0200 wader (0.1.0-1) hardy; urgency=low * Initial Release * Renamed to wader -- Fri, 04 Jul 2008 10:38:41 +0100 wader-0.5.13/packaging/debian/generic/debian/compat000066400000000000000000000000021257646610200221150ustar00rootroot000000000000004 wader-0.5.13/packaging/debian/generic/debian/control000066400000000000000000000030151257646610200223210ustar00rootroot00000000000000Source: wader Priority: optional Maintainer: Andrew Bird Build-Depends: debhelper (>= 5), python-nose, python-m2crypto Build-Depends-Indep: python-central (>= 0.5), python-setuptools, python-dbus, python-twisted-core, python-messaging (>= 0.5.11), python-tz Standards-Version: 3.8.0 Homepage: http://www.wader-project.org XS-Python-Version: >= 2.5 Package: wader-core Architecture: all Section: net Replaces: vmc-core, modemmanager Depends: python-wader, python (>= 2.5), ${python:Depends}, python-twisted-core, python-epsilon, python-serial, python-dbus, python-tz, python-m2crypto, python-messaging (>= 0.5.11), usb-modeswitch (>= 1.1.0), usb-modeswitch-data (>= 20100322), python-gudev, wvdial | network-manager (>=0.8), mobile-broadband-provider-info Recommends: python-twisted-conch Conflicts: modemmanager Provides: modemmanager XB-Python-Version: >= 2.5 Description: Internet connection assistant for mobile devices. Wader is a tool that manages 3G devices and mobile phones offering a dbus interface so other applications can use its services as connecting to the Internet, sending SMS, managing contacts, and such. This is the core package. . Package: python-wader Architecture: all Section: python Depends: python (>= 2.5), ${python:Depends}, python-epsilon, python-dbus, python-tz, python-m2crypto, python-messaging (>= 0.5.11), mobile-broadband-provider-info Conflicts: wader-core (<= 0.5.8) XB-Python-Version: >= 2.5 Description: Library that provides access to wader core This is the core package. . wader-0.5.13/packaging/debian/generic/debian/copyright000066400000000000000000000014221257646610200226510ustar00rootroot00000000000000Copyright 2006-2008, Vodafone España S.A. Copyright 2008-2009, Warp Networks S.L. 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, 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 with your Debian GNU system, in /usr/share/common-licenses/GPL. If not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. wader-0.5.13/packaging/debian/generic/debian/manpages000066400000000000000000000000251257646610200224320ustar00rootroot00000000000000doc/wader-core-ctl.1 wader-0.5.13/packaging/debian/generic/debian/python-wader.install000066400000000000000000000000611257646610200247250ustar00rootroot00000000000000debian/tmp/usr/lib/python2.*/dist-packages/wader wader-0.5.13/packaging/debian/generic/debian/rules000077500000000000000000000023731257646610200220040ustar00rootroot00000000000000#!/usr/bin/make -f export DH_VERBOSE=1 build: build-stamp build-stamp: dh_testdir set -ex; for python in $(shell pyversions -r -i) ; do \ $$python -c 'import sys; print sys.version' >/dev/null 2>&1 \ && \ $$python /usr/bin/nosetests -w . test \ -m '(?:^|[\b_\./-])[Tt]est(?!_contact)(?!_dbus)' \ -e test_int_to_uint32_to_int_conversion \ -e test_populate_db_from_networks_py_real \ ; \ done python setup.py build touch build-stamp clean: dh_testdir dh_testroot rm -f build-stamp rm -rf build -find . -name '*.py[co]' | xargs rm -f dh_clean install: build dh_testdir dh_testroot dh_clean -k dh_installdirs python setup.py install \ --install-layout=deb \ --skip-build \ --root $(CURDIR)/debian/tmp -find $(CURDIR)/debian/tmp -name '*.py[co]' | xargs rm -f dh_install dh_installman DH_PYCENTRAL=nomove dh_pycentral # Build architecture-independent files here. binary-indep: build install dh_testdir dh_testroot dh_installchangelogs CHANGELOG dh_installdocs README dh_compress -X.py dh_fixperms dh_installdeb dh_gencontrol dh_md5sums dh_builddeb # Build architecture-dependent files here. binary-arch: build install binary: binary-indep binary-arch .PHONY: build clean binary-indep binary-arch binary install configure wader-0.5.13/packaging/debian/generic/debian/wader-core-restart-required.update-notifier000066400000000000000000000005161257646610200312720ustar00rootroot00000000000000Name: wader-core restart required Priority: High Terminal: False DontShowAfterReboot: True DisplayIf: ps aux | grep wader | grep twistd Command: gksudo -- /usr/bin/wader-core-ctl --restart Description: Wader has been upgraded (or reinstalled) and must be restarted. Please click on the "Run this action now" button to restart Wader. wader-0.5.13/packaging/debian/generic/debian/wader-core.install000066400000000000000000000002171257646610200243370ustar00rootroot00000000000000debian/wader-core-restart-required.update-notifier usr/share/wader-core/ debian/tmp/etc debian/tmp/usr/share debian/tmp/usr/bin debian/tmp/lib wader-0.5.13/packaging/debian/generic/debian/wader-core.postinst000066400000000000000000000036431257646610200245620ustar00rootroot00000000000000#!/bin/sh set -e UPDATENOTIFIERDIR=/var/lib/update-notifier/user.d LIBDIR=/usr/share/wader-core UPDATENOTIFIERTOUCH=/var/lib/update-notifier/dpkg-run-stamp fix_peers() { if [ -e /etc/ppp/peers ];then chown -R :dialout /etc/ppp/peers chmod -R g+w /etc/ppp/peers fi } fix_pap() { if [ -e /etc/ppp/pap-secrets ]; then chown :dialout /etc/ppp/pap-secrets chmod g+rw /etc/ppp/pap-secrets fi } fix_chap() { if [ -e /etc/ppp/chap-secrets ]; then chown :dialout /etc/ppp/chap-secrets chmod g+rw /etc/ppp/chap-secrets fi } show_update() { if [ -d $UPDATENOTIFIERDIR ] ; then if [ `ps aux | grep wader | grep twistd | wc -l` -ne 0 ] ; then cp -f $LIBDIR/wader-core-restart-required.update-notifier \ $UPDATENOTIFIERDIR/wader-core-restart-required # if we don't do this touch it wont work touch $UPDATENOTIFIERTOUCH else rm -f $UPDATENOTIFIERDIR/wader-core-restart-required fi fi } reload_udev_rules() { if [ ! -x /sbin/udevcontrol ] ; then (/sbin/udevadm control --reload-rules 2>/dev/null || /sbin/udevadm control --reload_rules) else /sbin/udevcontrol reload_rules fi } clear_plugin_cache() { rm -rf /usr/share/wader-core/plugins/dropin.cache python -c "import sys; sys.path.insert(0, '/usr/share/wader-core'); from twisted.plugin import IPlugin, getPlugins;import plugins; list(getPlugins(IPlugin, package=plugins))" } tidyup_modem_manager() { # when modem-manager has been removed it leaves its process running pkill modem-manager || true pkill -9 modem-manager || true } #DEBHELPER# case "$1" in (configure) fix_peers fix_pap fix_chap tidyup_modem_manager kill -HUP `cat /var/run/dbus/pid` [ -n "$2" ] && clear_plugin_cache && show_update ;; esac reload_udev_rules || true exit 0 wader-0.5.13/packaging/debian/generic/debian/wader-core.postrm000066400000000000000000000006601257646610200242170ustar00rootroot00000000000000#!/bin/sh -e get_pid() { [ -n "$1" ] || return 0 [ -S /var/run/dbus/system_bus_socket ] || return 0 dbus-send --system --dest=org.freedesktop.DBus --print-reply \ /org/freedesktop/DBus org.freedesktop.DBus.GetConnectionUnixProcessID \ string:$1 2>/dev/null | awk '/uint32/ {print $2}' } if [ "$1" = "remove" ]; then kill $(get_pid org.freedesktop.ModemManager) 2>/dev/null || true fi #DEBHELPER# wader-0.5.13/packaging/debian/generic/debian/wader-core.preinst000066400000000000000000000004551257646610200243610ustar00rootroot00000000000000#!/bin/sh -e case "$1" in (upgrade) # kill wader kill -9 `cat /var/run/wader.pid` 2>/dev/null || true # clean up rm /var/run/wader.pid 2>/dev/null || true # remove traces of old dir if [ -d /usr/share/wader ]; then rm -rf /usr/share/wader fi esac #DEBHELPER# wader-0.5.13/packaging/debian/generic/debian/wader-core.prerm000066400000000000000000000012271257646610200240200ustar00rootroot00000000000000#!/bin/sh -e case "$1" in (remove) rm -rf /usr/share/wader-core/plugins/dropin.cache rm -rf /usr/share/wader-core/plugins rm -rf /usr/share/wader-core/.setup-done if [ -d /usr/local/lib/python2.6/dist-packages/wader ]; then find /usr/local/lib/python2.6/dist-packages/wader -name "*.pyc" 2>/dev/null | xargs rm -rf fi if [ -d /usr/lib/python2.5/site-packages/wader ]; then find /usr/lib/python2.5/site-packages/wader -name "*.pyc" 2>/dev/null | xargs rm -rf fi ;; (purge) if [ -e /usr/share/wader-core/networks.db ]; then rm -f /usr/share/wader-core/networks.db fi ;; esac #DEBHELPER# wader-0.5.13/plugins/000077500000000000000000000000001257646610200143745ustar00rootroot00000000000000wader-0.5.13/plugins/__init__.py000066400000000000000000000000001257646610200164730ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/000077500000000000000000000000001257646610200162125ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/mm/000077500000000000000000000000001257646610200166235ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/mm/VERSION000066400000000000000000000000061257646610200176670ustar00rootroot000000000000000.5.0 wader-0.5.13/plugins/contacts/mm/doc/000077500000000000000000000000001257646610200173705ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/mm/doc/Makefile000066400000000000000000000034461257646610200210370ustar00rootroot00000000000000# You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html htmlhelp latex changes coverage help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview over all changed/added/deprecated items" @echo " coverage to check documentation coverage for library and C API" clean: -rm -rf _build/* html: mkdir -p _build/html _build/doctrees $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html @echo @echo "Build finished. The HTML pages are in _build/html." latex: mkdir -p _build/latex _build/doctrees $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex @echo @echo "Build finished; the LaTeX files are in _build/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." htmlhelp: mkdir -p _build/htmlhelp _build/doctrees $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in _build/htmlhelp." changes: mkdir -p _build/changes _build/doctrees $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes @echo @echo "The overview file is in _build/changes." coverage: mkdir -p _build/coverage _build/doctrees $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) _build/coverage @echo @echo "Coverage finished; see _build/coverage/python.txt" wader-0.5.13/plugins/contacts/mm/doc/README000066400000000000000000000007711257646610200202550ustar00rootroot00000000000000Introduction ============ This plugin is part of wader project, a cross-platform assistant for 3G connections. What's this =============== wader-plugins-contacts-mm provides wader with a backend for operating with contacts stored in a device sim with modemmanager. Tecnical Info ============= See html documentation for more tecnical info auto-generated with sphinx. Also, there is a sample of usage in samples directory. License ======== This package has the same license as the wader project itself. wader-0.5.13/plugins/contacts/mm/doc/conf.py000066400000000000000000000115231257646610200206710ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2009 Warp Networks, S.L. # Author: David Francos Cuartero # # 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. """ Wader documentation build configuration file, created by sphinx-quickstart on Mon Apr 20 09:16:21 2009. """ from wader.common.consts import APP_VERSION, APP_NAME # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.coverage'] # Add any paths that contain templates here, relative to this directory. templates_path = [] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General substitutions. project = APP_NAME copyright = 'The Wader project and contributors' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # # The short X.Y version. version = APP_VERSION release = version # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%d %B, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = False # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'trac' # Sphinx will recurse into subversion configuration folders and try to read # any document file within. These should be ignored. # Note: exclude_dirnames is new in Sphinx 0.5 exclude_dirnames = ['.svn', '.git'] # Options for HTML output # ----------------------- # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. #html_style = 'wader.css' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". #html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. html_use_smartypants = True # Content template for the index page. #html_index = '' # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. html_additional_pages = {} # If false, no module index is generated. html_use_modindex = True # If true, the reST sources are included in the HTML build as _sources/. html_copy_source = True # Output file base name for HTML help builder. htmlhelp_basename = 'Waderdoc' html_show_sourcelink = True # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, # document class [howto/manual]). #latex_documents = [] latex_documents = [ ('index', 'wader.tex', 'Wader Documentation', 'The Wader project', 'manual'), ] # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # If this isn't set to True, the LaTex writer can only handle six levels of # headers. latex_use_parts = True wader-0.5.13/plugins/contacts/mm/doc/index.rst000066400000000000000000000004201257646610200212250ustar00rootroot00000000000000:mod:`wader.plugins.mm_provider` ========================================= .. automodule:: wader.plugins.mm_provider Classes ------- .. autoclass:: ModemManagerContactProvider :members: :undoc-members: .. autoclass:: MMContact :members: :undoc-members: wader-0.5.13/plugins/contacts/mm/doc/samples/000077500000000000000000000000001257646610200210345ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/mm/doc/samples/mm_contacts_backend.py000066400000000000000000000072321257646610200253700ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Unittests for the ModemManager IContactProvider""" from optparse import OptionParser import sys import time import dbus import dbus.mainloop.glib from twisted.internet import defer from twisted.python import log from wader.common.consts import (WADER_SERVICE, WADER_INTFACE, WADER_OBJPATH, MDM_INTFACE, CRD_INTFACE) from wader.plugins.mm_provider import mm_provider, MMContact def _parse_args(): parser=OptionParser() parser.add_option('-p', '--pin', dest='pin', help='Insert sim pin card if needed',action="store") parser.add_option('-n', '--name', dest='name', help='Name of the contact.', action="store" ) parser.add_option('-m', '--number', dest='number', help='Number of the contact.', action="store") parser.add_option('-a', '--action', dest='action', help="Action to execute, one of: Add, List, Remove", action="store") parser.add_option('-c', '--contact', dest='contact', help='Contact (for delete)', action="store") return parser.parse_args() (opts,args) = _parse_args() class ModemManagerContactProvider(): """Test for the ModemManager IContactProvider""" def __init__(self): d = defer.Deferred() self.provider = mm_provider loop = dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) bus = dbus.SystemBus(mainloop=loop) def enable_device_cb(): time.sleep(1) self.provider.initialize(dict(opath=self.opath)) d.callback(True) def enable_device_eb(e): error = e.get_dbus_message() if 'SimPinRequired' in error: self.device.SendPin(opts.pin, dbus_interface=CRD_INTFACE, reply_handler=enable_device_cb, error_handler=log.err) else: sys.exit(1) def get_device_from_opath(opaths): self.opath = opaths[0] self.device = bus.get_object(WADER_SERVICE, self.opath) self.device.Enable(True, dbus_interface=MDM_INTFACE, reply_handler=enable_device_cb, error_handler=enable_device_eb) obj = bus.get_object(WADER_SERVICE, WADER_OBJPATH) obj.EnumerateDevices(dbus_interface=WADER_INTFACE, reply_handler=get_device_from_opath, error_handler=log.err) return d def _add_contact(self): return self.provider.add_contact(MMContact(opts.name, opts.number)) def _list_contacts(self): return self.provider.list_contacts() def _remove_contact(self): self.provider.remove_contact(opts.contact) cP=ModemManagerContactProvider() if opts.action == "add" : print cP._add_contact() elif opts.action == "list" : print cP._list_contacts() elif opts.action == 'remove': print cP._remove_contact() wader-0.5.13/plugins/contacts/mm/setup.py000066400000000000000000000023121257646610200203330ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2009 Warp Networks, S.L. # Author: Pablo Marti Gamboa # # 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 from setuptools import setup version = open(os.path.join('VERSION')).readline().rstrip() setup(name='wader-plugins-contacts-mm', description='ModemManager contacts plugin for wader', download_url="http://www.wader-project.org", author='Pablo Marti Gamboa', author_email='pmarti@warp.es', license='GPL', packages=['wader.plugins', 'wader.test']) wader-0.5.13/plugins/contacts/mm/wader/000077500000000000000000000000001257646610200177255ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/mm/wader/__init__.py000066400000000000000000000000001257646610200220240ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/mm/wader/plugins/000077500000000000000000000000001257646610200214065ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/mm/wader/plugins/__init__.py000066400000000000000000000000001257646610200235050ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/mm/wader/plugins/mm_provider.py000066400000000000000000000061761257646610200243150ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 dbus from zope.interface import implements from twisted.plugin import IPlugin from wader.common.contact import Contact from wader.common.consts import WADER_SERVICE, CTS_INTFACE from wader.common.interfaces import IContactProvider class MMContact(Contact): """ I am a ModemManager contact """ @classmethod def from_tuple(cls, t): return cls(t[1], t[2], index=t[0]) class ModemManagerContactProvider(object): """ModemManager IContactProvider backend""" implements(IPlugin, IContactProvider) name = "ModemManager contact backend" author = u"Pablo Martí" version = "0.1" def __init__(self): self.obj = None self.iface = None def initialize(self, init_obj): self.obj = dbus.SystemBus().get_object(WADER_SERVICE, init_obj['opath']) self.iface = dbus.Interface(self.obj, CTS_INTFACE) def close(self): del self.iface del self.obj def add_contact(self, contact): """See :meth:`IContactProvider.add_contact`""" if not isinstance(contact, MMContact): return index = self.iface.Add(contact.name, contact.number) return MMContact(contact.name, contact.number, index=index) def edit_contact(self, contact): """See :meth:`IContactProvider.add_contact`""" if not isinstance(contact, MMContact): return index = self.iface.Edit(contact.index, contact.name, contact.number) return MMContact(contact.name, contact.number, index=index) def find_contacts_by_name(self, name): """See :meth:`IContactProvider.find_contacts_by_name`""" return [MMContact.from_tuple(t) for t in self.iface.FindByName(name)] def find_contacts_by_number(self, number): """See :meth:`IContactProvider.find_contacts_by_number`""" return [MMContact.from_tuple(t) for t in self.iface.FindByNumber(number)] def list_contacts(self): """See :meth:`IContactProvider.list_contacts`""" return [MMContact.from_tuple(t) for t in self.iface.List()] def remove_contact(self, contact): """See :meth:`IContactProvider.remove_contact`""" # only remove ModemManager contacts if isinstance(contact, MMContact): self.iface.Delete(contact.index) mm_provider = ModemManagerContactProvider() wader-0.5.13/plugins/contacts/mm/wader/test/000077500000000000000000000000001257646610200207045ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/mm/wader/test/__init__.py000066400000000000000000000000001257646610200230030ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/mm/wader/test/test_contacts_mm.py000066400000000000000000000130661257646610200246320ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Mart?? # # 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. """Unittests for the ModemManager IContactProvider""" import time import dbus import dbus.mainloop.glib from twisted.trial import unittest from twisted.internet import defer from twisted.python import log from wader.plugins.mm_provider import mm_provider, MMContact from wader.common.consts import (WADER_SERVICE, WADER_INTFACE, WADER_OBJPATH, MDM_INTFACE, CRD_INTFACE) CARD_PIN = "0000" class TestModemManagerContactProvider(unittest.TestCase): """Test for the ModemManager IContactProvider""" def setUpClass(self): d = defer.Deferred() self.provider = mm_provider loop = dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) bus = dbus.SystemBus(mainloop=loop) def enable_device_cb(): time.sleep(1) self.provider.initialize(dict(opath=self.opath)) d.callback(True) def enable_device_eb(e): error = e.get_dbus_message() if 'SimPinRequired' in error: pin = CARD_PIN self.device.SendPin(pin, dbus_interface=CRD_INTFACE, reply_handler=enable_device_cb, error_handler=log.err) else: raise unittest.SkipTest("Cannot handle error %s" % error) def get_device_from_opath(opaths): if not len(opaths): raise unittest.SkipTest("Can't run this test without devices") self.opath = opaths[0] self.device = bus.get_object(WADER_SERVICE, self.opath) self.device.Enable(True, dbus_interface=MDM_INTFACE, reply_handler=enable_device_cb, error_handler=enable_device_eb) obj = bus.get_object(WADER_SERVICE, WADER_OBJPATH) obj.EnumerateDevices(dbus_interface=WADER_INTFACE, reply_handler=get_device_from_opath, error_handler=log.err) return d def tearDownClass(self): # leave everything as found self.device.Enable(False, dbus_interface=MDM_INTFACE) self.provider.close() def test_add_contact(self): name, number = 'John', '+4324343232' contact = self.provider.add_contact(MMContact(name, number)) self.failUnlessIsInstance(contact, MMContact) self.failUnlessEqual(contact.name, name) self.failUnlessEqual(contact.number, number) # leave everything as found self.provider.remove_contact(contact) def test_edit_contact(self): name, number = 'John', '+4324343232' contact = self.provider.add_contact(MMContact(name, number)) contact.name = 'Daniel' contact.number = '+1212121212' self.provider.edit_contact(contact) # now do a list and check the new values are there found = False for c in self.provider.list_contacts(): if c.name == 'Daniel' and c.number == '+1212121212': found = True break self.assertEqual(found, True) # leave everything as found self.provider.remove_contact(contact) def test_find_contacts_by_name(self): name, number = 'James', '+322323222' contact = self.provider.add_contact(MMContact(name, number)) contacts = list(self.provider.find_contacts_by_name("Jam")) self.failUnlessIn(contact, contacts) self.provider.remove_contact(contact) def test_find_contacts_by_number(self): name, number = 'James', '+322323222' contact = self.provider.add_contact(MMContact(name, number)) contacts = list(self.provider.find_contacts_by_number("+322323222")) self.failUnlessIn(contact, contacts) self.provider.remove_contact(contact) def test_list_contacts(self): added_contacts = [] name, number = 'Laura', '+223232222' append = added_contacts.append append(self.provider.add_contact(MMContact(name, number))) found = False for contact in self.provider.list_contacts(): if contact.name == name and contact.number == number: found = True break self.assertEqual(found, True) map(self.provider.remove_contact, added_contacts) def test_remove_contact(self): # add a contact, remove it, and make sure is no longer present name, number = 'Natasha', '+322322111' contact = self.provider.add_contact(MMContact(name, number)) # consume the generator and make it a list contacts = list(self.provider.list_contacts()) self.failUnlessIn(contact, contacts) self.provider.remove_contact(contact) # leave everything as found self.failIfIn(contact, list(self.provider.list_contacts())) wader-0.5.13/plugins/contacts/sqlite/000077500000000000000000000000001257646610200175135ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/sqlite/VERSION000066400000000000000000000000061257646610200205570ustar00rootroot000000000000000.5.0 wader-0.5.13/plugins/contacts/sqlite/doc/000077500000000000000000000000001257646610200202605ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/sqlite/doc/Makefile000066400000000000000000000034461257646610200217270ustar00rootroot00000000000000# You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html htmlhelp latex changes coverage help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview over all changed/added/deprecated items" @echo " coverage to check documentation coverage for library and C API" clean: -rm -rf _build/* html: mkdir -p _build/html _build/doctrees $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html @echo @echo "Build finished. The HTML pages are in _build/html." latex: mkdir -p _build/latex _build/doctrees $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex @echo @echo "Build finished; the LaTeX files are in _build/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." htmlhelp: mkdir -p _build/htmlhelp _build/doctrees $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in _build/htmlhelp." changes: mkdir -p _build/changes _build/doctrees $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes @echo @echo "The overview file is in _build/changes." coverage: mkdir -p _build/coverage _build/doctrees $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) _build/coverage @echo @echo "Coverage finished; see _build/coverage/python.txt" wader-0.5.13/plugins/contacts/sqlite/doc/README000066400000000000000000000007601257646610200211430ustar00rootroot00000000000000Introduction ============ This plugin is part of wader project, a cross-platform assistant for 3G connections. What's this =============== wader-plugins-contacts-SQLite provides wader with a backend for operating with contacts stored in a sqlite database. Tecnical Info ============= See html documentation for more tecnical info auto-generated with sphinx. Also, there is a sample of usage in samples directory. License ======== This package has the same license as the wader project itself. wader-0.5.13/plugins/contacts/sqlite/doc/conf.py000066400000000000000000000115231257646610200215610ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2009 Warp Networks, S.L. # Author: David Francos Cuartero # # 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. """ Wader documentation build configuration file, created by sphinx-quickstart on Mon Apr 20 09:16:21 2009. """ from wader.common.consts import APP_VERSION, APP_NAME # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.coverage'] # Add any paths that contain templates here, relative to this directory. templates_path = [] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General substitutions. project = APP_NAME copyright = 'The Wader project and contributors' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # # The short X.Y version. version = APP_VERSION release = version # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%d %B, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = False # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'trac' # Sphinx will recurse into subversion configuration folders and try to read # any document file within. These should be ignored. # Note: exclude_dirnames is new in Sphinx 0.5 exclude_dirnames = ['.svn', '.git'] # Options for HTML output # ----------------------- # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. #html_style = 'wader.css' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". #html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. html_use_smartypants = True # Content template for the index page. #html_index = '' # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. html_additional_pages = {} # If false, no module index is generated. html_use_modindex = True # If true, the reST sources are included in the HTML build as _sources/. html_copy_source = True # Output file base name for HTML help builder. htmlhelp_basename = 'Waderdoc' html_show_sourcelink = True # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, # document class [howto/manual]). #latex_documents = [] latex_documents = [ ('index', 'wader.tex', 'Wader Documentation', 'The Wader project', 'manual'), ] # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # If this isn't set to True, the LaTex writer can only handle six levels of # headers. latex_use_parts = True wader-0.5.13/plugins/contacts/sqlite/doc/index.rst000066400000000000000000000004231257646610200221200ustar00rootroot00000000000000:mod:`wader.plugins.sqlite_provider` ========================================= .. automodule:: wader.plugins.sqlite_provider Classes ------- .. autoclass:: SQLiteContactProvider :members: :undoc-members: .. autoclass:: SQLContact :members: :undoc-members: wader-0.5.13/plugins/contacts/sqlite/doc/samples/000077500000000000000000000000001257646610200217245ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/sqlite/doc/samples/sqlite_contacts_backend.py000066400000000000000000000043741257646610200271540ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 optparse import OptionParser from wader.plugins.sqlite_provider import sqlite_provider, SQLContact def _parse_args(): parser=OptionParser() parser.add_option('-n', '--name', dest='name', help='Name of the contact.', action="store" ) parser.add_option('-d', '--database', dest='path', help='Path to sqlite database.', action="store" ) parser.add_option('-m', '--number', dest='number', help='Number of the contact.', action="store") parser.add_option('-a', '--action', dest='action', help="Action to execute, one of: add, list, remove", action="store") parser.add_option('-c', '--contact', dest='contact', help='Contact id (for delete)', action="store") return parser.parse_args() opts, args = _parse_args() class SQLiteContactProvider(): """SQLite IContactProvider sample usage""" def __init__(self): self.provider = sqlite_provider self.provider.initialize(dict(path=opts.path)) def _add_contact(self): return self.provider.add_contact(SQLContact(opts.name, opts.number)) def _list_contacts(self): return self.provider.list_contacts() def _remove_contact(self): return self.provider.remove_contact(opts.contact) def _close(self): self.provider.close() cp = SQLiteContactProvider() if opts.action == "add": print cp._add_contact() elif opts.action == "list": print cp._list_contacts() elif opts.action == 'remove': print cp._remove_contact() wader-0.5.13/plugins/contacts/sqlite/setup.py000066400000000000000000000023101257646610200212210ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2009 Warp Networks, S.L. # Author: Pablo Marti Gamboa # # 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 from setuptools import setup version = open(os.path.join('VERSION')).readline().rstrip() setup(name='wader-plugins-contacts-sqlite', description='SQLite contacts plugin for wader', download_url="http://www.wader-project.org", author='Pablo Marti Gamboa', author_email='pmarti@warp.es', license='GPL', packages=['wader.plugins', 'wader.test']) wader-0.5.13/plugins/contacts/sqlite/wader/000077500000000000000000000000001257646610200206155ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/sqlite/wader/__init__.py000066400000000000000000000000001257646610200227140ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/sqlite/wader/plugins/000077500000000000000000000000001257646610200222765ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/sqlite/wader/plugins/__init__.py000066400000000000000000000000001257646610200243750ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/sqlite/wader/plugins/sqlite_provider.py000066400000000000000000000105761257646610200260740ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 sqlite3 from zope.interface import implements from twisted.plugin import IPlugin from wader.common.contact import Contact from wader.common.encoding import to_u from wader.common.interfaces import IContactProvider from wader.common.utils import get_value_and_pop contact_SCHEMA = """ create table contact ( id integer primary key autoincrement, name text not null, number text not null, email text, picture blob); create table version ( version integer default 1); """ class SQLContact(Contact): """I am a :class:`Contact` with email and a picture""" def __init__(self, *args, **kw): self.email = to_u(get_value_and_pop(kw, 'email', '')) self.picture = get_value_and_pop(kw, 'picture', '') super(SQLContact, self).__init__(*args, **kw) @classmethod def from_row(cls, row): """Returns a :class:`Contact` out of ``row``""" return cls(row[1], row[2], index=row[0], email=row[3], picture=row[4]) def to_row(self): """Returns a tuple object ready to be inserted in the DB""" return (self.index, self.name, self.number, self.email, self.picture) class SQLiteContactProvider(object): """SQLite IContactProvider backend""" implements(IPlugin, IContactProvider) name = "SQLite contact backend" author = u"Pablo Martí" version = "0.1" def __init__(self): self.cursor = None def initialize(self, init_obj): conn = sqlite3.connect(init_obj['path'], isolation_level=None) self.cursor = conn.cursor() try: self.cursor.executescript(contact_SCHEMA) except sqlite3.OperationalError: # database was present pass def close(self): return self.cursor.close() def add_contact(self, contact): """See :meth:`IContactProvider.add_contact`""" if not isinstance(contact, SQLContact): return self.cursor.execute("insert into contact values(?, ?, ?, ?, ?)", contact.to_row()) contact.index = self.cursor.lastrowid return contact def edit_contact(self, contact): """See :meth:`IContactProvider.edit_contact`""" if not isinstance(contact, SQLContact): return self.cursor.execute( "update contact set name=?, number=?, email=?, picture=? " "where id=?", (contact.name, contact.number, contact.email, contact.picture, contact.index)) return contact def find_contacts_by_name(self, name): """See :meth:`IContactProvider.find_contacts_by_name`""" sql = "select * from contact where name like ?" self.cursor.execute(sql, ("%%%s%%" % name,)) return [SQLContact.from_row(r) for r in self.cursor.fetchall()] def find_contacts_by_number(self, number): """See :meth:`IContactProvider.find_contacts_by_number`""" sql = "select * from contact where number like ?" self.cursor.execute(sql, ("%%%s%%" % number,)) return [SQLContact.from_row(r) for r in self.cursor.fetchall()] def list_contacts(self): """See :meth:`IContactProvider.list_contacts`""" self.cursor.execute("select * from contact") return [SQLContact.from_row(r) for r in self.cursor.fetchall()] def remove_contact(self, contact): """See :meth:`IContactProvider.remove_contact`""" if isinstance(contact, SQLContact): # filter out non SQLcontact sql = "delete from contact where id=?" self.cursor.execute(sql, (contact.index,)) sqlite_provider = SQLiteContactProvider() wader-0.5.13/plugins/contacts/sqlite/wader/test/000077500000000000000000000000001257646610200215745ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/sqlite/wader/test/__init__.py000066400000000000000000000000001257646610200236730ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/sqlite/wader/test/test_contacts_sqlite.py000066400000000000000000000075341257646610200264150ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Mart?? # # 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. """Unittests for the SQLite IContactProvider""" from twisted.trial import unittest from wader.plugins.sqlite_provider import sqlite_provider, SQLContact class TestSQLiteContactProvider(unittest.TestCase): """Test for the SQLite IContactProvider""" def setUpClass(self): self.provider = sqlite_provider self.provider.initialize(dict(path=':memory')) def tearDownClass(self): # leave everything as found self.provider.close() def test_add_contact(self): name, number = 'John', '+4324343232' contact = self.provider.add_contact(SQLContact(name, number)) self.failUnlessIsInstance(contact, SQLContact) self.failUnlessEqual(contact.name, name) self.failUnlessEqual(contact.number, number) # leave everything as found self.provider.remove_contact(contact) def test_edit_contact(self): name, number = 'John', '+4324343232' contact = self.provider.add_contact(SQLContact(name, number)) contact.name = 'Daniel' contact.number = '+1212121212' self.provider.edit_contact(contact) # now do a list and check the new values are there edited_contact = self.provider.list_contacts()[0] self.failUnlessEqual(edited_contact.name, 'Daniel') self.failUnlessEqual(edited_contact.number, '+1212121212') # leave everything as found self.provider.remove_contact(contact) def test_find_contacts_by_name(self): name, number = 'James', '+322323222' contact = self.provider.add_contact(SQLContact(name, number)) contacts = list(self.provider.find_contacts_by_name("Jam")) self.failUnlessIn(contact, contacts) self.provider.remove_contact(contact) def test_find_contacts_by_number(self): name, number = 'James', '+322323222' contact = self.provider.add_contact(SQLContact(name, number)) contacts = list(self.provider.find_contacts_by_number("+322323")) self.failUnlessIn(contact, contacts) self.provider.remove_contact(contact) def test_list_contacts(self): added_contacts = [] name, number = 'Laura', '+223232222' append = added_contacts.append append(self.provider.add_contact(SQLContact(name, number))) found = False for contact in self.provider.list_contacts(): if contact.name == name and contact.number == number: found = True break self.assertEqual(found, True) map(self.provider.remove_contact, added_contacts) def test_remove_contact(self): # add a contact, remove it, and make sure is no longer present name, number = 'Natasha', '+322322111' contact = self.provider.add_contact(SQLContact(name, number)) # consume the generator and make it a list contacts = list(self.provider.list_contacts()) self.failUnlessIn(contact, contacts) self.provider.remove_contact(contact) # leave everything as found self.failIfIn(contact, list(self.provider.list_contacts())) wader-0.5.13/plugins/contacts/zyb/000077500000000000000000000000001257646610200170165ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/zyb/VERSION000066400000000000000000000000061257646610200200620ustar00rootroot000000000000000.5.0 wader-0.5.13/plugins/contacts/zyb/doc/000077500000000000000000000000001257646610200175635ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/zyb/doc/Makefile000066400000000000000000000034461257646610200212320ustar00rootroot00000000000000# You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html htmlhelp latex changes coverage help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview over all changed/added/deprecated items" @echo " coverage to check documentation coverage for library and C API" clean: -rm -rf _build/* html: mkdir -p _build/html _build/doctrees $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html @echo @echo "Build finished. The HTML pages are in _build/html." latex: mkdir -p _build/latex _build/doctrees $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex @echo @echo "Build finished; the LaTeX files are in _build/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." htmlhelp: mkdir -p _build/htmlhelp _build/doctrees $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in _build/htmlhelp." changes: mkdir -p _build/changes _build/doctrees $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes @echo @echo "The overview file is in _build/changes." coverage: mkdir -p _build/coverage _build/doctrees $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) _build/coverage @echo @echo "Coverage finished; see _build/coverage/python.txt" wader-0.5.13/plugins/contacts/zyb/doc/README000066400000000000000000000007531257646610200204500ustar00rootroot00000000000000Introduction ============ This plugin is part of wader project, a cross-platform assistant for 3G connections. What's this =============== wader-plugins-contacts-zyb provides wader with a backend for operating with contacts stored in zyb web service. Tecnical Info ============= See html documentation for more tecnical info auto-generated with sphinx. Also, there is a sample of usage in samples directory. License ======== This package has the same license as the wader project itself. wader-0.5.13/plugins/contacts/zyb/doc/conf.py000066400000000000000000000115231257646610200210640ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2009 Warp Networks, S.L. # Author: David Francos Cuartero # # 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. """ Wader documentation build configuration file, created by sphinx-quickstart on Mon Apr 20 09:16:21 2009. """ from wader.common.consts import APP_VERSION, APP_NAME # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.coverage'] # Add any paths that contain templates here, relative to this directory. templates_path = [] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General substitutions. project = APP_NAME copyright = 'The Wader project and contributors' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # # The short X.Y version. version = APP_VERSION release = version # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%d %B, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = False # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'trac' # Sphinx will recurse into subversion configuration folders and try to read # any document file within. These should be ignored. # Note: exclude_dirnames is new in Sphinx 0.5 exclude_dirnames = ['.svn', '.git'] # Options for HTML output # ----------------------- # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. #html_style = 'wader.css' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". #html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. html_use_smartypants = True # Content template for the index page. #html_index = '' # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. html_additional_pages = {} # If false, no module index is generated. html_use_modindex = True # If true, the reST sources are included in the HTML build as _sources/. html_copy_source = True # Output file base name for HTML help builder. htmlhelp_basename = 'Waderdoc' html_show_sourcelink = True # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, # document class [howto/manual]). #latex_documents = [] latex_documents = [ ('index', 'wader.tex', 'Wader Documentation', 'The Wader project', 'manual'), ] # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # If this isn't set to True, the LaTex writer can only handle six levels of # headers. latex_use_parts = True wader-0.5.13/plugins/contacts/zyb/doc/index.rst000066400000000000000000000004031257646610200214210ustar00rootroot00000000000000:mod:`wader.plugins.zyb_provider` ========================================= .. automodule:: wader.plugins.zyb_provider Classes ------- .. autoclass:: ZYBProvider :members: :undoc-members: .. autoclass:: ZYBContact :members: :undoc-members: wader-0.5.13/plugins/contacts/zyb/doc/samples/000077500000000000000000000000001257646610200212275ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/zyb/doc/samples/zyb_contacts_backend.py000066400000000000000000000047021257646610200257550ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Sample ZYB IContactProvider usage""" from optparse import OptionParser from wader.plugins.zyb_provider import zyb_provider, ZYBContact def _parse_args(): parser=OptionParser() parser.add_option('-u', '--username', dest='username', help='ZYB username', action="store" ) parser.add_option('-p', '--password', dest='password', help='ZYB password', action="store" ) parser.add_option('-a', '--action', dest='action', help="Action to execute, one of: add, list, remove", action="store") parser.add_option('-n', '--name', dest='name', help='Name of the contact.', action="store" ) parser.add_option('-m', '--number', dest='number', help='Number of the contact.', action="store") parser.add_option('-c', '--contact', dest='contact', help='Contact id (for delete)', action="store") return parser.parse_args() (opts,args) = _parse_args() class ZYBProvider(): """Test for the ZYB IContactProvider""" def __init__(self): self.provider = zyb_provider self.provider.initialize(dict(username=opts.username, password=opts.password)) def _close(self): # leave everything as found self.provider.close() def _add_contact(self): return self.provider.add_contact(ZYBContact(opts.name, opts.number)) def _list_contacts(self): return self.provider.list_contacts() def _remove_contact(self): return self.provider.remove_contact(opts.contact) cP=ZYBProvider() if opts.action == "add" : print cP._add_contact() elif opts.action == "list" : print cP._list_contacts() elif opts.action == 'remove': print cP._remove_contact() wader-0.5.13/plugins/contacts/zyb/setup.py000066400000000000000000000023131257646610200205270ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2009 Warp Networks, S.L. # Author: Pablo Marti Gamboa # # 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 from setuptools import setup version = open(os.path.join('VERSION')).readline().rstrip() setup(name='wader-plugins-contacts-zyb', description='ModemManager contacts plugin for wader', download_url="http://www.wader-project.org", author='Pablo Marti Gamboa', author_email='pmarti@warp.es', license='GPL', packages=['wader.plugins', 'wader.test']) wader-0.5.13/plugins/contacts/zyb/wader/000077500000000000000000000000001257646610200201205ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/zyb/wader/__init__.py000066400000000000000000000000001257646610200222170ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/zyb/wader/plugins/000077500000000000000000000000001257646610200216015ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/zyb/wader/plugins/__init__.py000066400000000000000000000000001257646610200237000ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/zyb/wader/plugins/zyb_provider.py000066400000000000000000000122771257646610200247020ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 zope.interface import implements from twisted.plugin import IPlugin import vobject SUDS_AVAILABLE = True try: from suds.client import Client except ImportError: SUDS_AVAILABLE = False from wader.common.contact import Contact from wader.common.exceptions import PluginInitialisationError from wader.common.interfaces import IContactProvider class ZYBContact(Contact): def __init__(self, name, number, index=None, email="", contact=None): super(ZYBContact, self).__init__(name, number, index=index) self.email = email self._contact = contact @classmethod def from_soap(cls, c): contact = vobject.readOne(c.StringRepresentation, allowQP=True) email = contact.getChildValue('email', default="") number = contact.getChildValue('tel', default="") return cls(contact.n.value.given, number, index=c.ID, email=email, contact=contact) class ZYBProvider(object): """ZYB IContactProvider backend""" implements(IPlugin, IContactProvider) name = "SQLite contact backend" author = u"Pablo Martí" version = u"0.1" def __init__(self): self.client = None self.user_id = None self.password = None self.ptoken = None self.utoken = None def _get_last_error(self): return self.client.service.GetLastErrorInfo(self.ptoken, self.utoken) def close(self): del self.client self.client = None self.user_id = None self.password = None self.ptoken = None self.utoken = None def initialize(self, init_obj): if not SUDS_AVAILABLE: raise PluginInitialisationError("install python-suds") self.user_id = init_obj['username'] self.password = init_obj['password'] self.client = Client('https://api.zyb.com/zybservice.asmx?WSDL') self.ptoken = self.client.service.AuthenticatePartner(self.user_id, self.password) if not self.ptoken: msg = "Error getting partner token: %s" raise PluginInitialisationError(msg % self._get_last_error()) self.utoken = self.client.service.AuthenticateUser(self.ptoken, self.user_id, self.password) if not self.utoken: msg = "Error getting user token: %s" raise PluginInitialisationError(msg % self._get_last_error()) def add_contact(self, contact): """See :meth:`IContactProvider.add_contact`""" if not isinstance(contact, ZYBContact): return c = vobject.vCard() c.add('n') c.n.value = vobject.vcard.Name(given=contact.name) c.add('fn') c.fn.value = contact.name c.add('tel') c.tel.value = contact.number if contact.email is not None: c.add('email') c.email.value = contact.email c.email.type_param = 'INTERNET' _contact = self.client.service.CreateContact(self.ptoken, self.utoken, c.serialize()) return ZYBContact.from_soap(_contact) def edit_contact(self, contact): """See :meth:`IContactProvider.edit_contact`""" # ZYB does not offer a way to edit contacts! raise NotImplementedError def find_contacts_by_name(self, name): """See :meth:`IContactProvider.find_contacts_by_name`""" # ZYB does not offer a browse API, this emulates it O(N) return [c for c in self.list_contacts() if c.name.startswith(name)] def find_contacts_by_number(self, number): """See :meth:`IContactProvider.find_contacts_by_number`""" # ZYB does not offer a browse API, this emulates it O(N) return [c for c in self.list_contacts() if c.number.endswith(number)] def list_contacts(self): """See :meth:`IContactProvider.list_contacts`""" contacts = self.client.service.GetContactList(self.utoken, self.ptoken) return [ZYBContact.from_soap(c) for c in contacts[0]] def remove_contact(self, contact): """See :meth:`IContactProvider.remove_contact`""" if isinstance(contact, ZYBContact): self.client.service.DeleteContact(self.ptoken, self.utoken, contact.index) zyb_provider = ZYBProvider() wader-0.5.13/plugins/contacts/zyb/wader/test/000077500000000000000000000000001257646610200210775ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/zyb/wader/test/__init__.py000066400000000000000000000000001257646610200231760ustar00rootroot00000000000000wader-0.5.13/plugins/contacts/zyb/wader/test/test_contacts_zyb.py000066400000000000000000000064731257646610200252240ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Mart?? # # 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. """Unittests for the ZYB IContactProvider""" from twisted.trial import unittest from wader.plugins.zyb_provider import zyb_provider, ZYBContact class TestZYBContactProvider(unittest.TestCase): """Test for the ZYB IContactProvider""" def setUpClass(self): self.provider = zyb_provider self.provider.initialize(dict(username='warptest', password='warptest')) def tearDownClass(self): # leave everything as found self.provider.close() def test_add_contact(self): name, number = 'John', '+4324343232' contact = self.provider.add_contact(ZYBContact(name, number)) self.failUnlessIsInstance(contact, ZYBContact) self.failUnlessEqual(contact.name, name) self.failUnlessEqual(contact.number, number) # leave everything as found self.provider.remove_contact(contact) def test_find_contacts_by_name(self): name, number = 'James', '+322323222' contact = self.provider.add_contact(ZYBContact(name, number)) contacts = list(self.provider.find_contacts_by_name("Jam")) self.failUnlessIn(contact, contacts) self.provider.remove_contact(contact) def test_find_contacts_by_number(self): name, number = 'James', '+322323222' contact = self.provider.add_contact(ZYBContact(name, number)) contacts = list(self.provider.find_contacts_by_number("+3223")) self.failUnlessIn(contact, contacts) self.provider.remove_contact(contact) def test_list_contacts(self): added_contacts = [] name, number = 'Laura', '+223232222' append = added_contacts.append append(self.provider.add_contact(ZYBContact(name, number))) found = False for contact in self.provider.list_contacts(): if contact.name == name and contact.number == number: found = True break self.assertEqual(found, True) map(self.provider.remove_contact, added_contacts) def test_remove_contact(self): # add a contact, remove it, and make sure is no longer present name, number = 'Natasha', '+322322111' contact = self.provider.add_contact(ZYBContact(name, number)) # consume the generator and make it a list contacts = list(self.provider.list_contacts()) self.failUnlessIn(contact, contacts) self.provider.remove_contact(contact) # leave everything as found self.failIfIn(contact, list(self.provider.list_contacts())) wader-0.5.13/plugins/devices/000077500000000000000000000000001257646610200160165ustar00rootroot00000000000000wader-0.5.13/plugins/devices/__init__.py000066400000000000000000000000001257646610200201150ustar00rootroot00000000000000wader-0.5.13/plugins/devices/dell_d5520.py000066400000000000000000000052111257646610200201260ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2009 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 serial from wader.common import consts from core.hardware.base import build_band_dict from core.hardware.novatel import (NovatelWCDMADevicePlugin, NovatelWCDMACustomizer, NOVATEL_BAND_DICT) class NovatelD5520Customizer(NovatelWCDMACustomizer): """ :class:`~core.hardware.novatel.NovatelWCDMACustomizer` for D5520 """ # Quad-Band 850/900/1800/1900 MHz GPRS/EDGE # Tri-Band 850/1900/2100 MHz HSUPA/HSDPA/UMTS band_dict = build_band_dict( NOVATEL_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, consts.MM_NETWORK_BAND_EGSM, consts.MM_NETWORK_BAND_DCS, consts.MM_NETWORK_BAND_PCS, consts.MM_NETWORK_BAND_U850, consts.MM_NETWORK_BAND_U1900, consts.MM_NETWORK_BAND_U2100]) class NovatelD5520(NovatelWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Novatel's Dell D5520""" name = "Dell D5520" version = "0.1" author = u"Pablo Martí" custom = NovatelD5520Customizer() __remote_name__ = "Expedite EU870D MiniCard" __properties__ = { 'ID_VENDOR_ID': [0x413c], 'ID_MODEL_ID': [0x8137], } conntype = consts.WADER_CONNTYPE_EMBEDDED def preprobe_init(self, ports, info): # Novatel secondary port needs to be flipped from DM to AT mode # before it will answer our AT queries. So the primary port # needs this string first or auto detection of ctrl port fails. # Note: Early models/firmware were DM only ser = serial.Serial(ports[0], timeout=1) ser.write('AT$NWDMAT=1\r\n') ser.close() novateld5520 = NovatelD5520() wader-0.5.13/plugins/devices/dell_d5530.py000066400000000000000000000024301257646610200201270ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2009 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_EMBEDDED from core.hardware.ericsson import EricssonDevicePlugin class EricssonD5530(EricssonDevicePlugin): """:class:`~core.plugin.DevicePlugin` for Ericsson's Dell D5530""" name = "Dell D5530" version = "0.1" author = u"Andrew Bird" __remote_name__ = "D5530" __properties__ = { 'ID_VENDOR_ID': [0x413c], 'ID_MODEL_ID': [0x8147], } conntype = WADER_CONNTYPE_EMBEDDED ericssonD5530 = EricssonD5530() wader-0.5.13/plugins/devices/dell_d5540.py000066400000000000000000000026151257646610200201350ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2010 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_EMBEDDED from core.hardware.ericsson import (EricssonDevicePlugin, EricssonF3607gwCustomizer) class EricssonD5540(EricssonDevicePlugin): """:class:`~core.plugin.DevicePlugin` for Ericsson's Dell D5540""" name = "Dell D5540" version = "0.1" author = u"Andrew Bird" custom = EricssonF3607gwCustomizer() __remote_name__ = "D5540" __properties__ = { 'ID_VENDOR_ID': [0x413c], 'ID_MODEL_ID': [0x8183, 0x8184], } conntype = WADER_CONNTYPE_EMBEDDED ericssonD5540 = EricssonD5540() wader-0.5.13/plugins/devices/dell_gobi2000.py000066400000000000000000000025351257646610200206170ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2010 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_EMBEDDED from core.hardware.qualcomm import QualcommWCDMADevicePlugin class DellWCDMAG2000(QualcommWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Dell's Gobi 2000""" name = "Dell Gobi 2000" version = "0.1" author = u"Andrew Bird" __remote_name__ = "Dell Gobi 2000" __properties__ = { 'ID_VENDOR_ID': [0x413c], 'ID_MODEL_ID': [0x8186], } conntype = WADER_CONNTYPE_EMBEDDED dellwcdmag2000 = DellWCDMAG2000() wader-0.5.13/plugins/devices/ericsson_f3307.py000066400000000000000000000027271257646610200210470ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2010 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_EMBEDDED from core.hardware.ericsson import (EricssonDevicePlugin, EricssonF3607gwCustomizer) # # Note F3307 is a F3607gw without GPS and only 2 bands - same firmware # class EricssonF3307(EricssonDevicePlugin): """:class:`~core.plugin.DevicePlugin` for Ericsson's F3307""" name = "Ericsson F3307" version = "0.1" author = u"Andrew Bird" custom = EricssonF3607gwCustomizer() __remote_name__ = "F3307" __properties__ = { 'ID_VENDOR_ID': [0x0bdb], 'ID_MODEL_ID': [0x1909, 0x190a], } conntype = WADER_CONNTYPE_EMBEDDED ericssonF3307 = EricssonF3307() wader-0.5.13/plugins/devices/ericsson_f3507g.py000066400000000000000000000024451257646610200212150ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2009 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_EMBEDDED from core.hardware.ericsson import EricssonDevicePlugin class EricssonF3507G(EricssonDevicePlugin): """:class:`~core.plugin.DevicePlugin} for Ericsson's F3507G""" name = "Ericsson F3507G" version = "0.1" author = u"Andrew Bird" __remote_name__ = "F3507g" __properties__ = { 'ID_VENDOR_ID': [0x0bdb], 'ID_MODEL_ID': [0x1900, 0x1902], } conntype = WADER_CONNTYPE_EMBEDDED ericssonF3507G = EricssonF3507G() wader-0.5.13/plugins/devices/ericsson_f3607gw.py000066400000000000000000000026501257646610200214030ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2010 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_EMBEDDED from core.hardware.ericsson import (EricssonDevicePlugin, EricssonF3607gwCustomizer) class EricssonF3607gw(EricssonDevicePlugin): """:class:`~core.plugin.DevicePlugin` for Ericsson's F3607gw""" name = "Ericsson F3607gw" version = "0.1" author = u"Andrew Bird" custom = EricssonF3607gwCustomizer() __remote_name__ = "F3607gw" __properties__ = { 'ID_VENDOR_ID': [0x0bdb], 'ID_MODEL_ID': [0x1904, 0x1905, 0x1906, 0x1907], } conntype = WADER_CONNTYPE_EMBEDDED ericssonF3607gw = EricssonF3607gw() wader-0.5.13/plugins/devices/ericsson_f5521gw.py000066400000000000000000000026151257646610200214010ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2010-2012 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_EMBEDDED from core.hardware.ericsson import (EricssonDevicePlugin, EricssonF3607gwCustomizer) class EricssonF5521gw(EricssonDevicePlugin): """:class:`~core.plugin.DevicePlugin` for Ericsson's F5521gw""" name = "Ericsson F5521gw" version = "0.1" author = u"Andrew Bird" custom = EricssonF3607gwCustomizer() __remote_name__ = "F5521gw" __properties__ = { 'ID_VENDOR_ID': [0x0bdb], 'ID_MODEL_ID': [0x1911], } conntype = WADER_CONNTYPE_EMBEDDED ericssonF5521gw = EricssonF5521gw() wader-0.5.13/plugins/devices/ericsson_md300.py000066400000000000000000000040651257646610200211250ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2009 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.ericsson import (EricssonDevicePlugin, EricssonWrapper, EricssonCustomizer, ERICSSON_CONN_DICT) from wader.common.utils import revert_dict ERICSSON_CONN_DICT_REV = revert_dict(ERICSSON_CONN_DICT) class EricssonMD300Wrapper(EricssonWrapper): def get_network_mode(self): def get_radio_status_cb(mode): if mode in ERICSSON_CONN_DICT_REV: return ERICSSON_CONN_DICT_REV[mode] raise KeyError("Unknown network mode %d" % mode) d = self.get_radio_status() d.addCallback(get_radio_status_cb) return d class EricssonMD300Customizer(EricssonCustomizer): wrapper_klass = EricssonMD300Wrapper class EricssonMD300(EricssonDevicePlugin): """:class:`~core.plugin.DBusDevicePlugin` for Ericsson's MD300""" name = "Ericsson MD300" version = "0.1" author = u"Andrew Bird" custom = EricssonMD300Customizer() __remote_name__ = "MD300" __properties__ = { 'ID_VENDOR_ID': [0x0fce], 'ID_MODEL_ID': [0xd0cf], } conntype = WADER_CONNTYPE_USB ericssonMD300 = EricssonMD300() wader-0.5.13/plugins/devices/fourgsystems_xsp10.py000066400000000000000000000037711257646610200222050ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2012 Sphere Systems Ltd # Author: Andrew Bird # # 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 wader.common import consts from core.hardware.longcheer import (LongcheerWCDMACustomizer, LongcheerWCDMADevicePlugin, LongcheerWCDMAWrapper) class FourGSystemsXSP10Wrapper(LongcheerWCDMAWrapper): def find_contacts(self, pattern): """Returns a list of `Contact` whose name matches pattern""" # AT+CPBF function is broken, it seems to cause a modem firmware crash d = self.list_contacts() d.addCallback(lambda contacts: [c for c in contacts if c.name.lower().startswith(pattern.lower())]) return d class FourGSystemsXSP10Customizer(LongcheerWCDMACustomizer): wrapper_klass = FourGSystemsXSP10Wrapper class FourGSystemsXSP10(LongcheerWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for 4GSystems' XSStick P10""" name = "XSStick P10" version = "0.1" author = u"Andrew Bird" custom = FourGSystemsXSP10Customizer() __remote_name__ = "XS Stick P10+" __properties__ = { 'ID_VENDOR_ID': [0x1c9e], 'ID_MODEL_ID': [0x9603], } conntype = consts.WADER_CONNTYPE_USB fourgsystemsxsp10 = FourGSystemsXSP10() wader-0.5.13/plugins/devices/hp_gobi2000.py000066400000000000000000000025211257646610200203010ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2010 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_EMBEDDED from core.hardware.qualcomm import QualcommWCDMADevicePlugin class HPWCDMAG2000(QualcommWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for HP's Gobi 2000""" name = "HP Gobi 2000" version = "0.1" author = u"Andrew Bird" __remote_name__ = "HP Gobi 2000" __properties__ = { 'ID_VENDOR_ID': [0x03f0], 'ID_MODEL_ID': [0x251d], } conntype = WADER_CONNTYPE_EMBEDDED hpwcdmag2000 = HPWCDMAG2000() wader-0.5.13/plugins/devices/huawei_b970.py000066400000000000000000000046401257646610200204170ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2009-2010 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_USB from wader.common.encoding import pack_ucs2_bytes from core.command import ATCmd from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper) class HuaweiB970Wrapper(HuaweiWCDMAWrapper): """ :class:`~core.hardware.huawei.HuaweiWCDMAWrapper` for the B970 """ def _add_contact(self, name, number, index): """ Adds a contact to the SIM card """ raw = 0 try: # are all ascii chars name.encode('ascii') except: # write in TS31.101 type 80 raw format # B970 doesn't need the "FF" suffix # AT^CPBW=1,"28780808",129,"80534E4E3A",1 name = '80%s' % pack_ucs2_bytes(name) raw = 1 category = 145 if number.startswith('+') else 129 args = (index, number, category, name, raw) cmd = ATCmd('AT^CPBW=%d,"%s",%d,"%s",%d' % args, name='add_contact') return self.queue_at_cmd(cmd) class HuaweiB970Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the B970 """ wrapper_klass = HuaweiB970Wrapper class HuaweiB970(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's B970""" name = "Huawei B970" version = "0.1" author = u"Andrew Bird" custom = HuaweiB970Customizer() __remote_name__ = "B970" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1003], } conntype = WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/huawei_e1550.py000066400000000000000000000047311257646610200204760ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2010 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 wader.common import consts from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMAWrapper, HuaweiWCDMACustomizer, HUAWEI_BAND_DICT) class HuaweiE1550Wrapper(HuaweiWCDMAWrapper): """ :class:`~core.hardware.huawei.HuaweiWCDMAWrapper` for the E1550 """ def send_ussd(self, ussd): return self._send_ussd_old_mode(ussd) class HuaweiE1550Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the E1550 """ wrapper_klass = HuaweiE1550Wrapper # GSM/GPRS/EDGE 850/900/1800/1900 MHz # HSDPA/UMTS 900/1900/2100 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, # 850 consts.MM_NETWORK_BAND_EGSM, # 900 consts.MM_NETWORK_BAND_DCS, # 1800 consts.MM_NETWORK_BAND_PCS, # 1900 # consts.MM_NETWORK_BAND_U900, consts.MM_NETWORK_BAND_U1900, consts.MM_NETWORK_BAND_U2100]) class HuaweiE1550(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's E1550""" name = "Huawei E1550" version = "0.1" author = u"Andrew Bird" custom = HuaweiE1550Customizer() __remote_name__ = "E1550" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x140c], } conntype = consts.WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/huawei_e160.py000066400000000000000000000046501257646610200204120ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2009-2011 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common import consts from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMAWrapper, HuaweiWCDMACustomizer, HUAWEI_BAND_DICT) class HuaweiE160Wrapper(HuaweiWCDMAWrapper): """ :class:`~core.hardware.huawei.HuaweiWCDMAWrapper` for the E160 """ def send_ussd(self, ussd): return self._send_ussd_old_mode(ussd) class HuaweiE160Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the E160 """ wrapper_klass = HuaweiE160Wrapper # GSM/GPRS/EDGE 850/900/1800/1900 MHz # HSDPA/UMTS 900/1900/2100 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, # 850 consts.MM_NETWORK_BAND_EGSM, # 900 consts.MM_NETWORK_BAND_DCS, # 1800 consts.MM_NETWORK_BAND_PCS, # 1900 # consts.MM_NETWORK_BAND_U900, consts.MM_NETWORK_BAND_U1900, consts.MM_NETWORK_BAND_U2100]) class HuaweiE160(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's E160""" name = "Huawei E160" version = "0.1" author = u"Andrew Bird" custom = HuaweiE160Customizer() __remote_name__ = "E160" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1001, 0x1003], } conntype = consts.WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/huawei_e160b.py000066400000000000000000000023631257646610200205530ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2009-2010 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.huawei import HuaweiWCDMADevicePlugin class HuaweiE160B(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's E160B""" name = "Huawei E160B" version = "0.1" author = u"Andrew Bird" __remote_name__ = "E160B" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1001, 0x1003], } conntype = WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/huawei_e169.py000066400000000000000000000024011257646610200204130ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.huawei import HuaweiWCDMADevicePlugin class HuaweiE169(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's E169""" name = "Huawei E169" version = "0.1" author = u"Pablo Martí" __remote_name__ = "E169" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1406], } conntype = WADER_CONNTYPE_USB huaweiE169 = HuaweiE169() wader-0.5.13/plugins/devices/huawei_e1692.py000066400000000000000000000025411257646610200205020ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2010 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.huawei import (HuaweiWCDMACustomizer, HuaweiWCDMADevicePlugin) class HuaweiE1692(HuaweiWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for Huawei's E1692 """ name = "Huawei E1692" version = "0.1" author = u"Jose Bernardo Bandos" custom = HuaweiWCDMACustomizer() __remote_name__ = "E1692" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x140c], } conntype = WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/huawei_e173.py000066400000000000000000000053371257646610200204210ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Author: Andrew Bird # # 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 twisted.internet import defer from wader.common import consts from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper, HUAWEI_BAND_DICT) class HuaweiE173Wrapper(HuaweiWCDMAWrapper): """ :class:`~core.hardware.huawei.HuaweiWCDMAWrapper` for the E173 """ def get_manufacturer_name(self): """Returns the manufacturer name""" # Seems Huawei didn't implement +GMI return defer.succeed('Huawei') def send_ussd(self, ussd): return self._send_ussd_old_mode(ussd) class HuaweiE173Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the E173 """ wrapper_klass = HuaweiE173Wrapper # GSM/GPRS/EDGE 850/900/1800/1900 MHz # HSDPA/UMTS 2100/900 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, consts.MM_NETWORK_BAND_EGSM, consts.MM_NETWORK_BAND_DCS, consts.MM_NETWORK_BAND_PCS, # consts.MM_NETWORK_BAND_U900, # waiting for docs consts.MM_NETWORK_BAND_U2100]) class HuaweiE173(HuaweiWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for Huawei's E173 """ name = "Huawei E173" version = "0.1" author = u"Andrew Bird" custom = HuaweiE173Customizer() __remote_name__ = "E173" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x14a5], } conntype = consts.WADER_CONNTYPE_USB def preprobe_init(self, ports, info): if info['ID_MODEL_ID'] == 0x1436: self.__properties__['ID_MODEL_ID'][0] = 0x1436 huaweie173 = HuaweiE173() wader-0.5.13/plugins/devices/huawei_e1750.py000066400000000000000000000053371257646610200205030ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2011 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 wader.common import consts from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMAWrapper, HuaweiWCDMACustomizer, HUAWEI_BAND_DICT) class HuaweiE1750Wrapper(HuaweiWCDMAWrapper): """ :class:`~core.hardware.huawei.HuaweiWCDMAWrapper` for the E1750 """ def send_ussd(self, ussd): return self._send_ussd_old_mode(ussd) class HuaweiE1750Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the E1750 """ wrapper_klass = HuaweiE1750Wrapper # GSM/GPRS/EDGE 850/900/1800/1900 MHz # HSDPA/UMTS 2100 MHz # Device with PID 0x1436 only shows UMTS 2100 support, but other # sources on the web suggest there may be variants band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, # 850 consts.MM_NETWORK_BAND_EGSM, # 900 consts.MM_NETWORK_BAND_DCS, # 1800 consts.MM_NETWORK_BAND_PCS, # 1900 consts.MM_NETWORK_BAND_U2100]) class HuaweiE1750(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's E1750""" name = "Huawei E1750" version = "0.1" author = u"Andrew Bird" custom = HuaweiE1750Customizer() __remote_name__ = "E1750" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x0000], } conntype = consts.WADER_CONNTYPE_USB def preprobe_init(self, ports, info): if info['ID_MODEL_ID'] == 0x140c: self.__properties__['ID_MODEL_ID'][0] = 0x140c if info['ID_MODEL_ID'] == 0x1436: self.__properties__['ID_MODEL_ID'][0] = 0x1436 wader-0.5.13/plugins/devices/huawei_e1752.py000066400000000000000000000025651257646610200205050ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2010 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.huawei import (HuaweiWCDMACustomizer, HuaweiWCDMADevicePlugin) class HuaweiE1752(HuaweiWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for Huawei's E1752 """ name = "Huawei E1752" version = "0.1" author = u"Andrew Bird" custom = HuaweiWCDMACustomizer() __remote_name__ = "E1752" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x141b], } conntype = WADER_CONNTYPE_USB huaweie1752 = HuaweiE1752() wader-0.5.13/plugins/devices/huawei_e17X.py000066400000000000000000000076671257646610200204760ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2010-2011 Vodafone España, S.A. # Author: Jaime Soriano # # 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 twisted.internet import defer import wader.common.aterrors as E from wader.common import consts from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper, HUAWEI_BAND_DICT) class HuaweiE17XWrapper(HuaweiWCDMAWrapper): def get_phonebook_size(self): # the E170 that we have around keeps raising Generals whenever # is asked for its size, we'll have to cheat till we have time # to find a workaround d = super(HuaweiE17XWrapper, self).get_phonebook_size() d.addErrback(lambda failure: defer.succeed(200)) return d def list_contacts(self): # Return a list of all the contacts without knowing the phonebook size # # 1. We first find the highest index of what's there already # 2. We can't use the results of the AT+CPBF='' search because it # returns rubbish for valid contacts stored on the SIM by other # devices. That means any contact not written by an E172 using # AT+CPBW is invalid without this method. # 3. Now we can use the derived range to use the Huawei proprietary # command to return the proper results. def get_max_index_cb(matches): indexes = map(int, [m.group('id') for m in matches]) return max(indexes) def no_contacts_eb(failure): failure.trap(E.NotFound, E.General) return 0 def get_valid_contacts(_max): if not _max: return defer.succeed([]) def results_cb(matches): return [self._regexp_to_contact(m) for m in matches] return self.send_at('AT^CPBR=1,%d' % _max, name='list_contacts', callback=results_cb) d = self.send_at('AT+CPBF=""', name='find_contacts', callback=get_max_index_cb) d.addErrback(no_contacts_eb) d.addCallback(get_valid_contacts) return d def send_ussd(self, ussd): return self._send_ussd_ucs2_mode(ussd) class HuaweiE17XCustomizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the E17X """ wrapper_klass = HuaweiE17XWrapper # GSM/GPRS/EDGE 850/900/1800 MHz # HSDPA/UMTS 2100 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, # 850 consts.MM_NETWORK_BAND_EGSM, # 900 consts.MM_NETWORK_BAND_DCS, # 1800 consts.MM_NETWORK_BAND_U2100]) class HuaweiE17X(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's E17X""" name = "Huawei E17X" version = "0.1" author = u"Jaime Soriano" custom = HuaweiE17XCustomizer() __remote_name__ = "E17X" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1003], } conntype = consts.WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/huawei_e180.py000066400000000000000000000023461257646610200204140ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.huawei import HuaweiWCDMADevicePlugin class HuaweiE180(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's E180""" name = "Huawei E180" version = "0.1" author = u"Pablo Martí" __remote_name__ = "E180" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1003], } conntype = WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/huawei_e1820.py000066400000000000000000000062421257646610200204750ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011 Vodafone España, S.A. # Author: Andrew Bird # # 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 twisted.internet import reactor from twisted.internet.task import deferLater from wader.common import consts from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper, HUAWEI_BAND_DICT) class HuaweiE1820Wrapper(HuaweiWCDMAWrapper): """ :class:`~core.hardware.huawei.HuaweiWCDMAWrapper` for the E1820 """ def check_pin(self): """ Returns the SIM's auth state :raise SimPinRequired: Raised if SIM PIN is required :raise SimPukRequired: Raised if SIM PUK is required :raise SimPuk2Required: Raised if SIM PUK2 is required """ # XXX: this device needs to be enabled before pin can be checked d = self.get_radio_status() def get_radio_status_cb(status): if status != 1: self.send_at('AT+CFUN=1') # delay here 2 secs, or we perhaps we should wait for ^SRVST:1 return deferLater(reactor, 2, lambda: None) d.addCallback(get_radio_status_cb) d.addCallback(lambda x: super(HuaweiE1820Wrapper, self).check_pin()) return d def send_ussd(self, ussd): return self._send_ussd_old_mode(ussd) class HuaweiE1820Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the E1820 """ wrapper_klass = HuaweiE1820Wrapper # GSM/GPRS/EDGE 850/900/1800/1900 MHz # HSDPA/UMTS 2100 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, consts.MM_NETWORK_BAND_EGSM, consts.MM_NETWORK_BAND_DCS, consts.MM_NETWORK_BAND_PCS, consts.MM_NETWORK_BAND_U2100]) class HuaweiE1820(HuaweiWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for Huawei's E1820 """ name = "Huawei E1820" version = "0.1" author = u"Andrew Bird" custom = HuaweiE1820Customizer() __remote_name__ = "E1820" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x14ac], } conntype = consts.WADER_CONNTYPE_USB huaweie1820 = HuaweiE1820() wader-0.5.13/plugins/devices/huawei_e220.py000066400000000000000000000105601257646610200204040ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 twisted.internet import defer from wader.common import consts from wader.common.encoding import pack_ucs2_bytes from core.command import ATCmd from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper, HuaweiSIMClass, HUAWEI_BAND_DICT) class HuaweiE220Wrapper(HuaweiWCDMAWrapper): """ :class:`~core.hardware.huawei.HuaweiWCDMAWrapper` for the E220 """ def _add_contact(self, name, number, index): """ Adds a contact to the SIM card """ raw = 0 try: # are all ascii chars name.encode('ascii') except: # write in TS31.101 type 80 raw format # E220 doesn't need the "FF" suffix # AT^CPBW=1,"28780808",129,"80534E4E3A",1 name = '80%s' % pack_ucs2_bytes(name) raw = 1 category = 145 if number.startswith('+') else 129 args = (index, number, category, name, raw) cmd = ATCmd('AT^CPBW=%d,"%s",%d,"%s",%d' % args, name='add_contact') return self.queue_at_cmd(cmd) def get_roaming_ids(self): # Note: We have to disable this and return an empty list or the device # resets and drops off the USB bus return defer.succeed([]) def send_ussd(self, ussd): return self._send_ussd_ucs2_mode(ussd) class HuaweiE220Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the E220 """ wrapper_klass = HuaweiE220Wrapper # GSM/GPRS/EDGE 900/1800 MHz # HSDPA/UMTS 2100 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_EGSM, # 900 consts.MM_NETWORK_BAND_DCS, # 1800 consts.MM_NETWORK_BAND_U2100]) class HuaweiE220SIMClass(HuaweiSIMClass): """Huawei E220 SIM Class""" def __init__(self, sconn): super(HuaweiE220SIMClass, self).__init__(sconn) def initialize(self, set_encoding=False): d = super(HuaweiE220SIMClass, self).initialize(set_encoding) def init_cb(size): self.sconn.get_smsc() # before switching to UCS2, we need to get once the SMSC number # otherwise as soon as we send a SMS, the device would reset # as if it had been unplugged and replugged to the system def process_charset(charset): """ Do not set charset to UCS2 if is not necessary, returns size """ if charset == "UCS2": return size else: d3 = self.sconn.set_charset("UCS2") d3.addCallback(lambda ignored: size) return d3 d2 = self.sconn.get_charset() d2.addCallback(process_charset) return d2 d.addCallback(init_cb) return d class HuaweiE220(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's E220""" name = "Huawei E220" version = "0.1" author = u"Pablo Martí" custom = HuaweiE220Customizer() sim_klass = HuaweiE220SIMClass __remote_name__ = "E220" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1003, 0x1004], } conntype = consts.WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/huawei_e270.py000066400000000000000000000033541257646610200204140ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper) class HuaweiE270Wrapper(HuaweiWCDMAWrapper): def send_ussd(self, ussd): return self._send_ussd_ucs2_mode(ussd) class HuaweiE270Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the E270 """ wrapper_klass = HuaweiE270Wrapper class HuaweiE270(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's E270""" name = "Huawei E270" version = "0.1" author = u"Pablo Martí" custom = HuaweiE270Customizer() __remote_name__ = "E270" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1003], } conntype = WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/huawei_e272.py000066400000000000000000000033541257646610200204160ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper) class HuaweiE272Wrapper(HuaweiWCDMAWrapper): def send_ussd(self, ussd): return self._send_ussd_ucs2_mode(ussd) class HuaweiE272Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the E272 """ wrapper_klass = HuaweiE272Wrapper class HuaweiE272(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's E272""" name = "Huawei E272" version = "0.1" author = u"Pablo Martí" custom = HuaweiE272Customizer() __remote_name__ = "E272" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1003], } conntype = WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/huawei_e3735.py000066400000000000000000000062701257646610200205050ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2009-2010 Vodafone España, S.A. # Author: Andrew Bird # # 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 twisted.internet import reactor from twisted.internet.task import deferLater from wader.common import consts from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper, HUAWEI_BAND_DICT) class HuaweiK3735Wrapper(HuaweiWCDMAWrapper): """ :class:`~core.hardware.huawei.HuaweiWCDMAWrapper` for the K3735 """ def check_pin(self): """ Returns the SIM's auth state :raise SimPinRequired: Raised if SIM PIN is required :raise SimPukRequired: Raised if SIM PUK is required :raise SimPuk2Required: Raised if SIM PUK2 is required """ # XXX: this device needs to be enabled before pin can be checked d = self.get_radio_status() def get_radio_status_cb(status): if status != 1: d = self.send_at('AT+CFUN=1') def cb(arg): # delay here 2 secs return deferLater(reactor, 2, lambda: arg) d.addCallback(cb) return d d.addCallback(get_radio_status_cb) d.addCallback(lambda x: super(HuaweiK3735Wrapper, self).check_pin()) return d class HuaweiE3735Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the E3735 """ wrapper_klass = HuaweiK3735Wrapper # GSM/GPRS/EDGE 850/900/1800/1900 MHz # HSDPA/UMTS 900/2100 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, # 850 consts.MM_NETWORK_BAND_EGSM, # 900 consts.MM_NETWORK_BAND_DCS, # 1800 consts.MM_NETWORK_BAND_PCS, # 1900 # consts.MM_NETWORK_BAND_U900, consts.MM_NETWORK_BAND_U2100]) class HuaweiE3735(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's E3735""" name = "Huawei E3735" version = "0.1" author = u"Andrew Bird" custom = HuaweiE3735Customizer() __remote_name__ = "E3735" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1001], } conntype = consts.WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/huawei_e510.py000066400000000000000000000050021257646610200204010ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common import consts from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper, HUAWEI_BAND_DICT) class HuaweiE510Wrapper(HuaweiWCDMAWrapper): def send_ussd(self, ussd): return self._send_ussd_ucs2_mode(ussd) class HuaweiE510Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the E510 """ wrapper_klass = HuaweiE510Wrapper # GSM/GPRS/EDGE 900/1800/1900 MHz # HSDPA/UMTS 2100/900 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_EGSM, # 900 consts.MM_NETWORK_BAND_DCS, # 1800 consts.MM_NETWORK_BAND_PCS, # 1900 # XXX: const needs to be enabled in family first # consts.MM_NETWORK_BAND_U900, consts.MM_NETWORK_BAND_U2100]) class HuaweiE510(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's E510""" name = "Huawei E510" version = "0.1" author = u"Andrew Bird" custom = HuaweiE510Customizer() __remote_name__ = "E510" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1411], } conntype = consts.WADER_CONNTYPE_USB def preprobe_init(self, ports, info): # This device might be found by means of the mother plugin too if info['ID_MODEL_ID'] == 0x1001: self.__properties__['ID_MODEL_ID'][0] = 0x1001 huaweie510 = HuaweiE510() wader-0.5.13/plugins/devices/huawei_e620.py000066400000000000000000000035231257646610200204110ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 re from wader.common.consts import WADER_CONNTYPE_PCMCIA from core.command import build_cmd_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer) E620_CMD_DICT = HuaweiWCDMACustomizer.cmd_dict.copy() E620_CMD_DICT['get_roaming_ids'] = build_cmd_dict(re.compile( """ \r\n \+CPOL:\s(?P\d+),"(?P\d+)" """, re.VERBOSE)) class HuaweiE620Customizer(HuaweiWCDMACustomizer): cmd_dict = E620_CMD_DICT class HuaweiE620(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's E620""" name = "Huawei E620" version = "0.1" author = u"Pablo Martí" custom = HuaweiE620Customizer() __remote_name__ = "E620" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1001], } conntype = WADER_CONNTYPE_PCMCIA wader-0.5.13/plugins/devices/huawei_e660.py000066400000000000000000000024351257646610200204160ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 wader.common.consts import WADER_CONNTYPE_PCMCIA from core.hardware.huawei import HuaweiWCDMADevicePlugin class HuaweiE660(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's E660""" name = "Huawei E660" version = "0.1" author = u"Pablo Martí" __remote_name__ = "183" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1001], } conntype = WADER_CONNTYPE_PCMCIA wader-0.5.13/plugins/devices/huawei_e660a.py000066400000000000000000000057411257646610200205620ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2010 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 wader.common import consts from core.command import ATCmd from wader.common.encoding import pack_ucs2_bytes from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper, HUAWEI_BAND_DICT) class HuaweiE660aWrapper(HuaweiWCDMAWrapper): def _add_contact(self, name, number, index): """ Adds a contact to the SIM card """ raw = 0 try: # are all ascii chars name.encode('ascii') except: # write in TS31.101 type 80 raw format # K2540 doesn't need the "FF" suffix # AT^CPBW=1,"28780808",129,"80534E4E3A",1 name = '80%s' % pack_ucs2_bytes(name) raw = 1 category = 145 if number.startswith('+') else 129 args = (index, number, category, name, raw) cmd = ATCmd('AT^CPBW=%d,"%s",%d,"%s",%d' % args, name='add_contact') return self.queue_at_cmd(cmd) class HuaweiE660aCustomizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the E660a """ wrapper_klass = HuaweiE660aWrapper # GSM/GPRS/EDGE 850/900/1800 MHz # HSDPA/UMTS 850/1900/2100 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, # 850 consts.MM_NETWORK_BAND_EGSM, # 900 consts.MM_NETWORK_BAND_DCS, # 1800 consts.MM_NETWORK_BAND_U850, consts.MM_NETWORK_BAND_U1900, consts.MM_NETWORK_BAND_U2100]) class HuaweiE660A(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's E660A""" name = "Huawei E660A" version = "0.1" author = u"Pablo Martí" custom = HuaweiE660aCustomizer() __remote_name__ = "E660A" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1001], } conntype = consts.WADER_CONNTYPE_PCMCIA wader-0.5.13/plugins/devices/huawei_e870.py000066400000000000000000000057301257646610200204220ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2010 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 wader.common import consts from wader.common.encoding import pack_ucs2_bytes from core.command import ATCmd from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper, HUAWEI_BAND_DICT) class HuaweiE870Wrapper(HuaweiWCDMAWrapper): def _add_contact(self, name, number, index): """ Adds a contact to the SIM card """ raw = 0 try: # are all ascii chars name.encode('ascii') except: # write in TS31.101 type 80 raw format # K2540 doesn't need the "FF" suffix # AT^CPBW=1,"28780808",129,"80534E4E3A",1 name = '80%s' % pack_ucs2_bytes(name) raw = 1 category = 145 if number.startswith('+') else 129 args = (index, number, category, name, raw) cmd = ATCmd('AT^CPBW=%d,"%s",%d,"%s",%d' % args, name='add_contact') return self.queue_at_cmd(cmd) class HuaweiE870Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the E870 """ wrapper_klass = HuaweiE870Wrapper # GSM/GPRS/EDGE 850/900/1800 MHz # HSDPA/UMTS 850/1900/2100 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, # 850 consts.MM_NETWORK_BAND_EGSM, # 900 consts.MM_NETWORK_BAND_DCS, # 1800 consts.MM_NETWORK_BAND_U850, consts.MM_NETWORK_BAND_U1900, consts.MM_NETWORK_BAND_U2100]) class HuaweiE870(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's E870""" name = "Huawei E870" version = "0.1" author = u"Pablo Martí" custom = HuaweiE870Customizer() __remote_name__ = "E870" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1003], } conntype = consts.WADER_CONNTYPE_PCMCIA wader-0.5.13/plugins/devices/huawei_em730v.py000066400000000000000000000047521257646610200207630ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2010 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 wader.common import consts from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMAWrapper, HuaweiWCDMACustomizer, HUAWEI_BAND_DICT) class HuaweiEM730VWrapper(HuaweiWCDMAWrapper): """ :class:`~core.hardware.huawei.HuaweiWCDMAWrapper` for the EM730V """ def send_ussd(self, ussd): return self._send_ussd_old_mode(ussd) class HuaweiEM730VCustomizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the EM730V """ wrapper_klass = HuaweiEM730VWrapper # GSM/GPRS/EDGE 850/900/1800/1900 MHz # HSDPA/UMTS 900/1900/2100 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, # 850 consts.MM_NETWORK_BAND_EGSM, # 900 consts.MM_NETWORK_BAND_DCS, # 1800 consts.MM_NETWORK_BAND_PCS, # 1900 # consts.MM_NETWORK_BAND_U900, consts.MM_NETWORK_BAND_U1900, consts.MM_NETWORK_BAND_U2100]) class HuaweiEM730V(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's EM730V""" name = "Huawei EM730V" version = "0.1" author = u"Pablo Martí" custom = HuaweiEM730VCustomizer() __remote_name__ = "EM730V" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1001], } conntype = consts.WADER_CONNTYPE_EMBEDDED wader-0.5.13/plugins/devices/huawei_em770.py000066400000000000000000000027131257646610200205740ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2007 Vodafone España, S.A. # Author: Pablo Martí # # 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 wader.common.consts import WADER_CONNTYPE_EMBEDDED from core.hardware.huawei import (HuaweiWCDMACustomizer, HuaweiWCDMADevicePlugin) class HuaweiEM770(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's EM770""" name = "Huawei EM770" version = "0.1" author = u"Andrew Bird" custom = HuaweiWCDMACustomizer # Radio Switch query/response different from EM730V :-) # so ignore that for now until we get a device __remote_name__ = "EM770" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1001], } conntype = WADER_CONNTYPE_EMBEDDED wader-0.5.13/plugins/devices/huawei_exxx.py000066400000000000000000000117451257646610200207360ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 core.hardware.huawei import HuaweiWCDMADevicePlugin from plugins.huawei_e160 import HuaweiE160 from plugins.huawei_e160b import HuaweiE160B from plugins.huawei_e169 import HuaweiE169 from plugins.huawei_e17X import HuaweiE17X from plugins.huawei_e173 import HuaweiE173 from plugins.huawei_e180 import HuaweiE180 from plugins.huawei_e220 import HuaweiE220 from plugins.huawei_e270 import HuaweiE270 from plugins.huawei_e272 import HuaweiE272 from plugins.huawei_e510 import HuaweiE510 from plugins.huawei_e620 import HuaweiE620 from plugins.huawei_e660 import HuaweiE660 from plugins.huawei_e660a import HuaweiE660A from plugins.huawei_e870 import HuaweiE870 from plugins.huawei_e1550 import HuaweiE1550 from plugins.huawei_e1692 import HuaweiE1692 from plugins.huawei_e1750 import HuaweiE1750 from plugins.huawei_e3735 import HuaweiE3735 from plugins.huawei_k2540 import HuaweiK2540 from plugins.huawei_k3520 import HuaweiK3520 from plugins.huawei_k3565 import HuaweiK3565 from plugins.huawei_k3715 import HuaweiK3715 from plugins.huawei_em730v import HuaweiEM730V from plugins.huawei_em770 import HuaweiEM770 from plugins.huawei_b970 import HuaweiB970 class HuaweiEXXX1436(HuaweiWCDMADevicePlugin): """:class:`~plugin.DevicePlugin` for Huawei's 1436 family""" name = "Huawei EXXX" version = "0.1" author = u"Andrew Bird" __remote_name__ = None __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1436], } def __init__(self): super(HuaweiEXXX1436, self).__init__() self.mapping = { 'E173': HuaweiE173, 'E1750': HuaweiE1750, 'default': HuaweiE1750, } class HuaweiEXXX140c(HuaweiWCDMADevicePlugin): """:class:`~plugin.DevicePlugin` for Huawei's 140c family""" name = "Huawei EXXX" version = "0.1" author = u"Andrew Bird" __remote_name__ = None __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x140c], } def __init__(self): super(HuaweiEXXX140c, self).__init__() self.mapping = { 'E1550': HuaweiE1550, 'E1692': HuaweiE1692, 'E1750': HuaweiE1750, 'default': HuaweiE1550, } class HuaweiEXXX1003(HuaweiWCDMADevicePlugin): """:class:`~plugin.DevicePlugin` for Huawei's 1003 family""" name = "Huawei EXXX" version = "0.1" author = u"Pablo Martí" __remote_name__ = None __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1003, 0x1004], } def __init__(self): super(HuaweiEXXX1003, self).__init__() self.mapping = { 'E870': HuaweiE870, # Expresscards 'E220': HuaweiE220, # USB dongles 'E270': HuaweiE270, 'E272': HuaweiE272, 'E160': HuaweiE160, # USB Sticks 'E160B': HuaweiE160B, 'E17X': HuaweiE17X, 'E180': HuaweiE180, 'K3565': HuaweiK3565, 'B970': HuaweiB970, # Routers 'default': HuaweiE220, } class HuaweiEXXX1001(HuaweiWCDMADevicePlugin): """:class:`~plugin.DevicePlugin` for Huawei's 1001 family""" name = "Huawei EXXX" version = "0.1" author = u"Pablo Martí" __remote_name__ = None __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1001, 0x1003], } def __init__(self): super(HuaweiEXXX1001, self).__init__() self.mapping = { 'E620': HuaweiE620, # Cardbus 'E660': HuaweiE660, 'E660A': HuaweiE660A, 'E3735': HuaweiE3735, # Expresscards 'E510': HuaweiE510, # USB dongles 'E169': HuaweiE169, # USB Sticks 'E1550': HuaweiE1550, 'K2540': HuaweiK2540, 'K3520': HuaweiK3520, 'K3565': HuaweiK3565, 'K3715': HuaweiK3715, 'EM730V': HuaweiEM730V, # Embedded Modules 'EM770': HuaweiEM770, 'default': HuaweiE660, } huaweiexxx1436 = HuaweiEXXX1436() huaweiexxx140c = HuaweiEXXX140c() huaweiexxx1003 = HuaweiEXXX1003() huaweiexxx1001 = HuaweiEXXX1001() wader-0.5.13/plugins/devices/huawei_k2540.py000066400000000000000000000111301257646610200204730ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2009-2010 Vodafone España, S.A. # Author: Andrew Bird # # 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 re from twisted.internet import defer from wader.common import consts from wader.common.encoding import pack_ucs2_bytes from core.command import ATCmd, build_cmd_dict from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper, HUAWEI_BAND_DICT, HUAWEI_CMD_DICT) from core.middleware import WCDMAWrapper ALLOWED_DICT = { consts.MM_ALLOWED_MODE_ANY: None, consts.MM_ALLOWED_MODE_2G_ONLY: None, } CONN_DICT = { consts.MM_NETWORK_MODE_ANY: None, consts.MM_NETWORK_MODE_2G_ONLY: None, } CMD_DICT = HUAWEI_CMD_DICT.copy() CMD_DICT['get_radio_status'] = \ build_cmd_dict(re.compile('\r\n\+CFUN:\s?(?P\d)\r\n')) class HuaweiK2540Wrapper(HuaweiWCDMAWrapper): """ :class:`~core.hardware.huawei.HuaweiWCDMAWrapper` for the K2540 """ def _add_contact(self, name, number, index): """ Adds a contact to the SIM card """ raw = 0 try: # are all ascii chars name.encode('ascii') except: # write in TS31.101 type 80 raw format # K2540 doesn't need the "FF" suffix # AT^CPBW=1,"28780808",129,"80534E4E3A",1 name = '80%s' % pack_ucs2_bytes(name) raw = 1 category = 145 if number.startswith('+') else 129 args = (index, number, category, name, raw) cmd = ATCmd('AT^CPBW=%d,"%s",%d,"%s",%d' % args, name='add_contact') return self.queue_at_cmd(cmd) def enable_radio(self, enable): # This device may not return anything from +CFUN?, so just do it blind d = super(WCDMAWrapper, self).enable_radio(enable) d.addCallback(lambda response: response[0].group('resp')) return d def get_roaming_ids(self): # Note: We have to disable this and return an empty list or the device # resets and drops off the USB bus return defer.succeed([]) def get_network_mode(self): """Returns the current network mode preference""" return defer.succeed(consts.MM_NETWORK_MODE_2G_ONLY) def set_allowed_mode(self, mode): """Sets the allowed mode to ``mode``""" if mode in self.custom.allowed_dict: self.device.set_property(consts.NET_INTFACE, "AllowedMode", mode) return defer.succeed("OK") else: raise KeyError("Mode %s not found" % mode) def set_network_mode(self, mode): """Sets the network mode to ``mode``""" if mode in self.custom.conn_dict: return defer.succeed("OK") else: raise KeyError("Unsupported mode %d" % mode) def send_ussd(self, ussd): return self._send_ussd_old_mode(ussd) class HuaweiK2540Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the K2540 """ wrapper_klass = HuaweiK2540Wrapper # GSM/GPRS/EDGE 850/900/1800/1900 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, # 850 consts.MM_NETWORK_BAND_EGSM, # 900 consts.MM_NETWORK_BAND_DCS, # 1800 consts.MM_NETWORK_BAND_PCS]) # 1900 allowed_dict = ALLOWED_DICT conn_dict = CONN_DICT cmd_dict = CMD_DICT class HuaweiK2540(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's K2540""" name = "Huawei K2540" version = "0.1" author = u"Andrew Bird" custom = HuaweiK2540Customizer __remote_name__ = "K2540" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1001], } conntype = consts.WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/huawei_k3520.py000066400000000000000000000056341257646610200205060ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Copyright (C) 2010-2011 Vodafone España, S.A. # Author: Pablo Martí # # 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 wader.common import consts from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMAWrapper, HuaweiWCDMACustomizer, HUAWEI_BAND_DICT) from core.hardware.base import build_band_dict class HuaweiK3520Wrapper(HuaweiWCDMAWrapper): def list_contacts(self): def list_contacts_cb(contacts): d = self.set_charset("UCS2") d.addCallback(lambda _: contacts) return d d = self.set_charset("IRA") d.addCallback(lambda ign: super(HuaweiK3520Wrapper, self).list_contacts()) d.addCallback(list_contacts_cb) return d def find_contacts(self, pattern): d = self.list_contacts() d.addCallback(lambda contacts: [c for c in contacts if c.name.lower().startswith(pattern.lower())]) return d def send_ussd(self, ussd): return self._send_ussd_ucs2_mode(ussd) class HuaweiK3520Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the K3520 """ wrapper_klass = HuaweiK3520Wrapper # GSM/GPRS/EDGE 850/900/1800/1900 MHz # HSDPA/UMTS 2100/900 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, consts.MM_NETWORK_BAND_EGSM, consts.MM_NETWORK_BAND_DCS, consts.MM_NETWORK_BAND_PCS, # consts.MM_NETWORK_BAND_U900, # waiting for docs consts.MM_NETWORK_BAND_U2100]) class HuaweiK3520(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's K3520""" name = "Huawei K3520" version = "0.1" author = u"Pablo Martí" custom = HuaweiK3520Customizer() __remote_name__ = "K3520" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1001], } conntype = consts.WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/huawei_k3565.py000066400000000000000000000044361257646610200205160ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2009-2011 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common import consts from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper, HUAWEI_BAND_DICT) class HuaweiK3565Wrapper(HuaweiWCDMAWrapper): def send_ussd(self, ussd): return self._send_ussd_ucs2_mode(ussd) class HuaweiK3565Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the K3565 """ wrapper_klass = HuaweiK3565Wrapper # GSM/GPRS/EDGE 850/900/1800/1900 MHz # HSDPA/UMTS 2100/900 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, consts.MM_NETWORK_BAND_EGSM, consts.MM_NETWORK_BAND_DCS, consts.MM_NETWORK_BAND_PCS, # consts.MM_NETWORK_BAND_U900, # waiting for docs consts.MM_NETWORK_BAND_U2100]) class HuaweiK3565(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's K3565""" name = "Huawei K3565" version = "0.1" author = u"Andrew Bird" custom = HuaweiK3565Customizer() __remote_name__ = "K3565" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1001, 0x1003], } conntype = consts.WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/huawei_k3715.py000066400000000000000000000062411257646610200205070ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Author: Pablo Martí # # 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 twisted.internet import defer, reactor from twisted.internet.task import deferLater from wader.common import consts from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper, HUAWEI_BAND_DICT) class HuaweiK3715Wrapper(HuaweiWCDMAWrapper): """ :class:`~core.hardware.huawei.HuaweiWCDMAWrapper` for the K3715 """ def enable_radio(self, enable): """ Enables the radio according to ``enable`` It will not enable it if it's already enabled and viceversa """ def check_if_necessary(status): if (status == 1 and enable) or (status in [0, 7] and not enable): return defer.succeed('OK') if enable: d = self.send_at('AT+CFUN=1') else: d = self.send_at('AT+CFUN=7') d.addCallback(lambda x: deferLater(reactor, 5, lambda: x)) return d d = self.get_radio_status() d.addCallback(check_if_necessary) return d def send_ussd(self, ussd): return self._send_ussd_ucs2_mode(ussd) class HuaweiK3715Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the K3715 """ wrapper_klass = HuaweiK3715Wrapper # GSM/GPRS/EDGE 850/900/1800/1900 MHz # HSDPA/UMTS 2100/900 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, consts.MM_NETWORK_BAND_EGSM, consts.MM_NETWORK_BAND_DCS, consts.MM_NETWORK_BAND_PCS, # consts.MM_NETWORK_BAND_U900, # waiting for docs consts.MM_NETWORK_BAND_U2100]) class HuaweiK3715(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's K3715""" name = "Huawei K3715" version = "0.1" author = u"Andrew Bird" custom = HuaweiK3715Customizer() quirks = { 'needs_enable_before_pin_check': True, } __remote_name__ = "K3715" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1001], } conntype = consts.WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/huawei_k3765.py000066400000000000000000000045161257646610200205170ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common import consts from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper, HUAWEI_BAND_DICT) class HuaweiK3765Wrapper(HuaweiWCDMAWrapper): def send_ussd(self, ussd): return self._send_ussd_ucs2_mode(ussd) class HuaweiK3765Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the K3765 """ wrapper_klass = HuaweiK3765Wrapper # GSM/GPRS/EDGE 850/900/1800/1900 MHz # HSDPA/UMTS 2100/900 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, consts.MM_NETWORK_BAND_EGSM, consts.MM_NETWORK_BAND_DCS, consts.MM_NETWORK_BAND_PCS, # consts.MM_NETWORK_BAND_U900, # waiting for docs consts.MM_NETWORK_BAND_U2100]) class HuaweiK3765(HuaweiWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for Huawei's Vodafone K3765 """ name = "Huawei K3765" version = "0.1" author = u"Andrew Bird" custom = HuaweiK3765Customizer() __remote_name__ = "K3765" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1451, 0x1465], } conntype = consts.WADER_CONNTYPE_USB huaweik3765 = HuaweiK3765() wader-0.5.13/plugins/devices/huawei_k3770.py000066400000000000000000000052221257646610200205060ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011 Canonical, Ltd. # Author: Alex Chiang # # 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 twisted.internet import defer from wader.common import consts from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper, HUAWEI_BAND_DICT) class HuaweiK3770Wrapper(HuaweiWCDMAWrapper): def get_manufacturer_name(self): """Returns the manufacturer name""" # Seems Huawei didn't implement +GMI return defer.succeed('Huawei') def send_ussd(self, ussd): """Sends the ussd command ``ussd``""" # K3770 wants request in ascii chars even though current # set might be ucs2 return super(HuaweiK3770Wrapper, self).send_ussd(ussd, force_ascii=True) class HuaweiK3770Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the K3770 """ wrapper_klass = HuaweiK3770Wrapper # GSM/GPRS/EDGE 850/900/1800/1900 MHz # HSDPA/UMTS 2100 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, consts.MM_NETWORK_BAND_EGSM, consts.MM_NETWORK_BAND_DCS, consts.MM_NETWORK_BAND_PCS, consts.MM_NETWORK_BAND_U2100]) class HuaweiK3770(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's K3770""" name = "Huawei K3770" version = "0.1" author = u"Alex Chiang" custom = HuaweiK3770Customizer() __remote_name__ = "K3770" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x14c9], } conntype = consts.WADER_CONNTYPE_USB huaweik3770 = HuaweiK3770() wader-0.5.13/plugins/devices/huawei_k3771.py000066400000000000000000000053231257646610200205110ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Author: Andrew Bird # # 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 twisted.internet import defer from wader.common import consts from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper, HUAWEI_BAND_DICT) class HuaweiK3771Wrapper(HuaweiWCDMAWrapper): def get_manufacturer_name(self): """Returns the manufacturer name""" # Seems Huawei didn't implement +GMI return defer.succeed('Huawei') def send_ussd(self, ussd): """Sends the ussd command ``ussd``""" # K3771 wants request in ascii chars even though current # set might be ucs2 return super(HuaweiK3771Wrapper, self).send_ussd(ussd, force_ascii=True) class HuaweiK3771Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the K3771 """ wrapper_klass = HuaweiK3771Wrapper # GSM/GPRS/EDGE 850/900/1800/1900 MHz # HSDPA/UMTS 850/2100 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, consts.MM_NETWORK_BAND_EGSM, consts.MM_NETWORK_BAND_DCS, consts.MM_NETWORK_BAND_PCS, consts.MM_NETWORK_BAND_U850, consts.MM_NETWORK_BAND_U2100]) class HuaweiK3771(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's K3771""" name = "Huawei K3771" version = "0.1" author = u"Andrew Bird" custom = HuaweiK3771Customizer() __remote_name__ = "K3771" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x14ca], } conntype = consts.WADER_CONNTYPE_USB huaweik3771 = HuaweiK3771() wader-0.5.13/plugins/devices/huawei_k3806.py000066400000000000000000000052701257646610200205110ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2010 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common import consts from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper, HUAWEI_BAND_DICT) class HuaweiK3806Wrapper(HuaweiWCDMAWrapper): """ :class:`~core.hardware.huawei.HuaweiWCDMAWrapper` for the K3806 """ def enable_radio(self, enable): d = self.get_radio_status() def get_radio_status_cb(status): if status in [0, 7] and enable: return self.send_at('AT+CFUN=1') elif status == 1 and not enable: return self.send_at('AT+CFUN=7') d.addCallback(get_radio_status_cb) return d class HuaweiK3806Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the K3806 """ wrapper_klass = HuaweiK3806Wrapper # GSM/GPRS/EDGE 850/900/1800/1900 MHz # HSDPA/UMTS 2100/900 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, consts.MM_NETWORK_BAND_EGSM, consts.MM_NETWORK_BAND_DCS, consts.MM_NETWORK_BAND_PCS, # consts.MM_NETWORK_BAND_U900, # waiting for docs consts.MM_NETWORK_BAND_U2100]) class HuaweiK3806(HuaweiWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for Huawei's Vodafone K3806 """ name = "Huawei K3806" version = "0.1" author = u"Andrew Bird" custom = HuaweiK3806Customizer() __remote_name__ = "K3806" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x14ae], } conntype = consts.WADER_CONNTYPE_USB huaweik3806 = HuaweiK3806() wader-0.5.13/plugins/devices/huawei_k4505.py000066400000000000000000000062131257646610200205040ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Author: Andrew Bird # # 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 twisted.internet import defer, reactor from twisted.internet.task import deferLater from wader.common import consts from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper, HUAWEI_BAND_DICT) class HuaweiK4505Wrapper(HuaweiWCDMAWrapper): """ :class:`~core.hardware.huawei.HuaweiWCDMAWrapper` for the K4505 """ def enable_radio(self, enable): """ Enables the radio according to ``enable`` It will not enable it if it's already enabled and viceversa """ def check_if_necessary(status): if (status == 1 and enable) or (status == 0 and not enable): return defer.succeed('OK') d = super(HuaweiK4505Wrapper, self).enable_radio(enable) d.addCallback(lambda x: deferLater(reactor, 5, lambda: x)) return d d = self.get_radio_status() d.addCallback(check_if_necessary) return d def send_ussd(self, ussd): return self._send_ussd_ucs2_mode(ussd) class HuaweiK4505Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the K4505 """ wrapper_klass = HuaweiK4505Wrapper # GSM/GPRS/EDGE 850/900/1800/1900 MHz # HSDPA/UMTS 2100/900 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, consts.MM_NETWORK_BAND_EGSM, consts.MM_NETWORK_BAND_DCS, consts.MM_NETWORK_BAND_PCS, # consts.MM_NETWORK_BAND_U900, # waiting for docs consts.MM_NETWORK_BAND_U2100]) class HuaweiK4505(HuaweiWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for Huawei's Vodafone K4505 """ name = "Huawei K4505" version = "0.1" author = u"Andrew Bird" custom = HuaweiK4505Customizer() quirks = { 'needs_enable_before_pin_check': True, } __remote_name__ = "K4505" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x1464], } conntype = consts.WADER_CONNTYPE_USB huaweik4505 = HuaweiK4505() wader-0.5.13/plugins/devices/huawei_k4510.py000066400000000000000000000044401257646610200205000ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common import consts from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper, HUAWEI_BAND_DICT) class HuaweiK4510Wrapper(HuaweiWCDMAWrapper): def send_ussd(self, ussd): return self._send_ussd_ucs2_mode(ussd) class HuaweiK4510Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the K4510 """ wrapper_klass = HuaweiK4510Wrapper # GSM/GPRS/EDGE 850/900/1800/1900 MHz # HSDPA/UMTS 900/2100 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, consts.MM_NETWORK_BAND_EGSM, consts.MM_NETWORK_BAND_DCS, consts.MM_NETWORK_BAND_PCS, # consts.MM_NETWORK_BAND_U900, consts.MM_NETWORK_BAND_U2100]) class HuaweiK4510(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's K4510""" name = "Huawei K4510" version = "0.1" author = u"Andrew Bird" custom = HuaweiK4510Customizer() __remote_name__ = "K4510" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x14cb], } conntype = consts.WADER_CONNTYPE_USB huaweik4510 = HuaweiK4510() wader-0.5.13/plugins/devices/huawei_k4511.py000066400000000000000000000046121257646610200205020ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common import consts from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper, HUAWEI_BAND_DICT) class HuaweiK4511Wrapper(HuaweiWCDMAWrapper): def send_ussd(self, ussd): return self._send_ussd_ucs2_mode(ussd) class HuaweiK4511Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the K4511 """ wrapper_klass = HuaweiK4511Wrapper # GSM/GPRS/EDGE 850/900/1800/1900 MHz # HSDPA/UMTS 850/900/1900/2100 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, consts.MM_NETWORK_BAND_EGSM, consts.MM_NETWORK_BAND_DCS, consts.MM_NETWORK_BAND_PCS, consts.MM_NETWORK_BAND_U850, # consts.MM_NETWORK_BAND_U900, consts.MM_NETWORK_BAND_U1900, consts.MM_NETWORK_BAND_U2100]) class HuaweiK4511(HuaweiWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Huawei's K4511""" name = "Huawei K4511" version = "0.1" author = u"Andrew Bird" custom = HuaweiK4511Customizer() __remote_name__ = "K4511" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x14cc], } conntype = consts.WADER_CONNTYPE_USB huaweik4511 = HuaweiK4511() wader-0.5.13/plugins/devices/huawei_k4605.py000066400000000000000000000046651257646610200205160ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common import consts from core.hardware.base import build_band_dict from core.hardware.huawei import (HuaweiWCDMADevicePlugin, HuaweiWCDMACustomizer, HuaweiWCDMAWrapper, HUAWEI_BAND_DICT) class HuaweiK4605Wrapper(HuaweiWCDMAWrapper): def send_ussd(self, ussd): return self._send_ussd_ucs2_mode(ussd) class HuaweiK4605Customizer(HuaweiWCDMACustomizer): """ :class:`~core.hardware.huawei.HuaweiWCDMACustomizer` for the K4605 """ wrapper_klass = HuaweiK4605Wrapper # GSM/GPRS/EDGE 850/900/1800/1900 MHz # HSDPA/UMTS 850/900/1900/2100 MHz band_dict = build_band_dict( HUAWEI_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, consts.MM_NETWORK_BAND_EGSM, consts.MM_NETWORK_BAND_DCS, consts.MM_NETWORK_BAND_PCS, consts.MM_NETWORK_BAND_U850, # consts.MM_NETWORK_BAND_U900, # waiting for full test consts.MM_NETWORK_BAND_U1900, consts.MM_NETWORK_BAND_U2100]) class HuaweiK4605(HuaweiWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for Huawei's Vodafone K4605 """ name = "Huawei K4605" version = "0.1" author = u"Andrew Bird" custom = HuaweiK4605Customizer() __remote_name__ = "K4605" __properties__ = { 'ID_VENDOR_ID': [0x12d1], 'ID_MODEL_ID': [0x14c6], } conntype = consts.WADER_CONNTYPE_USB huaweik4605 = HuaweiK4605() wader-0.5.13/plugins/devices/novatel_eu740.py000066400000000000000000000023711257646610200207670ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Author: Pablo Martí Gamboa # # 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 wader.common.consts import WADER_CONNTYPE_EMBEDDED from core.hardware.novatel import NovatelWCDMADevicePlugin class NovatelEU740(NovatelWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Novatel's EU740""" name = "Novatel EU740" version = "0.1" author = u"Pablo Martí" __remote_name__ = "Expedite EU740" __properties__ = { 'ID_VENDOR_ID': [0x930], 'ID_MODEL_ID': [0x1303], } conntype = WADER_CONNTYPE_EMBEDDED novateleu740 = NovatelEU740() wader-0.5.13/plugins/devices/novatel_eu870d.py000066400000000000000000000024351257646610200211400ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Author: Pablo Martí # # 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 wader.common.consts import WADER_CONNTYPE_EMBEDDED from core.hardware.novatel import NovatelWCDMADevicePlugin class NovatelEU870D(NovatelWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for Novatel's EU870D MiniCard """ name = "Novatel EU870D MiniCard" version = "0.1" author = u"Pablo Martí" __remote_name__ = "Expedite EU870D MiniCard" __properties__ = { 'ID_VENDOR_ID': [0x1410], 'ID_MODEL_ID': [0x2420], } conntype = WADER_CONNTYPE_EMBEDDED novateleu870d = NovatelEU870D() wader-0.5.13/plugins/devices/novatel_mc950d.py000066400000000000000000000052301257646610200211210ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2009 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 serial from wader.common import consts from core.hardware.novatel import (NovatelWCDMADevicePlugin, NovatelWCDMACustomizer, NOVATEL_BAND_DICT) from core.hardware.base import build_band_dict class NovatelMC950DCustomizer(NovatelWCDMACustomizer): """ :class:`~core.hardware.novatel.NovatelWCDMACustomizer` for MC950D """ # Quad-Band 850/900/1800/1900 MHz GPRS/EDGE # Tri-Band 850/1900/2100 MHz HSUPA/HSDPA/UMTS band_dict = build_band_dict( NOVATEL_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, consts.MM_NETWORK_BAND_EGSM, consts.MM_NETWORK_BAND_DCS, consts.MM_NETWORK_BAND_PCS, consts.MM_NETWORK_BAND_U850, consts.MM_NETWORK_BAND_U1900, consts.MM_NETWORK_BAND_U2100]) class NovatelMC950D(NovatelWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for Novatel's Ovation MC950D """ name = "Novatel MC950D" version = "0.1" author = u"Pablo Martí" custom = NovatelMC950DCustomizer() __remote_name__ = "Ovation MC950D Card" __properties__ = { 'ID_VENDOR_ID': [0x1410], 'ID_MODEL_ID': [0x4400], } conntype = consts.WADER_CONNTYPE_USB def preprobe_init(self, ports, info): # Novatel secondary port needs to be flipped from DM to AT mode # before it will answer our AT queries. So the primary port # needs this string first or auto detection of ctrl port fails. # Note: Early models/firmware were DM only ser = serial.Serial(ports[0], timeout=1) ser.write('AT$NWDMAT=1\r\n') ser.close() novatelmc950d = NovatelMC950D() wader-0.5.13/plugins/devices/novatel_mc990d.py000066400000000000000000000032641257646610200211320ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 serial from wader.common.consts import WADER_CONNTYPE_USB from core.hardware.novatel import NovatelWCDMADevicePlugin class NovatelMC990D(NovatelWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for Novatel MC990D """ name = "Novatel MC990D" version = "0.1" author = u"Pablo Martí" __remote_name__ = "Ovation MC990D Card" __properties__ = { 'ID_VENDOR_ID': [0x1410], 'ID_MODEL_ID': [0x7001], } conntype = WADER_CONNTYPE_USB def preprobe_init(self, ports, info): # Novatel secondary port needs to be flipped from DM to AT mode # before it will answer our AT queries. So the primary port # needs this string first or auto detection of ctrl port fails. # Note: Early models/firmware were DM only ser = serial.Serial(ports[0], timeout=1) ser.write('AT$NWDMAT=1\r\n') ser.close() wader-0.5.13/plugins/devices/novatel_mifi2352.py000066400000000000000000000036001257646610200213570ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2009 Vodafone España, S.A. # Author: Andrew Bird # # 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 serial from wader.common.consts import WADER_CONNTYPE_USB from core.hardware.novatel import NovatelWCDMADevicePlugin class NovatelMiFi2352(NovatelWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for Novatel's MiFi 2352 """ name = "Novatel MiFi2352" version = "0.1" author = u"Andrew Bird" __remote_name__ = "MiFi2352 " __properties__ = { 'ID_VENDOR_ID': [0x1410], 'ID_MODEL_ID': [0x7003], } conntype = WADER_CONNTYPE_USB def preprobe_init(self, ports, info): # This device might be found by means of the mother plugin too if info['ID_MODEL_ID'] == 0x7001: self.__properties__['ID_MODEL_ID'][0] = 0x7001 # Novatel secondary port needs to be flipped from DM to AT mode # before it will answer our AT queries. So the primary port # needs this string first or auto detection of ctrl port fails. # Note: Early models/firmware were DM only ser = serial.Serial(ports[0], timeout=1) ser.write('AT$NWDMAT=1\r\n') ser.close() novatelmifi2352 = NovatelMiFi2352() wader-0.5.13/plugins/devices/novatel_mother.py000066400000000000000000000030761257646610200214240ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2009 Vodafone España, S.A. # Author: Andrew Bird # # 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 core.hardware.novatel import NovatelWCDMADevicePlugin from plugins.novatel_mc990d import NovatelMC990D from plugins.novatel_mifi2352 import NovatelMiFi2352 class NovatelMother(NovatelWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` Novatel's Ovation/MiFi devices that share a common PID """ name = "Novatel Mother" version = "0.1" author = u"Andrew Bird" __remote_name__ = None __properties__ = { 'ID_VENDOR_ID': [0x1410], 'ID_MODEL_ID': [0x7001], } def __init__(self): super(NovatelMother, self).__init__() self.mapping = { 'Ovation MC990D Card': NovatelMC990D, 'MiFi2352 ': NovatelMiFi2352, 'default': NovatelMiFi2352, } novatelmother = NovatelMother() wader-0.5.13/plugins/devices/novatel_s720.py000066400000000000000000000023411257646610200206130ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008 Warp Networks S.L. # Author: Pablo Martí # # 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. # Plugin temporally disabled while we sort out the CDMA part from wader.common.consts import WADER_CONNTYPE_PCMCIA class NovatelS720(object): """:class:`~core.plugin.DevicePlugin` for Novatel S720""" name = "Novatel S720" version = "0.1" author = u"Pablo Martí" __remote_name__ = "MERLIN S720" __properties__ = { 'ID_VENDOR_ID': [0x1410], 'ID_MODEL_ID': [0x1130], } conntype = WADER_CONNTYPE_PCMCIA wader-0.5.13/plugins/devices/novatel_u630.py000066400000000000000000000032601257646610200206160ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2009 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 wader.common.consts import WADER_CONNTYPE_PCMCIA from core.hardware.novatel import NovatelWCDMADevicePlugin class NovatelU630(NovatelWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Novatel's U630""" name = "Novatel U630" version = "0.1" author = u"Pablo Martí" __remote_name__ = "Merlin U630 (HW REV Rev 2)" __properties__ = { 'ID_VENDOR_ID': [0x00a4], 'ID_MODEL_ID': [0x0276], } conntype = WADER_CONNTYPE_PCMCIA # Unfortunately it is not possible to switch the Second port from DM # to AT mode on the x6xx models, so we have to run with only one port # device disabled in the udev transition as we couldn't find # a way to obtain its vendor and product ids cleanly. I'll # leave it commented in case someone decides to set up and # make it work. # novatelu630 = NovatelU630() wader-0.5.13/plugins/devices/novatel_u740.py000066400000000000000000000053251257646610200206240ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2009 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Adam King - heavily based on Pablo Marti's U630 plugin # # 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 serial from wader.common.consts import WADER_CONNTYPE_PCMCIA from core.hardware.novatel import (NovatelWCDMADevicePlugin, NovatelWCDMACustomizer, NovatelWrapper) class NovatelU740Wrapper(NovatelWrapper): def find_contacts(self, pattern): """Returns a list of `Contact` whose name matches pattern""" # U740's AT+CPBF function is broken, it always raises a # CME ERROR: Not Found # We have no option but to use this little hack and emulate AT+CPBF # getting all contacts and returning those whose name match pattern # this will be slower than AT+CPBF with many contacts but at least # works d = self.list_contacts() d.addCallback(lambda contacts: [c for c in contacts if c.name.lower().startswith(pattern.lower())]) return d class NovatelU740Customizer(NovatelWCDMACustomizer): wrapper_klass = NovatelU740Wrapper class NovatelU740(NovatelWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Novatel's U740""" name = "Novatel U740" version = "0.1" author = "Adam King" custom = NovatelU740Customizer() __remote_name__ = "Merlin U740 (HW REV [0:33])" __properties__ = { 'ID_VENDOR_ID': [0x1410], 'ID_MODEL_ID': [0x1400, 0x1410], } conntype = WADER_CONNTYPE_PCMCIA def preprobe_init(self, ports, info): # Novatel secondary port needs to be flipped from DM to AT mode # before it will answer our AT queries. So the primary port # needs this string first or auto detection of ctrl port fails. # Note: Early models/firmware were DM only ser = serial.Serial(ports[0], timeout=1) ser.write('AT$NWDMAT=1\r\n') ser.close() novatelu740 = NovatelU740() wader-0.5.13/plugins/devices/novatel_x950d.py000066400000000000000000000052061257646610200207740ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2009 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 serial from wader.common import consts from core.hardware.base import build_band_dict from core.hardware.novatel import (NovatelWCDMADevicePlugin, NovatelWCDMACustomizer, NOVATEL_BAND_DICT) class NovatelX950DCustomizer(NovatelWCDMACustomizer): """ :class:`~core.hardware.novatel.NovatelWCDMACustomizer` for X950D """ # Quad-Band 850/900/1800/1900 MHz GPRS/EDGE # Tri-Band 850/1900/2100 MHz HSUPA/HSDPA/UMTS band_dict = build_band_dict( NOVATEL_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, consts.MM_NETWORK_BAND_EGSM, consts.MM_NETWORK_BAND_DCS, consts.MM_NETWORK_BAND_PCS, consts.MM_NETWORK_BAND_U850, consts.MM_NETWORK_BAND_U1900, consts.MM_NETWORK_BAND_U2100]) class NovatelX950D(NovatelWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Novatel's X950D""" name = "Novatel X950D" version = "0.1" author = u"Pablo Martí" custom = NovatelX950DCustomizer() __remote_name__ = "Merlin X950D ExpressCard" __properties__ = { 'ID_VENDOR_ID': [0x1410], 'ID_MODEL_ID': [0x1450], } conntype = consts.WADER_CONNTYPE_PCMCIA def preprobe_init(self, ports, info): # Novatel secondary port needs to be flipped from DM to AT mode # before it will answer our AT queries. So the primary port # needs this string first or auto detection of ctrl port fails. # Note: Early models/firmware were DM only ser = serial.Serial(ports[0], timeout=1) ser.write('AT$NWDMAT=1\r\n') ser.close() novatelx950d = NovatelX950D() wader-0.5.13/plugins/devices/novatel_xu870.py000066400000000000000000000057361257646610200210260ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2009 Vodafone España, S.A. # Author: Pablo Martí # # 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 serial from wader.common import consts from core.hardware.novatel import (NovatelWCDMADevicePlugin, NovatelWCDMACustomizer, NOVATEL_BAND_DICT) from core.hardware.base import build_band_dict class NovatelXU870Customizer(NovatelWCDMACustomizer): """ :class:`~core.hardware.novatel.NovatelWCDMACustomizer` for XU870 """ # Supported bands (from Novatel docs) # GSM/GPRS # GSM 850 824 -894MHz # EGSM 900 880-960MHz # DCS 1800 1710-1880MHz # PCS 1900 1850-1990MHz # WCDMA # UMTS 850 (Band V) 824 -894MHz # UMTS 1900 (Band II) 1850-1990MHz # UMTS 2100 (Band I) 1920-2170MHz band_dict = build_band_dict( NOVATEL_BAND_DICT, [consts.MM_NETWORK_BAND_ANY, consts.MM_NETWORK_BAND_G850, consts.MM_NETWORK_BAND_EGSM, consts.MM_NETWORK_BAND_DCS, consts.MM_NETWORK_BAND_PCS, consts.MM_NETWORK_BAND_U850, # XXX: Novatel docs show UMTS 1900 (Band II) # but consts.py has this as UMTS 1900 Class IX consts.MM_NETWORK_BAND_U1900, consts.MM_NETWORK_BAND_U2100]) class NovatelXU870(NovatelWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Novatel's XU870""" name = "Novatel XU870" version = "0.1" author = u"Pablo Martí" custom = NovatelXU870Customizer() __remote_name__ = "Merlin XU870 ExpressCard" __properties__ = { 'ID_VENDOR_ID': [0x1410], 'ID_MODEL_ID': [0x1430], } conntype = consts.WADER_CONNTYPE_PCMCIA def preprobe_init(self, ports, info): # Novatel secondary port needs to be flipped from DM to AT mode # before it will answer our AT queries. So the primary port # needs this string first or auto detection of ctrl port fails. # Note: Early models/firmware were DM only ser = serial.Serial(ports[0], timeout=1) ser.write('AT$NWDMAT=1\r\n') ser.close() novatelxu870 = NovatelXU870() wader-0.5.13/plugins/devices/onda_msa405hs.py000066400000000000000000000024631257646610200207420ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2010 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.zte import ZTEWCDMADevicePlugin class ONDAMSA405HS(ZTEWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ONDA's MSA405HS""" name = "ONDA MSA405HS" version = "0.1" author = "Igor Moura" __remote_name__ = "MSA405" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x0037], } conntype = WADER_CONNTYPE_USB onda_msa405hs = ONDAMSA405HS() wader-0.5.13/plugins/devices/onda_mt503hs.py000066400000000000000000000024221257646610200205740ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.zte import ZTEWCDMADevicePlugin class ONDAMT503HS(ZTEWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ONDA's MT503HS""" name = "ONDA MT503HS" version = "0.1" author = "Andrew Bird" __remote_name__ = "MT503HS" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x0002], } conntype = WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/option_colt.py000066400000000000000000000067421257646610200207320ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """ DevicePlugin for Option Colt (end of life reached) """ from epsilon.modal import mode from twisted.python import log from wader.common.consts import WADER_CONNTYPE_PCMCIA from core.hardware.option import (OptionWCDMADevicePlugin, OptionWCDMACustomizer) from core.sim import SIMBaseClass from core.statem.auth import AuthStateMachine class OptionColtAuthStateMachine(AuthStateMachine): """ Custom AuthStateMachine for Option Colt This device has a rather buggy firmware that yields all sort of weird errors. For example, if PIN authentication is disabled on the SIM and you issue an AT+CPIN? command, it will reply with a +CPIN: SIM PUK2 """ pin_needed_status = AuthStateMachine.pin_needed_status puk_needed_status = AuthStateMachine.puk_needed_status puk2_needed_status = AuthStateMachine.puk2_needed_status class get_pin_status(mode): """ Returns the authentication status The SIM can be in one of the following states: - SIM is ready (already authenticated, or PIN disabled) - PIN is needed - PIN2 is needed (not handled) - PUK is needed - PUK2 is needed - SIM is not inserted - SIM's firmware error """ def __enter__(self): pass def __exit__(self): pass def do_next(self): log.msg("Instantiating get_pin_status mode....") d = self.device.sconn.get_pin_status() d.addCallback(self.get_pin_status_cb) d.addErrback(self.sim_failure_eb) d.addErrback(self.sim_busy_eb) d.addErrback(self.sim_no_present_eb) d.addErrback(log.err) class OptionColtSIMClass(SIMBaseClass): """Option Colt SIM Class""" def __init__(self, sconn): super(OptionColtSIMClass, self).__init__(sconn) def initialize(self, set_encoding=False): self.charset = 'IRA' d = super(OptionColtSIMClass, self).initialize(set_encoding) d.addCallback(self.set_size) return d class OptionColtCustomizer(OptionWCDMACustomizer): """:class:`~core.hardware.Customizer` for Option Colt""" auth_klass = OptionColtAuthStateMachine class OptionColt(OptionWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Option Colt""" name = "Option Colt" version = "0.1" author = u"Pablo Martí" custom = OptionColtCustomizer() sim_klass = OptionColtSIMClass __remote_name__ = "129" __properties__ = { 'ID_VENDOR_ID': [0x0af0], 'ID_MODEL_ID': [0x5000], } conntype = WADER_CONNTYPE_PCMCIA optioncolt = OptionColt() wader-0.5.13/plugins/devices/option_e3730.py000066400000000000000000000024511257646610200205230ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2009 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_PCMCIA from core.hardware.option import OptionHSOWCDMADevicePlugin class OptionE3730(OptionHSOWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for Option's E3730 """ name = "Option E3730" version = "0.1" author = "Andrew Bird" __remote_name__ = 'GlobeTrotter HSUPA Modem' __properties__ = { 'ID_VENDOR_ID': [0x0af0], 'ID_MODEL_ID': [0x7301], } conntype = WADER_CONNTYPE_PCMCIA optione3730 = OptionE3730() wader-0.5.13/plugins/devices/option_etna.py000066400000000000000000000045541257646610200207170ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 wader.common.consts import WADER_CONNTYPE_PCMCIA from core.hardware.option import (OptionWCDMADevicePlugin, OptionWCDMACustomizer, OptionWrapper) class OptionEtnaWrapper(OptionWrapper): def find_contacts(self, pattern): """Returns a list of `Contact` whose name matches pattern""" # ETNA's AT+CPBF function is broken, it always raises a # CME ERROR: Not Found (at least with the following firmware rev: # FW 2.8.0Hd (Date: Oct 11 2007, Time: 10:20:29)) # we have no option but to use this little hack and emulate AT+CPBF # getting all contacts and returning those whose name match pattern # this will be slower than AT+CPBF with many contacts but at least # works d = self.list_contacts() d.addCallback(lambda contacts: [c for c in contacts if c.name.lower().startswith(pattern.lower())]) return d class OptionEtnaCustomizer(OptionWCDMACustomizer): wrapper_klass = OptionEtnaWrapper class OptionEtna(OptionWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Options's Etna""" name = "Option Etna" version = "0.1" author = u"Pablo Martí" custom = OptionEtnaCustomizer() __remote_name__ = "GlobeTrotter HSUPA Modem" __properties__ = { 'ID_VENDOR_ID': [0x0af0], 'ID_MODEL_ID': [0x7001], } conntype = WADER_CONNTYPE_PCMCIA optionetna = OptionEtna() wader-0.5.13/plugins/devices/option_etna_ndis.py000066400000000000000000000046761257646610200217410ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2010 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird, inspired by Pablo Martí # # 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 wader.common.consts import WADER_CONNTYPE_PCMCIA from core.hardware.option import (OptionHSOWCDMADevicePlugin, OptionHSOWCDMACustomizer, OptionHSOWrapper) class OptionEtnaNDISWrapper(OptionHSOWrapper): def find_contacts(self, pattern): """Returns a list of `Contact` whose name matches pattern""" # ETNA's AT+CPBF function is broken, it always raises a # CME ERROR: Not Found (at least with the following firmware rev: # FW 2.8.0Hd (Date: Oct 11 2007, Time: 10:20:29)) # we have no option but to use this little hack and emulate AT+CPBF # getting all contacts and returning those whose name match pattern # this will be slower than AT+CPBF with many contacts but at least # works d = self.list_contacts() d.addCallback(lambda contacts: [c for c in contacts if c.name.lower().startswith(pattern.lower())]) return d class OptionEtnaNDISCustomizer(OptionHSOWCDMACustomizer): wrapper_klass = OptionEtnaNDISWrapper class OptionEtnaNDIS(OptionHSOWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Options's Etna(NDIS)""" name = "Option Etna(NDIS)" version = "0.1" author = u"Andrew Bird" custom = OptionEtnaNDISCustomizer() __remote_name__ = "GlobeTrotter HSUPA Modem" __properties__ = { 'ID_VENDOR_ID': [0x0af0], 'ID_MODEL_ID': [0x7011], } conntype = WADER_CONNTYPE_PCMCIA optionetnandis = OptionEtnaNDIS() wader-0.5.13/plugins/devices/option_gi0335.py000066400000000000000000000035251257646610200206770ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2010 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.option import (OptionHSOWCDMADevicePlugin, OptionHSOWCDMACustomizer, OptionHSOWrapper) class OptionGI0335Wrapper(OptionHSOWrapper): def find_contacts(self, pattern): d = self.list_contacts() d.addCallback(lambda contacts: [c for c in contacts if c.name.lower().startswith(pattern.lower())]) return d class OptionGI0335Customizer(OptionHSOWCDMACustomizer): wrapper_klass = OptionGI0335Wrapper class OptionGI0335(OptionHSOWCDMADevicePlugin): """:class:`core.plugin.DevicePlugin` for Options's GI0335""" name = "Option GI0335" version = "0.1" author = u"Andrew Bird" custom = OptionGI0335Customizer() __remote_name__ = "GlobeTrotter HSPA Modem" __properties__ = { 'ID_VENDOR_ID': [0xaf0], 'ID_MODEL_ID': [0x8300], } conntype = WADER_CONNTYPE_USB option_gi0335 = OptionGI0335() wader-0.5.13/plugins/devices/option_globesurfericon.py000066400000000000000000000026121257646610200231510ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Simone Tolotti # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.option import OptionWCDMADevicePlugin class OptionGlobesurferIcon(OptionWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for Options's Globesurfer Icon """ name = "Option Globesurfer Icon" version = "0.1" author = "Simone Tolotti" __remote_name__ = "GlobeSurfer ICON" __properties__ = { 'ID_VENDOR_ID': [0x0af0], 'ID_MODEL_ID': [0x6600], } conntype = WADER_CONNTYPE_USB optionglobesurfericon = OptionGlobesurferIcon() wader-0.5.13/plugins/devices/option_gtfusion.py000066400000000000000000000026711257646610200216240ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. # Stefano Rivera contributed this info through email on 20 Sept 2007 from wader.common.consts import WADER_CONNTYPE_PCMCIA from core.hardware.option import OptionWCDMADevicePlugin class OptionGTFusion(OptionWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for Option's GT Fusion """ name = "Option GlobeTrotter Fusion" version = "0.1" author = "Stefano Rivera" __remote_name__ = 'GlobeTrotter Fusion' __properties__ = { 'ID_VENDOR_ID': [0x0af0], 'ID_MODEL_ID': [0x6000], } conntype = WADER_CONNTYPE_PCMCIA optiongtfusion = OptionGTFusion() wader-0.5.13/plugins/devices/option_gtfusionquadlite.py000066400000000000000000000026411257646610200233520ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Stefano Rivera # # 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 wader.common.consts import WADER_CONNTYPE_PCMCIA from core.hardware.option import OptionWCDMADevicePlugin class OptionGTFusionQuadLite(OptionWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for Option's GT Fusion Quad Lite """ name = "Option GT Fusion Quad Lite" version = "0.1" author = "Stefano Rivera" __remote_name__ = 'GlobeTrotter Fusion Quad Lite' __properties__ = { 'ID_VENDOR_ID': [0x0af0], 'ID_MODEL_ID': [0x6300], } conntype = WADER_CONNTYPE_PCMCIA optiongtfusionquadlite = OptionGTFusionQuadLite() wader-0.5.13/plugins/devices/option_gtm378.py000066400000000000000000000037301257646610200210140ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2012 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Jaime Soriano # # 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. # Ulf Michel contributed this info: # http://forge.vodafonebetavine.net/forum/message.php?msg_id=630 # # OptionGTM378 integrated in Fuijitsu-Siemens Esprimo Mobile U Series from wader.common.consts import WADER_CONNTYPE_EMBEDDED from core.hardware.option import (OptionWCDMADevicePlugin, OptionHSOWCDMADevicePlugin) class OptionGTM378(OptionWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for Option's GTM378 """ name = "Option GT M378" version = "0.1" author = "Ulf Michel" __remote_name__ = 'GTM378' __properties__ = { 'ID_VENDOR_ID': [0x0af0], 'ID_MODEL_ID': [0x6901], } conntype = WADER_CONNTYPE_EMBEDDED optiongtm378 = OptionGTM378() class OptionHSOGTM378(OptionHSOWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for Option's GTM378 """ name = "Option GT M378" version = "0.1" author = "Ulf Michel" __remote_name__ = 'GTM378' __properties__ = { 'ID_VENDOR_ID': [0x0af0], 'ID_MODEL_ID': [0x6911], } conntype = WADER_CONNTYPE_EMBEDDED optionhsogtm378 = OptionHSOGTM378() wader-0.5.13/plugins/devices/option_gtm380.py000066400000000000000000000025401257646610200210030ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2012 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_EMBEDDED from core.hardware.option import OptionHSOWCDMADevicePlugin class OptionHSOGTM380(OptionHSOWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for Option's GTM380 """ name = "Option GT M380" version = "0.1" author = "Andrew Bird" __remote_name__ = 'GTMxxx' __properties__ = { 'ID_VENDOR_ID': [0x0af0], 'ID_MODEL_ID': [0x7211], } conntype = WADER_CONNTYPE_EMBEDDED optionhsogtm380 = OptionHSOGTM380() wader-0.5.13/plugins/devices/option_gtmax36.py000066400000000000000000000025211257646610200212510ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: kgb0y # # 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 wader.common.consts import WADER_CONNTYPE_PCMCIA from core.hardware.option import OptionWCDMADevicePlugin class OptionGTMax36(OptionWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Option's GT Max 36""" name = "Option GT Max 36" version = "0.1" author = "kgb0y" __remote_name__ = "GlobeTrotter HSDPA Modem" __properties__ = { 'ID_VENDOR_ID': [0x0af0], 'ID_MODEL_ID': [0x6701], } conntype = WADER_CONNTYPE_PCMCIA optiongtmax36 = OptionGTMax36() wader-0.5.13/plugins/devices/option_icon225.py000066400000000000000000000024651257646610200211500ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Isaac Clerencia # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.option import OptionHSOWCDMADevicePlugin class OptionIcon225(OptionHSOWCDMADevicePlugin): """:class:`core.plugin.DevicePlugin` for Options's Icon 225""" name = "Option Icon 225" version = "0.1" author = "Isaac Clerencia" __remote_name__ = "GlobeTrotter HSDPA Modem" __properties__ = { 'ID_VENDOR_ID': [0x0af0], 'ID_MODEL_ID': [0x6971], } conntype = WADER_CONNTYPE_USB optionicon225 = OptionIcon225() wader-0.5.13/plugins/devices/option_icon401.py000066400000000000000000000024551257646610200211430ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Marti # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.option import OptionHSOWCDMADevicePlugin class OptionIcon401(OptionHSOWCDMADevicePlugin): """:class:`core.plugin.DevicePlugin` for Options's Icon 401""" name = "Option Icon 401" version = "0.1" author = "Pablo Marti" __remote_name__ = "GlobeTrotter HSUPA Modem" __properties__ = { 'ID_VENDOR_ID': [0x0af0], 'ID_MODEL_ID': [0x7401], } conntype = WADER_CONNTYPE_USB optionicon401 = OptionIcon401() wader-0.5.13/plugins/devices/option_k3760.py000066400000000000000000000041301257646610200205300ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 wader.common.consts import MM_NETWORK_BAND_U2100, WADER_CONNTYPE_USB from core.hardware.option import (OPTION_BAND_DICT, OptionHSOWCDMADevicePlugin, OptionHSOWCDMACustomizer, OptionHSOWrapper) class OptionK3760Wrapper(OptionHSOWrapper): def find_contacts(self, pattern): d = self.list_contacts() d.addCallback(lambda contacts: [c for c in contacts if c.name.lower().startswith(pattern.lower())]) return d class OptionK3760Customizer(OptionHSOWCDMACustomizer): wrapper_klass = OptionK3760Wrapper band_dict = OPTION_BAND_DICT.copy() band_dict.pop(MM_NETWORK_BAND_U2100) # VF firmware seems to support this # but has trouble unsetting it class OptionK3760(OptionHSOWCDMADevicePlugin): """:class:`core.plugin.DevicePlugin` for Options's K3760""" name = "Option K3760" version = "0.1" author = u"Pablo Martí" custom = OptionK3760Customizer() __remote_name__ = "GlobeTrotter HSUPA Modem" __properties__ = { 'ID_VENDOR_ID': [0xaf0], 'ID_MODEL_ID': [0x7501, 0x7601], } conntype = WADER_CONNTYPE_USB option_k3760 = OptionK3760() wader-0.5.13/plugins/devices/option_nozomi.py000066400000000000000000000030341257646610200212730ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 wader.common.consts import WADER_CONNTYPE_PCMCIA from core.hardware.option import OptionWCDMADevicePlugin class OptionNozomi(OptionWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Option's Nozomi""" name = "Option GlobeTrotter 3G+" version = "0.1" author = u"Pablo Martí" __remote_name__ = "GlobeTrotter 3G+" __properties__ = { 'ID_VENDOR_ID': [0x1931], 'ID_MODEL_ID': [0xc], } conntype = WADER_CONNTYPE_PCMCIA # device disabled in the udev transition as we couldn't find # a way to obtain its vendor and product ids cleanly. I'll # leave it commented in case someone decides to set up and # make it work. # option_nozomi = OptionNozomi() wader-0.5.13/plugins/devices/qualcomm_gobi2000.py000066400000000000000000000027051257646610200215140ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2010 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_EMBEDDED from core.hardware.qualcomm import QualcommWCDMADevicePlugin class QualcommWCDMAG2000(QualcommWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for Qualcomm's Gobi 2000""" name = "Qualcomm Gobi 2000" version = "0.1" author = u"Andrew Bird" __remote_name__ = "Qualcomm Gobi 2000" __properties__ = { 'ID_VENDOR_ID': [0x05c6], 'ID_MODEL_ID': [0x9205, 0x920b, 0x9215, 0x9225, 0x9235, 0x9245, 0x9265, 0x9275], } conntype = WADER_CONNTYPE_EMBEDDED qualcommwcdmag2000 = QualcommWCDMAG2000() wader-0.5.13/plugins/devices/sierrawireless_850.py000066400000000000000000000026211257646610200220300ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """DevicePlugin for the Sierra Wireless 850 datacard""" from wader.common.consts import WADER_CONNTYPE_PCMCIA from core.hardware.sierra import SierraWCDMADevicePlugin class SierraWireless850(SierraWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for SierraWireless 850""" name = "SierraWireless 850" version = "0.1" author = u"Pablo Martí" __remote_name__ = "AC850" __properties__ = { 'ID_VENDOR_ID': [0x192], 'ID_MODEL_ID': [0x710], } conntype = WADER_CONNTYPE_PCMCIA sierrawireless850 = SierraWireless850() wader-0.5.13/plugins/devices/sierrawireless_875.py000066400000000000000000000026231257646610200220410ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """DevicePlugin for the Sierra Wireless 875 datacard""" from wader.common.consts import WADER_CONNTYPE_PCMCIA from core.hardware.sierra import SierraWCDMADevicePlugin class SierraWireless875(SierraWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for SierraWireless 875""" name = "SierraWireless 875" version = "0.1" author = "anmsid, kgb0y" __remote_name__ = "AC875" __properties__ = { 'ID_VENDOR_ID': [0x1199], 'ID_MODEL_ID': [0x6820], } conntype = WADER_CONNTYPE_PCMCIA sierrawireless875 = SierraWireless875() wader-0.5.13/plugins/devices/sierrawireless_mc8755.py000066400000000000000000000026071257646610200224500ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2009 Vodafone España, S.A. # Author: Andrew Bird # # 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. """ DevicePlugin for the HP branded Sierra Wireless MC8755 embedded device """ from wader.common.consts import WADER_CONNTYPE_EMBEDDED from core.hardware.sierra import SierraWCDMADevicePlugin class SierraWirelessMC8755(SierraWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for SierraWireless MC 8755""" name = "SierraWireless MC8755" version = "0.1" author = "John J Doe" __remote_name__ = "MC8755" __properties__ = { 'ID_VENDOR_ID': [0x03f0], 'ID_MODEL_ID': [0x1e1d], } conntype = WADER_CONNTYPE_EMBEDDED sierrawirelessmc8755 = SierraWirelessMC8755() wader-0.5.13/plugins/devices/sonyericsson_k610i.py000066400000000000000000000036121257646610200220420ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.sonyericsson import SonyEricssonCustomizer from core.plugin import DevicePlugin from core.middleware import WCDMAWrapper class K610iWrapper(WCDMAWrapper): def set_charset(self, charset): if charset == 'UCS2': d = super(K610iWrapper, self).set_charset('IRA') else: d = super(K610iWrapper, self).set_charset(charset) d.addCallback(lambda ignord: self.device.sim.set_charset(charset)) return d class SonyEricssonK610iCustomizer(SonyEricssonCustomizer): wrapper_klass = K610iWrapper class SonyEricssonK610iUSB(DevicePlugin): """:class:`~core.plugin.DevicePlugin` for Sony Ericsson k610i""" name = "Sony Ericsson K610i" version = "0.1" author = u"Jaime Soriano" custom = SonyEricssonK610iCustomizer() __remote_name__ = "AAD-3022041-BV" __properties__ = { 'ID_VENDOR_ID': [0x0fce], 'ID_MODEL_ID': [0xd046], } conntype = WADER_CONNTYPE_USB sonyericsson_k610iUSB = SonyEricssonK610iUSB() wader-0.5.13/plugins/devices/sonyericsson_k618i.py000066400000000000000000000025471257646610200220600ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 wader.common.consts import WADER_CONNTYPE_UNKNOWN from core.hardware.sonyericsson import SonyEricssonCustomizer from core.plugin import RemoteDevicePlugin class SonyEricssonK618i(RemoteDevicePlugin): """ :class:`~core.plugin.RemoteDevicePlugin` for SonyEricsson's K618i """ name = "SonyEricsson K618i" version = "0.1" author = u"Pablo Martí" custom = SonyEricssonCustomizer __remote_name__ = "AAD-3022042-BV" conntype = WADER_CONNTYPE_UNKNOWN sonyericsson_k618i = SonyEricssonK618i() wader-0.5.13/plugins/devices/toshiba_f3607gw.py000066400000000000000000000026341257646610200212110ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2010 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_EMBEDDED from core.hardware.ericsson import (EricssonDevicePlugin, EricssonF3607gwCustomizer) class ToshibaF3607gw(EricssonDevicePlugin): """:class:`~core.plugin.DevicePlugin` for Ericsson's Toshiba F3607gw""" name = "Toshiba F3607gw" version = "0.1" author = u"Andrew Bird" custom = EricssonF3607gwCustomizer() __remote_name__ = "F3607gw" __properties__ = { 'ID_VENDOR_ID': [0x0930], 'ID_MODEL_ID': [0x130c, 0x1311], } conntype = WADER_CONNTYPE_EMBEDDED toshibaF3607gw = ToshibaF3607gw() wader-0.5.13/plugins/devices/zte_k2525.py000066400000000000000000000215621257646610200200300ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2012 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 re from twisted.internet import defer, reactor from twisted.internet.task import deferLater from wader.common import consts import wader.common.aterrors as E from wader.common.encoding import (unpack_ucs2_bytes, pack_ucs2_bytes, check_if_ucs2) from wader.common.exceptions import LimitedServiceNetworkError from core.command import build_cmd_dict from core.middleware import WCDMAWrapper, NetworkOperator from core.hardware.zte import (ZTEWCDMADevicePlugin, ZTEWCDMACustomizer, ZTEWrapper, ZTE_CMD_DICT) from core.sim import SIMBaseClass ZTEK2525_ALLOWED_DICT = { consts.MM_ALLOWED_MODE_ANY: None, consts.MM_ALLOWED_MODE_2G_ONLY: None, } ZTEK2525_CONN_DICT = { consts.MM_NETWORK_MODE_ANY: None, consts.MM_NETWORK_MODE_2G_ONLY: None, } ZTEK2525_CMD_DICT = ZTE_CMD_DICT.copy() ZTEK2525_CMD_DICT['get_network_info'] = build_cmd_dict(re.compile(r""" \r\n \+COPS:\s+ ( (?P\d) | \d,\d, # or followed by num,num,str (fixed bearer) "(?P[\w\S ]*)" ) # end of group \r\n """, re.VERBOSE)) ZTEK2525_CMD_DICT['get_network_mode'] = build_cmd_dict(re.compile(r""" \r\n \+ZSNT= (?P\d+), (?P\d+), (?P\d+) \r\n """, re.VERBOSE)) ZTEK2525_CMD_DICT['get_network_names'] = build_cmd_dict(re.compile(r""" \( (?P\d+), "(?P[^"]*)", "(?P[^"]*)", "(?P\d+)" \),? """, re.VERBOSE), end=re.compile('\r\n\r\nOK\r\n')) ZTEK2525_CMD_DICT['get_sms'] = build_cmd_dict(re.compile(r""" \r\n \+CMGR:\s (?P\d), (?P.*), \d+\r\n (?P\w+) \r\n """, re.VERBOSE)) ZTEK2525_CMD_DICT['list_sms'] = build_cmd_dict(re.compile(r""" \r\n \+CMGL:\s (?P\d+), (?P\d), (?P.*), \d+\r\n (?P\w+) """, re.VERBOSE)) class ZTEK2525Wrapper(ZTEWrapper): def enable_radio(self, enable): # It's really difficult to bring device back from +cfun=0, so let's # just turn the radio off instead d = self.get_radio_status() def get_radio_status_cb(status): if status in [0, 4] and enable: d = self.send_at('AT+CFUN=1') def cb(arg): # delay here 5 secs return deferLater(reactor, 5, lambda: arg) d.addCallback(cb) return d elif status == 1 and not enable: return self.send_at('AT+CFUN=4') d.addCallback(get_radio_status_cb) return d def get_network_info(self, _type=None): """ Returns the network info (a.k.a AT+COPS?) The response will be a tuple as (OperatorName, ConnectionType) if it returns a (None, None) that means that some error occurred while obtaining the info. The class that requested the info should take care of insisting before this problem. This method will convert numeric network IDs to alphanumeric. """ d = super(WCDMAWrapper, self).get_network_info(_type) def get_net_info_cb(netinfo): """ Returns a (Networkname, ConnType) tuple It returns None if there's no info """ if not netinfo: return None netinfo = netinfo[0] if netinfo.group('error'): # this means that we've received a response like # +COPS: 0 which means that we don't have network temporaly # we should raise an exception here raise E.NoNetwork() conn_type = consts.MM_GSM_ACCESS_TECH_GPRS netname = netinfo.group('netname') if netname in ['Limited Service', pack_ucs2_bytes('Limited Service')]: raise LimitedServiceNetworkError() # netname can be in UCS2, as a string, or as a network id (int) if check_if_ucs2(netname): return unpack_ucs2_bytes(netname), conn_type else: # now can be either a string or a network id (int) try: netname = int(netname) except ValueError: # we got a string ID return netname, conn_type # if we have arrived here, that means that the network id # is a five digit integer return str(netname), conn_type d.addCallback(get_net_info_cb) return d def get_network_names(self): """ Performs a network search :rtype: list of :class:`NetworkOperator` """ d = super(WCDMAWrapper, self).get_network_names() def get_network_names_cb(resp): # K2525 is GPRS/EDGE only, so won't provide bearer type in results return [NetworkOperator(*match.groups() + (0,)) for match in resp] d.addCallback(get_network_names_cb) return d def get_network_mode(self): """Returns the current network mode preference""" return defer.succeed(consts.MM_NETWORK_MODE_2G_ONLY) def set_allowed_mode(self, mode): """Sets the allowed mode to ``mode``""" if mode in self.custom.allowed_dict: self.device.set_property(consts.NET_INTFACE, "AllowedMode", mode) return defer.succeed("OK") else: raise KeyError("Mode %s not found" % mode) def set_network_mode(self, mode): """Sets the network mode to ``mode``""" if mode in self.custom.conn_dict: return defer.succeed("OK") else: raise KeyError("Unsupported mode %d" % mode) def set_smsc(self, smsc): """Sets the SIM's SMSC number to ``smsc``""" # K2525 never requires UCS2 d = super(WCDMAWrapper, self).set_smsc(smsc) d.addCallback(lambda response: response[0].group('resp')) return d class ZTEK2525SIMClass(SIMBaseClass): """SIM class for ZTE K2525 devices""" def __init__(self, sconn): super(ZTEK2525SIMClass, self).__init__(sconn) def setup_sms(self): # Select SIM storage self.sconn.send_at('AT+CPMS="SM","SM","SM"') # Notification when a SMS arrives... self.sconn.set_sms_indication(1, 1, 0, 1, 0) # set PDU mode self.sconn.set_sms_format(0) class ZTEK2525Customizer(ZTEWCDMACustomizer): allowed_dict = ZTEK2525_ALLOWED_DICT band_dict = {} conn_dict = ZTEK2525_CONN_DICT cmd_dict = ZTEK2525_CMD_DICT wrapper_klass = ZTEK2525Wrapper class ZTEK2525(ZTEWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's K2525""" name = "Vodafone K2525" version = "0.1" author = "Andrew Bird" sim_klass = ZTEK2525SIMClass custom = ZTEK2525Customizer() __remote_name__ = "K2525" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x0022], } conntype = consts.WADER_CONNTYPE_USB zte_k2525 = ZTEK2525() wader-0.5.13/plugins/devices/zte_k3520.py000066400000000000000000000025051257646610200200200ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2010 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.zte import ZTEWCDMADevicePlugin class ZTEK3520(ZTEWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's K3520""" name = "Vodafone K3520-Z" version = "0.1" author = "Andrew Bird" __remote_name__ = "K3520-Z" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x0025, 0x0055], # depends on fw rev } conntype = WADER_CONNTYPE_USB zte_k3520 = ZTEK3520() wader-0.5.13/plugins/devices/zte_k3565.py000066400000000000000000000025251257646610200200330ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2009 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.zte import ZTEWCDMADevicePlugin class ZTEK3565(ZTEWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for ZTE's K3565-Z """ name = "ZTE K3565-Z" version = "0.1" author = "Andrew Bird" __remote_name__ = "K3565-Z" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x0049, 0x0052, 0x0063], # depends on fw ver } conntype = WADER_CONNTYPE_USB zte_k3565 = ZTEK3565() wader-0.5.13/plugins/devices/zte_k3570.py000066400000000000000000000037771257646610200200410ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.zte import (ZTEWCDMADevicePlugin, ZTEWCDMACustomizer, ZTEWrapper) class ZTEK3570Wrapper(ZTEWrapper): def send_ussd(self, ussd): """Sends the ussd command ``ussd``""" # K3570-Z / K3571-Z want requests in ASCII chars even though the # current character set might be UCS2. Some versions of firmware # reply in UCS2 or ASCII at different times, so we need a loose check return super(ZTEK3570Wrapper, self).send_ussd(ussd, force_ascii=True, loose_charset_check=True) class ZTEK3570Customizer(ZTEWCDMACustomizer): wrapper_klass = ZTEK3570Wrapper class ZTEK3570(ZTEWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's K3570-Z""" name = "Vodafone K3570-Z" version = "0.1" author = "Andrew Bird" custom = ZTEK3570Customizer() __remote_name__ = "K3570-Z" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x1008], } conntype = WADER_CONNTYPE_USB zte_k3570 = ZTEK3570() wader-0.5.13/plugins/devices/zte_k3571.py000066400000000000000000000037771257646610200200420ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.zte import (ZTEWCDMADevicePlugin, ZTEWCDMACustomizer, ZTEWrapper) class ZTEK3571Wrapper(ZTEWrapper): def send_ussd(self, ussd): """Sends the ussd command ``ussd``""" # K3570-Z / K3571-Z want requests in ASCII chars even though the # current character set might be UCS2. Some versions of firmware # reply in UCS2 or ASCII at different times, so we need a loose check return super(ZTEK3571Wrapper, self).send_ussd(ussd, force_ascii=True, loose_charset_check=True) class ZTEK3571Customizer(ZTEWCDMACustomizer): wrapper_klass = ZTEK3571Wrapper class ZTEK3571(ZTEWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's K3571-Z""" name = "Vodafone K3571-Z" version = "0.1" author = "Andrew Bird" custom = ZTEK3571Customizer() __remote_name__ = "K3571-Z" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x1010], } conntype = WADER_CONNTYPE_USB zte_k3571 = ZTEK3571() wader-0.5.13/plugins/devices/zte_k3765.py000066400000000000000000000051331257646610200200330ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2010 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 twisted.internet import reactor from twisted.internet.task import deferLater from wader.common.consts import WADER_CONNTYPE_USB from core.hardware.zte import (ZTEWCDMADevicePlugin, ZTEWCDMACustomizer, ZTEWrapper) class ZTEK3765Wrapper(ZTEWrapper): def check_pin(self): """ Returns the SIM's auth state :raise SimPinRequired: Raised if SIM PIN is required :raise SimPukRequired: Raised if SIM PUK is required :raise SimPuk2Required: Raised if SIM PUK2 is required """ # XXX: this device needs to be enabled before pin can be checked d = self.get_radio_status() def get_radio_status_cb(status): if status != 1: self.send_at('AT+CFUN=1') # delay here 2 secs return deferLater(reactor, 2, lambda: None) d.addCallback(get_radio_status_cb) d.addCallback(lambda x: super(ZTEK3765Wrapper, self).check_pin()) return d def send_ussd(self, ussd): """Sends the ussd command ``ussd``""" # K3765-Z wants request in ascii chars even though current # set might be ucs2 return super(ZTEK3765Wrapper, self).send_ussd(ussd, force_ascii=True) class ZTEK3765Customizer(ZTEWCDMACustomizer): wrapper_klass = ZTEK3765Wrapper class ZTEK3765(ZTEWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's K3765-Z""" name = "Vodafone K3765-Z" version = "0.1" author = "Andrew Bird" custom = ZTEK3765Customizer() __remote_name__ = "K3765-Z" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x2002], } conntype = WADER_CONNTYPE_USB zte_k3765 = ZTEK3765() wader-0.5.13/plugins/devices/zte_k3770.py000066400000000000000000000025731257646610200200340ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2007-2011 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 wader.common.consts import MM_IP_METHOD_STATIC, WADER_CONNTYPE_USB from core.hardware.icera import IceraWCDMADevicePlugin class ZTEK3770(IceraWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's Vodafone K3770""" name = "ZTE K3770" version = "0.1" author = "Andrew Bird" __remote_name__ = "K3770-Z" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x1177], } conntype = WADER_CONNTYPE_USB dialer = 'hso' ipmethod = MM_IP_METHOD_STATIC zte_k3770 = ZTEK3770() wader-0.5.13/plugins/devices/zte_k3772.py000066400000000000000000000025731257646610200200360ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2007-2011 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 wader.common.consts import MM_IP_METHOD_STATIC, WADER_CONNTYPE_USB from core.hardware.icera import IceraWCDMADevicePlugin class ZTEK3772(IceraWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's Vodafone K3772""" name = "ZTE K3772" version = "0.1" author = "Andrew Bird" __remote_name__ = "K3772-Z" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x1181], } conntype = WADER_CONNTYPE_USB dialer = 'hso' ipmethod = MM_IP_METHOD_STATIC zte_k3772 = ZTEK3772() wader-0.5.13/plugins/devices/zte_k3805.py000066400000000000000000000025731257646610200200330ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2007-2010 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 wader.common.consts import MM_IP_METHOD_STATIC, WADER_CONNTYPE_USB from core.hardware.icera import IceraWCDMADevicePlugin class ZTEK3805(IceraWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's Vodafone K3805""" name = "ZTE K3805" version = "0.1" author = "Andrew Bird" __remote_name__ = "K3805-z" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x1003], } conntype = WADER_CONNTYPE_USB dialer = 'hso' ipmethod = MM_IP_METHOD_STATIC zte_k3805 = ZTEK3805() wader-0.5.13/plugins/devices/zte_k3806.py000066400000000000000000000025731257646610200200340ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2007-2010 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 wader.common.consts import MM_IP_METHOD_STATIC, WADER_CONNTYPE_USB from core.hardware.icera import IceraWCDMADevicePlugin class ZTEK3806(IceraWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's Vodafone K3806""" name = "ZTE K3806" version = "0.1" author = "Andrew Bird" __remote_name__ = "K3806-z" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x1015], } conntype = WADER_CONNTYPE_USB dialer = 'hso' ipmethod = MM_IP_METHOD_STATIC zte_k3806 = ZTEK3806() wader-0.5.13/plugins/devices/zte_k4505.py000066400000000000000000000034501257646610200200240ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Author: Andrew Bird, inspired by Pablo Martí # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.zte import (ZTEWCDMADevicePlugin, ZTEWCDMACustomizer, ZTEWrapper) class ZTEK4505Wrapper(ZTEWrapper): def send_ussd(self, ussd): """Sends the ussd command ``ussd``""" # K4505-Z wants request in ascii chars even though current # set might be ucs2 return super(ZTEK4505Wrapper, self).send_ussd(ussd, force_ascii=True) class ZTEK4505Customizer(ZTEWCDMACustomizer): wrapper_klass = ZTEK4505Wrapper class ZTEK4505(ZTEWCDMADevicePlugin): """ :class:`~core.plugin.DevicePlugin` for ZTE's K4505-Z """ name = "ZTE K4505-Z" version = "0.1" author = "Andrew Bird" custom = ZTEK4505Customizer() __remote_name__ = "K4505-Z" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x0016, 0x0104], } conntype = WADER_CONNTYPE_USB zte_k4505 = ZTEK4505() wader-0.5.13/plugins/devices/zte_k4510.py000066400000000000000000000025731257646610200200250ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2007-2011 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 wader.common.consts import MM_IP_METHOD_STATIC, WADER_CONNTYPE_USB from core.hardware.icera import IceraWCDMADevicePlugin class ZTEK4510(IceraWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's Vodafone K4510""" name = "ZTE K4510" version = "0.1" author = "Andrew Bird" __remote_name__ = "K4510-Z" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x1173], } conntype = WADER_CONNTYPE_USB dialer = 'hso' ipmethod = MM_IP_METHOD_STATIC zte_k4510 = ZTEK4510() wader-0.5.13/plugins/devices/zte_mf180.py000066400000000000000000000034421257646610200201100ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011-2012 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.zte import (ZTEWCDMADevicePlugin, ZTEWCDMACustomizer, ZTEWrapper) class ZTEMF180Wrapper(ZTEWrapper): def send_ussd(self, ussd): """Sends the ussd command ``ussd``""" # XXX: assumes it's the same as 637U # MF180 wants request in ascii chars even though current # set might be ucs2 return super(ZTEMF180Wrapper, self).send_ussd(ussd, force_ascii=True) class ZTEMF180Customizer(ZTEWCDMACustomizer): wrapper_klass = ZTEMF180Wrapper class ZTEMF180(ZTEWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's MF180""" name = "ZTE MF180" version = "0.1" author = u"Andrew Bird" custom = ZTEMF180Customizer() __remote_name__ = "MF180" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x2003], } conntype = WADER_CONNTYPE_USB zte_mf180 = ZTEMF180() wader-0.5.13/plugins/devices/zte_mf181.py000066400000000000000000000034421257646610200201110ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011-2012 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.zte import (ZTEWCDMADevicePlugin, ZTEWCDMACustomizer, ZTEWrapper) class ZTEMF181Wrapper(ZTEWrapper): def send_ussd(self, ussd): """Sends the ussd command ``ussd``""" # XXX: assumes it's the same as 637U # MF181 wants request in ascii chars even though current # set might be ucs2 return super(ZTEMF181Wrapper, self).send_ussd(ussd, force_ascii=True) class ZTEMF181Customizer(ZTEWCDMACustomizer): wrapper_klass = ZTEMF181Wrapper class ZTEMF181(ZTEWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's MF181""" name = "ZTE MF181" version = "0.1" author = u"Andrew Bird" custom = ZTEMF181Customizer() __remote_name__ = "MF181" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x0117], } conntype = WADER_CONNTYPE_USB zte_mf181 = ZTEMF181() wader-0.5.13/plugins/devices/zte_mf190.py000066400000000000000000000034521257646610200201120ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011-2012 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.zte import (ZTEWCDMADevicePlugin, ZTEWCDMACustomizer, ZTEWrapper) class ZTEMF190Wrapper(ZTEWrapper): def send_ussd(self, ussd): """Sends the ussd command ``ussd``""" # XXX: assumes it's the same as 637U # MF190 wants request in ascii chars even though current # set might be ucs2 return super(ZTEMF190Wrapper, self).send_ussd(ussd, force_ascii=True) class ZTEMF190Customizer(ZTEWCDMACustomizer): wrapper_klass = ZTEMF190Wrapper class ZTEMF190(ZTEWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's MF190""" name = "ZTE MF190" version = "0.1" author = u"Andrew Bird" custom = ZTEMF190Customizer() __remote_name__ = "MF190" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x0082, 0x0124], } conntype = WADER_CONNTYPE_USB zte_mf190 = ZTEMF190() wader-0.5.13/plugins/devices/zte_mf620.py000066400000000000000000000024031257646610200201030ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2007 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Zhao Ming # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.zte import ZTEWCDMADevicePlugin class ZTEMF620(ZTEWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's MF620""" name = "ZTE MF620" version = "0.1" author = "Zhao Ming" __remote_name__ = "MF620" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x0001], } conntype = WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/zte_mf626.py000066400000000000000000000034121257646610200201120ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011-2012 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.zte import (ZTEWCDMADevicePlugin, ZTEWCDMACustomizer, ZTEWrapper) class ZTEMF626Wrapper(ZTEWrapper): def send_ussd(self, ussd): """Sends the ussd command ``ussd``""" # XXX: assumes it's the same as 637U # MF626 wants request in ascii chars even though current # set might be ucs2 return super(ZTEMF626Wrapper, self).send_ussd(ussd, force_ascii=True) class ZTEMF626Customizer(ZTEWCDMACustomizer): wrapper_klass = ZTEMF626Wrapper class ZTEMF626(ZTEWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's MF626""" name = "ZTE MF626" version = "0.1" author = u"Andrew Bird" custom = ZTEMF626Customizer() __remote_name__ = "MF626" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x0031], } conntype = WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/zte_mf628.py000066400000000000000000000024421257646610200201160ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.zte import ZTEWCDMADevicePlugin class ZTEMF628(ZTEWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's MF628""" name = "ZTE MF628" version = "0.1" author = "Andrew Bird" __remote_name__ = "MF628" # guessed, please confirm __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x0002], } conntype = WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/zte_mf632.py000066400000000000000000000023271257646610200201130ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks S.L. # Author: Pablo Martí # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.zte import ZTEWCDMADevicePlugin class ZTEMF632(ZTEWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's MF632""" name = "ZTE MF632" version = "0.1" author = u"Pablo Martí" __remote_name__ = "MF632" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x0002], } conntype = WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/zte_mf636.py000066400000000000000000000034121257646610200201130ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011-2012 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.zte import (ZTEWCDMADevicePlugin, ZTEWCDMACustomizer, ZTEWrapper) class ZTEMF636Wrapper(ZTEWrapper): def send_ussd(self, ussd): """Sends the ussd command ``ussd``""" # XXX: assumes it's the same as 637U # MF636 wants request in ascii chars even though current # set might be ucs2 return super(ZTEMF636Wrapper, self).send_ussd(ussd, force_ascii=True) class ZTEMF636Customizer(ZTEWCDMACustomizer): wrapper_klass = ZTEMF636Wrapper class ZTEMF636(ZTEWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's MF636""" name = "ZTE MF636" version = "0.1" author = u"Andrew Bird" custom = ZTEMF636Customizer() __remote_name__ = "MF636" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x0031], } conntype = WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/zte_mf637u.py000066400000000000000000000033471257646610200203100ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011 Vodafone España, S.A. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.zte import (ZTEWCDMADevicePlugin, ZTEWCDMACustomizer, ZTEWrapper) class ZTEMF637UWrapper(ZTEWrapper): def send_ussd(self, ussd): """Sends the ussd command ``ussd``""" # MF637U wants request in ascii chars even though current # set might be ucs2 return super(ZTEMF637UWrapper, self).send_ussd(ussd, force_ascii=True) class ZTEMF637UCustomizer(ZTEWCDMACustomizer): wrapper_klass = ZTEMF637UWrapper class ZTEMF637U(ZTEWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's MF637U""" name = "ZTE MF637U" version = "0.1" author = u"Andrew Bird" custom = ZTEMF637UCustomizer() __remote_name__ = "MF637U" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x0031], } conntype = WADER_CONNTYPE_USB wader-0.5.13/plugins/devices/zte_mf651.py000066400000000000000000000024641257646610200201160ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2007-2009 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.icera import IceraWCDMADevicePlugin class ZTEMF651(IceraWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's MF651""" name = "ZTE MF651" version = "0.1" author = "Andrew Bird" __remote_name__ = "HSDPA mobile station" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x0116], } conntype = WADER_CONNTYPE_USB zte_mf651 = ZTEMF651() wader-0.5.13/plugins/devices/zte_mf668.py000066400000000000000000000024461257646610200201260ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2007-2012 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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 wader.common.consts import WADER_CONNTYPE_USB from core.hardware.icera import IceraWCDMADevicePlugin class ZTEMF668(IceraWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's MF668""" name = "ZTE MF668" version = "0.1" author = "Andrew Bird" __remote_name__ = "MF668A" __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x0017], } conntype = WADER_CONNTYPE_USB zte_mf668 = ZTEMF668() wader-0.5.13/plugins/devices/zte_mf6xx.py000066400000000000000000000041211257646610200203200ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Copyright (C) 2011 Vodafone España, S.A. # Author: Pablo Martí # # 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 core.hardware.zte import ZTEWCDMADevicePlugin from plugins.zte_mf620 import ZTEMF620 from plugins.zte_mf632 import ZTEMF632 from plugins.zte_mf628 import ZTEMF628 from plugins.onda_mt503hs import ONDAMT503HS class ZTEMF6XX(ZTEWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's MF6XX Family""" name = "ZTE MF6XX" version = "0.1" author = u"Pablo Martí" __remote_name__ = None __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x0001], } def __init__(self): super(ZTEMF6XX, self).__init__() self.mapping = { 'MF620': ZTEMF620, 'MF632': ZTEMF632, 'default': ZTEMF620, } zte_mf6xx = ZTEMF6XX() class ZTEMF628X(ZTEWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's MF628 Family""" name = "ZTE MF628X" version = "0.1" author = u"Andrew Bird" __remote_name__ = None __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x0002], } def __init__(self): super(ZTEMF628X, self).__init__() self.mapping = { 'MF628': ZTEMF628, 'MT503HS': ONDAMT503HS, 'default': ZTEMF628, } zte_mf628x = ZTEMF628X() wader-0.5.13/plugins/devices/zte_mother_0031.py000066400000000000000000000033071257646610200212160ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011-2012 Vodafone España, S.A. # Author: Andrew Bird # # 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 core.hardware.zte import ZTEWCDMADevicePlugin from plugins.zte_mf626 import ZTEMF626 from plugins.zte_mf636 import ZTEMF636 from plugins.zte_mf637u import ZTEMF637U class ZTEMother0031(ZTEWCDMADevicePlugin): """:class:`~core.plugin.DevicePlugin` for ZTE's PID 0x031 Family""" name = "ZTE Mother 0x0031" version = "0.1" author = u"Andrew Bird" __remote_name__ = None __properties__ = { 'ID_VENDOR_ID': [0x19d2], 'ID_MODEL_ID': [0x0031], } def __init__(self): super(ZTEMother0031, self).__init__() self.mapping = { 'MF620': ZTEMF637U, # Just MF637U now but usb-modeswitch-data # suggests that M110 / MF112 are also # candidates 'MF626': ZTEMF626, 'MF636': ZTEMF636, 'default': ZTEMF637U, } zte_mother0031 = ZTEMother0031() wader-0.5.13/plugins/oses/000077500000000000000000000000001257646610200153455ustar00rootroot00000000000000wader-0.5.13/plugins/oses/__init__.py000066400000000000000000000000001257646610200174440ustar00rootroot00000000000000wader-0.5.13/plugins/oses/debian.py000066400000000000000000000024451257646610200171460ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """ Debian OSPlugin """ from os.path import exists from core.oses.linux import LinuxPlugin class DebianBasedDistro(LinuxPlugin): def is_valid(self): if exists('/etc/debian_version'): # do not recognize Ubuntu as a DebianBasedDistro (is true tho) return not exists('/etc/lsb-release') return False def get_additional_wvdial_ppp_options(self): return "replacedefaultroute\n" debian = DebianBasedDistro() wader-0.5.13/plugins/oses/fedora.py000066400000000000000000000027351257646610200171660ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Fedora OSPlugin""" from os.path import exists import re from twisted.internet.utils import getProcessValue from core.oses.linux import LinuxPlugin from wader.common.utils import get_file_data class FedoraBasedDistro(LinuxPlugin): """ OSPlugin for Fedora-based distros """ def is_valid(self): paths = ['/etc/redhat-release', '/etc/fedora-release'] return any(map(exists, paths)) def update_dns_cache(self): if exists("/usr/sbin/nscd"): getProcessValue("/etc/init.d/nscd", ["condrestart"]) getProcessValue("/etc/init.d/nscd", ["-i", "hosts"]) fedora = FedoraBasedDistro() wader-0.5.13/plugins/oses/freebsd.py000066400000000000000000000020051257646610200173260ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """ FreeBSD-based OS plugin """ from core.oses.bsd import FreeBSDPlugin class FreeBSD7Plugin(FreeBSDPlugin): """Plugin for FreeBSD 7""" freebsd7plugin = FreeBSD7Plugin() wader-0.5.13/plugins/oses/mint.py000066400000000000000000000040311257646610200166640ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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. """Linux Mint OSPlugin""" import re from os.path import exists from twisted.internet.utils import getProcessValue from core.oses.linux import LinuxPlugin from wader.common.utils import get_file_data VERS_REGEX = '^DISTRIB_RELEASE=\s*(?P\d+)\s*$' class MintBasedDistro(LinuxPlugin): """A plugin to be used on Linux Mint systems""" def is_valid(self): if not exists('/etc/lsb-release'): return False lsb = get_file_data('/etc/lsb-release') if 'LinuxMint' in lsb: match = re.search(VERS_REGEX, lsb, re.MULTILINE) if match: vers = match.group('version') try: self.version = int(vers) except ValueError: pass return True return False def update_dns_cache(self): if exists("/usr/sbin/nscd"): return getProcessValue("/usr/sbin/nscd", ["-i", "hosts"]) def get_additional_wvdial_ppp_options(self): options = "replacedefaultroute\n" #if hasattr(self, 'version'): # if self.version <= 11: # options += "usepeerdns\n" return options mint = MintBasedDistro() wader-0.5.13/plugins/oses/osx.py000066400000000000000000000015611257646610200165330ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """ OSX plugin """ from core.oses.osx import OSXPlugin osxplugin = OSXPlugin() wader-0.5.13/plugins/oses/suse.py000066400000000000000000000025661257646610200167070ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """SLED OSPlugin""" from os.path import exists import re from twisted.internet.utils import getProcessValue from core.oses.linux import LinuxPlugin from wader.common.utils import get_file_data class SUSEDistro(LinuxPlugin): def is_valid(self): return exists('/etc/SuSE-release') def update_dns_cache(self): if exists("/usr/sbin/nscd"): getProcessValue("/usr/sbin/nscd", ["-i", "hosts"]) def get_additional_wvdial_ppp_options(self): return "replacedefaultroute\n" susedistro = SUSEDistro() wader-0.5.13/plugins/oses/ubuntu.py000066400000000000000000000040521257646610200172420ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Ubuntu OSPlugin""" import re from os.path import exists from twisted.internet.utils import getProcessValue from core.oses.linux import LinuxPlugin from wader.common.utils import get_file_data VERS_REGEX = '^DISTRIB_RELEASE=\s*(?P\d+\.\d+)\s*$' class UbuntuBasedDistro(LinuxPlugin): """A plugin to be used on Ubuntu systems""" def is_valid(self): if not exists('/etc/lsb-release'): return False lsb = get_file_data('/etc/lsb-release') if 'Ubuntu' in lsb: match = re.search(VERS_REGEX, lsb, re.MULTILINE) if match: vers = match.group('version').replace('.','') try: self.version = int(vers) except ValueError: pass return True return False def update_dns_cache(self): if exists("/usr/sbin/nscd"): return getProcessValue("/usr/sbin/nscd", ["-i", "hosts"]) def get_additional_wvdial_ppp_options(self): options = "replacedefaultroute\n" #if hasattr(self, 'version'): # if self.version <= 904: # options += "usepeerdns\n" return options ubuntu = UbuntuBasedDistro() wader-0.5.13/resources/000077500000000000000000000000001257646610200147255ustar00rootroot00000000000000wader-0.5.13/resources/dbus/000077500000000000000000000000001257646610200156625ustar00rootroot00000000000000wader-0.5.13/resources/dbus/org.freedesktop.ModemManager.conf000066400000000000000000000031371257646610200241710ustar00rootroot00000000000000 512 wader-0.5.13/resources/dbus/org.freedesktop.ModemManager.service000066400000000000000000000001411257646610200246740ustar00rootroot00000000000000[D-BUS Service] Name=org.freedesktop.ModemManager Exec=/usr/bin/wader-core-ctl --start User=root wader-0.5.13/resources/extra/000077500000000000000000000000001257646610200160505ustar00rootroot00000000000000wader-0.5.13/resources/extra/__init__.py000066400000000000000000000000001257646610200201470ustar00rootroot00000000000000wader-0.5.13/resources/extra/networks.py000066400000000000000000000663441257646610200203130ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2012 Vodafone Group Services Limited # Author: Andrew Bird # # 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. """ Automatically generated from VMB Opco_Settings_10.3.200.36472_RC1 file """ class NetworkOperator(object): netid = [] name = None dname = None country = None type = None apn = None auth = 'ANY' username = None password = None dns1 = None dns2 = None smsc = None mmsc = None wap1 = None wap2 = None wap_apn = None wap_username = None wap_password = None wap_auth = 'ANY' def __repr__(self): args = (self.name, self.country, self.netid[0]) return "" % args class Vodafone_20205_Contract(NetworkOperator): netid = ["20205"] name = "Vodafone Greece" country = "Greece" type = "Contract" smsc = "+306942190000" apn = "internet" auth = "PAP" class Vodafone_20205_Prepaid(NetworkOperator): netid = ["20205"] name = "Vodafone Greece" country = "Greece" type = "Prepaid" smsc = "+306942190000" apn = "web.session" auth = "PAP" class Vodafone_20404_Contract(NetworkOperator): netid = ["20404"] name = "Vodafone NL" country = "Netherlands" type = "Contract" smsc = "+316540881000" apn = "office.vodafone.nl" auth = "CHAP" username = "vodafone" password = "vodafone" class Vodafone_20404_Prepaid(NetworkOperator): netid = ["20404"] name = "Vodafone NL" country = "Netherlands" type = "Prepaid" smsc = "+316540881000" apn = "office.vodafone.nl" auth = "CHAP" username = "vodafone" password = "vodafone" class Vodafone_20601_Contract(NetworkOperator): netid = ["20601"] name = "Proximus" country = "Belgium" type = "Contract" smsc = "+32475161616" apn = "internet.proximus.be" auth = "PAP" class Vodafone_20601_Prepaid(NetworkOperator): netid = ["20601"] name = "Proximus" country = "Belgium" type = "Prepaid" smsc = "+32475161616" apn = "internet.proximus.be" auth = "PAP" class Vodafone_20810_Contract(NetworkOperator): netid = ["20810"] name = "SFR" country = "France" type = "Contract" smsc = "+33609001390" apn = "websfr" auth = "PAP" class Vodafone_20810_SFR_slsfr(NetworkOperator): netid = ["20810"] name = "SFR" dname = "SFR slsfr" country = "France" type = "Contract" smsc = "+33609001390" apn = "slsfr" auth = "PAP" class Vodafone_20810_SFR_internetpro(NetworkOperator): netid = ["20810"] name = "SFR" dname = "SFR internetpro" country = "France" type = "Contract" smsc = "+33609001390" apn = "internetpro" class Vodafone_20810_SFR_ipnet(NetworkOperator): netid = ["20810"] name = "SFR" dname = "SFR ipnet" country = "France" type = "Contract" smsc = "+33609001390" apn = "ipnet" class Vodafone_23801_Contract(NetworkOperator): netid = ["23801"] name = "TDC Denmark" country = "Denmark" type = "Contract" smsc = "+4540390999" apn = "internet" class Vodafone_2380171_Contract(NetworkOperator): netid = ["2380171"] name = "TDC Norway" country = "Norway" type = "Contract" smsc = "+4540390966" apn = "internet.no" class Vodafone_2380172_Contract(NetworkOperator): netid = ["2380172"] name = "TDC Sweden" country = "Sweden" type = "Contract" smsc = "+4540390955" apn = "internet.se" class Vodafone_24802_Contract(NetworkOperator): netid = ["24802"] name = "Elisa Estonia" country = "Estonia" type = "Contract" smsc = "+37256100020" apn = "internet" dns1 = "194.204.0.1" class Vodafone_27801_Contract(NetworkOperator): netid = ["27801"] name = "Vodafone Malta" country = "Malta" type = "Contract" smsc = "+356941816" apn = "internet" auth = "PAP" username = "internet" password = "internet" dns1 = "80.85.96.131" dns2 = "80.85.97.70" class Vodafone_50503_Contract(NetworkOperator): netid = ["50503"] name = "Vodafone Australia" country = "Australia" type = "Contract" smsc = "+61415011501" apn = "vfinternet.au" class Vodafone_50503_Prepaid(NetworkOperator): netid = ["50503"] name = "Vodafone Australia" country = "Australia" type = "Prepaid" smsc = "+61415011501" apn = "vfprepaymbb" username = "web" password = "web" class Vodafone_22210_Contract(NetworkOperator): netid = ["22210"] name = "vodafone IT" country = "Italy" type = "Contract" smsc = "+393492000200" apn = "web.omnitel.it" auth = "CHAP" class Vodafone_23415_Contract(NetworkOperator): netid = ["23415"] name = "Vodafone UK" country = "United Kingdom" type = "Contract" smsc = "+447785016005" apn = "internet" auth = "PAP" username = "web" password = "web" mmsc = "http://mms.vodafone.co.uk/servlets/mms" wap_apn = "wap.vodafone.co.uk" wap_username = "wap" wap_password = "wap" wap2 = "212.183.137.12:8799" class Vodafone_23415_Prepaid(NetworkOperator): netid = ["23415"] name = "Vodafone UK" country = "United Kingdom" type = "Prepaid" smsc = "+447785016005" apn = "SMART" auth = "PAP" username = "web" password = "web" mmsc = "http://mms.vodafone.co.uk/servlets/mms" wap_apn = "pp.vodafone.co.uk" wap_username = "wap" wap_password = "wap" wap2 = "212.183.137.12:8799" class Vodafone_23415_Prepaid_Segment_3(NetworkOperator): netid = ["23415"] name = "Vodafone UK" country = "United Kingdom" type = "Prepaid" smsc = "+447785016005" apn = "PPBUNDLE.INTERNET" auth = "PAP" username = "web" password = "web" class Vodafone_23415_Prepaid_Segment_4(NetworkOperator): netid = ["23415"] name = "Vodafone UK" country = "United Kingdom" type = "Prepaid" smsc = "+447785016005" apn = "PP.INTERNET" auth = "PAP" username = "web" password = "web" class Vodafone_23415_Prepaid_Segment_5(NetworkOperator): netid = ["23415"] name = "Vodafone UK" country = "United Kingdom" type = "Prepaid" smsc = "+447785016005" apn = "PP.VODAFONE.CO.UK" auth = "PAP" username = "web" password = "web" class Vodafone_23415_Prepaid_Segment_6(NetworkOperator): netid = ["23415"] name = "Vodafone UK" country = "United Kingdom" type = "Prepaid" smsc = "+447785016005" apn = "PP.VODAFONE.CO.UK" auth = "PAP" username = "web" password = "web" class Vodafone_26202_Contract(NetworkOperator): netid = ["26202"] name = "Vodafone.de" country = "Germany" type = "Contract" smsc = "+491722270333" apn = "web.vodafone.de" auth = "CHAP" dns1 = "139.7.30.125" dns2 = "139.7.30.126" class Vodafone_26202_WebSession(NetworkOperator): netid = ["26202"] name = "Vodafone.de" country = "Germany" type = "WebSession" smsc = "+491722270333" apn = "event.vodafone.de" auth = "CHAP" class Vodafone_26801_Contract(NetworkOperator): netid = ["26801"] name = "vodafone P" country = "Portugal" type = "Contract" smsc = "+351911616161" apn = "internet.vodafone.pt" auth = "CHAP" username = "vodafone" password = "vodafone" class Vodafone_27201_Contract(NetworkOperator): netid = ["27201"] name = "Vodafone Ireland" country = "Ireland" type = "Contract" smsc = "+35387699989" apn = "hs.vodafone.ie" username = "vodafone" password = "vodafone" class Vodafone_27201_Prepaid(NetworkOperator): netid = ["27201"] name = "Vodafone Ireland" country = "Ireland" type = "Prepaid" smsc = "+35387699989" apn = "hs.vodafone.ie" username = "vodafone" password = "vodafone" class Vodafone_21401_Contract(NetworkOperator): netid = ["21401"] name = "vodafone ES" country = "Spain" type = "Contract" smsc = "+34607003110" apn = "ac.vodafone.es" auth = "PAP" username = "vodafone" password = "vodafone" class Vodafone_21401_Prepaid(NetworkOperator): netid = ["21401"] name = "vodafone ES" country = "Spain" type = "Prepaid" smsc = "+34607003110" apn = "ac.vodafone.es" auth = "PAP" username = "vodafone" password = "vodafone" class Vodafone_21670_Contract(NetworkOperator): netid = ["21670"] name = "Vodafone Hungary" country = "Hungary" type = "Contract" smsc = "+36709996500" apn = "internet.vodafone.net" class Vodafone_21670_Prepaid(NetworkOperator): netid = ["21670"] name = "Vodafone Hungary" country = "Hungary" type = "Prepaid" smsc = "+36709996500" apn = "vitamax.internet.vodafone.net" class Vodafone_23201_Contract(NetworkOperator): netid = ["23201"] name = "A1" country = "Austria" type = "Contract" smsc = "+436640501" apn = "A1.net" auth = "CHAP" username = "ppp@A1plus.at" password = "ppp" class Vodafone_23201_Prepaid(NetworkOperator): netid = ["23201"] name = "A1" country = "Austria" type = "Prepaid" smsc = "+436640501" apn = "PPBUNDLE.INTERNET" auth = "PAP" username = "web" password = "web" class Vodafone_65501_Prepaid(NetworkOperator): netid = ["65501"] name = "Vodacom" country = "South Africa" type = "Prepaid" smsc = "+27829129" apn = "internet" class Vodafone_27402_Contract(NetworkOperator): netid = ["27402"] name = "Vodafone Iceland" country = "Iceland" type = "Contract" smsc = "+3546999099" apn = "vmc.gprs.is" class Vodafone_45406_Contract(NetworkOperator): netid = ["45406"] name = "SmarTone-Vodafone" country = "Hong Kong" type = "Contract" smsc = "+85290100000" apn = "Internet" class Vodafone_42602_Contract(NetworkOperator): netid = ["42602"] name = "Zain BH" country = "Bahrain" type = "Contract" smsc = "+97336135135" apn = "internet" username = "internet" password = "internet" class Vodafone_21910_Contract(NetworkOperator): netid = ["21910"] name = "Vipnet" country = "Croatia" type = "Contract" smsc = "+385910401" apn = "data.vip.hr" auth = "PAP" class Vodafone_24405_Contract(NetworkOperator): netid = ["24405"] name = "Elisa" country = "Finland" type = "Contract" smsc = "+358508771010" apn = "internet" class Vodafone_29340_Contract(NetworkOperator): netid = ["29340"] name = "Si.mobil" country = "Slovenia" type = "Contract" smsc = "+38640441000" apn = "internet.simobil.si" auth = "CHAP" username = "simobil" password = "internet" class Vodafone_29340_Prepaid(NetworkOperator): netid = ["29340"] name = "Si.mobil" country = "Slovenia" type = "Prepaid" smsc = "+38640441000" apn = "internet.simobil.si" auth = "CHAP" username = "simobil" password = "internet" class Vodafone_53001_Contract(NetworkOperator): netid = ["53001"] name = "Vodafone NZ" country = "New Zealand" type = "Contract" smsc = "+6421600600" apn = "www.vodafone.net.nz" auth = "PAP" class Vodafone_53001_Prepaid(NetworkOperator): netid = ["53001"] name = "Vodafone NZ" country = "New Zealand" type = "Prepaid" smsc = "+6421600600" apn = "www.vodafone.net.nz" auth = "PAP" class Vodafone_60202_Contract(NetworkOperator): netid = ["60202"] name = "Vodafone Egypt" country = "Egypt" type = "Contract" smsc = "+20105996500" apn = "internet.vodafone.net" auth = "PAP" username = "internet" password = "internet" dns1 = "163.121.128.134" dns2 = "212.103.160.18" class Vodafone_60202_Prepaid(NetworkOperator): netid = ["60202"] name = "Vodafone Egypt" country = "Egypt" type = "Prepaid" smsc = "+20105996500" apn = "internet.vodafone.net" auth = "PAP" username = "internet" password = "internet" dns1 = "163.121.128.134" dns2 = "212.103.160.18" class Vodafone_54201_Contract(NetworkOperator): netid = ["54201"] name = "Vodafone Fiji" country = "Fiji" type = "Contract" smsc = "+679901400" apn = "vfinternet.fj" class Vodafone_54201_Prepaid(NetworkOperator): netid = ["54201"] name = "Vodafone Fiji" country = "Fiji" type = "Prepaid" smsc = "+679901400" apn = "prepay.vfinternet.fj" class Vodafone_5420171_Contract(NetworkOperator): netid = ["5420171"] name = "Kidanet" country = "Fiji" type = "Contract" smsc = "+679901400" apn = "kidanet.fj" class Vodafone_28001_Contract(NetworkOperator): netid = ["28001"] name = "Cytamobile-Vodafone" country = "Cyprus" type = "Contract" smsc = "+35799700000" apn = "internet" auth = "CHAP" class Vodafone_28001_Prepaid(NetworkOperator): netid = ["28001"] name = "Cytamobile-Vodafone" country = "Cyprus" type = "Prepaid" smsc = "+35799700000" apn = "pp.internet" auth = "CHAP" class Vodafone_22601_Contract(NetworkOperator): netid = ["22601"] name = "Vodafone RO" country = "Romania" type = "Contract" smsc = "+40722004000" apn = "internet.vodafone.ro" auth = "PAP" username = "internet.vodafone.ro" password = "vodafone" class Vodafone_22601_Prepaid(NetworkOperator): netid = ["22601"] name = "Vodafone RO" country = "Romania" type = "Prepaid" smsc = "+40722004000" apn = "tobe.vodafone.ro" auth = "PAP" username = "tobe.vodafone.ro" password = "vodafone" class Vodafone_52503_Contract(NetworkOperator): netid = ["52503"] name = "M1" country = "Singapore" type = "Contract" smsc = "+6596845999" apn = "sunsurf" auth = "PAP" class Vodafone_52503_Prepaid(NetworkOperator): netid = ["52503"] name = "M1" country = "Singapore" type = "Prepaid" smsc = "+6596845999" apn = "prepaidbb" auth = "PAP" class Vodafone_27602_Contract(NetworkOperator): netid = ["27602"] name = "Vodafone Albania" country = "Albania" type = "Contract" smsc = "+355692000200" apn = "vodafoneweb" class Vodafone_23003_Contract(NetworkOperator): netid = ["23003"] name = "Vodafone CZ" country = "Czech Republic" type = "Contract" smsc = "+420608005681" apn = "internet" class Vodafone_28401_Contract(NetworkOperator): netid = ["28401"] name = "M-Tel BG" country = "Bulgaria" type = "Contract" smsc = "+35988000301" apn = "inet-gprs.mtel.bg" class Vodafone_24705_Contract(NetworkOperator): netid = ["24705"] name = "Bite Latvija" country = "Latvia" type = "Contract" smsc = "+37125850115" apn = "Internet" class Vodafone_24602_Contract(NetworkOperator): netid = ["24602"] name = "Bite Lietuva" country = "Lithuania" type = "Contract" smsc = "+37069950115" apn = "banga" class Vodafone_50213_Contract(NetworkOperator): netid = ["50213"] name = "Celcom Malaysia" country = "Malaysia" type = "Contract" smsc = "+60193900000" apn = "celcom3g" class Vodafone_50219_Contract(NetworkOperator): netid = ["50219"] name = "Celcom Malaysia" country = "Malaysia" type = "Contract" smsc = "+60193900000" apn = "celcom3g" class Vodafone_41302_Contract(NetworkOperator): netid = ["41302"] name = "DIALOG" country = "Sri Lanka" type = "Contract" smsc = "+9477000003" apn = "Dialogbb" auth = "CHAP" class Vodafone_41302_Prepaid(NetworkOperator): netid = ["41302"] name = "DIALOG" country = "Sri Lanka" type = "Prepaid" smsc = "+9477000003" apn = "kitbb.com" auth = "CHAP" class Vodafone_22801_Contract(NetworkOperator): netid = ["22801"] name = "Swisscom" country = "Switzerland" type = "Contract" smsc = "+417949990000" apn = "gprs.swisscom.ch" auth = "PAP" class Vodafone_28602_Faturali(NetworkOperator): netid = ["28602"] name = "Vodafone TR" dname = "Faturali" country = "Turkey" type = "Contract" smsc = "+905429800033" apn = "internet" username = "vodafone" password = "vodafone" class Vodafone_28602_Kontorlu(NetworkOperator): netid = ["28602"] name = "Vodafone TR" dname = "Kontorlu" country = "Turkey" type = "Prepaid" smsc = "+905429800033" apn = "internet" username = "vodafone" password = "vodafone" class Vodafone_2860251_Faturali(NetworkOperator): netid = ["2860251"] name = "VF KKTC Telsim" dname = "Faturali" country = "KKTC" type = "Contract" smsc = "+905429800033" apn = "edge.kktctelsim.com" class Vodafone_2860251_Kontorlu(NetworkOperator): netid = ["2860251"] name = "VF KKTC Telsim" dname = "Kontorlu" country = "KKTC" type = "Prepaid" smsc = "+905429800033" apn = "edge.kktctelsim.com" class Vodafone_23403_Contract(NetworkOperator): netid = ["23403"] name = "Airtel-Vodafone" country = "Jersey" type = "Contract" smsc = "+447829791004" apn = "airtel-ci-gprs.com" class Vodafone_73001_Contract(NetworkOperator): netid = ["73001"] name = "Entel PCS" country = "Chile" type = "Contract" smsc = "+5698890005" apn = "imovil.entelpcs.cl" username = "entelpcs" password = "entelpcs" class Vodafone_62002_Contract(NetworkOperator): netid = ["62002"] name = "Vodafone Ghana" country = "Ghana" type = "Contract" smsc = "+233200000007" apn = "browse" auth = "PAP" class Vodafone_62002_Prepaid(NetworkOperator): netid = ["62002"] name = "Vodafone Ghana" country = "Ghana" type = "Prepaid" smsc = "+233200000007" apn = "browse" auth = "PAP" class Vodafone_65101_Contract(NetworkOperator): netid = ["65101"] name = "Vodacom Lesotho" country = "Lesotho" type = "Contract" smsc = "+26655820088" apn = "internet" class Vodafone_65101_Prepaid(NetworkOperator): netid = ["65101"] name = "Vodacom Lesotho" country = "Lesotho" type = "Prepaid" smsc = "+26655820088" apn = "internet" class Vodafone_28802_Contract(NetworkOperator): netid = ["28802"] name = "Vodafone FO" country = "Faroe Islands" type = "Contract" smsc = "+298501440" apn = "vmc.vodafone.fo" class Vodafone_42702_Contract(NetworkOperator): netid = ["42702"] name = "Vodafone Qatar" country = "Qatar" type = "Contract" smsc = "+9747922222" apn = "web.vodafone.com.qa" class Vodafone_64004_Contract(NetworkOperator): netid = ["64004"] name = "Vodacom Tanzania" country = "Tanzania" type = "Contract" smsc = "+25575114" apn = "internet" auth = "CHAP" class Vodafone_64004_Prepaid(NetworkOperator): netid = ["64004"] name = "Vodacom Tanzania" country = "Tanzania" type = "Prepaid" smsc = "+25575114" apn = "internet" auth = "MSCHAP" class Vodafone_40401_Contract(NetworkOperator): netid = ["40401"] name = "Vodafone India Haryana" country = "India" type = "Contract" smsc = "+919839099999" apn = "www" class Vodafone_40405_Contract(NetworkOperator): netid = ["40405"] name = "Vodafone India Gujarat" country = "India" type = "Contract" smsc = "+919825001002" apn = "www" class Vodafone_40411_Contract(NetworkOperator): netid = ["40411"] name = "Vodafone India Delhi" country = "India" type = "Contract" smsc = "+919811009998" apn = "www" class Vodafone_40413_Contract(NetworkOperator): netid = ["40413"] name = "Vodafone India Andhra Pradesh" country = "India" type = "Contract" smsc = "+919885005444" apn = "www" class Vodafone_40415_Contract(NetworkOperator): netid = ["40415"] name = "Vodafone India UP East" country = "India" type = "Contract" smsc = "+919839099999" apn = "www" class Vodafone_40420_Contract(NetworkOperator): netid = ["40420"] name = "Vodafone India Mumbai" country = "India" type = "Contract" smsc = "+919820005444" apn = "www" class Vodafone_40427_Contract(NetworkOperator): netid = ["40427"] name = "Vodafone India Maharashtra and Goa" country = "India" type = "Contract" smsc = "+919823000040" apn = "www" class Vodafone_40430_Contract(NetworkOperator): netid = ["40430"] name = "Vodafone India Kolkata" country = "India" type = "Contract" smsc = "+919830099990" apn = "www" class Vodafone_40443_Contract(NetworkOperator): netid = ["40443"] name = "Vodafone India Tamilnadu" country = "India" type = "Contract" smsc = "+919843000040" apn = "www" class Vodafone_40446_Contract(NetworkOperator): netid = ["40446"] name = "Vodafone India Kerala" country = "India" type = "Contract" smsc = "+919846000040" apn = "www" class Vodafone_40460_Contract(NetworkOperator): netid = ["40460"] name = "Vodafone India Rajasthan" country = "India" type = "Contract" smsc = "+919839099999" apn = "www" class Vodafone_40484_Contract(NetworkOperator): netid = ["40484"] name = "Vodafone India Chennai" country = "India" type = "Contract" smsc = "+919884005444" apn = "www" class Vodafone_40486_Contract(NetworkOperator): netid = ["40486"] name = "Vodafone India Karnataka" country = "India" type = "Contract" smsc = "+919886005444" apn = "www" class Vodafone_40488_Contract(NetworkOperator): netid = ["40488"] name = "Vodafone India Punjab" country = "India" type = "Contract" smsc = "+919888009998" apn = "www" class Vodafone_40566_Contract(NetworkOperator): netid = ["40566"] name = "Vodafone India UP West" country = "India" type = "Contract" smsc = "+919719009998" apn = "www" class Vodafone_40567_Contract(NetworkOperator): netid = ["40567"] name = "Vodafone India West Bengal" country = "India" type = "Contract" smsc = "+919732099990" apn = "www" class Vodafone_405750_Contract(NetworkOperator): netid = ["405750"] name = "Vodafone India Jammu and Kasmir" country = "India" type = "Contract" smsc = "+919796009905" apn = "www" class Vodafone_405751_Contract(NetworkOperator): netid = ["405751"] name = "Vodafone India Assam" country = "India" type = "Contract" smsc = "+919706099990" apn = "www" class Vodafone_405752_Contract(NetworkOperator): netid = ["405752"] name = "Vodafone India Bihar" country = "India" type = "Contract" smsc = "+919709099990" apn = "www" class Vodafone_405753_Contract(NetworkOperator): netid = ["405753"] name = "Vodafone India Orissa" country = "India" type = "Contract" smsc = "+919776099990" apn = "www" class Vodafone_405754_Contract(NetworkOperator): netid = ["405754"] name = "Vodafone India Himachal Pradesh" country = "India" type = "Contract" smsc = "+919796009905" apn = "www" class Vodafone_405755_Contract(NetworkOperator): netid = ["405755"] name = "Vodafone India North East" country = "India" type = "Contract" smsc = "+919774099990" apn = "www" class Vodafone_405756_Contract(NetworkOperator): netid = ["405756"] name = "Vodafone India Madhya Pradesh" country = "India" type = "Contract" smsc = "+919713099990" apn = "www" class Vodafone_42403_Contract(NetworkOperator): netid = ["42403"] name = "du" country = "Dubai" type = "Contract" smsc = "+971555515515" apn = "du" auth = "CHAP" class Vodafone_42403_Prepaid(NetworkOperator): netid = ["42403"] name = "du" country = "Dubai" type = "Prepaid" smsc = "+971555515515" apn = "du" auth = "CHAP" class Vodafone_64710_Contract(NetworkOperator): netid = ["64710"] name = "SRR" country = "Reunion" type = "Contract" smsc = "+262850909" apn = "websfr" auth = "PAP" class Vodafone_64710_SFR_slsfr(NetworkOperator): netid = ["64710"] name = "SRR" dname = "SFR slsfr" country = "Reunion" type = "Contract" smsc = "+262850909" apn = "slsfr" auth = "PAP" class Vodafone_64710_SFR_internetpro(NetworkOperator): netid = ["64710"] name = "SRR" dname = "SFR internetpro" country = "Reunion" type = "Contract" smsc = "+262850909" apn = "internetpro" class Vodafone_64710_SFR_ipnet(NetworkOperator): netid = ["64710"] name = "SRR" dname = "SFR ipnet" country = "Reunion" type = "Contract" smsc = "+262850909" apn = "ipnet" class Vodafone_27077_Contract(NetworkOperator): netid = ["27077"] name = "Tango" country = "Luxembourg" type = "Contract" smsc = "+352091000030" apn = "hspa" auth = "CHAP" username = "tango" password = "tango" class Vodafone_40004_Contract(NetworkOperator): netid = ["40004"] name = "Azerfon" country = "Azerbaijan" type = "Contract" smsc = "+994702000700" apn = "Azerfon" class Vodafone_40004_Prepaid(NetworkOperator): netid = ["40004"] name = "Azerfon" country = "Azerbaijan" type = "Prepaid" smsc = "+994702000700" apn = "Azerfon" class Vodafone_64304_Contract(NetworkOperator): netid = ["64304"] name = "Vodacom Mozambique" country = "Mozambique" type = "Contract" smsc = "+25884080011" apn = "internet" class Vodafone_64304_Prepaid(NetworkOperator): netid = ["64304"] name = "Vodacom Mozambique" country = "Mozambique" type = "Prepaid" smsc = "+25884080011" apn = "internet" class Vodafone_63001_Contract(NetworkOperator): netid = ["63001"] name = "Vodacom Congo" country = "Congo (DRC)" type = "Contract" smsc = "+243811030" apn = "vodanet" auth = "PAP" class Vodafone_46692_Contract(NetworkOperator): netid = ["46692"] name = "Chunghwa" country = "Taiwan" type = "Contract" smsc = "+886932400821" apn = "internet" auth = "PAP" class Vodafone_46692_Prepaid(NetworkOperator): netid = ["46692"] name = "Chunghwa" country = "Taiwan" type = "Prepaid" smsc = "+886932400821" apn = "emome" auth = "PAP" username = "web" password = "web" class Vodafone_29403_Contract(NetworkOperator): netid = ["29403"] name = "Vip Operator" country = "Macedonia" type = "Contract" smsc = "+38977000301" apn = "vipoperator" auth = "CHAP" username = "vipoperator" password = "vipoperator" class Vodafone_29403_Prepaid(NetworkOperator): netid = ["29403"] name = "Vip Operator" country = "Macedonia" type = "Prepaid" smsc = "+38977000301" apn = "vipoperator" auth = "CHAP" username = "vipoperator" password = "vipoperator" wader-0.5.13/resources/platform/000077500000000000000000000000001257646610200165515ustar00rootroot00000000000000wader-0.5.13/resources/platform/ubuntu/000077500000000000000000000000001257646610200200735ustar00rootroot00000000000000wader-0.5.13/resources/platform/ubuntu/etc/000077500000000000000000000000001257646610200206465ustar00rootroot00000000000000wader-0.5.13/resources/platform/ubuntu/etc/ppp/000077500000000000000000000000001257646610200214455ustar00rootroot00000000000000wader-0.5.13/resources/platform/ubuntu/etc/ppp/ip-down.local000077500000000000000000000006241257646610200240430ustar00rootroot00000000000000#!/bin/sh # Ubuntu ip-down script passes all args to us as single arg so use the env # PPP_IFACE="$1" # PPP_TTY="$2" # PPP_SPEED="$3" # PPP_LOCAL="$4" # PPP_REMOTE="$5" # PPP_IPPARAM="$6" if [ "$PPP_IPPARAM" = "wader" ] ; then unset USEPEERDNS fi run-parts /etc/ppp/ip-down.d \ --arg="$PPP_IFACE" --arg="$PPP_TTY" --arg="$PPP_SPEED" \ --arg="$PPP_LOCAL" --arg="$PPP_REMOTE" --arg="$PPP_IPPARAM" wader-0.5.13/resources/platform/ubuntu/etc/ppp/ip-up.local000077500000000000000000000010401257646610200235110ustar00rootroot00000000000000#!/bin/sh # Ubuntu ip-up script passes all args to us as single arg so use the env # PPP_IFACE="$1" # PPP_TTY="$2" # PPP_SPEED="$3" # PPP_LOCAL="$4" # PPP_REMOTE="$5" # PPP_IPPARAM="$6" if [ "$PPP_IPPARAM" = "wader" ] ; then unset USEPEERDNS fi run-parts /etc/ppp/ip-up.d \ --arg="$PPP_IFACE" --arg="$PPP_TTY" --arg="$PPP_SPEED" \ --arg="$PPP_LOCAL" --arg="$PPP_REMOTE" --arg="$PPP_IPPARAM" # if pon was called with the "quick" argument, stop pppd if [ -e /var/run/ppp-quick ]; then rm /var/run/ppp-quick wait kill $PPPD_PID fi wader-0.5.13/resources/rpm/000077500000000000000000000000001257646610200155235ustar00rootroot00000000000000wader-0.5.13/resources/rpm/wader.spec000066400000000000000000000135761257646610200175150ustar00rootroot00000000000000%define wader_root %{_datadir}/wader-core %{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} Name: wader Version: %(%{__python} -c 'from wader.common.consts import APP_VERSION; print APP_VERSION') Release: 1%{?dist} Summary: A ModemManager implementation written in Python Source: ftp://ftp.noexists.org/pub/wader/%{name}-%{version}.tar.bz2 License: GPL BuildRoot: %{_tmppath}/%{name}-%{version}-build BuildArch: noarch BuildRequires: python-devel %if 0%{?fedora} >= 8 BuildRequires: python-setuptools-devel %else BuildRequires: python-setuptools %endif %if 0%{?suse_version} BuildRequires: dbus-1-python, python-zopeinterface, python-twisted %else BuildRequires: dbus-python, python-zope-interface, python-twisted-core %endif %description Wader is a fork of the core of "Vodafone Mobile Connect Card driver for Linux", with some of its parts rewritten and improved to be able to interact via DBus with other applications of the Linux/OSX desktop. Wader has two main components, a core and a client access library. The core can be extended to support more devices and distros/OSes through plugins. %package core Version: %(%{__python} -c 'from wader.common.consts import APP_VERSION; print APP_VERSION') Summary: The core that controls modem devices and provides DBus services. Group: System Environment/Daemons Requires: python-epsilon, python-gudev %if 0%{?suse_version} Requires: python-twisted, python-serial, dbus-1-python, python-tz, usb_modeswitch-data %else Requires: python-twisted-core, pyserial, dbus-python, pytz, usb_modeswitch-data >= 20100322 %endif Obsoletes: ModemManager >= 0.4 Provides: ModemManager >= 0.4 %description core Wader is a fork of the core of "Vodafone Mobile Connect Card driver for Linux", this package provides the core modem device access and DBus services. %package -n python-wader Version: %(%{__python} -c 'from wader.common.consts import APP_VERSION; print APP_VERSION') Summary: Library that provides access to wader core. Group: System Environment/Libraries Requires: python >= 2.5, python-crypto, python-messaging >= 0.5.11, dbus-python, pytz Conflicts: wader-core <= 0.5.8 %description -n python-wader Wader is a fork of the core of "Vodafone Mobile Connect Card driver for Linux", this package provides those common parts that are likely to be reused by Modem Manager clients written in python. %prep %setup -q -n %{name}-%{version} %build %{__python} -c 'import setuptools; execfile("setup.py")' build %install %{__python} -c 'import setuptools; execfile("setup.py")' install -O1 --skip-build --root %{buildroot} --prefix=%{_prefix} # avoid ghost warning touch %{buildroot}%{_datadir}/wader-core/plugins/dropin.cache %clean [ "%{buildroot}" != "/" ] && rm -rf %{buildroot} %post core if [ $1 = 1 ]; then # kill modem-manager asap kill -9 `pidof modem-manager` 2> /dev/null fi if [ $1 = 2 ]; then # remove traces of old dir if [ -d /usr/share/wader ]; then rm -rf /usr/share/wader fi # update plugins cache rm -rf /usr/share/wader-core/plugins/dropin.cache python -c "import sys; sys.path.insert(0, '/usr/share/wader-core'); from twisted.plugin import IPlugin, getPlugins;import plugins; list(getPlugins(IPlugin, package=plugins))" # restart wader-core if [ -e /var/run/wader.pid ]; then /usr/bin/wader-core-ctl --restart 2>/dev/null || true fi fi %files core %defattr(-,root,root) %dir %{wader_root}/ %dir %{wader_root}/plugins/ %dir %{wader_root}/core/ %{wader_root}/core/*.py* %dir %{wader_root}/core/hardware/ %{wader_root}/core/hardware/*.py* %dir %{wader_root}/core/oses/ %{wader_root}/core/oses/*.py* %dir %{wader_root}/core/statem/ %{wader_root}/core/statem/*.py* %dir %{wader_root}/core/backends/ %{wader_root}/core/backends/*.py* %{wader_root}/*.py* %{wader_root}/plugins/*.py* %{wader_root}/test/test_dbus*.py* %dir %{wader_root}/resources %{wader_root}/resources/config %{wader_root}/resources/extra %ghost %{wader_root}/plugins/dropin.cache %dir /lib/udev/ %dir /lib/udev/rules.d/ /lib/udev/rules.d/* %config %{_datadir}/dbus-1/system-services/org.freedesktop.ModemManager.service %config %{_sysconfdir}/dbus-1/system.d/org.freedesktop.ModemManager.conf %{_bindir}/wader-core-ctl %doc LICENSE README %files -n python-wader %defattr(-,root,root) %dir %{python_sitelib}/wader %{python_sitelib}/Wader-* %dir %{python_sitelib}/wader/common/ %{python_sitelib}/wader/*.py %{python_sitelib}/wader/*.py[co] %{python_sitelib}/wader/common/*.py %{python_sitelib}/wader/common/*.py[co] %{python_sitelib}/wader/common/backends/*.py %{python_sitelib}/wader/common/backends/*.py[co] %doc LICENSE README %changelog * Thu Sep 17 2015 Andrew Bird 0.5.13 - 0.5.13 Release * Tue May 22 2012 Andrew Bird 0.5.12 - 0.5.12 Release * Thu Apr 19 2012 Andrew Bird 0.5.11 - 0.5.11 Release * Mon Jan 23 2012 Andrew Bird 0.5.10 - 0.5.10 Release * Fri Dec 02 2011 Andrew Bird 0.5.9 - 0.5.9 Release * Mon Nov 14 2011 Andrew Bird 0.5.8 - 0.5.8 Release * Sun Sep 11 2011 Andrew Bird 0.5.7 - 0.5.7 Release * Mon Jun 06 2011 Andrew Bird 0.5.6 - 0.5.6 Update spec file * Tue May 05 2009 Pablo Marti 0.3.6 - 0.3.6 Release * Fri Apr 03 2009 Pablo Marti 0.3.5 - 0.3.5 Release * Tue Mar 03 2009 Pablo Marti 0.3.4 - 0.3.4 Release * Mon Feb 23 2009 Pablo Marti 0.3.3 - 0.3.3 Release * Thu Feb 13 2009 Pablo Marti 0.3.2 - 0.3.2 Release * Mon Feb 02 2009 Pablo Marti 0.3.1 - 0.3.1 Release * Mon Dec 01 2008 Pablo Marti 0.3.0 - 0.3.0 Release wader-0.5.13/resources/udev/000077500000000000000000000000001257646610200156705ustar00rootroot00000000000000wader-0.5.13/resources/udev/77-wader-dell.rules000066400000000000000000000013711257646610200212210ustar00rootroot00000000000000# Used to tag the ports with their usage, based on USB interface numbers. # This can remove the need to probe at all, but generally it's used to # ensure that the data/modem port is identified ACTION!="add|change", GOTO="wader_dell_end" SUBSYSTEM!="tty", GOTO="wader_dell_end" SUBSYSTEMS=="usb", ATTRS{idVendor}=="413c", GOTO="wader_dell_vendormatch" GOTO="wader_dell_end" LABEL="wader_dell_vendormatch" SUBSYSTEMS=="usb", ATTRS{idProduct}=="?*", ENV{.MM_PRODUCT}="$attr{idProduct}" # Dell D5520 module (rebranded Novatel Expedite EU870) # Needs confirmation ENV{.MM_PRODUCT}=="8137", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="8137", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" LABEL="wader_dell_end" wader-0.5.13/resources/udev/77-wader-huawei.rules000066400000000000000000000077251257646610200215740ustar00rootroot00000000000000# Used to tag the ports with their usage, based on USB interface numbers. # This can remove the need to probe at all, but generally it's used to # ensure that the data/modem port is identified ACTION!="add|change", GOTO="wader_huawei_end" SUBSYSTEM!="tty", GOTO="wader_huawei_end" SUBSYSTEMS=="usb", ATTRS{idVendor}=="12d1", GOTO="wader_huawei_vendormatch" GOTO="wader_huawei_end" LABEL="wader_huawei_vendormatch" SUBSYSTEMS=="usb", ATTRS{idProduct}=="?*", ENV{.MM_PRODUCT}="$attr{idProduct}" SUBSYSTEMS=="usb", ATTRS{bNumInterfaces}=="?*", ENV{.MM_USBNUMINT}="$attr{bNumInterfaces}" # Generic 0x1001 ENV{.MM_PRODUCT}=="1001", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="1001", ATTRS{bInterfaceNumber}=="02", ENV{ID_MM_PORT_TYPE_AUX}="1" # Generic 0x1003 ENV{.MM_PRODUCT}=="1003", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="1003", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" # E1692 ENV{.MM_PRODUCT}=="140c", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="140c", ATTRS{bInterfaceNumber}=="03", ENV{ID_MM_PORT_TYPE_AUX}="1" # E510 ENV{.MM_PRODUCT}=="1411", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="1411", ATTRS{bInterfaceNumber}=="02", ENV{ID_MM_PORT_TYPE_AUX}="1" # E1752 ENV{.MM_PRODUCT}=="141b", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="141b", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" # Generic 0x1436 (so far seen E173, E1750) ENV{.MM_PRODUCT}=="1436", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="1436", ATTRS{bInterfaceNumber}=="04", ENV{ID_MM_PORT_TYPE_AUX}="1" # K4505 ENV{.MM_PRODUCT}=="1464", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="1464", ENV{.MM_USBNUMINT}==" 6", ATTRS{bInterfaceNumber}=="03", ENV{ID_MM_PORT_TYPE_AUX}="1" ENV{.MM_PRODUCT}=="1464", ENV{.MM_USBNUMINT}==" 7", ATTRS{bInterfaceNumber}=="04", ENV{ID_MM_PORT_TYPE_AUX}="1" # K3765 ENV{.MM_PRODUCT}=="1465", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="1465", ENV{.MM_USBNUMINT}==" 6", ATTRS{bInterfaceNumber}=="03", ENV{ID_MM_PORT_TYPE_AUX}="1" ENV{.MM_PRODUCT}=="1465", ENV{.MM_USBNUMINT}==" 7", ATTRS{bInterfaceNumber}=="04", ENV{ID_MM_PORT_TYPE_AUX}="1" # E173 ENV{.MM_PRODUCT}=="14a5", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="14a5", ATTRS{bInterfaceNumber}=="03", ENV{ID_MM_PORT_TYPE_AUX}="1" # E1820 ENV{.MM_PRODUCT}=="14ac", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="14ac", ATTRS{bInterfaceNumber}=="04", ENV{ID_MM_PORT_TYPE_AUX}="1" # K3806 (breaks convention on ordering, but confirmed) ENV{.MM_PRODUCT}=="14ae", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_AUX}="1" ENV{.MM_PRODUCT}=="14ae", ATTRS{bInterfaceNumber}=="02", ENV{ID_MM_PORT_TYPE_MODEM}="1" # K4605 ENV{.MM_PRODUCT}=="14c6", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="14c6", ENV{.MM_USBNUMINT}==" 6", ATTRS{bInterfaceNumber}=="03", ENV{ID_MM_PORT_TYPE_AUX}="1" ENV{.MM_PRODUCT}=="14c6", ENV{.MM_USBNUMINT}==" 7", ATTRS{bInterfaceNumber}=="04", ENV{ID_MM_PORT_TYPE_AUX}="1" # K3770 ENV{.MM_PRODUCT}=="14c9", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="14c9", ATTRS{bInterfaceNumber}=="03", ENV{ID_MM_PORT_TYPE_AUX}="1" # K3771 ENV{.MM_PRODUCT}=="14ca", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="14ca", ATTRS{bInterfaceNumber}=="03", ENV{ID_MM_PORT_TYPE_AUX}="1" # K4510 ENV{.MM_PRODUCT}=="14cb", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="14cb", ATTRS{bInterfaceNumber}=="04", ENV{ID_MM_PORT_TYPE_AUX}="1" # K4511 ENV{.MM_PRODUCT}=="14cc", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="14cc", ATTRS{bInterfaceNumber}=="04", ENV{ID_MM_PORT_TYPE_AUX}="1" LABEL="wader_huawei_end" wader-0.5.13/resources/udev/77-wader-longcheer.rules000066400000000000000000000013571257646610200222530ustar00rootroot00000000000000# Used to tag the ports with their usage, based on USB interface numbers. # This can remove the need to probe at all, but generally it's used to # ensure that the data/modem port is identified ACTION!="add|change", GOTO="wader_longcheer_end" SUBSYSTEM!="tty", GOTO="wader_longcheer_end" SUBSYSTEMS=="usb", ATTRS{idVendor}=="1c9e", GOTO="wader_longcheer_vendormatch" GOTO="wader_longcheer_end" LABEL="wader_longcheer_vendormatch" SUBSYSTEMS=="usb", ATTRS{idProduct}=="?*", ENV{.MM_PRODUCT}="$attr{idProduct}" # Badged as "4GSystems XSStick P10" ENV{.MM_PRODUCT}=="9603", ATTRS{bInterfaceNumber}=="02", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="9603", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" LABEL="wader_longcheer_end" wader-0.5.13/resources/udev/77-wader-novatel.rules000066400000000000000000000035351257646610200217550ustar00rootroot00000000000000# Used to tag the ports with their usage, based on USB interface numbers. # This can remove the need to probe at all, but generally it's used to # ensure that the data/modem port is identified ACTION!="add|change", GOTO="wader_novatel_end" SUBSYSTEM!="tty", GOTO="wader_novatel_end" SUBSYSTEMS=="usb", ATTRS{idVendor}=="1410", GOTO="wader_novatel_vendormatch" GOTO="wader_novatel_end" LABEL="wader_novatel_vendormatch" SUBSYSTEMS=="usb", ATTRS{idProduct}=="?*", ENV{.MM_PRODUCT}="$attr{idProduct}" # U740 ENV{.MM_PRODUCT}=="1400", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="1400", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" # XU870 ENV{.MM_PRODUCT}=="1430", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="1430", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" # X950D ENV{.MM_PRODUCT}=="1450", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="1450", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" # EU870 module # Needs confirmation ENV{.MM_PRODUCT}=="2420", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="2420", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" # MC950D ENV{.MM_PRODUCT}=="4400", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="4400", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" # Generic PID (MiFi2352, MC990D) ENV{.MM_PRODUCT}=="7001", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="7001", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" # MiFi2352 (Vodafone only PID) ENV{.MM_PRODUCT}=="7003", ATTRS{bInterfaceNumber}=="00", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="7003", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" LABEL="wader_novatel_end" wader-0.5.13/resources/udev/77-wader-zte.rules000066400000000000000000000073301257646610200211040ustar00rootroot00000000000000# Used to tag the ports with their usage, based on USB interface numbers. # This can remove the need to probe at all, but generally it's used to # ensure that the data/modem port is identified ACTION!="add|change", GOTO="wader_zte_end" SUBSYSTEM!="tty", GOTO="wader_zte_end" SUBSYSTEMS=="usb", ATTRS{idVendor}=="19d2", GOTO="wader_zte_vendormatch" GOTO="wader_zte_end" LABEL="wader_zte_vendormatch" SUBSYSTEMS=="usb", ATTRS{idProduct}=="?*", ENV{.MM_PRODUCT}="$attr{idProduct}" # ONDA MT503HS ENV{.MM_PRODUCT}=="0002", ATTRS{bInterfaceNumber}=="02", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="0002", ATTRS{bInterfaceNumber}=="04", ENV{ID_MM_PORT_TYPE_AUX}="1" # MF668 ENV{.MM_PRODUCT}=="0017", ATTRS{bInterfaceNumber}=="02", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="0017", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" # MF626, MF636, MF637U and others ENV{.MM_PRODUCT}=="0031", ATTRS{bInterfaceNumber}=="03", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="0031", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" # ONDA MSA405HS ENV{.MM_PRODUCT}=="0037", ATTRS{bInterfaceNumber}=="03", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="0037", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" # K3520-Z ENV{.MM_PRODUCT}=="0025", ATTRS{bInterfaceNumber}=="02", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="0025", ATTRS{bInterfaceNumber}=="04", ENV{ID_MM_PORT_TYPE_AUX}="1" ENV{.MM_PRODUCT}=="0055", ATTRS{bInterfaceNumber}=="02", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="0055", ATTRS{bInterfaceNumber}=="04", ENV{ID_MM_PORT_TYPE_AUX}="1" # K3565-Z ENV{.MM_PRODUCT}=="0049", ATTRS{bInterfaceNumber}=="04", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="0049", ATTRS{bInterfaceNumber}=="02", ENV{ID_MM_PORT_TYPE_AUX}="1" ENV{.MM_PRODUCT}=="0052", ATTRS{bInterfaceNumber}=="03", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="0052", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" ENV{.MM_PRODUCT}=="0063", ATTRS{bInterfaceNumber}=="03", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="0063", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" # K3570-Z ENV{.MM_PRODUCT}=="1008", ATTRS{bInterfaceNumber}=="03", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="1008", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" # K3571-Z ENV{.MM_PRODUCT}=="1010", ATTRS{bInterfaceNumber}=="03", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="1010", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" # K3765-Z ENV{.MM_PRODUCT}=="2002", ATTRS{bInterfaceNumber}=="03", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="2002", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" # K4505-Z ENV{.MM_PRODUCT}=="0016", ATTRS{bInterfaceNumber}=="02", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="0016", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" ENV{.MM_PRODUCT}=="0104", ATTRS{bInterfaceNumber}=="03", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="0104", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" # MF180 ENV{.MM_PRODUCT}=="2003", ATTRS{bInterfaceNumber}=="03", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="2003", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" # MF181 ENV{.MM_PRODUCT}=="0117", ATTRS{bInterfaceNumber}=="02", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="0117", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" # MF190 ENV{.MM_PRODUCT}=="0082", ATTRS{bInterfaceNumber}=="03", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="0082", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" ENV{.MM_PRODUCT}=="0124", ATTRS{bInterfaceNumber}=="04", ENV{ID_MM_PORT_TYPE_MODEM}="1" ENV{.MM_PRODUCT}=="0124", ATTRS{bInterfaceNumber}=="01", ENV{ID_MM_PORT_TYPE_AUX}="1" LABEL="wader_zte_end" wader-0.5.13/resources/udev/99-wader-quirks.rules000066400000000000000000000035631257646610200216300ustar00rootroot00000000000000 # Vodafone (ZTE) K3805-Z ACTION=="add", SUBSYSTEM=="net", ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="1003", RUN="/sbin/ifconfig %k -arp" # Vodafone (ZTE) K3806-Z ACTION=="add", SUBSYSTEM=="net", ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="1015", RUN="/sbin/ifconfig %k -arp" # Vodafone (ZTE) K4510-Z ACTION=="add", SUBSYSTEM=="net", ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="1173", RUN="/sbin/ifconfig %k -arp" # Vodafone (ZTE) K3770-Z ACTION=="add", SUBSYSTEM=="net", ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="1177", RUN="/sbin/ifconfig %k -arp" # Vodafone (ZTE) K3772-Z ACTION=="add", SUBSYSTEM=="net", ATTRS{idVendor}=="19d2", ATTRS{idProduct}=="1181", RUN="/sbin/ifconfig %k -arp" # Dell (Ericsson) D5540 ACTION=="add", SUBSYSTEM=="net", ATTRS{idVendor}=="413c", ATTRS{idProduct}=="8183", RUN="/sbin/ifconfig %k -arp" ACTION=="add", SUBSYSTEM=="net", ATTRS{idVendor}=="413c", ATTRS{idProduct}=="8184", RUN="/sbin/ifconfig %k -arp" # Ericsson F3307 ACTION=="add", SUBSYSTEM=="net", ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1909", RUN="/sbin/ifconfig %k -arp" ACTION=="add", SUBSYSTEM=="net", ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="190a", RUN="/sbin/ifconfig %k -arp" # Ericsson F3607gw ACTION=="add", SUBSYSTEM=="net", ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1904", RUN="/sbin/ifconfig %k -arp" ACTION=="add", SUBSYSTEM=="net", ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1905", RUN="/sbin/ifconfig %k -arp" ACTION=="add", SUBSYSTEM=="net", ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1906", RUN="/sbin/ifconfig %k -arp" ACTION=="add", SUBSYSTEM=="net", ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1907", RUN="/sbin/ifconfig %k -arp" # Toshiba F3607gw ACTION=="add", SUBSYSTEM=="net", ATTRS{idVendor}=="0930", ATTRS{idProduct}=="130c", RUN="/sbin/ifconfig %k -arp" ACTION=="add", SUBSYSTEM=="net", ATTRS{idVendor}=="0930", ATTRS{idProduct}=="1311", RUN="/sbin/ifconfig %k -arp" wader-0.5.13/setup.py000066400000000000000000000106361257646610200144330ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """ setuptools file for Wader """ from os.path import join, isdir, walk import sys from distutils.core import Extension from setuptools import setup from wader.common.consts import (APP_VERSION, APP_NAME, APP_SLUG_NAME, BASE_DIR) DATA_DIR = join(BASE_DIR, 'usr', 'share', APP_SLUG_NAME) BIN_DIR = join(BASE_DIR, 'usr', 'bin') RESOURCES = join(DATA_DIR, 'resources') DBUS_SYSTEMD = join(BASE_DIR, 'etc', 'dbus-1', 'system.d') DBUS_SYSTEM_SERVICES = join(BASE_DIR, 'usr', 'share', 'dbus-1', 'system-services') UDEV_RULESD = join(BASE_DIR, 'lib', 'udev', 'rules.d') def list_files(path, exclude=None): result = [] def walk_callback(arg, directory, files): for ext in ['.svn', '.git']: if ext in files: files.remove(ext) if exclude: for f in files: if f.startswith(exclude): files.remove(f) result.extend(join(directory, f) for f in files if not isdir(join(directory, f))) walk(path, walk_callback, None) return result data_files = [ (join(RESOURCES, 'extra'), list_files('resources/extra')), (join(RESOURCES, 'config'), list_files('resources/config')), (join(DATA_DIR, 'plugins'), list_files('plugins/devices')), (join(DATA_DIR, 'plugins'), list_files('plugins/oses')), (join(DATA_DIR, 'core'), list_files('core')), (join(DATA_DIR, 'core', 'backends'), list_files('core/backends')), (join(DATA_DIR, 'core', 'hardware'), list_files('core/hardware')), (join(DATA_DIR, 'core', 'oses'), list_files('core/oses')), (join(DATA_DIR, 'core', 'statem'), list_files('core/statem')), (join(DATA_DIR, 'test'), ['test/test_dbus.py', 'test/test_dbus_ussd_de.py']), (DATA_DIR, ['core-tap.py']), (BIN_DIR, ['bin/wader-core-ctl']), (DBUS_SYSTEMD, ['resources/dbus/org.freedesktop.ModemManager.conf']), (DBUS_SYSTEM_SERVICES, ['resources/dbus/org.freedesktop.ModemManager.service']), ] ext_modules = [] if sys.platform.startswith('linux'): data_files.append((UDEV_RULESD, list_files('resources/udev'))) elif sys.platform == 'darwin': # XXX This is broken. osxserialports = Extension('wader.common.oses.osxserialports', sources=['wader/common/oses/_osxserialports.c'], extra_link_args=['-framework', 'CoreFoundation', '-framework', 'IOKit']) ext_modules.append(osxserialports) packages = [ 'wader', 'wader.common', 'wader.common.backends', ] setup(name=APP_NAME, version=APP_VERSION, description='3G device manager for Linux and OSX', download_url="http://www.wader-project.org", author='Pablo Martí Gamboa', author_email='pmarti@warp.es', license='GPL', packages=packages, data_files=data_files, ext_modules=ext_modules, zip_safe=False, test_suite='test', classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: No Input/Output (Daemon)', 'Framework :: Twisted', 'Intended Audience :: Developers', 'Intended Audience :: Telecommunications Industry', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Natural Language :: English', 'Operating System :: POSIX :: Linux', 'Operating System :: MacOS :: MacOS X', 'Programming Language :: Python :: 2.5', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Topic :: Communications :: Telephony', ], ) wader-0.5.13/test/000077500000000000000000000000001257646610200136725ustar00rootroot00000000000000wader-0.5.13/test/test_aes.py000066400000000000000000000154421257646610200160610ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011 Vodafone España, S.A. # Author: Andrew Bird # # 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. """Unittests for the aes module""" from cStringIO import StringIO import os import pickle import random from twisted.trial import unittest from wader.common.aes import encryptData, decryptData def transform_passwd(passwd): valid_lengths = [16, 24, 32] for l in valid_lengths: if l >= len(passwd): return passwd.zfill(l) msg = "Password '%s' is too long, max length is 32 chars" raise Exception(msg % passwd) def make_gsm_dict(passwd): return {'gsm': {'passwd': passwd}} class TestEncoding(unittest.TestCase): """Tests for encoding""" def test_decode_fixed(self): T = [('b7b96d81cc86e6f9', '', 'cb867e4037a302198ee51523393923996cf56621de3e21ca46c147c7635a' '4597287a45f17dd984ccf9195db3b21e404007959d57fdd12594e96cb65d' 'fc453f6d'), ('0000000000497a9b', '2ed9eff633', 'e53100eac23d4afb2a0ed0f3b16b0a35066f71fea80a3559c2d21d368173' '8c0042d2f8bdbb4467eea3bf7102922fdb5f14ddc810837bc8ded8fcf40e' '717e82c5cee71acbe17d1135b99e5115c6cf881d'), ('000000000000d07d', '9771d7edad6b', '2a682d46c4ce665ba7acc238f11f50c836defd05de1431a2af5ab47dd4f4' '6c775a91b26fc087f347d5f1fe592575d3b1681372777e943ffaa2f77e81' 'e5c5ab6d32fa420c261ecebc72a3daf96e69ba4e'), ('39db1d780077cf13', '', '583c69ee32ced39574654a64701eb36d04e98d052976d840be4691d12a51' '7e1839f6208354ca244abcf28d6e9fecbef7c5559417c1e23285802a8ffc' 'ca907ff7'), ('00000000000052e0', '17a0b94b488e', '15e06ff63bd85c6b06a2d2f6a36128fa0154cdb7778676c5bd589ca0c3ea' 'ac4b8f179002b69d322b110f10a208e365285f7aff5ada1d36ee6cfc140e' '77f1f25b8ea13185b2add3896071a2fbf0bdd051'), ('00000000004ccb76', '034604574e', '3a058a4cb44af041b7e9609e0b05d2607b74e358f2046402671a715ee5c2' '453d5beb41acb8a01fd4278ceb2a58d07002ee92b567b3f60d68311aadd4' '538715022930390e43fe41853f6587261eae4dde'), ('e477fd4e2ab84397', '', 'd9b1ee5dadede634e5e508a9de6d2c5f5179efb5da65dcd4721d8c9fe6d1' '011eec6672a93e619413b20960b66b8c2a770ab736f83767434a0e03f392' '6fd967ca'), ('00000073966b9008', '15eed9', 'f87df3422f9a8e95c89149be782de06ca6c4d4189a2c5686d1186b51ebf2' 'dad879f15c1eb1943dd6e321e05dcc9759ef25189cc7980e6b3a8ae5ab38' 'f6fb20e31b4fe49929c8613f699dfe6b2adc71bf'), ('0000e34a67f172f8', '41dfd679b972', '490cc79eed800e217ab62702284d3e5c61022b8ec0459eb54ec1fd2071e1' 'e486f6a7d895b2ef34553dba1daed165a1d8a61e439cfa83777f1b5a97d0' 'c8e2ac081ef3fa703083c3db97211b33630ec2b1'), ('0000000000000018', 'f462f7e7aa525d1125cd7f', '17bfeab05df5be698372e71c107b5ac750846cc9e5d0259ea706945889a7' '70699f2f5229e4ed03f47a5b755d66cceb0d9796ec0e09ed28413986e447' '84fa9158a6ed086d98aca1558266b4db6150c1b9fa83b5597ea17559422b' 'ee95b9272afd'), ('0000000986b93eda', '785964d244f22e', '1459254a9dbe14a088acb911badff5b17d2993541a7080c6cc01de99096b' '48a81fed141f38a7843a55a88ad8b065394afde5a8a735b773a266cdbc20' '59fa73b2ebd25d47e71350c0e5b5f6bb352a7def'), ('0000002ef2b543f79bb97371', '447759', '55d47083f0cb00b9dd4065530f2ea67eb0065c20d4efe9f0125cf4b47785' '6268bd1718d544d3346c7d12d6293a222318890cc3c32ec776e711add688' '13a0bb0bdcbdb82e6244fd56f477a578d452b763'), ('b5f166646d9dde97', '9c082a9c', 'a3408b3e99b0fced6bda3eb648714169814f2260157c82d28780aacb30b0' '9905928fbf5bcfd98188d7dd185e625a3b05bd312e211b4a96f88e796a4a' 'ee137319afa8a7726f2dc0190c1e3c8377633dfe'), ('0000000085532a66', 'db57406690243f06', 'c0015c98db59407a43d09401aadef3bdab330d9926cffb8f9d7b0f8bbff5' 'b07a062b6fe6c2bc77bcd5d55f8c81fc943e772c86666cd875c007804226' '3f6a3dcca0acd83483156a744cbafbee0d43c0b3'), ('00000000005bd30e', '559958a4411bf08e0f', '270bba1a98649e37ec0de94d46085fcbbe50f50688f2eb5848bcb5aa601d' 'c98eb6e714b85c3679d21dd4a59beec82d48f09db11da49283bc261df59b' '770c20027ab75c190126875d118c8e659e4a7465'), ('00000b78961f4a341ce23df7', '4c73', 'f65de12f497d67e1d65902f4ed34e5c3ef0e881ae3942f29391e0f7fc7f8' '4a7bf67dea6b2d7c3e5cbbdb0801ed89703bb3b325b063d96b9eec7a11ed' '229361dc')] for t in T: original = make_gsm_dict(t[1]) # decode pickledobj = decryptData(t[0], t[2].decode('hex'), testforpickle=True) decrypted = pickle.load(StringIO(pickledobj)) self.assertEqual(decrypted, original) def test_encode_decode(self): n_keys = 8 n_passwds = 8 LEN = 12 for keys in range(n_keys): length = int(random.random() * LEN) key = os.urandom(LEN - length).encode('hex') padded_key = transform_passwd(key) for passwds in range(n_passwds): passwd = os.urandom(length).encode('hex') original = make_gsm_dict(passwd) # encode encrypted = encryptData(padded_key, pickle.dumps(original)) # decode pickledobj = decryptData(padded_key, encrypted) decrypted = pickle.load(StringIO(pickledobj)) self.assertEqual(decrypted, original) wader-0.5.13/test/test_aterrors.py000066400000000000000000000040501257646610200171430ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Unittests for the aterrors module""" from twisted.trial import unittest from wader.common.aterrors import extract_error import wader.common.aterrors as E class TestATErrors(unittest.TestCase): """Tests for wader.common.aterrors""" def test_cme_errors_string(self): raw = '\r\n+CME ERROR: SIM interface not started\r\n' self.assertEqual(extract_error(raw)[0], E.SimNotStarted) raw2 = 'AT+CPIN=1330\r\n\r\n+CME ERROR: operation not allowed\r\n' self.assertEqual(extract_error(raw2)[0], E.OperationNotAllowed) raw3 = '\r\n+CME ERROR: SIM busy\r\n' self.assertEqual(extract_error(raw3)[0], E.SimBusy) def test_cme_errors_numeric(self): raw = '\r\n+CME ERROR: 30\r\n' self.assertEqual(extract_error(raw)[0], E.NoNetwork) raw = '\r\n+CME ERROR: 100\r\n' self.assertEqual(extract_error(raw)[0], E.Unknown) raw = '\r\n+CME ERROR: 14\r\n' self.assertEqual(extract_error(raw)[0], E.SimBusy) def test_cms_errors(self): raw = '\r\n+CMS ERROR: 500\r\n' self.assertEqual(extract_error(raw)[0], E.CMSError500) raw2 = '\r\n+CMS ERROR: 301\r\n' self.assertEqual(extract_error(raw2)[0], E.CMSError301) wader-0.5.13/test/test_command.py000066400000000000000000000327541257646610200167340ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Unittests for the command module""" import re import sys from twisted.trial import unittest sys.path.insert(0, '..') from core.command import get_cmd_dict_copy cmd_dict = get_cmd_dict_copy() class TestCommandsRegexps(unittest.TestCase): """Test for the regexps associated with at commands""" def test_check_pin_regexp(self): # [-] SENDING ATCMD 'AT+CPIN?\r\n' # [-] WAITING: DATA_RCV = '\r\n+CPIN: READY\r\n\r\nOK\r\n' extract = cmd_dict['check_pin']['extract'] text = '\r\n+CPIN: READY\r\n\r\nOK\r\n' match = extract.match(text) self.failIf(match is None) self.assertEqual(match.group('resp'), 'READY') # [-] WAITING: DATA_RCV = '\r\n+CPIN: SIM PIN\r\n\r\nOK\r\n' text2 = '\r\n+CPIN: SIM PIN\r\n\r\nOK\r\n' match = extract.match(text2) self.failIf(match is None) self.assertEqual(match.group('resp'), 'SIM PIN') # [-] WAITING: DATA_RCV = '\r\n+CPIN: SIM PUK2\r\n\r\nOK\r\n' text3 = '\r\n+CPIN: SIM PUK2\r\n\r\nOK\r\n' match = extract.match(text3) self.failIf(match is None) self.assertEqual(match.group('resp'), 'SIM PUK2') def test_find_contacts(self): extract = cmd_dict['find_contacts']['extract'] text = '\r\n+CPBF: 1,"+23434432",145,"0050006100620065006C0073"\r\n+CPBF: 53,"342239262",129,"005000410043004F0020004D0056002F004D"\r\n+CPBF: 36,"34233231481",129,"005000410043004F002F004D"\r\n+CPBF: 92,"43223963522",129,"0050004100500041002000500041005200540020004D0056002F004D"\r\n+CPBF: 93,"543453302",129,"005000410050004100200054005200420020004D0076002F004D"\r\n+CPBF: 103,"666",129,"005000610073006300750061006C002000560061006C002F004D"\r\n+CPBF: 109,"4534534532070",129,"00500045004F0050004C0045002F004D"\r\n+CPBF: 115,"623434212",129,"005000720069006D006F0020004E00630068006F002F004D"\r\n\r\nOK\r\n' matches = list(re.finditer(extract, text)) self.assertEqual(len(matches), 8) self.assertEqual(matches[0].group('name'), '0050006100620065006C0073') self.assertEqual(matches[0].group('number'), '+23434432') self.assertEqual(matches[7].group('name'), '005000720069006D006F0020004E00630068006F002F004D') self.assertEqual(matches[7].group('number'), '623434212') def test_find_contacts_ericsson(self): extract = cmd_dict['find_contacts']['extract'] text = '\r\n+CPBF: 7,"002B003400330035003300340035003300340032003100320031",145,"0045007500670065006E0065"\r\n' matches = list(re.finditer(extract, text)) self.assertEqual(len(matches), 1) self.assertEqual(matches[0].group('name'), '0045007500670065006E0065') self.assertEqual(matches[0].group('number'), '002B003400330035003300340035003300340032003100320031') def test_get_charsets(self): extract = cmd_dict['get_charsets']['extract'] text = '\r\n+CSCS: ("IRA","GSM","UCS2")\r\n' matches = list(re.finditer(extract, text)) self.assertEqual(len(matches), 3) self.assertEqual(matches[0].group('lang'), "IRA") self.assertEqual(matches[2].group('lang'), "UCS2") text2 = '\r\n+CSCS: ("IRA")\r\n' matches = list(re.finditer(extract, text2)) self.assertEqual(len(matches), 1) self.assertEqual(matches[0].group('lang'), "IRA") text3 = '\r\n+CSCS: ("IRA","8859-A","UCS2","PCDN")\r\n' matches = list(re.finditer(extract, text3)) self.assertEqual(len(matches), 4) self.assertEqual(matches[0].group('lang'), "IRA") self.assertEqual(matches[1].group('lang'), "8859-A") def test_get_charset_regexp(self): # [-] SENDING ATCMD 'AT+CSCS?\r\n' # [-] WAITING: DATA_RCV = '\r\n+CSCS: "UCS2"\r\n\r\nOK\r\n' extract = cmd_dict['get_charset']['extract'] text = '\r\n+CSCS: "UCS2"\r\n\r\nOK\r\n' match = extract.match(text) self.failIf(match is None) self.assertEqual(match.group('lang'), 'UCS2') def test_get_card_model_regexp(self): # [-] SENDING ATCMD 'AT+CGMM\r\n' # [-] WAITING: DATA_RCV = '\r\nE220\r\n\r\nOK\r\n' extract = cmd_dict['get_card_model']['extract'] text = '\r\nE220\r\n\r\nOK\r\n' match = extract.match(text) self.failIf(match is None) self.assertEqual(match.group('model'), 'E220') def test_get_card_version_regexp(self): # [-] SENDING ATCMD 'AT+GMR\r\n' # [-] WAITING: DATA_RCV = '\r\n11.110.01.03.00\r\n\r\nOK\r\n' extract = cmd_dict['get_card_version']['extract'] text = '\r\n11.110.01.03.00\r\n\r\nOK\r\n' match = extract.match(text) self.failIf(match is None) self.assertEqual(match.group('version'), '11.110.01.03.00') def test_list_contacts_regexp(self): # [-] SENDING ATCMD 'AT+CPBR=1,250\r\n' # [-] WAITING: DATA_RCV = '\r\n+CPBR: 1,"+23434432",145,"0050006100620065006C0073"\r\n\r\nOK\r\n' extract = cmd_dict['list_contacts']['extract'] text = '\r\n+CPBR: 1,"+23434432",145,"0050006100620065006C0073"\r\n\r\nOK\r\n' match = extract.match(text) self.failIf(match is None) self.assertEqual(match.group('number'), '+23434432') self.assertEqual(match.group('name'), '0050006100620065006C0073') def test_get_imei_regexp(self): # [-] SENDING ATCMD 'AT+CGSN\r\n' # [-] WAITING: DATA_RCV = '\r\n351834012602323\r\n\r\nOK\r\n extract = cmd_dict['get_imei']['extract'] text = '\r\n351834012602323\r\n\r\nOK\r\n' match = extract.match(text) self.failIf(match is None) self.assertEqual(match.group('imei'), '351834012602323') def test_get_imsi_regexp(self): # [-] SENDING ATCMD 'AT+CIMI\r\n' # [-] WAITING: DATA_RCV = '\r\n214012001727332\r\n\r\nOK\r\n' extract = cmd_dict['get_imsi']['extract'] text = '\r\n214012001727132\r\n\r\nOK\r\n' match = extract.match(text) self.failIf(match is None) self.assertEqual(match.group('imsi'), '214012001727132') def test_get_netreg_status_regexp(self): extract = cmd_dict['get_netreg_status']['extract'] text = '\r\n+CREG: 0,1\r\n\r\nOK\r\n' match = extract.match(text) self.failIf(match is None) self.assertEqual(match.group('mode'), '0') self.assertEqual(match.group('status'), '1') def test_get_network_info_regexp(self): # [-] SENDING ATCMD 'AT+COPS?\r\n' # [-] WAITING: DATA_RCV = '\r\n+COPS: 0,2,"21401",0\r\n\r\nOK\r\n' extract = cmd_dict['get_network_info']['extract'] text = '\r\n+COPS: 0,2,"21401",0\r\n\r\nOK\r\n' match = extract.match(text) self.failIf(match is None) self.assertEqual(match.group('netname'), '21401') self.assertEqual(match.group('status'), '0') text2 = '\r\n+COPS: 0,0,"vodafone ES",0\r\n\r\nOK\r\n' match2 = extract.match(text2) self.failIf(match2 is None) self.assertEqual(match2.group('netname'), 'vodafone ES') self.assertEqual(match2.group('status'), '0') def test_get_network_info_regexp_failure(self): # [-] SENDING ATCMD 'AT+COPS?\r\n' # [-] WAITING: DATA_RCV = '\r\n+COPS: 0\r\n\r\nOK\r\n' extract = cmd_dict['get_network_info']['extract'] text = '\r\n+COPS: 0\r\n\r\nOK\r\n' match = extract.match(text) self.failIf(match is None) self.failIf(int(match.group('error')), 0) def test_get_network_info_regexp_ucs2(self): extract = cmd_dict['get_network_info']['extract'] text = '\r\n+COPS: 0,0,"0076006f006400610066006f006e0065002000450053",2\r\n\r\nOK\r\n' match = extract.match(text) self.failIf(match is None) self.assertEqual(match.group('netname'), '0076006f006400610066006f006e0065002000450053') self.assertEqual(match.group('status'), '2') def test_get_network_names_ucs2_regexp(self): extract = cmd_dict['get_network_names']['extract'] text = '\r\n+COPS: (1,"0076006f006400610066006f006e0065002000450053","0076006f00640061002000450053","21401",0),(2,"0076006f0064006100660)\r\n' matches = list(re.finditer(extract, text)) self.failIf(matches is None) self.assertEqual(matches[0].group('lname'), '0076006f006400610066006f006e0065002000450053') self.assertEqual(matches[0].group('sname'), '0076006f00640061002000450053') self.assertEqual(matches[0].group('netid'), '21401') def test_get_network_names_ovation_regexp(self): """ Novatel ovation's output wasnt matched by previous regexp """ extract = cmd_dict['get_network_names']['extract'] text = '\r\n+COPS: (1,"vodafone ES","voda ES","21401",0)\r\n+COPS: (2,"vodafone ES","voda ES","21401",2)\r\n+COPS: (1,"Orange","Orange","21403",2)\r\n+COPS: (1,"Yoigo","YOIGO","21404",2)\r\n+COPS: (1,"Orange","Orange","21403",0)\r\n+COPS: (1,"movistar","movistar","21407",0)\r\n+COPS: (1,"movistar","movistar","21407",2)\r\n\r\nOK\r\n' matches = list(re.finditer(extract, text)) self.failIf(matches is None) self.assertEqual(matches[0].group('lname'), 'vodafone ES') self.assertEqual(matches[0].group('sname'), 'voda ES') self.assertEqual(matches[0].group('netid'), '21401') def test_get_network_names_k3715_regexp(self): extract = cmd_dict['get_network_names']['extract'] text = '\r\n+COPS: (1,"vodafone ES","voda ES","21401",0),(2,"vodafone ES@","vodafone","21401",2),(1,"movistar","movistar","21407",2),(1,"OrangeES","Orange","21403",2),(1,"Yoigo Moviles SA","Yoigo","21404",2),(1,"OrangeES","Orange","21403",0),(1,"movistar","movistar","21407",0),,(0,1,2,3,4),(0,1,2)\r\n\r\nOK\r\n' matches = list(re.finditer(extract, text)) self.failIf(matches is None) self.assertEqual(matches[0].group('lname'), 'vodafone ES') self.assertEqual(matches[0].group('sname'), 'voda ES') self.assertEqual(matches[0].group('netid'), '21401') def test_get_phonebook_size(self): # [-] SENDING ATCMD 'AT+CPBR=?\r\n' # [-] WAITING: DATA_RCV = '\r\n+CPBR: (1-250),40,16\r\n\r\nOK\r\n' extract = cmd_dict['get_phonebook_size']['extract'] text = '\r\n+CPBR: (1-250),40,16\r\n\r\nOK\r\n' match = extract.match(text) self.failIf(match == None) self.assertEqual(match.group('size'), '250') def test_get_pin_status(self): # [-] SENDING ATCMD 'AT+CLCK="SC",2\r\n' # [-] WAITING: DATA_RCV = '\r\n+CLCK: 1\r\n\r\nOK\r\n' extract = cmd_dict['get_pin_status']['extract'] text = '\r\n+CLCK: 1\r\n\r\nOK\r\n' match = extract.match(text) self.failIf(match == None) self.assertEqual(match.group('status'), '1') def test_get_roaming_ids_e620(self): text = '\r\n+CPOL: 1,"20810"\r\n+CPOL: 2,"22210"\r\n+CPOL: 3,"26202"\r\n+CPOL: 4,"26801"\r\n+CPOL: 5,"23415"\r\n+CPOL: 6,"20601"\r\n+CPOL: 7,"22801"\r\n+CPOL: 8,"20404"\r\n+CPOL: 9,"60202"\r\n+CPOL: 10,"27201"\r\n+CPOL: 11,"20205"\r\n+CPOL: 12,"24008"\r\n+CPOL: 13,"22601"\r\n+CPOL: 14,"26001"\r\n+CPOL: 15,"27602"\r\n+CPOL: 16,"65501"\r\n+CPOL: 17,"27801"\r\n+CPOL: 18,"50503"\r\n+CPOL: 19,"63902"\r\n+CPOL: 20,"53001"\r\n+CPOL: 21,"21670"\r\n+CPOL: 22,"44020"\r\n+CPOL: 23,"54201"\r\n+CPOL: 24,"40441"\r\n+CPOL: 25,"23801"\r\n+CPOL: 26,"24405"\r\n+CPOL: 27,"41902"\r\n+CPOL: 28,"24802"\r\n+CPOL: 29,"23201"\r\n+CPOL: 30,"21910"\r\n+CPOL: 31,"29340"\r\n+CPOL: 32,"27402"\r\n+CPOL: 33,"27403"\r\n+CPOL: 34,"24602"\r\n+CPOL: 35,"46000"\r\n+CPOL: 36,"52503"\r\n\r\n\r\nOK\r\n' extract = re.compile( """ \r\n \+CPOL:\s(?P\d+),"(?P\d+)" """, re.VERBOSE) matches = list(re.finditer(extract, text)) self.assertEqual(len(matches), 36) self.assertEqual(matches[0].group('netid'), "20810") def test_get_signal_quality_regexp(self): # [-] SENDING ATCMD 'AT+CSQ?\r\n' # [-] WAITING: DATA_RCV = '\r\n+CSQ: 25,99\r\n\r\nOK\r\n' extract = cmd_dict['get_signal_quality']['extract'] text = '\r\n+CSQ: 25,99\r\n\r\nOK\r\n' match = extract.match(text) self.failIf(match == None) self.assertEqual(match.group('rssi'), '25') self.assertEqual(match.group('ber'), '99') def test_get_smsc_regexp(self): # [-] SENDING ATCMD 'AT+CSCA?\r\n' # [-] WAITING: DATA_RCV = '\r\n+CSCA: "002B00330034003600300037003000300033003100310030",145\r\n\r\nOK\r\n' extract = cmd_dict['get_smsc']['extract'] text = '\r\n+CSCA: "002B00330034003600300037003000300033003100310030",145\r\n\r\nOK\r\n' match = extract.match(text) self.failIf(match == None) self.assertEqual(match.group('smsc'), '002B00330034003600300037003000300033003100310030') wader-0.5.13/test/test_contact.py000066400000000000000000000113651257646610200167440ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Unittests for the contact module""" from twisted.trial import unittest from twisted.internet import defer from core.contact import ContactStore SKIP_TEST = False try: from wader.plugins.sqlite_provider import sqlite_provider, SQLContact sqlite_provider.initialize(dict(path=':memory:')) providers = [sqlite_provider] except ImportError: SKIP_TEST = True store = None numtests = None executed = None class TestContactStore(unittest.TestCase): """Tests for core.contact.ContactStore""" skip = "No contact providers found" if SKIP_TEST else None def setUpOnce(self): global store, numtests if store is None: store = ContactStore() map(store.add_provider, providers) if numtests is None: numtests = len([m for m in dir(self) if m.startswith('test_')]) self.store = store return defer.succeed(True) def tearDownOnce(self): global store return defer.maybeDeferred(store.close) def setUp(self): return self.setUpOnce() def tearDown(self): global numtests, executed if executed is None: executed = 1 else: executed += 1 if numtests == executed: return self.tearDownOnce() return defer.succeed(True) def test_add_contact(self): c = SQLContact("John", "+433333223", email="john@mail.net") contact = self.store.add_contact(c) # now check that the contact is present contacts = self.store.list_contacts() self.assertIn(contact, contacts) # leave it as we found it self.store.remove_contact(contact) def test_remove_contact(self): # add a contact and remove it c = SQLContact("John", "+433333223", email="john@mail.net") contact = self.store.add_contact(c) self.store.remove_contact(contact) # now check that is not present anymore contacts = list(self.store.list_contacts()) self.assertNotIn(contact, contacts) def test_list_contacts(self): # add a couple of contacts and check they are present contact1 = self.store.add_contact( SQLContact("Daniel", "+213333223", email="dan@mail.net")) contact2 = self.store.add_contact( SQLContact("Andy", "+113333223", email="andy@mail.net")) contacts = self.store.list_contacts() self.assertIn(contact1, contacts) self.assertIn(contact2, contacts) # leave it as we found it self.store.remove_contact(contact1) self.store.remove_contact(contact2) def test_find_contacts_by_name(self): contact1 = self.store.add_contact( SQLContact("Daniel", "+213333223", email="dan@mail.net")) matches = self.store.find_contacts_by_name("Daniel") self.assertIn(contact1, matches) # leave it as we found it self.store.remove_contact(contact1) def test_find_contacts_by_number_full_match(self): contact1 = self.store.add_contact( SQLContact("Daniel", "+213333223", email="dan@mail.net")) matches = self.store.find_contacts_by_number("+213333223") self.assertIn(contact1, matches) # leave it as we found it self.store.remove_contact(contact1) def test_find_contacts_by_number_uk_match(self): contact1 = self.store.add_contact( SQLContact("Daniel", "073333223", email="dan@mail.net")) matches = self.store.find_contacts_by_number("+4473333223") self.assertIn(contact1, matches) # leave it as we found it self.store.remove_contact(contact1) def test_find_contacts_by_number_ie_match(self): contact1 = self.store.add_contact( SQLContact("Daniel", "081234567", email="dan@mail.net")) matches = self.store.find_contacts_by_number("+35381234567") self.assertIn(contact1, matches) # leave it as we found it self.store.remove_contact(contact1) wader-0.5.13/test/test_dbus.py000066400000000000000000001733741257646610200162570ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011-2012 Sphere Systems, U.K. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Andrew Bird # # 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. """ Self-contained unittest suite for ModemManager implementations """ # install the following packages on Ubuntu # python-dbus, python-gobject # # install the following packages On OpenSuSE # dbus-1-python, python-gnome, python-gobject2 # # to run all the tests: # nosetests -v --with-xunit test/test_dbus.py # or selectively # nosetests -v --with-xunit test/test_dbus.py -m test_ZDisableReEnable10 # Make sure the very first thing we do is to set the glib loop as default import dbus import dbus.mainloop.glib loop = dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) import gobject import random import string import sys import time try: import unittest2 as unittest except ImportError: import unittest if sys.platform.startswith('linux'): import gudev from core.oses.linux import get_ancestor, get_devices # Duplicate any consts rather than include wader/common/consts MM_SERVICE = 'org.freedesktop.ModemManager' MM_OBJPATH = '/org/freedesktop/ModemManager' MM_INTFACE = MM_SERVICE MDM_INTFACE = 'org.freedesktop.ModemManager.Modem' SPL_INTFACE = 'org.freedesktop.ModemManager.Modem.Simple' CRD_INTFACE = 'org.freedesktop.ModemManager.Modem.Gsm.Card' CTS_INTFACE = 'org.freedesktop.ModemManager.Modem.Gsm.Contacts' SMS_INTFACE = 'org.freedesktop.ModemManager.Modem.Gsm.SMS' NET_INTFACE = 'org.freedesktop.ModemManager.Modem.Gsm.Network' MM_NETWORK_BAND_ANY = 0x1 # MM_NETWORK_MODE_* is a confusing mess of modes and prefs MM_NETWORK_MODE_UNKNOWN = 0x00000000 MM_NETWORK_MODE_ANY = 0x00000001 MM_NETWORK_MODE_GPRS = 0x00000002 MM_NETWORK_MODE_EDGE = 0x00000004 MM_NETWORK_MODE_UMTS = 0x00000008 MM_NETWORK_MODE_HSDPA = 0x00000010 MM_NETWORK_MODE_2G_PREFERRED = 0x00000020 MM_NETWORK_MODE_3G_PREFERRED = 0x00000040 MM_NETWORK_MODE_2G_ONLY = 0x00000080 MM_NETWORK_MODE_3G_ONLY = 0x00000100 MM_NETWORK_MODE_HSUPA = 0x00000200 MM_NETWORK_MODE_HSPA = 0x00000400 MM_ALLOWED_MODE_ANY = 0 MM_ALLOWED_MODE_2G_PREFERRED = 1 MM_ALLOWED_MODE_3G_PREFERRED = 2 MM_ALLOWED_MODE_2G_ONLY = 3 MM_ALLOWED_MODE_3G_ONLY = 4 # should the extensions introduced by the Wader project be tested? TEST_WADER_EXTENSIONS = True # generic message for [wader] skipped tests GENERIC_SKIP_MSG = "Wader extension to MM" CONFIG = { 'pin': '0000', 'puk': None, } def get_bit_list(value): return [(1 << bit) for bit in range(32) if (1 << bit) & value] def get_dbus_error(e): if hasattr(e, 'get_dbus_name'): return e.get_dbus_name() return e.message def get_dbus_message(e): if hasattr(e, 'get_dbus_message'): return e.get_dbus_message() return '' def get_random_string(): return 'Test-' + ''.join(random.choice( string.ascii_uppercase + string.digits) for x in range(11)) def convert_network_mode_to_allowed_mode(mode): trans_table = { MM_NETWORK_MODE_ANY: MM_ALLOWED_MODE_ANY, MM_NETWORK_MODE_2G_PREFERRED: MM_ALLOWED_MODE_2G_PREFERRED, MM_NETWORK_MODE_3G_PREFERRED: MM_ALLOWED_MODE_3G_PREFERRED, MM_NETWORK_MODE_2G_ONLY: MM_ALLOWED_MODE_2G_ONLY, MM_NETWORK_MODE_3G_ONLY: MM_ALLOWED_MODE_3G_ONLY, } return trans_table.get(mode) class DBusTestCase(unittest.TestCase): """Test-suite for ModemManager DBus exported methods""" @classmethod def setUpClass(cls): bus = dbus.SystemBus() obj = bus.get_object(MM_SERVICE, MM_OBJPATH) opaths = obj.EnumerateDevices(dbus_interface=MM_INTFACE) if not len(opaths): raise RuntimeError("Can't run these tests without a device") cls.device = bus.get_object(MM_SERVICE, opaths[0]) def enable_device_cb(): pass def enable_device_eb(e): def send_pin_eb(e): raise RuntimeError("Send PIN failed \"%s\"" % str(e)) def send_puk_eb(e): raise RuntimeError("Send PUK failed \"%s\"" % str(e)) def send_pin_cb(): try: # enable the device cls.device.Enable(True, dbus_interface=MDM_INTFACE) enable_device_cb() except dbus.DBusException, e: send_pin_eb(e) if 'SimPinRequired' in get_dbus_error(e): pin = CONFIG.get('pin', '0000') try: cls.device.SendPin(pin, dbus_interface=CRD_INTFACE) send_pin_cb() except dbus.DBusException, e: send_puk_eb(e) elif 'SimPukRequired' in get_dbus_error(e): puk = CONFIG.get('puk') if not puk: msg = "SimPukRequired error and no PUK defined in config" raise RuntimeError(msg) try: cls.device.SendPuk(puk, pin, dbus_interface=CRD_INTFACE) send_pin_cb() except dbus.DBusException, e: send_puk_eb(e) else: raise RuntimeError("Cannot handle error \"%s\"" % str(e)) try: # enable the device cls.device.Enable(True, dbus_interface=MDM_INTFACE) enable_device_cb() except dbus.DBusException, e: enable_device_eb(e) cls.flag_simready = False @classmethod def tearDownClass(cls): def enable_device_cb(): pass def enable_device_eb(e): raise RuntimeError("Disabling failed \"%s\"" % str(e)) try: # disable the device cls.device.Enable(False, dbus_interface=MDM_INTFACE) enable_device_cb() except dbus.DBusException, e: enable_device_eb(e) ############################################################################### # Helpers follow ############################################################################### def do_wait_for_signal(self, iface, name, duration, func=None, *fargs, **fkwargs): """ Sets up the glib loop and waits for the signal ``name`` to occur on the specified interface ``iface``. On receipt it returns the signal args in a tuple, or if the total ``duration`` was exceeded, None. If a function is specified, it is called once the loop is running """ result = {} main_loop = gobject.MainLoop() def timeout(): main_loop.quit() def handler(*args): result['args'] = args main_loop.quit() sm = self.device.connect_to_signal(name, handler, dbus_interface=iface) if func is not None: gobject.timeout_add(250, func, *fargs, **fkwargs) gid = gobject.timeout_add_seconds(duration, timeout) main_loop.run() gobject.source_remove(gid) sm.remove() return result.get('args') def do_when_registered(self): """ Waits upto 2 minutes for registration and fails the test if it doesn't occur. Many prior tests can leave the card unregistered, use this if you need registration for your test to be successful """ timeout = 120 for t in range(timeout/5): reply = self.device.GetRegistrationInfo(dbus_interface=NET_INTFACE) status, numeric_oper = reply[:2] # we must be registered to our home network or roaming if status in [1, 5]: return time.sleep(5) self.fail("Timeout after %d seconds" % timeout) def do_when_sim_ready(self): name = "Test-Ready" number = "00000000" while not self.flag_simready: try: self.device.Add(name, number, dbus_interface=CTS_INTFACE) self.flag_simready = True except dbus.DBusException, e: if 'SimBusy' in get_dbus_error(e): time.sleep(5) continue else: self.fail(str(e)) def do_remove_all_contacts(self): contacts = self.device.List(dbus_interface=CTS_INTFACE) for contact in contacts: self.device.Delete(contact[0], dbus_interface=CTS_INTFACE) ############################################################################### # Tests follow ############################################################################### # org.freedesktop.DBus.Properties tests def test_MmPropertiesChangedSignal(self): """Test for DBus.Properties.MmPropertiesChanged signal""" # Note: hopefully the trigger for this is normal operation args = self.do_wait_for_signal(None, "MmPropertiesChanged", 120) if args is None: self.fail("Timeout") self.assertEqual(len(args), 2) self.assertIsInstance(args[0], basestring) self.assertIsInstance(args[1], dict) test_MmPropertiesChangedSignal.timeout = 240 # org.freedesktop.ModemManager.Modem tests def test_ModemDeviceProperty(self): """Test for Modem.Device property""" if not sys.platform.startswith('linux'): raise unittest.SkipTest("Cannot be tested on OS != Linux") def check_if_valid_device(device): # Huawei, Novatel, ZTE, Old options, etc. for name in ['tty', 'hso', 'usb', 'wwan']: if name in device: return True return False prop = self.device.Get(MDM_INTFACE, 'Device', dbus_interface=dbus.PROPERTIES_IFACE) self.assertIsInstance(prop, basestring) self.assertTrue(check_if_valid_device(prop)) def test_ModemConnTypeProperty(self): """Test for Modem.ConnType property""" if not TEST_WADER_EXTENSIONS: raise unittest.SkipTest(GENERIC_SKIP_MSG) prop = self.device.Get(MDM_INTFACE, 'ConnType', dbus_interface=dbus.PROPERTIES_IFACE) self.assertIsInstance(prop, dbus.UInt32) # Note: Don't accept '0' as valid here because we want to flag # unknown devices so that we notice and add the correct value self.assertIn(prop, [1, 2, 3, 4, 5, 6, 7]) # XXX: not implemented in Wader def test_ModemDeviceIdentifierProperty(self): """Test for Modem.DeviceIdentifier property""" raise unittest.SkipTest("not implemented in Wader") prop = self.device.Get(MDM_INTFACE, 'DeviceIdentifier', dbus_interface=dbus.PROPERTIES_IFACE) self.assertIsInstance(prop, basestring) def test_ModemDriverProperty(self): """Test for Modem.Driver property""" if not sys.platform.startswith('linux'): raise unittest.SkipTest("Cannot be tested on OS != Linux") prop = self.device.Get(MDM_INTFACE, 'Driver', dbus_interface=dbus.PROPERTIES_IFACE) self.assertIn(prop, ['hso', 'option', 'mbm', 'sierra', 'cdc_ether', 'cdc_wdm', 'cdc_acm', 'qcserial']) def test_ModemEnabledProperty(self): """Test for Modem.Enabled property""" prop = self.device.Get(MDM_INTFACE, 'Enabled', dbus_interface=dbus.PROPERTIES_IFACE) self.assertIsInstance(prop, dbus.Boolean) def test_ModemEquipmentIdentifierProperty(self): """Test for Modem.EquipmentIdentifier property""" prop = self.device.Get(MDM_INTFACE, 'EquipmentIdentifier', dbus_interface=dbus.PROPERTIES_IFACE) self.assertIsInstance(prop, basestring) def test_ModemIpMethodProperty(self): """Test for Modem.IpMethod property""" prop = self.device.Get(MDM_INTFACE, 'IpMethod', dbus_interface=dbus.PROPERTIES_IFACE) self.assertIsInstance(prop, dbus.UInt32) self.assertIn(prop, [0, 1, 2]) # XXX: not implemented in Wader def test_ModemIpTimeoutProperty(self): """Test for Modem.IpTimeout property""" raise unittest.SkipTest("not implemented in Wader") prop = self.device.Get(MDM_INTFACE, 'IpTimeout', dbus_interface=dbus.PROPERTIES_IFACE) self.assertIsInstance(prop, dbus.UInt32) def test_ModemMasterDeviceProperty(self): """Test for Modem.MasterDevice property""" prop = self.device.Get(MDM_INTFACE, 'MasterDevice', dbus_interface=dbus.PROPERTIES_IFACE) self.assertIsInstance(prop, basestring) # XXX: not implemented in Wader # def test_ModemStateChangedSignal(self): """Test for Modem.StateChanged signal""" # XXX: Need a good trigger here, Disable/Enable is no good as if it # fails due to a bug unrelated to this test, all following tests # fail. raise unittest.SkipTest("Untested") # def trigger(): # self.device.Enable(False, dbus_interface=MDM_INTFACE) # # def cleanup(): # try: # self.device.Enable(True, dbus_interface=MDM_INTFACE) # # except dbus.DBusException, e: # if 'SimPinRequired' in get_dbus_error(e): # pin = CONFIG.get('pin', '0000') # try: # self.device.SendPin(pin, dbus_interface=CRD_INTFACE) # # except dbus.DBusException, e: # self.fail(str(e)) # else: # self.fail(str(e)) # # # We need to give a chance to reregister, but don't want to raise # # an error if it doesn't occur # time.sleep(30) # # self.addCleanup(cleanup) # # args = self.do_wait_for_signal(MDM_INTFACE, "StateChanged", 120, # trigger) # if args is None: # self.fail("Timeout") # # self.assertEqual(len(args), 3) # self.assertIsInstance(args[0], dbus.UInt32) # self.assertIsInstance(args[1], dbus.UInt32) # self.assertIsInstance(args[2], dbus.UInt32) # # self.assertIn(args[0], [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]) # old # self.assertIn(args[1], [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]) # new # self.assertIn(args[2], [0, 1, 2]) # reason # # test_ModemStateChangedSignal.timeout = 240 def test_ModemStateProperty(self): """Test for Modem.State property""" prop = self.device.Get(MDM_INTFACE, 'State', dbus_interface=dbus.PROPERTIES_IFACE) self.assertIsInstance(prop, dbus.UInt32) self.assertIn(prop, [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]) def test_ModemUnlockRequiredProperty(self): """Test for Modem.UnlockRequired property""" prop = self.device.Get(MDM_INTFACE, 'UnlockRequired', dbus_interface=dbus.PROPERTIES_IFACE) self.assertIsInstance(prop, basestring) def test_ModemUnlockRetriesProperty(self): """Test for Modem.UnlockRetries property""" prop = self.device.Get(MDM_INTFACE, 'UnlockRetries', dbus_interface=dbus.PROPERTIES_IFACE) self.assertIsInstance(prop, dbus.UInt32) def test_ModemGetInfo(self): """Test for Modem.GetInfo""" info = self.device.GetInfo(dbus_interface=MDM_INTFACE) self.assertEqual(len(info), 3) self.assertIsInstance(info[0], basestring) self.assertIsInstance(info[1], basestring) self.assertIsInstance(info[2], basestring) def test_ModemFactoryReset(self): """Test for Modem.FactoryReset""" raise unittest.SkipTest("Untested") # org.freedesktop.ModemManager.Modem.Gsm.Card tests def _test_CardChangePin(self, charset): self.device.SetCharset(charset, dbus_interface=CRD_INTFACE) good_pin = CONFIG.get('pin', '0000') bad_pin = '1111' # if this operations don't fail we can assume it is working self.device.ChangePin(good_pin, bad_pin, dbus_interface=CRD_INTFACE) self.device.ChangePin(bad_pin, good_pin, dbus_interface=CRD_INTFACE) def test_CardChangePinGsm(self): """Test for Modem.Gsm.Card.ChangePin in GSM charset""" self._test_CardChangePin('GSM') def test_CardChangePinUcs2(self): """Test for Modem.Gsm.Card.ChangePin in UCS2 charset""" self._test_CardChangePin('UCS2') def test_CardCheck(self): """Test for Modem.Gsm.Card.Check""" if not TEST_WADER_EXTENSIONS: raise unittest.SkipTest(GENERIC_SKIP_MSG) status = self.device.Check(dbus_interface=CRD_INTFACE) self.assertEqual(status, "READY") def test_CardEnableEcho(self): """Test for Modem.Gsm.Card.EnableEcho""" # disabling Echo will probably leave Wader unusable raise unittest.SkipTest("Untestable method") def _test_CardEnablePin(self, charset): self.device.SetCharset(charset, dbus_interface=CRD_INTFACE) pin = CONFIG.get('pin', '0000') # disable and enable PIN auth, if these operations don't fail # we can assume that the underlying implementation works self.device.EnablePin(pin, False, dbus_interface=CRD_INTFACE) self.device.EnablePin(pin, True, dbus_interface=CRD_INTFACE) def test_CardEnablePinGsm(self): """Test for Modem.Gsm.Card.EnablePin in GSM charset""" self._test_CardEnablePin('GSM') def test_CardEnablePinUcs2(self): """Test for Modem.Gsm.Card.EnablePin in UCS2 charset""" self._test_CardEnablePin('UCS2') def test_CardGetCharset(self): """Test for Modem.Gsm.Card.GetCharset""" charset = self.device.GetCharset(dbus_interface=CRD_INTFACE) self.assertIn(charset, ['GSM', 'IRA', 'UCS2']) def test_CardGetCharsets(self): """Test for Modem.Gsm.Card.GetCharsets""" charsets = self.device.GetCharsets(dbus_interface=CRD_INTFACE) self.assertIn('IRA', charsets) self.assertIn('UCS2', charsets) def test_CardGetImei(self): """Test for Modem.Gsm.Card.GetImei""" imei = self.device.GetImei(dbus_interface=CRD_INTFACE) self.assertRegexpMatches(imei, '^\d{14,17}$') # 14 <= IMEI <= 17 def test_CardGetImsi(self): """Test for Modem.Gsm.Card.GetImsi""" imsi = self.device.GetImsi(dbus_interface=CRD_INTFACE) # according to http://en.wikipedia.org/wiki/IMSI there are # also IMSIs with 14 digits self.assertRegexpMatches(imsi, '^\d{14,15}$') # 14 <= IMSI <= 15 def test_CardGetOperatorId(self): """Test for Modem.Gsm.Card.GetOperatorId""" imsi = self.device.GetImsi(dbus_interface=CRD_INTFACE) known_good_sims = [] known_good_sims.append('234159222401636') # ASDA Mobile known_good_sims.append('234107305239842') # TESCO known_good_sims.append('214012300907507') # VF-ES try: # We have to order this list so the longest matches are first. # IMSI prefix, length of MCC+MNC for sim in [('23415', 5), # VF-UK ('23410', 5), # O2-UK ('21403', 5), # Orange-ES, Masmovil-ES ('21401', 5)]: # VF-ES if imsi.startswith(sim[0]): mcc_mnc = self.device.GetOperatorId( dbus_interface=CRD_INTFACE) self.assertEqual(mcc_mnc, sim[0][:sim[1]], "mcc_mnc=%s : sim_first=%s" % \ (mcc_mnc, sim[0][:sim[1]])) return except dbus.DBusException as e: if imsi in known_good_sims: self.fail("Failure with known good SIM") # We know some SIM cards will fail. GENERAL = MDM_INTFACE + '.General' txt = '%s not in %s' % (GENERAL, get_dbus_error(e)) msg = get_dbus_message(e) if len(msg): txt = '%s: dbus_message=%s' % (txt, msg) self.assertIn(GENERAL, get_dbus_error(e), txt) raise unittest.SkipTest("Failure, but not known if SIM is old") raise unittest.SkipTest("Untested") def test_CardGetSpn(self): """Test for Modem.Gsm.Card.GetSpn""" self.do_when_registered() known_good_sims = [] known_good_sims.append('234159222401636') # ASDA Mobile known_good_sims.append('234107305239842') # TESCO known_good_sims.append('214035453022694') # MASmovil imsi = self.device.GetImsi(dbus_interface=CRD_INTFACE) # Note: It's difficult to determine MVNO SIMs from MNOs issued ones # so unless we can find a better method of telling them apart # we have to do exact matching on the whole IMSI. try: for sim in [('234159222401636', 'ASDA Mobile'), ('23415', ''), # VF-UK ('234107305239842', 'TESCO'), ('23410', ''), # O2-UK ('214035453022694', 'MASmovil'), ('21403', ''), # Orange-ES ('21401', '')]: # Vodafone-ES if imsi.startswith(sim[0]): spn = self.device.GetSpn(dbus_interface=CRD_INTFACE) self.assertEqual(spn, sim[1]) return except dbus.DBusException as e: if imsi in known_good_sims: self.fail("Failure with known good SIM") # We know some SIM cards will fail. GENERAL = MDM_INTFACE + '.General' txt = '%s not in %s' % (GENERAL, get_dbus_error(e)) msg = get_dbus_message(e) if len(msg): txt = '%s: dbus_message=%s' % (txt, msg) self.assertIn(GENERAL, get_dbus_error(e), txt) raise unittest.SkipTest( "Failure, but not known if SIM has a populated SPN") raise unittest.SkipTest("Untested") test_CardGetSpn.timeout = 60 def test_CardSimIdentifier(self): """Test for Modem.Gsm.Card.SimIdentifier property.""" iccid = self.device.Get(CRD_INTFACE, 'SimIdentifier', dbus_interface=dbus.PROPERTIES_IFACE) msg = 'ICCID "%s" is not valid number string' % iccid self.assertRegexpMatches(iccid, r'89\d{16,17}', msg) try: from stdnum import luhn msg = 'ICCID "%s" does not pass Luhn algorithm validity test.' \ % iccid self.assertTrue(luhn.is_valid(iccid), msg) except ImportError: raise unittest.SkipTest('stdnum module not installed') def test_CardResetSettings(self): """Test for Modem.Gsm.Card.ResetSettings""" if not TEST_WADER_EXTENSIONS: raise unittest.SkipTest(GENERIC_SKIP_MSG) raise unittest.SkipTest("Untested") def test_CardSendATString(self): """Test for Modem.Gsm.Card.SendATString""" if not TEST_WADER_EXTENSIONS: raise unittest.SkipTest(GENERIC_SKIP_MSG) raise unittest.SkipTest("Untested") def test_CardSendPin(self): """Test for Modem.Gsm.Card.SendPin""" raise unittest.SkipTest("Untested") def test_CardSendPuk(self): """Test for Modem.Gsm.Card.SendPuk""" raise unittest.SkipTest("Untested") def test_CardSetCharset(self): """Test for Modem.Gsm.Card.SetCharset""" charsets = ["IRA", "GSM", "UCS2"] # get the current charset charset = self.device.GetCharset(dbus_interface=CRD_INTFACE) self.assertIn(charset, charsets) # now pick a new charset new_charset = random.choice(charsets) while new_charset == charset: new_charset = random.choice(charsets) # set the charset to new_charset self.device.SetCharset(new_charset, dbus_interface=CRD_INTFACE) _charset = self.device.GetCharset(dbus_interface=CRD_INTFACE) # leave everything as found self.device.SetCharset(charset, dbus_interface=CRD_INTFACE) # check that the new charset is the expected one self.assertEqual(new_charset, _charset) def test_CardSupportedBandsProperty(self): """Test for Modem.Gsm.Card.SupportedBands property""" bands = self.device.Get(CRD_INTFACE, 'SupportedBands', dbus_interface=dbus.PROPERTIES_IFACE) if not bands: raise unittest.SkipTest("Cannot be tested") self.assertNotIn(MM_NETWORK_BAND_ANY, get_bit_list(bands)) def test_CardSupportedModesProperty(self): """Test for Modem.Gsm.Card.SupportedModes property""" modes = self.device.Get(CRD_INTFACE, 'SupportedModes', dbus_interface=dbus.PROPERTIES_IFACE) if not modes: raise unittest.SkipTest("Cannot be tested") blist = get_bit_list(modes) self.assertNotIn(MM_NETWORK_MODE_ANY, blist) # Check we are only advertising preferences self.assertNotIn(MM_NETWORK_MODE_GPRS, blist) self.assertNotIn(MM_NETWORK_MODE_EDGE, blist) self.assertNotIn(MM_NETWORK_MODE_UMTS, blist) self.assertNotIn(MM_NETWORK_MODE_HSDPA, blist) self.assertNotIn(MM_NETWORK_MODE_HSUPA, blist) self.assertNotIn(MM_NETWORK_MODE_HSPA, blist) # org.freedesktop.ModemManager.Modem.Gsm.Contacts tests def test_ContactsAdd(self): """Test for Modem.Gsm.Contacts.Add Ascii""" self.do_when_sim_ready() self.do_remove_all_contacts() name, number = "John", "+435443434343" # add a contact with ascii data index = self.device.Add(name, number, dbus_interface=CTS_INTFACE) # get the object via DBus and check that its data is correct _index, _name, _number = self.device.Get(index, dbus_interface=CTS_INTFACE) # leave everything as found self.device.Delete(_index, dbus_interface=CTS_INTFACE) self.assertEqual(name, _name) self.assertEqual(number, _number) def test_ContactsAdd_UTF8_name(self): """Test for Modem.Gsm.Contacts.Add UTF8""" self.do_when_sim_ready() self.do_remove_all_contacts() name, number = u"中华人民共和", "+43544311113" # add a contact with UTF8 data index = self.device.Add(name, number, dbus_interface=CTS_INTFACE) # get the object via DBus and check that its data is correct _index, _name, _number = self.device.Get(index, dbus_interface=CTS_INTFACE) # leave everything as found self.device.Delete(_index, dbus_interface=CTS_INTFACE) self.assertEqual(name, _name) self.assertEqual(number, _number) def test_ContactsDelete(self): """Test for Modem.Gsm.Contacts.Delete""" self.do_when_sim_ready() self.do_remove_all_contacts() name, number = "Juan", "+21544343434" # add a contact, and delete it index = self.device.Add(name, number, dbus_interface=CTS_INTFACE) # now delete it and check that its index is no longer present # if we list all the contacts self.device.Delete(index, dbus_interface=CTS_INTFACE) contacts = self.device.List(dbus_interface=CTS_INTFACE) self.assertNotIn(index, [c[0] for c in contacts]) def test_ContactsEdit(self): """Test for Modem.Gsm.Contacts.Edit""" self.do_when_sim_ready() self.do_remove_all_contacts() name, number = "Eugenio", "+435345342121" new_name, new_number = "Eugenia", "+43542323122" # add a contact index = self.device.Add(name, number, dbus_interface=CTS_INTFACE) # edit it and get by index to check that the new values are set self.device.Edit(index, new_name, new_number, dbus_interface=CTS_INTFACE) _index, _name, _number = self.device.Get(index, dbus_interface=CTS_INTFACE) # leave everything as found self.device.Delete(_index, dbus_interface=CTS_INTFACE) self.assertEqual(_name, new_name) self.assertEqual(_number, new_number) def test_ContactsFindByName(self): """Test for Modem.Gsm.Contacts.FindByName""" self.do_when_sim_ready() self.do_remove_all_contacts() # add three contacts with similar names data = [('JohnOne', '+34656575757'), ('JohnTwo', '+34666575757'), ('JohnThree', '+34766575757')] indexes = [self.device.Add(name, number, dbus_interface=CTS_INTFACE) for name, number in data] def cleanup(): for index in indexes: self.device.Delete(index, dbus_interface=CTS_INTFACE) self.addCleanup(cleanup) # now search by name and make sure the matches match search_data = [('John', 3), ('JohnT', 2), ('JohnOne', 1)] for name, expected_matches in search_data: contacts = self.device.FindByName(name, dbus_interface=CTS_INTFACE) self.assertEqual(len(contacts), expected_matches) def test_ContactsFindByNumber(self): """Test for Modem.Gsm.Contacts.FindByNumber""" self.do_when_sim_ready() self.do_remove_all_contacts() # add three contacts with similar numbers data = [('JohnOne', '+34656575757'), ('JohnTwo', '+34666575757'), ('JohnThree', '+34766575757')] indexes = [self.device.Add(name, number, dbus_interface=CTS_INTFACE) for name, number in data] def cleanup(): for index in indexes: self.device.Delete(index, dbus_interface=CTS_INTFACE) self.addCleanup(cleanup) # now search by number and make sure the matches match search_data = [('575757', 3), ('66575757', 2), ('+34666575757', 1)] for number, expected_matches in search_data: contacts = self.device.FindByNumber(number, dbus_interface=CTS_INTFACE) self.assertEqual(len(contacts), expected_matches) def test_ContactsGet(self): """Test for Modem.Gsm.Contacts.Get""" self.do_when_sim_ready() self.do_remove_all_contacts() name, number = "Mario", "+312232332" index = self.device.Add(name, number, dbus_interface=CTS_INTFACE) reply = self.device.Get(index, dbus_interface=CTS_INTFACE) self.device.Delete(index, dbus_interface=CTS_INTFACE) self.assertIn(name, reply) self.assertIn(number, reply) self.assertIn(index, reply) def test_ContactsGetCount(self): """Test for Modem.Gsm.Contacts.GetCount""" self.do_when_sim_ready() count = self.device.GetCount(dbus_interface=CTS_INTFACE) contacts = self.device.List(dbus_interface=CTS_INTFACE) self.assertIsInstance(count, dbus.UInt32) self.assertEqual(count, len(contacts)) def test_ContactsGetCount_2(self): """Test for Modem.Gsm.Contacts.GetCount 2""" self.do_when_sim_ready() self.do_remove_all_contacts() count = self.device.GetCount(dbus_interface=CTS_INTFACE) index = self.device.Add("Boethius", "+21123322323", dbus_interface=CTS_INTFACE) count2 = self.device.GetCount(dbus_interface=CTS_INTFACE) self.device.Delete(index, dbus_interface=CTS_INTFACE) self.assertEqual(count + 1, count2) def test_ContactsGetPhonebookSize(self): """Test for Modem.Gsm.Contacts.GetPhonebookSize""" if not TEST_WADER_EXTENSIONS: raise unittest.SkipTest(GENERIC_SKIP_MSG) size = self.device.GetPhonebookSize(dbus_interface=CTS_INTFACE) self.assertIsInstance(size, dbus.Int32) self.assertGreaterEqual(size, 100) def test_ContactsList(self): """Test for Modem.Gsm.Contacts.List""" self.do_when_sim_ready() self.do_remove_all_contacts() name, number = "Jauma", "+356456445654" index = self.device.Add(name, number, dbus_interface=CTS_INTFACE) reply = self.device.List(dbus_interface=CTS_INTFACE) # leave everything as found self.device.Delete(index, dbus_interface=CTS_INTFACE) found = False for contact in reply: if (index, name, number) == contact: found = True break self.assertTrue(found) # org.freedesktop.ModemManager.Modem.Gsm.Network tests def test_NetworkGetApns(self): """Test for Modem.Gsm.Network.GetApns""" if not TEST_WADER_EXTENSIONS: raise unittest.SkipTest(GENERIC_SKIP_MSG) l = self.device.GetApns(dbus_interface=NET_INTFACE) self.assertIsInstance(l, list) if len(l): self.assertIsInstance(l[0], tuple) self.assertIsInstance(l[0][0], dbus.UInt32) self.assertIsInstance(l[0][1], basestring) def test_NetworkGetBand(self): """Test for Modem.Gsm.Network.GetBand""" band = self.device.GetBand(dbus_interface=NET_INTFACE) self.assertIsInstance(band, dbus.UInt32) self.assertGreater(band, 0) def test_NetworkGetNetworkMode(self): """Test for Modem.Gsm.Network.GetNetworkMode""" mode = self.device.GetNetworkMode(dbus_interface=NET_INTFACE) self.assertIsInstance(mode, dbus.UInt32) # currently goes between 0 and 0x400 self.assertGreaterEqual(mode, 0) self.assertLessEqual(mode, 0x400) def test_NetworkGetRegistrationInfo(self): """Test for Modem.Gsm.Network.GetRegistrationInfo""" reply = self.device.GetRegistrationInfo(dbus_interface=NET_INTFACE) status, numeric_oper = reply[:2] self.assertIsInstance(status, dbus.UInt32) self.assertIsInstance(numeric_oper, basestring) # we must be registered to our home network or roaming self.assertIn(status, [1, 5]) if status == 1: # home registration # get the IMSI and check that we are connected to a network # with a netid that matches the beginning of our IMSI imsi = self.device.GetImsi(dbus_interface=CRD_INTFACE) # we should be registered with our home network msg = "%s doesn't start with %s" % (imsi, numeric_oper) self.assertTrue(imsi.startswith(numeric_oper), msg) def test_NetworkGetRoamingIDs(self): """Test for Modem.Gsm.Network.GetRoamingIDs""" if not TEST_WADER_EXTENSIONS: raise unittest.SkipTest(GENERIC_SKIP_MSG) l = self.device.GetRoamingIDs(dbus_interface=NET_INTFACE) self.assertIsInstance(l, list) self.assertGreater(len(l), 0, "Empty list %s" % str(l)) self.assertIsInstance(l[0], basestring) self.assertTrue(all(map(int, l)), "IDs = %s" % str(l)) def test_NetworkGetSignalQuality(self): """Test for Modem.Gsm.Network.GetSignalQuality""" self.do_when_registered() quality = self.device.GetSignalQuality(dbus_interface=NET_INTFACE) self.assertIsInstance(quality, dbus.UInt32) self.assertGreaterEqual(quality, 0) self.assertLessEqual(quality, 100) def test_NetworkLastApnProperty(self): """Test for Modem.Gsm.Network.LastApn property""" if not TEST_WADER_EXTENSIONS: raise unittest.SkipTest(GENERIC_SKIP_MSG) prop = self.device.Get(NET_INTFACE, 'LastApn', dbus_interface=dbus.PROPERTIES_IFACE) self.assertIsInstance(prop, basestring) def test_NetworkScan(self): """Test for Modem.Gsm.Network.Scan""" netreg = self.device.GetRegistrationInfo(dbus_interface=NET_INTFACE) # potentially long operation, increasing timeout to 360 as core now # waits up to 300 secs networks = self.device.Scan(dbus_interface=NET_INTFACE, timeout=360) self.assertGreater(len(networks), 0) # our home network has to be around unless we are roaming ;) if netreg[0] == 1: # using the IMSI check that it's around imsi = self.device.GetImsi(dbus_interface=CRD_INTFACE) netids = [network['operator-num'] for network in networks] msg = "IMSI %s doesn't start with any of %s" % (imsi, str(netids)) self.assertIn(True, map(imsi.startswith, netids), msg) def _test_NetworkSetApn(self, charset): # Set Charset self.device.SetCharset(charset, dbus_interface=CRD_INTFACE) # Write and read back apn = get_random_string() self.device.SetApn(apn, dbus_interface=NET_INTFACE) l = self.device.GetApns(dbus_interface=NET_INTFACE) self.assertIsInstance(l, list) self.assertGreater(len(l), 0, "Empty list %s" % str(l)) self.assertIsInstance(l[0], tuple) self.assertIn(apn, [a[1] for a in l]) def test_NetworkSetApnGsm(self): """Test for Modem.Gsm.Network.SetApn using GSM charset""" self._test_NetworkSetApn('GSM') def test_NetworkSetApnUcs2(self): """Test for Modem.Gsm.Network.SetApn using UCS2 charset""" self._test_NetworkSetApn('UCS2') def test_NetworkSetApnWithMixedCharsets(self): """Test for Modem.Gsm.Network.SetApn using mixed charsets""" # GSM -> UCS2 apn1 = get_random_string() self.device.SetCharset('GSM', dbus_interface=CRD_INTFACE) self.device.SetApn(apn1, dbus_interface=NET_INTFACE) self.device.SetCharset('UCS2', dbus_interface=CRD_INTFACE) list1 = self.device.GetApns(dbus_interface=NET_INTFACE) # UCS2 -> GSM apn2 = get_random_string() self.device.SetCharset('UCS2', dbus_interface=CRD_INTFACE) self.device.SetApn(apn2, dbus_interface=NET_INTFACE) self.device.SetCharset('GSM', dbus_interface=CRD_INTFACE) list2 = self.device.GetApns(dbus_interface=NET_INTFACE) self.assertIsInstance(list1, list) self.assertIsInstance(list2, list) self.assertIn(apn1, [a[1] for a in list1]) self.assertIn(apn2, [a[1] for a in list2]) def test_NetworkSetBand(self): """Test for Modem.Gsm.Network.SetBand""" bands = self.device.Get(CRD_INTFACE, 'SupportedBands', dbus_interface=dbus.PROPERTIES_IFACE) if not bands: raise unittest.SkipTest("Cannot be tested") def set_band_any(): # leave it in BAND_ANY and give it some seconds to settle self.device.SetBand(MM_NETWORK_BAND_ANY) time.sleep(5) self.addCleanup(set_band_any) for band in get_bit_list(bands): self.device.SetBand(band) _band = self.device.GetBand() self.assertEqual(_band, band) def test_NetworkSetAllowedMode(self): """Test for Modem.Gsm.Network.SetAllowedMode""" modes = self.device.Get(CRD_INTFACE, 'SupportedModes', dbus_interface=dbus.PROPERTIES_IFACE) if not modes: raise unittest.SkipTest("Cannot be tested") def set_mode_any(): # leave it in MODE_ANY and give it some seconds to settle self.device.SetAllowedMode(MM_ALLOWED_MODE_ANY) time.sleep(5) self.addCleanup(set_mode_any) for net_mode in get_bit_list(modes): mode = convert_network_mode_to_allowed_mode(net_mode) if mode is not None: self.device.SetAllowedMode(mode) allowed_mode = self.device.Get(NET_INTFACE, 'AllowedMode', dbus_interface=dbus.PROPERTIES_IFACE) self.assertEqual(allowed_mode, mode) def test_NetworkSetNetworkMode(self): """Test for Modem.Gsm.Network.SetNetworkMode""" modes = self.device.Get(CRD_INTFACE, 'SupportedModes', dbus_interface=dbus.PROPERTIES_IFACE) if not modes: raise unittest.SkipTest("Cannot be tested") def set_mode_any(): # leave it in MODE_ANY and give it some seconds to settle self.device.SetAllowedMode(MM_ALLOWED_MODE_ANY) time.sleep(5) self.addCleanup(set_mode_any) for mode in get_bit_list(modes): self.device.SetNetworkMode(mode) network_mode = self.device.GetNetworkMode() self.assertEqual(network_mode, mode) def test_NetworkSetRegistrationNotification(self): """Test for Modem.Gsm.Network.SetRegistrationNotification""" raise unittest.SkipTest("Untested") def test_NetworkSetInfoFormat(self): """Test for Modem.Gsm.Network.SetInfoFormat""" raise unittest.SkipTest("Untested") def test_NetworkRegister(self): """Test for Modem.Gsm.Network.Register""" raise unittest.SkipTest("Untested") def test_NetworkAccessTechnologyProperty(self): """Test for Modem.Gsm.Network.AccessTechnology property""" access_tech = self.device.Get(NET_INTFACE, "AccessTechnology", dbus_interface=dbus.PROPERTIES_IFACE) self.assertIsInstance(access_tech, dbus.UInt32) self.assertGreaterEqual(access_tech, 0) self.assertLessEqual(access_tech, 10) def test_NetworkAllowedModeProperty(self): """Test for Modem.Gsm.Network.AllowedMode property""" # tested in NetworkSetAllowedMode pass def test_NetworkSignalQualitySignal(self): """Test for Modem.Gsm.Network.SignalQuality signal""" # Note: the trigger for this is the normal sig strength daemon args = self.do_wait_for_signal(NET_INTFACE, "SignalQuality", 180) if args is None: self.fail("Timeout") self.assertEqual(len(args), 1) self.assertIsInstance(args[0], dbus.UInt32) self.assertGreaterEqual(args[0], 0) self.assertLessEqual(args[0], 100) test_NetworkSignalQualitySignal.timeout = 240 def test_NetworkRegistrationInfoSignal(self): """Test for Modem.Gsm.Network.RegistrationInfo signal""" # Note: the trigger for this is the normal net registration daemon args = self.do_wait_for_signal(NET_INTFACE, "RegistrationInfo", 240) if args is None: self.fail("Timeout") self.assertEqual(len(args), 3) self.assertIsInstance(args[0], dbus.UInt32) self.assertIsInstance(args[1], basestring) self.assertIsInstance(args[2], basestring) self.assertIn(args[0], range(5)) test_NetworkRegistrationInfoSignal.timeout = 300 # org.freedesktop.ModemManager.Modem.Simple tests def test_SimpleConnect(self): """Test for Modem.Simple.Connect""" raise unittest.SkipTest("Untested") def test_SimpleDisconnect(self): """Test for Modem.Simple.Disconnect""" raise unittest.SkipTest("Untested") def test_SimpleGetStatus(self): """Test for Modem.Simple.GetStatus""" status = self.device.GetStatus(dbus_interface=SPL_INTFACE) self.assertIn('band', status) self.assertIn('signal_quality', status) self.assertIn('operator_code', status) self.assertIn('operator_name', status) self.assertIsInstance(status['operator_name'], basestring) self.assertIsInstance(status['operator_code'], basestring) self.assertIsInstance(status['signal_quality'], dbus.UInt32) self.assertIsInstance(status['band'], dbus.UInt32) # org.freedesktop.ModemManager.Modem.Gsm.SMS tests def test_SmsDelete(self): """Test for Modem.Gsm.SMS.Delete""" sms = {'number': '+33622754135', 'text': 'delete test'} # save a sms, delete it and check is no longer present indexes = self.device.Save(sms, dbus_interface=SMS_INTFACE) self.assertEqual(len(indexes), 1) self.device.Delete(indexes[0], dbus_interface=SMS_INTFACE) sms_found = False messages = self.device.List(dbus_interface=SMS_INTFACE) for msg in messages: if msg['index'] == indexes[0]: sms_found = True break # the index should not be present self.assertEqual(sms_found, False) def test_SmsDeleteMultiparted(self): """Test for Modem.Gsm.SMS.Delete - Multipart""" sms = {'number': '+34622754135', 'text': """test_SmsDeleteMultiparted test_SmsDeleteMultiparted test_SmsDeleteMultiparted test_SmsDeleteMultiparted test_SmsDeleteMultiparted test_SmsDeleteMultiparted test_SmsDeleteMultiparted test_SmsDeleteMultiparted """} # save a sms, delete it and check is no longer present indexes = self.device.Save(sms, dbus_interface=SMS_INTFACE) self.assertEqual(len(indexes), 1) self.device.Delete(indexes[0], dbus_interface=SMS_INTFACE) messages = self.device.List(dbus_interface=SMS_INTFACE) sms_found = False for msg in messages: if msg['index'] == indexes[0]: sms_found = True # the index should not be present self.assertEqual(sms_found, False) def test_SmsGet(self): """Test for Modem.Gsm.SMS.Get""" sms = {'number': '+33646754145', 'text': 'get test'} # save the message, get it by index, and check its values match indexes = self.device.Save(sms, dbus_interface=SMS_INTFACE) _sms = self.device.Get(indexes[0], dbus_interface=SMS_INTFACE) # leave everything as found self.device.Delete(_sms['index'], dbus_interface=SMS_INTFACE) self.assertEqual(sms['number'], _sms['number']) self.assertEqual(sms['text'], _sms['text']) def test_SmsGetMultiparted(self): """Test for Modem.Gsm.SMS.Get - Multipart""" sms = {'number': '+34622754135', 'text': """test_SmsGetMultiparted test_SmsGetMultiparted test_SmsGetMultiparted test_SmsGetMultiparted test_SmsGetMultiparted test_SmsGetMultiparted test_SmsGetMultiparted test_SmsGetMultiparted """} # save the message, get it by index, and check its values match indexes = self.device.Save(sms, dbus_interface=SMS_INTFACE) _sms = self.device.Get(indexes[0], dbus_interface=SMS_INTFACE) # leave everything as found self.device.Delete(_sms['index'], dbus_interface=SMS_INTFACE) self.assertEqual(sms['number'], _sms['number']) self.assertEqual(sms['text'], _sms['text']) def test_SmsGetSmsc(self): """Test for Modem.Gsm.SMS.GetSmsc""" smsc = self.device.GetSmsc(dbus_interface=SMS_INTFACE) self.assertTrue(smsc.startswith('+')) def test_SmsGetFormat(self): """Test for Modem.Gsm.SMS.GetFormat""" fmt = self.device.GetFormat(dbus_interface=SMS_INTFACE) self.assertIsInstance(fmt, dbus.UInt32) self.assertIn(fmt, [0, 1]) def test_SmsList(self): """Test for Modem.Gsm.SMS.List""" sms = {'number': '+33622754135', 'text': 'list test'} indexes = self.device.Save(sms, dbus_interface=SMS_INTFACE) messages = self.device.List(dbus_interface=SMS_INTFACE) # leave everything as found self.device.Delete(indexes[0], dbus_interface=SMS_INTFACE) # now check that the indexes are present in a List sms_found = False for msg in messages: if msg['index'] == indexes[0]: sms_found = True break self.assertEqual(sms_found, True) def test_SmsList_2(self): """Test for Modem.Gsm.SMS.List - 2""" # get the current number of Sms size_before = len(self.device.List(dbus_interface=SMS_INTFACE)) # add three new ones messages = [ {'number': '+324342322', 'text': 'hey there'}, {'number': '+334223312', 'text': 'where you at?'}, {'number': '+324323232', 'text': 'hows it going?'}] indexes = [] for sms in messages: indexes.extend(self.device.Save(sms, dbus_interface=SMS_INTFACE)) size_after = len(self.device.List(dbus_interface=SMS_INTFACE)) # leave everything as found for index in indexes: self.device.Delete(index, dbus_interface=SMS_INTFACE) # check that the size has increased just three self.assertEqual(size_before + 3, size_after) def test_SmsListMultiparted(self): """Test for Modem.Gsm.SMS.List - Multipart""" sms = {'number': '+34622754135', 'text': """test_SmsListMultiparted test_SmsListMultiparted test_SmsListMultiparted test_SmsListMultiparted test_SmsListMultiparted test_SmsListMultiparted test_SmsListMultiparted test_SmsListMultiparted"""} indexes = self.device.Save(sms, dbus_interface=SMS_INTFACE) # now check that the indexes are present in a List messages = self.device.List(dbus_interface=SMS_INTFACE) # leave everything as found self.device.Delete(indexes[0], dbus_interface=SMS_INTFACE) sms_found = False for msg in messages: if msg['index'] == indexes[0]: sms_found = True break self.assertEqual(sms_found, True) def test_SmsListMultiparted_2(self): """Test for Modem.Gsm.SMS.List - Multipart 2""" # get the current number of Sms size_before = len(self.device.List(dbus_interface=SMS_INTFACE)) # add three new ones what = [ {'number': '+324342322', 'text': 'hey there'}, {'number': '+34622754135', 'text': """test_SmsListMultiparted_2 test_SmsListMultiparted_2 test_SmsListMultiparted_2 test_SmsListMultiparted_2 test_SmsListMultiparted_2 test_SmsListMultiparted_2 test_SmsListMultiparted_2 test_SmsListMultiparted_2 """}, {'number': '+34622754135', 'text': """test_SmsListMultiparted_2 test_SmsListMultiparted_2 test_SmsListMultiparted_2 test_SmsListMultiparted_2 test_SmsListMultiparted_2 test_SmsListMultiparted_2 test_SmsListMultiparted_2 test_SmsListMultiparted_2 """}, {'number': '+324323232', 'text': 'hows it going?'}] indexes = [] for sms in what: indexes.extend(self.device.Save(sms, dbus_interface=SMS_INTFACE)) size_after = len(self.device.List(dbus_interface=SMS_INTFACE)) # leave everything as found for index in indexes: self.device.Delete(index, dbus_interface=SMS_INTFACE) # check that the size has increased just three self.assertEqual(size_before + 4, size_after) def test_SmsSave(self): """Test for Modem.Gsm.SMS.Save""" sms = {'number': '+34645454445', 'text': 'save test'} # save the message, get it by index, and check its values match indexes = self.device.Save(sms, dbus_interface=SMS_INTFACE) _sms = self.device.Get(indexes[0], dbus_interface=SMS_INTFACE) # leave everything as found self.device.Delete(_sms['index'], dbus_interface=SMS_INTFACE) self.assertEqual(sms['number'], _sms['number']) self.assertEqual(sms['text'], _sms['text']) def test_SmsSaveMultiparted(self): """Test for Modem.Gsm.SMS.Save - Multipart""" sms = {'number': '+34622754135', 'text': """test_SmsSaveMultiparted test_SmsSaveMultiparted test_SmsSaveMultiparted test_SmsSaveMultiparted test_SmsSaveMultiparted test_SmsSaveMultiparted test_SmsSaveMultiparted test_SmsSaveMultiparted """} # save the message, get it by index, and check its values match indexes = self.device.Save(sms, dbus_interface=SMS_INTFACE) _sms = self.device.Get(indexes[0], dbus_interface=SMS_INTFACE) # leave everything as found self.device.Delete(_sms['index'], dbus_interface=SMS_INTFACE) self.assertEqual(sms['number'], _sms['number']) self.assertEqual(sms['text'], _sms['text']) def test_SmsSend(self): """Test for Modem.Gsm.SMS.Send""" raise unittest.SkipTest("Not ready") #number = config.get('test', 'phone') #if not number: # raise unittest.SkipTest("Cannot run this test without a number") #d = defer.Deferred() #sm = None # SignalMatch #sms = {'number' : number, 'text' : 'send test'} #def on_sms_received_cb(index, complete): # def compare_messages(_sms): # self.assertEqual(_sms['text'], sms['text']) # sm.remove() # remove SignalMatch # # leave everything as found # self.device.Delete(index, dbus_interface=SMS_INTFACE, # reply_handler=lambda: d.callback(True), # error_handler=d.errback) # self.device.Get(index, dbus_interface=SMS_INTFACE, # reply_handler=compare_messages, # error_handler=d.errback) #sm = self.device.connect_to_signal("SmsReceived", on_sms_received_cb, # dbus_interface=SMS_INTFACE) #self.device.Send(sms, dbus_interface=SMS_INTFACE, # # we are not interested in the callback # reply_handler=lambda indexes: None, # error_handler=d.errback) #return d def test_SmsSendFromStorage(self): """Test for Modem.Gsm.SMS.SendFromStorage""" raise unittest.SkipTest("Not ready") #number = config.get('test', 'phone') #if not number: # raise unittest.SkipTest("Cannot run this test without a number") #d = defer.Deferred() #sm = None # SignalMatch #sms = {'number' : number, 'text' : 'send from storage test' } #def on_sms_received_cb(index, complete): # def compare_messages(_sms): # self.assertEqual(_sms['text'], sms['text']) # sm.remove() # remove SignalMatch # # leave everything as found # self.device.Delete(index, dbus_interface=SMS_INTFACE, # reply_handler=lambda: d.callback(True), # error_handler=d.errback) # # now get it by index and check text is the same # self.device.Get(index, dbus_interface=SMS_INTFACE, # reply_handler=compare_messages, # error_handler=d.errback) #def on_sms_saved_cb(indexes): # self.assertEqual(len(indexes), 1) # # send it from storage and wait for the signal # self.device.SendFromStorage(indexes[0], # dbus_interface=SMS_INTFACE, # # we are not interested in callback # reply_handler=lambda indexes: None, # error_handler=d.errback) #sm = self.device.connect_to_signal("SmsReceived", on_sms_received_cb) ## save the message and send it to ourselves #self.device.Save(sms, dbus_interface=SMS_INTFACE, # reply_handler=on_sms_saved_cb, # error_handler=d.errback) #return d def test_SmsSetFormat(self): """Test for Modem.Gsm.SMS.SetFormat""" # set text format and check immediately that a # GetFormat call returns 1 try: self.device.SetFormat(1, dbus_interface=SMS_INTFACE) except dbus.DBusException, e: if 'CMSError303' in get_dbus_error(e): # Ericsson doesn't allow us to set text format raise unittest.SkipTest() else: fmt = self.device.GetFormat(dbus_interface=SMS_INTFACE) # leave format as found self.device.SetFormat(0, dbus_interface=SMS_INTFACE) self.assertEqual(fmt, 1) def test_SmsSetIndication(self): """Test for Modem.Gsm.SMS.SetIndication""" # simple test for AT+CNMI, if this set command fails the # AT string needs to be changed. The reason the test is so simple # is because there's no GetIndication command in the spec, and I # didn't feel like coordinating an extension with the MM guys. # self.device.SetIndication(2, 1, 0, 1, 0) def test_SmsSetSmsc(self): """Test for Modem.Gsm.SMS.SetSmsc""" bad_smsc = '+3453456343' # get the original SMSC and memoize it smsc = self.device.GetSmsc(dbus_interface=SMS_INTFACE) # set the SMSC to a bad value and read it to confirm it worked self.device.SetSmsc(bad_smsc, dbus_interface=SMS_INTFACE) _bad_smsc = self.device.GetSmsc(dbus_interface=SMS_INTFACE) # leave everything as found self.device.SetSmsc(smsc, dbus_interface=SMS_INTFACE) # bad_smsc has been correctly set self.assertEqual(bad_smsc, _bad_smsc) def _test_Ussd(self, charset): self.do_when_registered() # get the IMSI and check if we have a suitable ussd request/regex imsi = self.device.GetImsi(dbus_interface=CRD_INTFACE) if imsi.startswith("21401"): request, regex = ('*118#', r'^Spain.*$') elif imsi.startswith("23415"): request, regex = ('*#100#', r'^07\d{9}$') elif imsi.startswith("26207"): request, regex = ('*100#', r'^(?:Hauptmen)|(?:Prepaid).*') else: raise unittest.SkipTest("Untested") self.device.SetCharset(charset, dbus_interface=CRD_INTFACE) response = self.device.Initiate(request) self.assertRegexpMatches(response, regex) def test_UssdGsm(self): """Test for Modem.Gsm.Ussd.Initiate using GSM charset""" self._test_Ussd('GSM') test_UssdGsm.timeout = 60 def test_UssdUcs2(self): """Test for Modem.Gsm.Ussd.Initiate using UCS2 charset""" self._test_Ussd('UCS2') test_UssdUcs2.timeout = 60 def _test_ZDisableReEnable(self, interval): # disable the device try: self.device.Enable(False, dbus_interface=MDM_INTFACE) except dbus.DBusException, e: self.fail(str(e)) # maybe sleep - some devices might be async WRT enable/disable so let's # try to catch them out if interval: time.sleep(interval) # reenable the device try: self.device.Enable(True, dbus_interface=MDM_INTFACE) except dbus.DBusException, e: if 'SimPinRequired' in get_dbus_error(e): pin = CONFIG.get('pin', '0000') try: self.device.SendPin(pin, dbus_interface=CRD_INTFACE) except dbus.DBusException, e: self.fail(str(e)) else: self.fail(str(e)) def test_ZDisableReEnable00(self): """Test for disable device and successful reenable - interval 0""" self._test_ZDisableReEnable(0) test_ZDisableReEnable00.timeout = 60 def test_ZDisableReEnable10(self): """Test for disable device and successful reenable - interval 10""" self._test_ZDisableReEnable(10) test_ZDisableReEnable10.timeout = 60 class UdevTestCase(unittest.TestCase): """Test the udev attributes if linux""" @classmethod def setUpClass(cls): if not sys.platform.startswith('linux'): raise unittest.SkipTest("Cannot be tested on OS != Linux") cls.gudev_client = gudev.Client(["tty", "usb", "net"]) def group_devices(self, devices): """ Returns a list of lists of devices that share common ancestors """ unique_ancestors = \ list(set([get_ancestor(x).get_sysfs_path() for x in devices])) ret = [] for ancestor in unique_ancestors: dlist = [device for device in devices \ if get_ancestor(device).get_sysfs_path() == ancestor] ret.append(dlist) return ret def test_ancestor(self): """Test for valid resolution of ancestor""" # Note: will break if more than one modem device plugged in devices = get_devices(self.gudev_client) unique = list(set([get_ancestor(x).get_sysfs_path() for x in devices])) self.assertEqual(len(unique), 1) self.assertNotRegexpMatches(unique[0], r'.*tty.*') def test_one_attr_only(self): """Test for unique ID_MM_PORT_TYPES""" for group in self.group_devices(get_devices(self.gudev_client)): aux = 0 modem = 0 for device in group: if device.get_property_as_int('ID_MM_PORT_TYPE_AUX'): aux += 1 if device.get_property_as_int('ID_MM_PORT_TYPE_MODEM'): modem += 1 self.assertLessEqual(aux, 1) self.assertLessEqual(modem, 1) wader-0.5.13/test/test_dbus_ussd_de.py000066400000000000000000000312701257646610200177510ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Copyright (C) 2009-2011 Vodafone España, S.A. # Author: Andrew Bird # # 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. """ Self-contained unittest suite for ModemManager implementations """ # install the following packages on Ubuntu # python-dbus, python-gconf, python-gobject, python-twisted-core # # install the following packages On OpenSuSE # dbus-1-python, python-gnome, python-gobject2, python-twisted # # to run the tests: # trial -e -r glib2 --tbformat=verbose /path/to/test_dbus.py import os import re import time import dbus import dbus.mainloop.glib import gconf from twisted.internet import defer, reactor from twisted.internet.task import deferLater from twisted.trial import unittest MM_SERVICE = 'org.freedesktop.ModemManager' MM_OBJPATH = '/org/freedesktop/ModemManager' MM_INTFACE = MM_SERVICE MDM_INTFACE = 'org.freedesktop.ModemManager.Modem' SPL_INTFACE = 'org.freedesktop.ModemManager.Modem.Simple' CRD_INTFACE = 'org.freedesktop.ModemManager.Modem.Gsm.Card' CTS_INTFACE = 'org.freedesktop.ModemManager.Modem.Gsm.Contacts' SMS_INTFACE = 'org.freedesktop.ModemManager.Modem.Gsm.SMS' NET_INTFACE = 'org.freedesktop.ModemManager.Modem.Gsm.Network' USD_INTFACE = 'org.freedesktop.ModemManager.Modem.Gsm.Ussd' # should the extensions introduced by the Wader project be tested? TEST_WADER_EXTENSIONS = True # generic message for [wader] skipped tests GENERIC_SKIP_MSG = "Wader extension to MM" GCONF_BASE = '/apps/wader-core' def get_dbus_error(e): if hasattr(e, 'get_dbus_name'): return e.get_dbus_name() return e.message class Config(object): """Simple GConf wrapper for string-only gets""" def __init__(self, path): self.path = path self.client = gconf.client_get_default() def get(self, section, key, default=None): path = os.path.join(self.path, section, key) value = self.client.get(path) if not value: return (default if default is not None else "") assert value.type == gconf.VALUE_STRING, "Unhandled type" return value.get_string() config = Config(GCONF_BASE) # ================================================== # ATTENTION # ================================================== # The following settings are required to be specified # for some tests otherwise they won't run: # # gconftool-2 -s -t string /apps/wader-core/test/pin 0000 # gconftool-2 -s -t string /apps/wader-core/test/puk 12345678 # Unused for now: # gconftool-2 -s -t string /apps/wader-core/test/phone 876543210 # # edit the GCONF_BASE variable above, to change the '/apps/wader-core' device = None numtests = None class DBusTestCase(unittest.TestCase): """Test-suite for ModemManager DBus exported methods""" def setUp(self): return self.setUpOnce() def setUpOnce(self): # setUpClass has been removed in twisted 10.0, and setUp should be # used instead, however setUp's behaviour doesn't replicate # setUpClass' one, so for now we're going to live with this horrid # hack global device, numtests if device: self.device = device return defer.succeed(True) if numtests is None: numtests = len([m for m in dir(self) if m.startswith('test_')]) d = defer.Deferred() self.device = None loop = dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) bus = dbus.SystemBus(mainloop=loop) def enable_device_cb(): # if we don't sleep for a sec, the test will start too soon # and Enable won't be finished yet, yielding spurious results. time.sleep(1) d.callback(True) def send_pin_cb(): # enable the device self.device.Enable(True, dbus_interface=MDM_INTFACE, reply_handler=enable_device_cb, error_handler=d.errback) def enable_device_eb(e): if 'SimPinRequired' in get_dbus_error(e): pin = config.get('test', 'pin', '0000') self.device.SendPin(pin, dbus_interface=CRD_INTFACE, reply_handler=send_pin_cb, error_handler=d.errback) elif 'SimPukRequired' in get_dbus_error(e): pin = config.get('test', 'pin', '0000') puk = config.get('test', 'puk') if not puk: msg = "SimPukRequired error and no PUK defined in config" raise unittest.SkipTest(msg) self.device.SendPuk(puk, pin, dbus_interface=CRD_INTFACE, reply_handler=send_pin_cb, error_handler=d.errback) else: raise unittest.SkipTest("Cannot handle error %s" % e) def get_device_from_opath(opaths): global device if not len(opaths): raise unittest.SkipTest("Can't run this test without devices") self.device = device = bus.get_object(MM_SERVICE, opaths[0]) # enable the device self.device.Enable(True, dbus_interface=MDM_INTFACE, reply_handler=enable_device_cb, error_handler=enable_device_eb) obj = bus.get_object(MM_SERVICE, MM_OBJPATH) obj.EnumerateDevices(dbus_interface=MM_INTFACE, reply_handler=get_device_from_opath, error_handler=d.errback) return d def tearDown(self): global numtests if numtests == 1: numtests = None return self.tearDownOnce() else: numtests -= 1 return defer.succeed(True) def tearDownOnce(self): global device # disable device at the end of the test # self.device.Enable(False, dbus_interface=MDM_INTFACE) self.device = device = None def do_when_registered(self, callback, errback=None): """ Waits for registration then fires callback Many prior tests can leave the card unregistered, use this if you need registration for your test to be successful """ reply = self.device.GetRegistrationInfo(dbus_interface=NET_INTFACE) status, numeric_oper = reply[:2] # we must be registered to our home network or roaming if status in [1, 5]: d = defer.succeed(status) d.addCallback(callback) return d elif status == 2: return deferLater(reactor, 5, self.do_when_registered, callback, errback) else: if errback is None: raise unittest.FailTest("Device is neither registered or" " trying: status == %d" % status) else: d = defer.fail(status) d.addErrback(errback) return d def test_UssdInteractiveStatesCancel(self): """Test for working ussd implementation""" def cb(*args): # get the IMSI and check if we have a suitable SIM imsi = self.device.GetImsi(dbus_interface=CRD_INTFACE) if not imsi.startswith("26202"): raise unittest.SkipTest("Untested") request, regex = ('*100#', '^Bitte w.*') # make sure we are good to test # should be idle now state = self.device.Get(USD_INTFACE, 'State', dbus_interface=dbus.PROPERTIES_IFACE) self.assertEqual(state, 'idle') d = defer.Deferred() def session_start(response): try: self.failUnless(re.compile(regex).match(response)) # check network is waiting for user response state = self.device.Get(USD_INTFACE, 'State', dbus_interface=dbus.PROPERTIES_IFACE) self.assertEqual(state, 'user-response') except unittest.FailTest, e: d.errback(unittest.FailTest(e)) def check_idle(): # check network is idle try: state = self.device.Get(USD_INTFACE, 'State', dbus_interface=dbus.PROPERTIES_IFACE) self.assertEqual(state, 'idle') d.callback(True) except unittest.FailTest, e: d.errback(unittest.FailTest(e)) # Cancel # Early TS27007 didn't have the facility to cancel a # current session, so our options are limited. We might # wait a while for the network to time it out though try: self.device.Cancel() return deferLater(reactor, 5, check_idle) except: return deferLater(reactor, 30, check_idle) def session_fail(failure): d.errback(unittest.FailTest(failure)) # get interactive menu self.device.Initiate(request, reply_handler=session_start, error_handler=session_fail) # should be 'active' now state = self.device.Get(USD_INTFACE, 'State', dbus_interface=dbus.PROPERTIES_IFACE) self.assertEqual(state, 'active') return d return self.do_when_registered(cb) test_UssdInteractiveStatesCancel.timeout = 75 def test_UssdInteractiveStatesComplete(self): """Test for working ussd implementation""" def cb(*args): # get the IMSI and check if we have a suitable SIM imsi = self.device.GetImsi(dbus_interface=CRD_INTFACE) if not imsi.startswith("26202"): raise unittest.SkipTest("Untested") request, menu_regex, menu_item, text_regex = \ ('*100#', '^Bitte w.*', '3', '.*MeinVodafone.*') # make sure we are good to test # should be idle now state = self.device.Get(USD_INTFACE, 'State', dbus_interface=dbus.PROPERTIES_IFACE) self.assertEqual(state, 'idle') d = defer.Deferred() def session_start(response): try: # Check we have the menu self.failUnless(re.compile(menu_regex).match(response)) # Check network is waiting for user response state = self.device.Get(USD_INTFACE, 'State', dbus_interface=dbus.PROPERTIES_IFACE) self.assertEqual(state, 'user-response') # Choose menu item, check it's the proper text response = self.device.Respond(menu_item) self.failUnless(re.compile(text_regex).match(response)) # Should be just simple text message, no reply required state = self.device.Get(USD_INTFACE, 'State', dbus_interface=dbus.PROPERTIES_IFACE) self.assertEqual(state, 'idle') d.callback(True) except unittest.FailTest, e: self.device.Cancel() d.errback(unittest.FailTest(e)) def session_fail(failure): d.errback(unittest.FailTest(failure)) # get interactive menu self.device.Initiate(request, reply_handler=session_start, error_handler=session_fail) # should be 'active' now state = self.device.Get(USD_INTFACE, 'State', dbus_interface=dbus.PROPERTIES_IFACE) self.assertEqual(state, 'active') return d return self.do_when_registered(cb) test_UssdInteractiveStatesComplete.timeout = 75 wader-0.5.13/test/test_encoding.py000066400000000000000000000172631257646610200171020ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Unittests for the encoding module""" from twisted.trial import unittest from wader.common.encoding import (CONTROL_0, CONTROL_1, LATIN_EX_A, LATIN_EX_B, check_if_ucs2, pack_ucs2_bytes, unpack_ucs2_bytes, unpack_ucs2_bytes_in_ts31101_80, unpack_ucs2_bytes_in_ts31101_81, unpack_ucs2_bytes_in_ts31101_82) CTL_0 = '007F' CTL_1 = '00FF' LTN_A = '017F' LTN_B = '024F' class TestEncoding(unittest.TestCase): """Tests for encoding""" def test_check_if_ucs2(self): self.assertEqual(check_if_ucs2(CTL_0), True) self.assertEqual(check_if_ucs2(CTL_1), True) self.assertEqual(check_if_ucs2(LTN_A), True) self.assertEqual(check_if_ucs2(LTN_B), True) self.assertEqual(check_if_ucs2('6C34'), True) self.assertEqual( check_if_ucs2('0056006F006400610066006F006E0065'), True) self.assertEqual(check_if_ucs2('003'), False) # XXX: This should be invalid but our code fails at the moment # XXX: Disable as people expect tests to run cleanly unless something # just broke, and there's no way to express 'known failure' #self.assertEqual(check_if_ucs2('D834DD1E'), False) def test_check_if_ucs2_limit_control_0(self): self.assertEqual(check_if_ucs2(CTL_0, limit=CONTROL_0), True) self.assertEqual(check_if_ucs2(CTL_1, limit=CONTROL_0), False) self.assertEqual(check_if_ucs2(LTN_A, limit=CONTROL_0), False) self.assertEqual(check_if_ucs2(LTN_B, limit=CONTROL_0), False) self.assertEqual(check_if_ucs2('6C34', limit=CONTROL_0), False) self.assertEqual( check_if_ucs2(CTL_0 + CTL_0 + CTL_0, limit=CONTROL_0), True) self.assertEqual( check_if_ucs2('6C34' + CTL_0 + CTL_0, limit=CONTROL_0), False) self.assertEqual( check_if_ucs2(CTL_0 + '6C34' + CTL_0, limit=CONTROL_0), False) self.assertEqual( check_if_ucs2(CTL_0 + CTL_0 + '6C34', limit=CONTROL_0), False) def test_check_if_ucs2_limit_control_1(self): self.assertEqual(check_if_ucs2(CTL_0, limit=CONTROL_1), True) self.assertEqual(check_if_ucs2(CTL_1, limit=CONTROL_1), True) self.assertEqual(check_if_ucs2(LTN_A, limit=CONTROL_1), False) self.assertEqual(check_if_ucs2(LTN_B, limit=CONTROL_1), False) self.assertEqual(check_if_ucs2('6C34', limit=CONTROL_1), False) self.assertEqual( check_if_ucs2(CTL_1 + CTL_1 + CTL_1, limit=CONTROL_1), True) self.assertEqual( check_if_ucs2('6C34' + CTL_1 + CTL_1, limit=CONTROL_1), False) self.assertEqual( check_if_ucs2(CTL_1 + '6C34' + CTL_1, limit=CONTROL_1), False) self.assertEqual( check_if_ucs2(CTL_1 + CTL_1 + '6C34', limit=CONTROL_1), False) def test_check_if_ucs2_limit_extended_latin_a(self): self.assertEqual(check_if_ucs2(CTL_0, limit=LATIN_EX_A), True) self.assertEqual(check_if_ucs2(CTL_1, limit=LATIN_EX_A), True) self.assertEqual(check_if_ucs2(LTN_A, limit=LATIN_EX_A), True) self.assertEqual(check_if_ucs2(LTN_B, limit=LATIN_EX_A), False) self.assertEqual(check_if_ucs2('6C34', limit=LATIN_EX_A), False) self.assertEqual( check_if_ucs2(LTN_A + LTN_A + LTN_A, limit=LATIN_EX_A), True) self.assertEqual( check_if_ucs2('6C34' + LTN_A + LTN_A, limit=LATIN_EX_A), False) self.assertEqual( check_if_ucs2(LTN_A + '6C34' + LTN_A, limit=LATIN_EX_A), False) self.assertEqual( check_if_ucs2(LTN_A + LTN_A + '6C34', limit=LATIN_EX_A), False) def test_check_if_ucs2_limit_extended_latin_b(self): self.assertEqual(check_if_ucs2(CTL_0, limit=LATIN_EX_B), True) self.assertEqual(check_if_ucs2(CTL_1, limit=LATIN_EX_B), True) self.assertEqual(check_if_ucs2(LTN_A, limit=LATIN_EX_B), True) self.assertEqual(check_if_ucs2(LTN_B, limit=LATIN_EX_B), True) self.assertEqual(check_if_ucs2('6C34', limit=LATIN_EX_B), False) self.assertEqual( check_if_ucs2(LTN_B + LTN_B + LTN_B, limit=LATIN_EX_B), True) self.assertEqual( check_if_ucs2('6C34' + LTN_B + LTN_B, limit=LATIN_EX_B), False) self.assertEqual( check_if_ucs2(LTN_B + '6C34' + LTN_B, limit=LATIN_EX_B), False) self.assertEqual( check_if_ucs2(LTN_B + LTN_B + '6C34', limit=LATIN_EX_B), False) def test_pack_ucs2_bytes(self): # 07911356131313F311000A9260214365870008AA080068006F006C0061 self.assertEqual(pack_ucs2_bytes('hola'), '0068006F006C0061') # 07911356131313F311000A9260214365870008AA0A0068006F006C00610073 self.assertEqual(pack_ucs2_bytes('holas'), '0068006F006C00610073') self.assertEqual(pack_ucs2_bytes(u"中华人民共和国"), '4E2D534E4EBA6C115171548C56FD') def test_unpack_ucs2_bytes(self): self.assertEqual(unpack_ucs2_bytes('0068006F006C0061'), 'hola') resp = 'holas' self.assertEqual(unpack_ucs2_bytes('0068006F006C00610073'), resp) def test_unpack_ucs2_bytes_in_ts31101_80(self): # From Huawei example self.assertEqual( unpack_ucs2_bytes_in_ts31101_80('534E4E3A'), u'华为') def test_unpack_ucs2_bytes_in_ts31101_81(self): # From our original Huawei contacts code self.assertEqual( unpack_ucs2_bytes_in_ts31101_81('0602A46563746F72FF'), u'Ĥector') # From Android code self.assertEqual( unpack_ucs2_bytes_in_ts31101_81('0A01566FEC6365204DE0696CFFFFFF'), u'Vo\u00ECce M\u00E0il') # From TS102221 # Byte 4 indicates GSM Default Alphabet character '53', i.e. 'S'. # Byte 5 indicates a UCS2 character offset to the base pointer of '15', # expressed in binary as follows 001 0101, which, when added # to the base pointer value results in a sixteen bit value of # 0000 1001 1001 0101, i.e. '0995', which is the Bengali # letter KA. # Byte 6 / 7 were not defined in TS102221 example, so just repeated 5 # Byte 8 contains the value 'FF', but as the string length is 5, this # is a valid character in the string, where the bit pattern # 111 1111 is added to the base pointer, yielding a sixteen # bit value of 0000 1001 1111 1111 for the UCS2 character # (i.e. '09FF'). self.assertEqual( unpack_ucs2_bytes_in_ts31101_81('051353959595FFFF'), u'Sককক\u09FF') def test_unpack_ucs2_bytes_in_ts31101_82(self): # From TS102221 self.assertEqual( unpack_ucs2_bytes_in_ts31101_82('0505302D82D32D31'), u'-Բփ-1') wader-0.5.13/test/test_provider.py000066400000000000000000001470551257646610200171510ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Unittests for the provider layer """ from datetime import datetime, timedelta, date import os from pytz import timezone, UTC import sqlite3 import sys from time import time from twisted.trial import unittest from wader.common.provider import (SMS_SCHEMA, SmsProvider, Message, Folder, Thread, DBError, inbox_folder, outbox_folder, drafts_folder, READ, UNREAD, message_read, NETWORKS_SCHEMA, TYPE_PREPAID, TYPE_CONTRACT, NetworkProvider, NetworkOperator, UsageProvider) from wader.common.utils import get_tz_aware_now class TestNetworkDBTriggers(unittest.TestCase): """Tests for the network DB triggers""" def setUp(self): self.conn = sqlite3.connect(':memory:', isolation_level=None) c = self.conn.cursor() # create schema args = dict(version=NetworkProvider.version) c.executescript(NETWORKS_SCHEMA % args) def tearDown(self): self.conn.close() def test_deleting_on_cascade_apn(self): """test trigger fkd_network_info_apn""" c = self.conn.cursor() c.execute("insert into network_info values " "('21401', 'Vodafone', 'Spain')") c.execute("insert into apn values (NULL, 'ac.vodafone.es', 'vodafone'," "'vodafone', '195.235.113.3', '10.0.0.1', 1, '+34324324342'," "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '21401')") # make sure we have 1 apn now c.execute("select count(*) from apn") self.assertEqual(c.fetchone()[0], 1) # now delete the only network_info c.execute("delete from network_info where id='21401'") # make sure we have 0 apns now c.execute("select count(*) from apn") self.assertEqual(c.fetchone()[0], 0) def test_insert_apn_with_unknown_network_id(self): """test trigger fki_apns_with_unknown_network_id""" c = self.conn.cursor() c.execute("insert into network_info values " "('21401', 'Vodafone', 'Spain')") self.assertRaises(sqlite3.IntegrityError, c.execute, "insert into apn values (1, 'ac.vodafone.es', 'vodafone'," "'vodafone', '195.235.113.3', '10.0.0.1', 1, NULL, NULL," "NULL, NULL, NULL, NULL, NULL, NULL, NULL, '21402')") # leave it as we found it c.execute("delete from network_info where id='21401'") def test_update_network_info_id_with_apns_associated(self): """test fku_prevent_apn_network_info_network_id_bad_update trigger""" c = self.conn.cursor() c.execute("insert into network_info values " "('21401', 'Vodafone', 'Spain')") c.execute("insert into apn values (NULL, 'ac.vodafone.es', 'vodafone'," "'vodafone', '195.235.113.3', '10.0.0.1', 1, NULL, NULL," "NULL, NULL, NULL, NULL, NULL, NULL, NULL, '21401')") # now update the netid self.assertRaises(sqlite3.IntegrityError, c.execute, "update network_info set id='21402' " "where name='Vodafone'") # leave it as we found it c.execute("delete from network_info where id='21401'") def test_update_apn_network_id_with_unknown_netid(self): """test fku_prevent_apn_network_id_bad_update trigger""" c = self.conn.cursor() c.execute("insert into network_info values " "('21401', 'Vodafone', 'Spain')") c.execute("insert into apn values (1, 'ac.vodafone.es', 'vodafone'," "'vodafone', '195.235.113.3', '10.0.0.1', 1, NULL, NULL," "NULL, NULL, NULL, NULL, NULL, NULL, NULL, '21401')") # now update the netid self.assertRaises(sqlite3.IntegrityError, c.execute, "update apn set network_id='21402' where id=1") # leave it as we found it c.execute("delete from network_info where id='21401'") class TestNetworkProvider(unittest.TestCase): """Tests for the network provider""" def setUp(self): self.provider = NetworkProvider(':memory:') def tearDown(self): self.provider.close() def create_test_mbpi(self): self.mbpi = 'mbpi' f = open(self.mbpi, 'w') xml = """ Etisalat Etisalat mnet mnet 194.170.1.6 194.170.1.7 Etisalat 3G etisalat.ae etisalat.ae Vodafone (D2) vodafone vodafone 139.7.30.125 139.7.30.126 WebSessions vodafone vodafone 139.7.30.125 139.7.30.126 """ f.write(xml) f.close def test_populate_db_from_objs(self): networks = [NetworkOperator(["21401"], "prepaid.vodafone.es", "vodafone", "vodafone", "10.0.0.1", "10.0.0.2", TYPE_PREPAID, '+23323232', '+23423232', "Spain", "Vodafone"), NetworkOperator(["21401"], "contract.vodafone.es", "vodafone", "vodafone", "10.0.0.1", "10.0.0.2", TYPE_CONTRACT, '+23123121', '+2132121', "Spain", "Vodafone")] self.provider.populate_networks_from_objs(networks) # we should get just two objects response = self.provider.get_network_by_id("214013241213122") self.assertEqual(len(response), 2) # leave it as we found it c = self.provider.conn.cursor() c.execute("delete from network_info where id='21401'") response = self.provider.get_network_by_id("214013241213122") self.assertEqual(len(response), 0) def test_populate_db_from_mbpi_real(self): mbpi = '/usr/share/mobile-broadband-provider-info/serviceproviders.xml' if not os.path.exists(mbpi): raise unittest.SkipTest("No MBPI installed") try: self.provider.populate_networks_from_mbpi(mbpi) except TypeError: raise unittest.SkipTest("MBPI is unsupported version") c = self.provider.conn.cursor() c.execute("select * from network_info") response = c.fetchall() self.failUnless(len(response) >= 1) # leave it as we found it c.execute("delete from network_info") def test_populate_db_from_networks_py_real(self): extra = '../resources/extra' if not os.path.exists(extra): raise unittest.SkipTest('Path to "networks.py" not valid') sys.path.insert(0, extra) import networks def is_valid(item): return not item.startswith(("__", "Base", "NetworkOperator")) self.provider.populate_networks_from_objs([getattr(networks, item)() for item in dir(networks) if is_valid(item)]) c = self.provider.conn.cursor() c.execute("select country from network_info where id = '23415'") self.assertEqual(c.fetchone()[0], 'United Kingdom') # leave it as we found it c.execute("delete from network_info") def test_get_network_by_id(self): extra = None for p in ['resources/extra', '../resources/extra']: if os.path.exists(p): extra = p break if extra is None: raise unittest.SkipTest('Path to "networks.py" not valid') sys.path.insert(0, extra) import networks def is_valid(item): return not item.startswith(("__", "Base", "NetworkOperator")) self.provider.populate_networks_from_objs([getattr(networks, item)() for item in dir(networks) if is_valid(item)]) for x in ['21401', '23415', '26202']: nets = self.provider.get_network_by_id(x + '9106787580') self.assertIn(x, [net.netid[0] for net in nets], msg="%s not in nets %s " % (x, str(nets))) def test_populate_db_from_mbpi_merge_network_no_clash(self): networks = [NetworkOperator(["21401"], "Preloaded", "Preloaded", "Preloaded", "10.0.0.1", "10.0.0.2", TYPE_PREPAID, '+23323232', '+23423232', "Spain", "Preloaded")] self.provider.populate_networks_from_objs(networks) self.create_test_mbpi() self.provider.populate_networks_from_mbpi(self.mbpi) c = self.provider.conn.cursor() c.execute("select * from network_info") response = c.fetchall() self.assertEqual(len(response), 5) # leave it as we found it c.execute("delete from network_info") def test_populate_db_from_mbpi_merge_network_conflict(self): networks = [NetworkOperator(["26202"], "Preloaded", "Preloaded", "Preloaded", "10.0.0.1", "10.0.0.2", TYPE_PREPAID, None, '+23323232', '+23423232', "Germany", "Preloaded")] self.provider.populate_networks_from_objs(networks) self.create_test_mbpi() self.provider.populate_networks_from_mbpi(self.mbpi) c = self.provider.conn.cursor() # make sure we merged c.execute("select * from network_info") response = c.fetchall() self.assertEqual(len(response), 4) # make sure we didn't overwrite the original c.execute("select name from network_info where id = '26202'") self.assertEqual(c.fetchone()[0], 'Preloaded') # leave it as we found it c.execute("delete from network_info") def test_populate_db_from_mbpi_merge_apn_no_clash(self): networks = [NetworkOperator(["21401"], "Preloaded", "Preloaded", "Preloaded", "10.0.0.1", "10.0.0.2", TYPE_PREPAID, '+23323232', '+23423232', "Spain", "Preloaded")] self.provider.populate_networks_from_objs(networks) self.create_test_mbpi() self.provider.populate_networks_from_mbpi(self.mbpi) c = self.provider.conn.cursor() c.execute("select * from apn") response = c.fetchall() self.assertEqual(len(response), 9) # make sure we prefixed the foreign entry c.execute("select type from apn where network_id = '26202'") self.assertEqual(c.fetchone()[0][:4], 'MBPI') # leave it as we found it c.execute("delete from network_info") def test_populate_db_from_mbpi_merge_apn_conflict(self): networks = [NetworkOperator(["26202"], "Preloaded", "Preloaded", "Preloaded", "10.0.0.1", "10.0.0.2", "Preloaded", '+23323232', '+23423232', "Germany", "Preloaded")] self.provider.populate_networks_from_objs(networks) self.create_test_mbpi() self.provider.populate_networks_from_mbpi(self.mbpi) c = self.provider.conn.cursor() # make sure we merged and didn't load any apns for the existing nets c.execute("select * from apn") response = c.fetchall() self.assertEqual(len(response), 7) # make sure we didn't overwrite the original c.execute("select type from apn where network_id = '26202'") self.assertEqual(c.fetchone()[0], 'Preloaded') # leave it as we found it c.execute("delete from network_info") def test_assert_passing_netid_raises_exception(self): self.assertRaises(ValueError, self.provider.get_network_by_id, "21401") def test_get_network_by_id_five_digit_netid(self): networks = [NetworkOperator(["21401"], "prepaid.vodafone.es", "vodafone", "vodafone", "10.0.0.1", "10.0.0.2", TYPE_PREPAID, '+23323232', '+23423232', "Spain", "Vodafone"), NetworkOperator(["21401"], "contract.vodafone.es", "vodafone", "vodafone", "10.0.0.1", "10.0.0.2", TYPE_CONTRACT, '+23123121', '+2132121', "Spain", "Vodafone")] self.provider.populate_networks_from_objs(networks) # we should get just two objects response = self.provider.get_network_by_id("2140153241213122") self.assertEqual(len(response), 2) # leave it as we found it c = self.provider.conn.cursor() c.execute("delete from network_info where 1=1") def test_get_network_by_id_six_digit_netid(self): networks = [NetworkOperator(["214016"], "prepaid.vodafone.es", "vodafone", "vodafone", "10.0.0.1", "10.0.0.2", TYPE_PREPAID, '+23323232', '+23423232', "Spain", "Vodafone"), NetworkOperator(["214015"], "contract.vodafone.es", "vodafone", "vodafone", "10.0.0.1", "10.0.0.2", TYPE_CONTRACT, '+23123121', '+2132121', "Spain", "Vodafone")] self.provider.populate_networks_from_objs(networks) # we should get just two objects response = self.provider.get_network_by_id("2140153241213122") self.assertEqual(len(response), 1) self.assertEqual(response[0].apn, "contract.vodafone.es") # leave it as we found it c = self.provider.conn.cursor() c.execute("delete from network_info where 1=1") def test_get_network_by_id_seven_digit_netid(self): networks = [NetworkOperator(["2140161"], "prepaid.vodafone.es", "vodafone", "vodafone", "10.0.0.1", "10.0.0.2", TYPE_PREPAID, '+23323232', '+23423232', "Spain", "Vodafone"), NetworkOperator(["2140162"], "contract.vodafone.es", "vodafone", "vodafone", "10.0.0.1", "10.0.0.2", TYPE_CONTRACT, '+23123121', '+2132121', "Spain", "Vodafone")] self.provider.populate_networks_from_objs(networks) # we should get just two objects response = self.provider.get_network_by_id("2140161213322323") self.assertEqual(response[0].apn, "prepaid.vodafone.es") self.assertEqual(len(response), 1) # leave it as we found it c = self.provider.conn.cursor() c.execute("delete from network_info where 1=1") class TestSmsDBTriggers(unittest.TestCase): """Tests for the SMS DB triggers""" def setUp(self): self.conn = sqlite3.connect(':memory:', isolation_level=None) c = self.conn.cursor() # add read function self.conn.create_function('msg_is_read', 1, message_read) # create schema c.executescript(SMS_SCHEMA) # initialize folder table c.execute("insert into folder values (1, 'Inbox')") c.execute("insert into folder values (2, 'Outbox')") c.execute("insert into folder values (3, 'Drafts')") def tearDown(self): self.conn.close() def test_deleting_on_cascade_folder(self): """test trigger fkd_folder_thread trigger""" c = self.conn.cursor() c.execute("insert into folder values (4, 'foo')") c.execute("insert into thread values (null, ?, '+3232323232'" ", 1, 'hey how are ya?', 1, 4)", (time(),)) # make sure that we have 1 threads now c.execute("select count(*) from thread") self.assertEqual(c.fetchone()[0], 1) # now delete the only folder c.execute("delete from folder where id = 4") c.execute("select count(*) from thread") # make sure that we have 0 threads now self.assertEqual(c.fetchone()[0], 0) def test_deleting_on_cascade_thread(self): """test trigger fkd_thread_message trigger""" c = self.conn.cursor() c.execute("insert into folder values (4, 'foo')") c.execute("insert into thread values (null, ?, '+3232323232'" ", 1, 'hey how are ya?', 1, 4)", (time(),)) c.execute("insert into message values (null, ?, '+3232323232'" ", 'hey how are ya?', 2, 1)", (time(),)) c.execute("select count(*) from message") self.assertEqual(c.fetchone()[0], 1) # now delete the only thread c.execute("delete from thread where id = 1") c.execute("select count(*) from message") # make sure that we have 0 threads now self.assertEqual(c.fetchone()[0], 0) def test_inserting_new_message_updates_thread_snippet(self): """test fki_update_thread_values snippet update""" c = self.conn.cursor() # insert a thread, add a new message and check the snippet # and the sms.text match snippet_text = 'thread snippet test' c.execute("insert into thread values (5, ?, '+34654321232'," "1, '', 1, 1)", (time(),)) c.execute("insert into message values (null, ?, '+34654321232'," "?, 2, 5)", (time(), snippet_text)) c.execute("select snippet from thread where id=5") snippet = c.fetchone()[0] self.assertEqual(snippet, snippet_text) # leave everything as we found it c.execute("delete from thread where id=5") def test_inserting_new_message_updates_thread_date(self): """test fki_update_thread_values date update""" c = self.conn.cursor() # insert a thread, add a new message and check the thread date # and the sms date match now = int(time()) c.execute("insert into thread values (5, ?, '+34654321232'," "1, '', 1, 1)", (now - 10,)) c.execute("insert into message values (null, ?, '+34654321232'," "'hey there', 2, 5)", (now,)) c.execute("select date from thread where id=5") _date = c.fetchone()[0] self.assertEqual(now, _date) # leave everything as we found it c.execute("delete from thread where id=5") def test_inserting_non_existing_folder_id_in_thread(self): """test fki_thread_folder trigger""" c = self.conn.cursor() self.assertRaises(sqlite3.IntegrityError, c.execute, "insert into thread values (null, ?, '+3232323232'" ", 1, 'hey how are ya?', 1, 9)", (time(),)) def test_inserting_non_existing_thread_id_in_message(self): c = self.conn.cursor() c.execute("insert into thread values (1, ?, '+3232323232', 1" ",'hey how are ya?', 1, 1)", (time(),)) self.assertRaises(sqlite3.IntegrityError, c.execute, "insert into message values (null, ?, '+3232323232'" ", 'hey how are ya?', 2, 2)", (time(),)) # leave everything as we found it c.execute("delete from thread where id=1") def test_updating_sms_flags_decreases_thread_read_attribute(self): """test fku_mark_message_unread trigger""" # add a thread to inbox_folder c = self.conn.cursor() c.execute("insert into thread values (1, 121322311, '+322121111'," "0, 'test_updating_sms_flags_decreases', 0, 1)") # add a couple of read messages c.execute("insert into message values (1, 121322311, '+322121111'," "'test_updating_sms_flags_decreases', ?, 1)", (READ,)) c.execute("insert into message values (2, 121322319, '+322121111'," "'test_updating_sms_flags_decreases 2', ?, 1)", (READ,)) c.execute("select read from thread where id=1") # make sure we have exactly two read messages self.assertEqual(c.fetchone()[0], 2) # update one of them and mark it as unread c.execute("update message set flags=? where id=1", (UNREAD,)) c.execute("select read from thread where id=1") # make sure we have exactly one read message self.assertEqual(c.fetchone()[0], 1) # leave everything as we found it c.execute("delete from thread where id=1") def test_updating_sms_flags_increases_thread_read_attribute(self): """test fku_mark_message_read trigger""" # add a thread to inbox_folder c = self.conn.cursor() c.execute("insert into thread values (1, 121322311, '+322121111'," "0, 'test_updating_sms_flags_increases', 0, 1)") # add a couple of read messages c.execute("insert into message values (1, 121322311, '+322121111'," "'test_updating_sms_flags_increases', 1, 1)") c.execute("insert into message values (2, 121322319, '+322121111'," "'test_updating_sms_flags_increases 2', 1, 1)") c.execute("select read from thread where id=1") # make sure we have exactly zero read messages self.assertEqual(c.fetchone()[0], 0) # update one of them and mark it as read c.execute("update message set flags = 2 where id=1") c.execute("select read from thread where id=1") # make sure we have exactly one read message self.assertEqual(c.fetchone()[0], 1) # leave everything as we found it c.execute("delete from thread where id=1") def test_that_update_message_count_increases_message_count(self): """test fki_update_message_count trigger""" c = self.conn.cursor() c.execute("insert into thread values (1, ?, '+3232323232', 1" ", 'hey how are ya?', 1, 1)", (time(),)) c.execute("select message_count from thread where id = 1") self.assertEqual(c.fetchone()[0], 1) c.execute("insert into message values (null, ?, '+3232323232'" ", 'hey how are ya?', 2, 1)", (time(),)) c.execute("select message_count from thread where id = 1") self.assertEqual(c.fetchone()[0], 2) # leave everything as we found it c.execute("delete from thread where id=1") def test_that_update_message_count_decreases_message_count(self): """test fkd_update_message_count trigger""" c = self.conn.cursor() c.execute("insert into thread values (1, ?, '+3232323232'" ", 1, 'hey how are ya?', 1, 1)", (time(),)) c.execute("select message_count from thread where id=1") self.assertEqual(c.fetchone()[0], 1) c.execute("insert into message values (null, ?, '+3232323232'" ", 'hey how are ya?', 2, 1)", (time(),)) c.execute("select message_count from thread where id=1") self.assertEqual(c.fetchone()[0], 2) c.execute("delete from message where id=1") c.execute("select message_count from thread where id=1") self.assertEqual(c.fetchone()[0], 1) # leave everything as we found it c.execute("delete from thread where id=1") def test_update_folder_id_with_threads_associated(self): """test fku_folder_thread trigger""" c = self.conn.cursor() c.execute("insert into thread values (1, ?, ',+3232323232'," "1, 'hey how are ya?', 1, 1)", (time(),)) self.assertRaises(sqlite3.IntegrityError, c.execute, "update folder set id = (select max(id) from folder) + 1") # leave everything as we found it c.execute("delete from thread where id=1") def test_update_message_thread_id_with_messages_associated(self): c = self.conn.cursor() c.execute("insert into thread values (1, ?, '+3232323232'," "1, 'hey how are ya?', 1, 1)", (time(),)) c.execute("insert into message values (null, ?, '+3232323232'," "'hey how are ya?', 2, 1)", (time(),)) self.assertRaises(sqlite3.IntegrityError, c.execute, "update thread set id = (select max(id) from thread) + 1") # leave everything as we found it c.execute("delete from thread where id=1") def test_update_message_thread_id_with_non_existing_thread_id(self): c = self.conn.cursor() c.execute("insert into thread values (1, ?, '+3232323232'," "1, 'hey how are ya?', 1, 1)", (time(),)) c.execute("insert into message values (null, ?, '+3232323232'," "'hey how are ya?', 2, 1)", (time(),)) self.assertRaises(sqlite3.IntegrityError, c.execute, "update thread set id = (select max(id) from thread) + 1") # leave everything as we found it c.execute("delete from thread where id=1") def test_update_thread_folder_id_with_non_existing_folder_id(self): """test fku_thread_folder trigger""" c = self.conn.cursor() c.execute("insert into thread values (1, ?, '+23423432423'," "1, 'hey how are ya?', 1, 1)", (time(),)) self.assertRaises(sqlite3.IntegrityError, c.execute, "update thread set folder_id = (select max(id) from folder) + 1") # leave everything as we found it c.execute("delete from thread where id=1") class TestSmsProvider(unittest.TestCase): """Tests for SmsProvider""" def setUp(self): self.provider = SmsProvider(':memory:') self.provider.add_folder(inbox_folder) self.provider.add_folder(outbox_folder) self.provider.add_folder(drafts_folder) def tearDown(self): self.provider.close() def test_add_folder(self): # add a folder and make sure it is there folder = self.provider.add_folder(Folder("Test")) self.assertIn(folder, list(self.provider.list_folders())) # leave everything as found self.provider.delete_folder(folder) self.assertNotIn(folder, list(self.provider.list_folders())) def test_add_sms(self): sms = Message('+3243243223', 'hey how are you?', _datetime=get_tz_aware_now()) sms = self.provider.add_sms(sms) self.assertIn(sms, list(self.provider.list_sms())) # leave everything as found self.provider.delete_sms(sms) self.assertNotIn(sms, list(self.provider.list_sms())) def test_sms_time_storage(self): tests = [ # UTC+0(without DST) ('Europe/London', datetime(2015, 1, 17, 23, 35, 41)), # UTC+1(with DST) ('Europe/London', datetime(2015, 6, 17, 23, 35, 41)), # UTC+1(without DST) ('Europe/Paris', datetime(2015, 1, 17, 23, 35, 41)), # UTC+2(with DST) ('Europe/Paris', datetime(2015, 6, 17, 23, 35, 41)), # UTC-5(without DST) ('America/New_York', datetime(2015, 1, 17, 23, 35, 41)), # UTC-4(with DST) ('America/New_York', datetime(2015, 6, 17, 23, 35, 41)), # UTC+7(no DST adjustment in 2010) ('Asia/Jakarta', datetime(2015, 1, 17, 23, 35, 41)) ] for tzstring, dt in tests: tz = timezone(tzstring) now = tz.localize(dt) sms = Message('+447917267410', tzstring, _datetime=now) sms = self.provider.add_sms(sms) for dbsms in self.provider.list_sms(): if dbsms.text == tzstring: break dbnow = dbsms.datetime.astimezone(tz) self.assertEqual(now, dbnow) # leave everything as found self.provider.delete_sms(sms) self.assertNotIn(sms, list(self.provider.list_sms())) def test_delete_folder(self): # add a folder and make sure it is there folder = self.provider.add_folder(Folder("Test 2")) self.assertIn(folder, list(self.provider.list_folders())) # delete the folder and make sure its gone self.provider.delete_folder(folder) self.assertNotIn(folder, list(self.provider.list_folders())) def test_delete_undeleteable_folder(self): # trying to delete an undeleteable folder shall raise DBError self.assertIn(inbox_folder, list(self.provider.list_folders())) self.assertRaises(DBError, self.provider.delete_folder, inbox_folder) def test_delete_folder_with_threads_attached(self): # add a folder folder = self.provider.add_folder(Folder("Test 2")) self.assertIn(folder, list(self.provider.list_folders())) # attach a couple of threads to the folder t1 = self.provider.add_thread( Thread(get_tz_aware_now(), '+322323233', index=5, folder=folder)) t2 = self.provider.add_thread( Thread(get_tz_aware_now(), '+322323233', index=6, folder=folder)) # make sure they appear self.assertIn(t1, list(self.provider.list_threads())) self.assertIn(t2, list(self.provider.list_threads())) # delete the folder and threads should be gone self.provider.delete_folder(folder) self.assertNotIn(folder, list(self.provider.list_folders())) self.assertNotIn(t1, list(self.provider.list_threads())) self.assertNotIn(t2, list(self.provider.list_threads())) def test_delete_thread(self): # add a thread to inbox and make sure its there t = self.provider.add_thread( Thread(get_tz_aware_now(), '+3443545333', index=3, folder=inbox_folder)) self.assertIn(t, list(self.provider.list_threads())) # delete it and make sure its gone self.provider.delete_thread(t) self.assertNotIn(t, list(self.provider.list_threads())) def test_delete_thread_with_sms_attached(self): # add a thread to inbox t = self.provider.add_thread( Thread(get_tz_aware_now(), '+3443545332', folder=inbox_folder)) # add a message attached to that thread and make sure its present sms = self.provider.add_sms( Message(number='+3443545332', text='how is it going then?', _datetime=get_tz_aware_now(), thread=t)) self.assertIn(sms, list(self.provider.list_sms())) # delete the thread and both the thread and SMS should be gone self.provider.delete_thread(t) self.assertNotIn(sms, list(self.provider.list_sms())) def test_delete_sms(self): # add a thread to inbox t = self.provider.add_thread( Thread(get_tz_aware_now(), '+3443545333', folder=inbox_folder)) # add a message attached to that thread and make sure its present sms = self.provider.add_sms( Message(number='+3443545333', text='how is it going then?', _datetime=get_tz_aware_now(), thread=t)) self.assertIn(sms, list(self.provider.list_sms())) # delete it and make sure its gone self.provider.delete_sms(sms) self.assertNotIn(sms, list(self.provider.list_sms())) # leave it as we found it self.provider.delete_thread(t) def test_list_folders(self): folders = list(self.provider.list_folders()) for folder in [inbox_folder, outbox_folder, drafts_folder]: self.assertIn(folder, folders) def test_list_from_folder(self): # add a folder and attach a thread to it folder = self.provider.add_folder(Folder("Test 3")) t = self.provider.add_thread( Thread(get_tz_aware_now(), '+3443545333', folder=folder)) # add a thread attached to inbox t2 = self.provider.add_thread( Thread(get_tz_aware_now(), '+3443545333', folder=inbox_folder)) # t should be present in threads, but not t2 threads = list(self.provider.list_from_folder(folder)) self.assertIn(t, threads) self.assertNotIn(t2, threads) # leave it as we found it self.provider.delete_folder(folder) self.provider.delete_thread(t2) def test_list_from_folder_and_the_results_order(self): """test that list_from_folder returns a correctly ordered result""" now = get_tz_aware_now() five_min_ago = now - timedelta(minutes=5) # add a couple of threads to inbox_folder, one of them # just got updated/created and the other five minuts ago t = self.provider.add_thread( Thread(now, '+3443545333', folder=inbox_folder)) t2 = self.provider.add_thread( Thread(five_min_ago, '+3443545331', folder=inbox_folder)) # if we list from inbox, t should appear before t2 threads = list(self.provider.list_from_folder(inbox_folder)) self.assertEqual(threads[0], t) self.assertEqual(threads[1], t2) # leave it as we found it self.provider.delete_thread(t) self.provider.delete_thread(t2) def test_list_from_thread(self): """test for list_from_thread""" t = self.provider.add_thread( Thread(get_tz_aware_now(), '+3443545333', folder=inbox_folder)) sms1 = self.provider.add_sms( Message(number='+3443545333', text='test_list_from_thread sms1', _datetime=get_tz_aware_now(), thread=t)) sms2 = self.provider.add_sms( Message(number='+3443545333', text='test_list_from_thread sms2', _datetime=get_tz_aware_now(), thread=t)) # sms1 and sms2 should be present in messages messages = list(self.provider.list_from_thread(t)) self.assertIn(sms1, messages) self.assertIn(sms2, messages) # leave it as we found it self.provider.delete_thread(t) def test_list_from_thread_and_the_results_order(self): """test that list_from_thread returns a correctly ordered result""" number = '+3443545333' now = get_tz_aware_now() five_min_ago = now - timedelta(minutes=5) # add a thread and attach two messages to it, one just sent # and the other is five minutes older t = self.provider.add_thread( Thread(now, number, folder=inbox_folder)) sms1 = self.provider.add_sms( Message(number=number, text='test_list_from_thread sms1', _datetime=now, thread=t)) sms2 = self.provider.add_sms( Message(number=number, text='test_list_from_thread sms2', _datetime=five_min_ago, thread=t)) # sms1 and sms2 should be present in messages messages = list(self.provider.list_from_thread(t)) self.assertEqual(messages[0], sms1) self.assertEqual(messages[1], sms2) # leave it as we found it self.provider.delete_thread(t) def test_list_sms(self): # add a thread to inbox and attach a couple of messages to it t = self.provider.add_thread( Thread(get_tz_aware_now(), '+3443545333', folder=inbox_folder)) sms1 = self.provider.add_sms( Message(number='+3443545333', text='test_list_sms', _datetime=get_tz_aware_now(), thread=t)) sms2 = self.provider.add_sms( Message(number='+3443545333', text='test_list_sms', _datetime=get_tz_aware_now(), thread=t)) # sms1 and sms2 should be present in messages messages = list(self.provider.list_sms()) self.assertIn(sms1, messages) self.assertIn(sms2, messages) # leave it as we found it self.provider.delete_thread(t) def test_list_threads(self): # add two threads to the inbox folder t1 = self.provider.add_thread( Thread(get_tz_aware_now(), '+3643445333', folder=inbox_folder)) t2 = self.provider.add_thread( Thread(get_tz_aware_now(), '+3443545333', folder=inbox_folder)) # make sure they are present if we list them threads = list(self.provider.list_threads()) self.assertIn(t1, threads) self.assertIn(t2, threads) # leave it as we found it self.provider.delete_thread(t1) self.provider.delete_thread(t2) def test_move_thread_from_folder_to_folder(self): # add a thread to inbox_folder and check its present t1 = self.provider.add_thread( Thread(get_tz_aware_now(), '+3643445333', folder=inbox_folder)) threads = list(self.provider.list_from_folder(inbox_folder)) self.assertIn(t1, threads) # create a new folder, move t1 to it and check its there folder = self.provider.add_folder(Folder("Test 4")) self.provider.move_to_folder(t1, folder) threads = list(self.provider.list_from_folder(folder)) self.assertIn(t1, threads) self.provider.delete_folder(folder) # leave it as we found it threads = list(self.provider.list_threads()) self.assertNotIn(t1, threads) def test_move_sms_from_folder_to_folder(self): # add a thread to inbox_folder t = self.provider.add_thread( Thread(get_tz_aware_now(), '+3643445333', folder=inbox_folder)) # add two sms to t1 sms1 = self.provider.add_sms( Message(number='+3443545333', text='test_list_sms', _datetime=get_tz_aware_now(), thread=t)) sms2 = self.provider.add_sms( Message(number='+3443545333', text='test_list_sms 2', _datetime=get_tz_aware_now(), thread=t)) # sms1 and sms2 should be present in thread messages = list(self.provider.list_from_thread(t)) self.assertIn(sms1, messages) self.assertIn(sms2, messages) # now create a new folder and move sms2 there folder = self.provider.add_folder(Folder("Test 6")) self.provider.move_to_folder(sms2, folder) # now only sms1 should be present in original thread inbox_messages = list(self.provider.list_from_thread(t)) new_thread = list(self.provider.list_from_folder(folder))[0] folder_messages = list(self.provider.list_from_thread(new_thread)) self.assertIn(sms1, inbox_messages) self.assertNotIn(sms2, inbox_messages) # and sms2 should be present in folder_messages self.assertIn(sms2, folder_messages) self.assertNotIn(sms1, folder_messages) # leave it as we found it self.provider.delete_folder(folder) self.provider.delete_thread(t) def test_update_sms_flags(self): """test that update_sms_flags works as expected""" # add a thread to inbox_folder number = '+3243242323' t = self.provider.add_thread( Thread(get_tz_aware_now(), number, folder=inbox_folder)) # add one sms to t1 sms1 = self.provider.add_sms( Message(number=number, text='test_update_sms_flags', _datetime=get_tz_aware_now(), thread=t)) self.assertEqual(sms1.flags, READ) # now mark sms1 as unread sms1 = self.provider.update_sms_flags(sms1, UNREAD) self.assertEqual(sms1.flags, UNREAD) # leave it as we found it self.provider.delete_thread(t) class TestUsageProvider(unittest.TestCase): def setUp(self): self.provider = UsageProvider(':memory:') def tearDown(self): self.provider.close() def test_add_usage_item(self): tz = timezone('Europe/London') dt = tz.localize(datetime(2015, 1, 17, 15, 45)) # DST not applied later = dt + timedelta(minutes=30) item = self.provider.add_usage_item(dt, later, 12345460, 12333211, True) usage_items = self.provider.get_usage_for_day(dt.date(), tz) self.assertIn(item, usage_items) def test_add_usage_item_cet(self): tz = timezone('Europe/Paris') dt = tz.localize(datetime(2015, 1, 17, 15, 45)) # DST not applied dt2 = datetime(2015, 1, 17, 14, 45, tzinfo=UTC) dt3 = dt2 + timedelta(minutes=30) later = dt + timedelta(minutes=30) item = self.provider.add_usage_item(dt, later, 12345460, 12333211, True) usage_items = self.provider.get_usage_for_day(dt.date(), tz) self.assertEqual(dt2, dt.astimezone(UTC)) self.assertEqual(dt2, item.start_time.astimezone(UTC)) self.assertEqual(dt2, usage_items[0].start_time.astimezone(UTC)) self.assertEqual(dt3, item.end_time.astimezone(UTC)) self.assertEqual(dt3, usage_items[0].end_time.astimezone(UTC)) def test_delete_usage_item(self): tz = timezone('Europe/London') dt = tz.localize(datetime(2015, 1, 17, 15, 45)) # DST not applied later = dt + timedelta(minutes=60) item = self.provider.add_usage_item(dt, later, 12345470, 12333212, True) # check it's there usage_items = self.provider.get_usage_for_day(dt.date(), tz) self.assertIn(item, usage_items) self.provider.delete_usage_item(item) # now check that it is indeed gone usage_items = self.provider.get_usage_for_day(dt.date(), tz) self.assertNotIn(item, usage_items) def _make_day_usage_items(self, dt): # add one usage item for today (45m) now1 = dt later1 = now1 + timedelta(minutes=45) item1 = self.provider.add_usage_item(now1, later1, 100034, 14566, True) # add another usage item for today (17m) 55 minutes later now2 = dt + timedelta(minutes=55) later2 = now2 + timedelta(minutes=17) item2 = self.provider.add_usage_item(now2, later2, 12000, 1245, True) # add another usage item for tomorrow (25m) now3 = dt + timedelta(days=1) later3 = now3 + timedelta(minutes=25) item3 = self.provider.add_usage_item(now3, later3, 14000, 1785, True) return (item1, item2, item3) def test_get_usage_for_day_bst_within(self): tz = timezone('Europe/London') dt = tz.localize(datetime(2015, 6, 17, 15, 30)) # DST applied item1, item2, item3 = self._make_day_usage_items(dt) # now get the usage for today today_items = self.provider.get_usage_for_day(dt.date(), tz) self.assertIn(item1, today_items) self.assertIn(item2, today_items) self.assertNotIn(item3, today_items) # get the usage for tomorrow tomorrow = dt.date() + timedelta(days=1) tomorrow_items = self.provider.get_usage_for_day(tomorrow, tz) self.assertNotIn(item1, tomorrow_items) self.assertNotIn(item2, tomorrow_items) self.assertIn(item3, tomorrow_items) def test_get_usage_for_day_bst_overlap(self): tz = timezone('Europe/London') dt = tz.localize(datetime(2015, 6, 17, 23, 30)) # DST applied item1, item2, item3 = self._make_day_usage_items(dt) # now get the usage for today, but all items have end time tomorrow today_items = self.provider.get_usage_for_day(dt.date(), tz) self.assertNotIn(item1, today_items) self.assertNotIn(item2, today_items) self.assertNotIn(item3, today_items) # get the usage for tomorrow tomorrow = dt.date() + timedelta(days=1) tomorrow_items = self.provider.get_usage_for_day(tomorrow, tz) self.assertIn(item1, tomorrow_items) self.assertIn(item2, tomorrow_items) self.assertIn(item3, tomorrow_items) def test_get_usage_for_day_est_within(self): tz = timezone('America/New_York') dt = tz.localize(datetime(2015, 1, 17, 15, 30)) # DST not applied item1, item2, item3 = self._make_day_usage_items(dt) # now get the usage for today today_items = self.provider.get_usage_for_day(dt.date(), tz) self.assertIn(item1, today_items) self.assertIn(item2, today_items) self.assertNotIn(item3, today_items) # get the usage for tomorrow tomorrow = dt.date() + timedelta(days=1) tomorrow_items = self.provider.get_usage_for_day(tomorrow, tz) self.assertNotIn(item1, tomorrow_items) self.assertNotIn(item2, tomorrow_items) self.assertIn(item3, tomorrow_items) def test_get_usage_for_day_est_overlap(self): tz = timezone('America/New_York') dt = tz.localize(datetime(2015, 1, 17, 23, 30)) # DST not applied item1, item2, item3 = self._make_day_usage_items(dt) # now get the usage for today, but all items have end time tomorrow today_items = self.provider.get_usage_for_day(dt.date(), tz) self.assertNotIn(item1, today_items) self.assertNotIn(item2, today_items) self.assertNotIn(item3, today_items) # get the usage for tomorrow tomorrow = dt.date() + timedelta(days=1) tomorrow_items = self.provider.get_usage_for_day(tomorrow, tz) self.assertIn(item1, tomorrow_items) self.assertIn(item2, tomorrow_items) self.assertIn(item3, tomorrow_items) def test_get_usage_for_day_gmt_within(self): tz = timezone('Europe/London') dt = tz.localize(datetime(2015, 1, 17, 15, 30)) # DST not applied item1, item2, item3 = self._make_day_usage_items(dt) # now get the usage for today today_items = self.provider.get_usage_for_day(dt.date(), tz) self.assertIn(item1, today_items) self.assertIn(item2, today_items) self.assertNotIn(item3, today_items) # get the usage for tomorrow tomorrow = dt.date() + timedelta(days=1) tomorrow_items = self.provider.get_usage_for_day(tomorrow, tz) self.assertNotIn(item1, tomorrow_items) self.assertNotIn(item2, tomorrow_items) self.assertIn(item3, tomorrow_items) def test_get_usage_for_day_gmt_overlap(self): tz = timezone('Europe/London') dt = tz.localize(datetime(2015, 1, 17, 23, 30)) # DST not applied item1, item2, item3 = self._make_day_usage_items(dt) # now get the usage for today, but all items have end time tomorrow today_items = self.provider.get_usage_for_day(dt.date(), tz) self.assertNotIn(item1, today_items) self.assertNotIn(item2, today_items) self.assertNotIn(item3, today_items) # get the usage for tomorrow tomorrow = dt.date() + timedelta(days=1) tomorrow_items = self.provider.get_usage_for_day(tomorrow, tz) self.assertIn(item1, tomorrow_items) self.assertIn(item2, tomorrow_items) self.assertIn(item3, tomorrow_items) def test_get_usage_for_month(self): tz = timezone('Europe/London') dt = tz.localize(datetime(2015, 1, 17, 15, 30)) # DST not applied current_month = dt.month current_year = dt.year # add one usage item for day 12 of this month (45m) now1 = tz.localize(datetime(current_year, current_month, 12, 13, 10)) later1 = now1 + timedelta(minutes=45) item1 = self.provider.add_usage_item(now1, later1, 100034, 12566, True) # add another usage item for day 13 of this month (17m) now2 = tz.localize(datetime(current_year, current_month, 13, 15, 10)) later2 = now2 + timedelta(minutes=17) item2 = self.provider.add_usage_item(now2, later2, 12000, 1245, True) # add another usage item for next month if current_month < 12: month = current_month + 1 year = current_year else: month = 1 year = current_year + 1 # next month at 6.50am (25m) now3 = tz.localize(datetime(year, month, 2, 6, 50)) later3 = now3 + timedelta(minutes=25) item3 = self.provider.add_usage_item(now3, later3, 14000, 1785, True) # now get the usage for this month this_month_items = self.provider.get_usage_for_month(now1.date(), tz) self.assertIn(item1, this_month_items) self.assertIn(item2, this_month_items) self.assertNotIn(item3, this_month_items) # now get the usage for next month next_month_items = self.provider.get_usage_for_month(now3.date(), tz) self.assertNotIn(item1, next_month_items) self.assertNotIn(item2, next_month_items) self.assertIn(item3, next_month_items) def test_get_total_usage(self): tz = timezone('Europe/London') dt = tz.localize(datetime(2015, 1, 17, 15, 30)) # DST not applied current_month = dt.month current_year = dt.year # add one usage item for day 12 of this month (45m) now1 = tz.localize(datetime(current_year, current_month, 12, 13, 10)) later1 = now1 + timedelta(minutes=45) item1 = self.provider.add_usage_item(now1, later1, 100034, 12566, True) # add another usage item for day 13 of this month (17m), one year ago now2 = tz.localize(datetime(current_year - 1, current_month, 13, 15, 10)) later2 = now2 + timedelta(minutes=17) item2 = self.provider.add_usage_item(now2, later2, 12000, 1245, True) items = self.provider.get_total_usage(tz=tz) self.assertIn(item1, items) self.assertIn(item2, items) def test_get_total_usage_passing_a_date(self): tz = timezone('Europe/London') dt = tz.localize(datetime(2015, 1, 17, 15, 30)) # DST not applied current_month = dt.month current_year = dt.year # add one usage item for day 12 of this month (45m) now1 = tz.localize(datetime(current_year, current_month, 12, 13, 10)) later1 = now1 + timedelta(minutes=45) item1 = self.provider.add_usage_item(now1, later1, 100034, 12566, True) # add another usage item for day 13 of this month (17m), one year ago now2 = tz.localize(datetime(current_year - 1, current_month, 13, 15, 10)) later2 = now2 + timedelta(minutes=17) item2 = self.provider.add_usage_item(now2, later2, 12000, 1245, True) items = self.provider.get_total_usage(now1.date(), tz=tz) self.assertIn(item1, items) self.assertNotIn(item2, items) wader-0.5.13/test/test_resolvconf.py000066400000000000000000000036241257646610200174700ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """unittests for resolvconf module""" import os import tempfile import sys from twisted.trial import unittest sys.path.insert(0, '..') from core.resolvconf import NamedManager from wader.common.utils import get_file_data class ResolvconfTestCase(unittest.TestCase): """Test-suite for resolvconf module""" def setUp(self): self.path = tempfile.mkstemp()[1] self.named_manager = NamedManager(self.path) def tearDown(self): del self.named_manager os.unlink(self.path) def test_add_dns_info(self): expected = """# Generated by Wader nameserver 10.0.0.1 nameserver 10.0.0.2 """ self.named_manager.add_dns_info(["10.0.0.1", "10.0.0.2"]) self.assertEqual(expected, get_file_data(self.path)) def test_delete_dns_info(self): expected = """# Generated by Wader nameserver 10.0.0.1 nameserver 10.0.0.2 """ self.named_manager.add_dns_info(["10.0.0.1", "10.0.0.2"]) self.assertEqual(expected, get_file_data(self.path)) self.named_manager.delete_dns_info(["10.0.0.1", "10.0.0.2"]) self.assertEqual("", get_file_data(self.path)) wader-0.5.13/test/test_utils.py000066400000000000000000000122511257646610200164440ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """ tests for the wader.common.utils module """ import os from random import shuffle, randint from datetime import datetime from pytz import timezone from twisted.trial import unittest from wader.common.utils import (get_file_data, save_file, natsort, convert_ip_to_int, convert_int_to_ip, convert_int_to_uint32, convert_uint32_to_int, rssi_to_percentage, flatten_list, revert_dict, get_tz_aware_now, get_tz_aware_mtime, get_localzone_wader_implementation) def ip_generator(n): c = 0 while c < n: yield "%d.%d.%d.%d" % (randint(0, 255), randint(0, 255), randint(0, 255), randint(0, 255)) c += 1 class TestUtilities(unittest.TestCase): def test_get_file_data(self): """ Test reading a random file with ``get_file_data`` """ text = os.urandom(2000) path = '/tmp/file.foo' fobj = open(path, 'w') fobj.write(text) fobj.close() self.assertEqual(text, get_file_data(path)) os.unlink(path) def test_save_file(self): """ Tests that saving a random file works with ``save_file`` """ text = os.urandom(2000) path = '/tmp/file.foo' save_file(path, text) fobj = open(path, 'r') data = fobj.read() fobj.close() self.assertEqual(text, data) os.unlink(path) def test_natsort(self): """ Test that the ``natsort`` function works as expected """ l = [] for i in range(15): l.append("ttyUSB%d" % i) unordered = l[:] shuffle(unordered) self.assertNotIdentical(l, unordered) natsort(unordered) self.assertEqual(l, unordered) def test_ip_to_int_conversion(self): for ip in ip_generator(50000): num = convert_ip_to_int(ip) self.failIf(num < 0) self.assertEqual(ip, convert_int_to_ip(num)) def test_int_to_uint32_to_int_conversion(self): c = 10000000 while c: i = randint(0, 0xffffffff) u32 = convert_int_to_uint32(i) i2 = convert_uint32_to_int(u32) self.assertEqual(i, i2) c -= 1 def test_rssi_to_percentage(self): self.assertEqual(rssi_to_percentage(31), 100) self.assertEqual(rssi_to_percentage(32), 0) self.assertEqual(rssi_to_percentage(0), 0) def test_flatten_list(self): self.assertEqual(flatten_list([1, 2, [5, 6]]), [1, 2, 5, 6]) self.assertEqual(flatten_list([1, 2, (5, 6)]), [1, 2, 5, 6]) self.assertEqual(flatten_list([1, iter([2, 3, 4])]), [1, 2, 3, 4]) def test_revert_dict(self): self.assertEqual(revert_dict({'a': 'b'}), {'b': 'a'}) self.assertEqual(revert_dict(dict(foo='bar')), dict(bar='foo')) def test_get_tz_aware_now(self): now1 = get_tz_aware_now() now2 = datetime.now(timezone('Europe/Paris')) self.assertNotEqual(now1.tzinfo, None) self.failIf(abs(now2 - now1).seconds > 5) def test_get_tz_aware_mtime(self): text = os.urandom(2000) path = 'mtime.test' fobj = open(path, 'w') fobj.write(text) fobj.close() now1 = get_tz_aware_now() now2 = get_tz_aware_mtime(path) self.assertNotEqual(now2.tzinfo, None) self.failIf(abs(now2 - now1).seconds > 5) # tidy up os.remove(path) def test_get_localzone_wader_implementation_env(self): # Test file name = 'America/New_York' os.environ['TZ'] = name path = 'get_localzone.test' with open(path, 'w') as f: f.write('Europe/London\n') self.assertEqual(get_localzone_wader_implementation(path), timezone(name)) # tidy up os.remove(path) def test_get_localzone_wader_implementation_file(self): # Test file name = 'America/New_York' os.environ['TZ'] = '' path = 'get_localzone.test' with open(path, 'w') as f: f.write(name + '\n') self.assertEqual(get_localzone_wader_implementation(path), timezone(name)) # tidy up os.remove(path) wader-0.5.13/test_dbus.sh000077500000000000000000000007171257646610200152530ustar00rootroot00000000000000#!/bin/sh if [ -z "$1" ] ; then echo "need name of device" exit 1 fi NAME="`date +%s`-$1" [ -d reports ] || mkdir reports sudo su -c "cat /dev/null > /var/log/wader.log" sudo wader-core-ctl --restart sleep 10 nosetests -v --with-xunit --xunit-file=reports/"${NAME}".xml test/test_dbus.py sync sleep 5 cp /var/log/wader.log reports/"${NAME}".log [ -f /usr/share/sounds/speech-dispatcher/test.wav ] && aplay /usr/share/sounds/speech-dispatcher/test.wav wader-0.5.13/wader/000077500000000000000000000000001257646610200140155ustar00rootroot00000000000000wader-0.5.13/wader/__init__.py000066400000000000000000000016201257646610200161250ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. # setup version from wader.common.consts import APP_VERSION version = tuple(map(int, APP_VERSION.split('.'))) wader-0.5.13/wader/common/000077500000000000000000000000001257646610200153055ustar00rootroot00000000000000wader-0.5.13/wader/common/__init__.py000066400000000000000000000015461257646610200174240ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Wader's core""" wader-0.5.13/wader/common/_dbus.py000066400000000000000000000110761257646610200167600ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """DBus-related helper classes""" import dbus import dbus.service from twisted.python import log class DBusExporterHelper(object): """I am a helper for classes that export methods over DBus""" def __init__(self): super(DBusExporterHelper, self).__init__() def add_callbacks(self, deferred, async_cb, async_eb): """Adds ``async_cb`` and ``async_eb`` to ``deferred``""" deferred.addCallback(async_cb) deferred.addErrback(self._process_failure, async_eb) return deferred def add_callbacks_and_swallow(self, deferred, async_cb, async_eb): """ Like previous method but swallows the result This method is useful for functions that might return some garbage, but we are not interested in the result """ deferred.addCallback(lambda _: async_cb()) deferred.addErrback(self._process_failure, async_eb) return deferred def _process_failure(self, failure, async_eb): """ Extracts the exception wrapped in ``failure`` and calls ``async_eb`` """ try: async_eb(failure.type(failure.value)) except: log.msg(dir(failure.type)) log.msg(failure.value) class DelayableDBusObject(dbus.service.Object): """Use me in classes that need to make asynchronous a synchronous method""" def __init__(self, *args): super(DelayableDBusObject, self).__init__(*args) def _message_cb(self, connection, message): method, parent_method = dbus.service._method_lookup(self, message.get_member(), message.get_interface()) super(DelayableDBusObject, self)._message_cb(connection, message) if "_dbus_is_delayable" in dir(parent_method): #if hasattr(parent_method, '_dbus_is_delayable'): member = message.get_member() signature_str = parent_method._dbus_out_signature signature = dbus.service.Signature(signature_str) def callback(result): dbus.service._method_reply_return(connection, message, member, signature, *result) def errback(e): dbus.service._method_reply_error(connection, message, e) parent_method._finished(self, callback, errback) def delayable(func): """ Make a synchronous method asynchronous decorator to be used on subclasses of :class:`DelayableDBusObject` """ assert func._dbus_is_method def delay_reply(): func._dbus_async_callbacks_before = func._dbus_async_callbacks func._dbus_async_callbacks = True def finished(self, cb, eb): if func._dbus_async_callbacks == True: if not self in func._reply_callbacks: func._reply_callbacks[self] = [] func._reply_callbacks[self].append((cb, eb)) func._dbus_async_callbacks = func._dbus_async_callbacks_before def reply(self, result=None, error=None): if self in func._reply_callbacks: for callback, errback in func._reply_callbacks[self]: if error: errback(error) elif result: callback(result) else: callback() del func._reply_callbacks[self] return True else: return False func._reply_callbacks = {} func._dbus_is_delayable = True func.delay_reply = delay_reply func.reply = reply func._finished = finished return func wader-0.5.13/wader/common/_gconf.py000066400000000000000000000042711257646610200171160ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """GConf helper classes""" import gconf class GConfHelper(object): """I am the base class for gconf-backed conf system""" def __init__(self): super(GConfHelper, self).__init__() self.client = gconf.client_get_default() def set_value(self, path, value): """Sets ``value`` at ``path``""" if isinstance(value, basestring): self.client.set_string(path, value) elif isinstance(value, bool): self.client.set_bool(path, value) elif isinstance(value, (int, long)): self.client.set_int(path, value) elif isinstance(value, float): self.client.set_float(path, value) elif isinstance(value, list): self.client.set_list(path, gconf.VALUE_INT, value) def get_value(self, value): """Gets the value of ``value``""" if value.type == gconf.VALUE_STRING: return value.get_string() elif value.type == gconf.VALUE_INT: return value.get_int() elif value.type == gconf.VALUE_FLOAT: return value.get_float() elif value.type == gconf.VALUE_BOOL: return value.get_bool() elif value.type == gconf.VALUE_LIST: _list = value.get_list() return [self.get_value(v) for v in _list] else: msg = "Unsupported type %s for %s" raise TypeError(msg % (type(value), value)) wader-0.5.13/wader/common/aes.py000066400000000000000000000030101257646610200164210ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011 Vodafone España, S.A. # Author: Andrew Bird # # 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. """Replacement for the old aes module""" from M2Crypto.EVP import Cipher ALGS = { 16: 'aes_128_cbc', 24: 'aes_192_cbc', 32: 'aes_256_cbc' } def decryptData(key, encoded, testforpickle=False): iv = '\0' * 16 cipher = Cipher(alg=ALGS[len(key)], key=key, iv=iv, op=0) decoded = cipher.update(encoded) decoded = decoded + cipher.final() # old format encryption seems to have 16 bytes before pickled data if testforpickle and not decoded.startswith('(dp'): return decoded[16:] return decoded def encryptData(key, data): iv = '\0' * 16 cipher = Cipher(alg=ALGS[len(key)], key=key, iv=iv, op=1) encoded = cipher.update(data) encoded = encoded + cipher.final() return encoded wader-0.5.13/wader/common/aterrors.py000066400000000000000000000474161257646610200175340ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Functions and exceptions that deal with AT errors""" import re import dbus from twisted.python import log GEN_ERROR = 'org.freedesktop.ModemManager.Modem' GSM_ERROR = 'org.freedesktop.ModemManager.Modem.Gsm' MMS_ERROR = 'org.freedesktop.ModemManager.Modem.Gsm.Mms' ERROR_REGEXP = re.compile(r""" # This regexp matches the following patterns: # ERROR # +CMS ERROR: 500 # +CME ERROR: foo bar # +CME ERROR: 30 # \r\n (?P # group named error \+CMS\sERROR:\s\d{3} | # CMS ERROR regexp \+CME\sERROR:\s\S+(\s\S+)* | # CME ERROR regexp \+CME\sERROR:\s\d+ | # CME ERROR regexp INPUT\sVALUE\sIS\sOUT\sOF\sRANGE | # INPUT VALUE IS OUT OF RANGE ERROR # Plain ERROR regexp ) \r\n """, re.VERBOSE) class General(dbus.DBusException): """Exception raised when an ERROR has occurred""" _dbus_error_name = "%s.%s" % (GEN_ERROR, 'General') class InputValueError(dbus.DBusException): """Exception raised when INPUT VALUE IS OUT OF RANGE is received""" _dbus_error_name = "%s.%s" % (GEN_ERROR, 'InputValueError') class SerialOpenFailed(dbus.DBusException): """Could not open serial device""" _dbus_error_name = "%s.%s" % (GEN_ERROR, 'SerialOpenFailed') class SerialSendFailed(dbus.DBusException): """Could not write to serial device""" _dbus_error_name = "%s.%s" % (GEN_ERROR, 'SerialSendFailed') class SerialResponseTimeout(dbus.DBusException): """Serial response timed out""" _dbus_error_name = "%s.%s" % (GEN_ERROR, 'SerialResponseTimeout') class Connected(dbus.DBusException): """Operation attempted whilst connected""" _dbus_error_name = "%s.%s" % (GEN_ERROR, 'Connected') class PhoneFailure(dbus.DBusException): """Phone failure""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'PhoneFailure') class NoConnection(dbus.DBusException): """No connection to phone""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'NoConnection') class LinkReserved(dbus.DBusException): """Phone-adaptor linkreserved""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'LinkReserved') class OperationNotAllowed(dbus.DBusException): """Operation not allowed""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'OperationNotAllowed') class OperationNotSupported(dbus.DBusException): """Operation not supported""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'OperationNotSupported') class PhSimPinRequired(dbus.DBusException): """PH-SIM PIN required""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'PhSimPinRequired') class PhFSimPinRequired(dbus.DBusException): """PH-FSIM PIN required""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'PhFSimPinRequired') class PhFPukRequired(dbus.DBusException): """PH-FSIM PUK required""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'PhFPukRequired') class SimNotInserted(dbus.DBusException): """PH-PUK required""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'SimNotInserted') class SimPinRequired(dbus.DBusException): """SIM PIN required""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'SimPinRequired') class SimPukRequired(dbus.DBusException): """SIM PUK required""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'SimPukRequired') class SimFailure(dbus.DBusException): """SIM failure""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'SimFailure') class SimBusy(dbus.DBusException): """SIM busy""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'SimBusy') class SimWrong(dbus.DBusException): """SIM wrong""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'SimWrong') class SimNotStarted(dbus.DBusException): """SIM interface not started""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'SimNotStarted') class IncorrectPassword(dbus.DBusException): """Incorrect password""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'IncorrectPassword') class SimPin2Required(dbus.DBusException): """SIM PIN2 required""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'SimPin2Required') class SimPuk2Required(dbus.DBusException): """SIM PUK2 required""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'SimPuk2Required') class MemoryFull(dbus.DBusException): """Memory full""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'MemoryFull') class InvalidIndex(dbus.DBusException): """Index invalid""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'InvalidIndex') class NotFound(dbus.DBusException): """Not found""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'NotFound') class MemoryFailure(dbus.DBusException): """Memory failure""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'MemoryFailure') class TextTooLong(dbus.DBusException): """Text string too long""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'TextTooLong') class InvalidChars(dbus.DBusException): """Invalid characters in text string""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'InvalidChars') class DialStringTooLong(dbus.DBusException): """Invalid dial string""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'DialStringTooLong') class InvalidDialString(dbus.DBusException): """Invalid dial string""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'InvalidDialString') class NoNetwork(dbus.DBusException): """No network service""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'NoNetwork') class NetworkTimeout(dbus.DBusException): """Network timeout""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'NetworkTimeout') class NetworkNotAllowed(dbus.DBusException): """Only emergency calls are allowed""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'NetworkNotAllowed') class NetworkPinRequired(dbus.DBusException): """Network PIN required""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'NetworkPinRequired') class NetworkPukRequired(dbus.DBusException): """Network PUK required""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'NetworkPukRequired') class NetworkSubsetPinRequired(dbus.DBusException): """Network subset PIN required""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'NetworkSubsetPinRequired') class NetworkSubsetPukRequired(dbus.DBusException): """Network subset PUK required""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'NetworkSubsetPukRequired') class ServicePinRequired(dbus.DBusException): """Service PIN required""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'ServicePinRequired') class ServicePukRequired(dbus.DBusException): """Service PUK required""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'ServicePukRequired') class CharsetError(dbus.DBusException): """Raised when Wader can't find an appropriate charset at startup""" _dbus_error_name = "%s.%s" % (GEN_ERROR, 'CharsetError') class CorporatePinRequired(dbus.DBusException): """Corporate PIN required""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'CorporatePinRequired') class CorporatePukRequired(dbus.DBusException): """Corporate PUK required""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'CorporatePukRequired') class HiddenKeyRequired(dbus.DBusException): """Hidden key required""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'HiddenKeyRequired') class EapMethodNotSupported(dbus.DBusException): """EAP method not supported""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'EapMethodNotSupported') class IncorrectParams(dbus.DBusException): """Incorrect params received""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'IncorrectParams') class Unknown(dbus.DBusException): """Unknown error""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'Unknown') class GprsIllegalMs(dbus.DBusException): """Illegal GPRS MS""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'GprsIllegalMs') class GprsIllegalMe(dbus.DBusException): """Illegal GPRS ME""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'GprsIllegalMe') class GprsServiceNotAllowed(dbus.DBusException): """GPRS service not allowed""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'GprsServiceNotAllowed') class GprsPlmnNotAllowed(dbus.DBusException): """GPRS PLMN not allowed""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'GprsPlmnNotAllowed') class GprsLocationNotAllowed(dbus.DBusException): """GPRS location not allowed""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'GprsLocationNotAllowed') class GprsRoamingNotAllowed(dbus.DBusException): """GPRS roaming not allowed""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'GprsRoamingNotAllowed') class GprsOptionNotSupported(dbus.DBusException): """GPRS used option not supported""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'GprsOptionNotSupported') class GprsNotSubscribed(dbus.DBusException): """GPRS not susbscribed""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'GprsNotSubscribed') class GprsOutOfOrder(dbus.DBusException): """GPRS out of order""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'GprsOutOfOrder') class GprsPdpAuthFailure(dbus.DBusException): """GPRS PDP authentication failure""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'GprsPdpAuthFailure') class GprsUnspecified(dbus.DBusException): """Unspecified GPRS error""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'GprsUnspecified') class GprsInvalidClass(dbus.DBusException): """Invalid GPRS class""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'GprsInvalidClass') class ServiceTemporarilyOutOfOrder(dbus.DBusException): """Exception raised when service temporarily out of order""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'ServiceTemporarilyOutOfOrder') class UnknownSubscriber(dbus.DBusException): """Exception raised when suscriber unknown""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'UnknownSubscriber') class ServiceNotInUse(dbus.DBusException): """Exception raised when service not in use""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'ServiceNotInUse') class ServiceNotAvailable(dbus.DBusException): """Exception raised when service not available""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'ServiceNotAvailable') class UnknownNetworkMessage(dbus.DBusException): """Exception raised upon unknown network message""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'UnknownNetworkMessage') class CallIndexError(dbus.DBusException): """Exception raised upon call index error""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'CallIndexError') class CallStateError(dbus.DBusException): """Exception raised upon call state error""" _dbus_error_name = "%s.%s" % (GSM_ERROR, 'CallStateError') class MalformedUssdPduError(dbus.DBusException): """Exception raised when a malformed Ussd Pdu is received""" _dbus_error_name = "%s.%s" % (GSM_ERROR, "MalformedUssdPduError") class ExpiredNotification(dbus.DBusException): """Exception raised when a GET.req petition fails""" _dbus_error_name = "%s.%s" % (MMS_ERROR, "ExpiredNotification") class CMSError300(dbus.DBusException): """Phone failure""" class CMSError301(dbus.DBusException): """SMS service of phone reserved """ class CMSError302(dbus.DBusException): """Operation not allowed""" class CMSError303(dbus.DBusException): """Operation not supported""" class CMSError304(dbus.DBusException): """Invalid PDU mode parameter""" class CMSError305(dbus.DBusException): """Invalid text mode parameter""" class CMSError310(dbus.DBusException): """SIM not inserted""" class CMSError311(dbus.DBusException): """SIM PIN necessary""" class CMSError313(dbus.DBusException): """SIM failure""" class CMSError314(dbus.DBusException): """SIM busy""" class CMSError315(dbus.DBusException): """SIM wrong""" class CMSError320(dbus.DBusException): """Memory failure""" class CMSError321(dbus.DBusException): """Invalid memory index""" class CMSError322(dbus.DBusException): """Memory full""" class CMSError330(dbus.DBusException): """SMSC address unknown""" class CMSError331(dbus.DBusException): """No network service""" class CMSError500(dbus.DBusException): """Unknown Error""" ERROR_DICT = { # Generic error 'ERROR': General, # CME Errors '+CME ERROR: incorrect password': IncorrectPassword, '+CME ERROR: invalid characters in dial string': InvalidDialString, '+CME ERROR: no network service': NoNetwork, '+CME ERROR: not found': NotFound, '+CME ERROR: operation not allowed': OperationNotAllowed, '+CME ERROR: text string too long': TextTooLong, '+CME ERROR: SIM busy': SimBusy, '+CME ERROR: SIM failure': SimFailure, '+CME ERROR: SIM interface not started': SimNotStarted, '+CME ERROR: SIM interface not started yet': SimNotStarted, '+CME ERROR: SIM not inserted': SimNotInserted, '+CME ERROR: SIM PIN required': SimPinRequired, '+CME ERROR: SIM PUK required': SimPukRequired, '+CME ERROR: SIM PUK2 required': SimPuk2Required, # NUMERIC CME ERRORS '+CME ERROR: 0': PhoneFailure, '+CME ERROR: 1': NoConnection, '+CME ERROR: 3': OperationNotAllowed, '+CME ERROR: 4': OperationNotSupported, '+CME ERROR: 5': PhSimPinRequired, '+CME ERROR: 6': PhFSimPinRequired, '+CME ERROR: 7': PhFPukRequired, '+CME ERROR: 10': SimNotInserted, '+CME ERROR: 11': SimPinRequired, '+CME ERROR: 12': SimPukRequired, '+CME ERROR: 13': SimFailure, '+CME ERROR: 14': SimBusy, '+CME ERROR: 15': SimWrong, '+CME ERROR: 16': IncorrectPassword, '+CME ERROR: 17': SimPin2Required, '+CME ERROR: 18': SimPuk2Required, '+CME ERROR: 20': MemoryFull, '+CME ERROR: 21': InvalidIndex, '+CME ERROR: 22': NotFound, '+CME ERROR: 23': MemoryFailure, '+CME ERROR: 24': TextTooLong, '+CME ERROR: 26': DialStringTooLong, '+CME ERROR: 27': InvalidDialString, '+CME ERROR: 30': NoNetwork, '+CME ERROR: 31': NetworkTimeout, '+CME ERROR: 32': NetworkNotAllowed, '+CME ERROR: 40': NetworkPinRequired, '+CME ERROR: 41': NetworkPukRequired, '+CME ERROR: 42': NetworkSubsetPinRequired, '+CME ERROR: 43': NetworkSubsetPukRequired, '+CME ERROR: 44': ServicePinRequired, '+CME ERROR: 45': ServicePukRequired, '+CME ERROR: 46': CorporatePinRequired, '+CME ERROR: 47': CorporatePukRequired, '+CME ERROR: 48': HiddenKeyRequired, '+CME ERROR: 49': EapMethodNotSupported, '+CME ERROR: 50': IncorrectParams, '+CME ERROR: 100': Unknown, '+CME ERROR: 103': GprsIllegalMs, '+CME ERROR: 106': GprsIllegalMe, '+CME ERROR: 107': GprsServiceNotAllowed, '+CME ERROR: 111': GprsPlmnNotAllowed, '+CME ERROR: 112': GprsLocationNotAllowed, '+CME ERROR: 113': GprsRoamingNotAllowed, '+CME ERROR: 132': GprsOptionNotSupported, '+CME ERROR: 133': GprsNotSubscribed, '+CME ERROR: 134': GprsOutOfOrder, '+CME ERROR: 148': GprsPdpAuthFailure, '+CME ERROR: 149': GprsUnspecified, '+CME ERROR: 150': GprsInvalidClass, # not implemented on ModemManager (yet) '+CME ERROR: 260': ServiceTemporarilyOutOfOrder, '+CME ERROR: 261': UnknownSubscriber, '+CME ERROR: 262': ServiceNotInUse, '+CME ERROR: 263': ServiceNotAvailable, '+CME ERROR: 264': UnknownNetworkMessage, '+CME ERROR: 65281': CallStateError, # CMS Errors '+CMS ERROR: 300': CMSError300, '+CMS ERROR: 301': CMSError301, '+CMS ERROR: 302': CMSError302, '+CMS ERROR: 303': CMSError303, '+CMS ERROR: 304': CMSError304, '+CMS ERROR: 305': CMSError305, '+CMS ERROR: 310': CMSError310, '+CMS ERROR: 311': CMSError311, '+CMS ERROR: 313': CMSError313, '+CMS ERROR: 314': CMSError314, '+CMS ERROR: 315': CMSError315, '+CMS ERROR: 320': CMSError320, '+CMS ERROR: 321': CMSError321, '+CMS ERROR: 322': CMSError322, '+CMS ERROR: 330': CMSError330, '+CMS ERROR: 331': CMSError331, '+CMS ERROR: 500': CMSError500, # USER GARBAGE ERRORS 'INPUT VALUE IS OUT OF RANGE': InputValueError, } def extract_error(s): """ Scans ``s`` looking for AT Errors Returns a tuple with the exception, error and the match """ try: match = ERROR_REGEXP.search(s) if match: try: error = match.group('error') exception = ERROR_DICT[error] return exception, error, match except KeyError, e: log.err(e, "%r didn't map to any of my keys" % error) except AttributeError: return None def error_to_human(e): """Returns a human error out of ``e``""" name = e.get_dbus_name().split('.')[-1] return EXCEPT_TO_HUMAN[name] EXCEPT_TO_HUMAN = { 'PhoneFailure': "Phone failure", 'NoConnection': "No Connection to phone", 'LinkReserved': "Phone-adaptor link reserved", 'OperationNotAllowed': "Operation not allowed", 'OperationNotSupported': "Operation not supported", 'PhSimPinRequired': "PH-SIM PIN required", 'PhFSimPinRequired': "PH-FSIM PIN required", 'PhFSimPukRequired': "PH-FSIM PUK required", 'SimNotInserted': "SIM not inserted", 'SimPinRequired': "SIM PIN required", 'SimPukRequired': "SIM PUK required", 'SimFailure': "SIM failure", 'SimBusy': "SIM busy", 'SimWrong': "SIM wrong", 'IncorrectPassword': "Incorrect password", 'SimPin2Required': "SIM PIN2 required", 'SimPuk2Required': "SIM PUK2 required", 'MemoryFull': "Memory full", 'InvalidIndex': "Invalid index", 'NotFound': "Not found", 'MemoryFailure': "Memory failure", 'TextTooLong': "Text string too long", 'InvalidChars': "Invalid characters in text string", 'DialStringTooLong': "Dial string too long", 'InvalidDialString': "Invalid dial string", 'NoNetwork': "No network service", 'NetworkTimeout': "Network timeout", 'NetworkNotAllowed': "Network not allowed - Emergency calls only", 'NetworkPinRequired': "Network personalization PIN required", 'NetworkPukRequired': "Network personalization PUK required", 'NetworkSubsetPinRequired': "Network subset personalization PIN required", 'NetworkSubsetPukRequired': "Network subset personalization PUK required", 'ServicePinRequired': "Service provider personalization PIN required", 'ServicePukRequired': "Service provider personalization PUK required", 'CorporatePinRequired': "Corporate personalization PIN required", 'CorporatePukRequired': "Corporate personalization PUK required", 'HiddenKeyRequired': "Hidden key required", 'EapMethodNotSupported': "EAP method not supported", 'IncorrectParams': "Incorrect parameters", 'Unknown': "Unknown error", 'GprsIllegalMs': "Illegal MS", 'GprsIllegalMe': "Illegal ME", 'GprsServiceNotAllowed': "GPRS services not allowed", 'GprsPlmnNotAllowed': "PLMN not allowed", 'GprsLocationNotAllowed': "Location are not allowed", 'GprsRoamingNotAllowed': "Roaming not allowed in this location area", 'GprsOptionNotSupported': "Service option not supported", 'GprsNotSuscribed': "Requested service option not suscribed", 'GprsOutOfOrder': "Service temporarily out of order", 'GprsPdpAuthFailure': "PDP authentication failure", 'GprsUnspecified': "Unspecified GPRS error", 'GprsInvalidClass': "Invalid mobile class", # Wader's own errors 'NetworkRegistrationError': "Could not register with home network", } wader-0.5.13/wader/common/backends/000077500000000000000000000000001257646610200170575ustar00rootroot00000000000000wader-0.5.13/wader/common/backends/__init__.py000066400000000000000000000006541257646610200211750ustar00rootroot00000000000000from wader.common.backends.nm import nm_backend from wader.common.backends.plain import plain_backend BACKEND_LIST = ['nm_backend', 'plain_backend'] __backend = None def get_backend(): global __backend if __backend is not None: return __backend for name in BACKEND_LIST: backend = globals()[name] if backend.should_be_used(): __backend = backend return __backend wader-0.5.13/wader/common/backends/nm.py000066400000000000000000001140321257646610200200440ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2011 Vodafone España, S.A. # Copyright (C) 2008-2010 Warp Networks, S.L. # Author: Pablo Martí # # 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 collections import defaultdict import os import copy import dbus from dbus.service import method, signal, Object, BusName from twisted.python import log from zope.interface import implements from wader.common._gconf import GConfHelper from wader.common.consts import (WADER_PROFILES_SERVICE, WADER_PROFILES_INTFACE, WADER_PROFILES_OBJPATH, MM_SYSTEM_SETTINGS_PATH, MM_ALLOWED_MODE_ANY, MM_ALLOWED_MODE_2G_PREFERRED, MM_ALLOWED_MODE_3G_PREFERRED, MM_ALLOWED_MODE_2G_ONLY, MM_ALLOWED_MODE_3G_ONLY) import wader.common.exceptions as ex from wader.common.interfaces import IBackend, IProfileManagerBackend from wader.common.keyring import (KeyringManager, KeyringInvalidPassword, KeyringIsClosed, KeyringNoMatchError) from wader.common.profile import Profile from wader.common.secrets import ProfileSecrets from wader.common.utils import (convert_int_to_uint32, convert_uint32_to_int, patch_list_signature, revert_dict) NM_SERVICE = 'org.freedesktop.NetworkManager' NM_OBJPATH = '/org/freedesktop/NetworkManager' NM_INTFACE = 'org.freedesktop.NetworkManager' NM_DEVICE = '%s.Device' % NM_INTFACE NM08_GSM_INTFACE = '%s.Gsm' % NM_DEVICE NM08_USER_SETTINGS = 'org.freedesktop.NetworkManagerUserSettings' NM08_SYSTEM_SETTINGS = 'org.freedesktop.NetworkManagerSettings' NM08_SYSTEM_SETTINGS_OBJ = '/org/freedesktop/NetworkManagerSettings' NM08_SYSTEM_SETTINGS_CONNECTION = '%s.Connection' % NM08_SYSTEM_SETTINGS NM08_SYSTEM_SETTINGS_SECRETS = '%s.Secrets' % NM08_SYSTEM_SETTINGS_CONNECTION NM084_SETTINGS = '%sUserSettings' % NM_SERVICE NM084_SETTINGS_INT = '%sSettings' % NM_INTFACE NM084_SETTINGS_OBJ = '%sSettings' % NM_OBJPATH NM084_SETTINGS_CONNECTION = '%s.Connection' % NM084_SETTINGS_INT NM084_SETTINGS_CONNECTION_SECRETS = '%s.Secrets' % NM084_SETTINGS_CONNECTION NM09_MODEM_INTFACE = '%s.Modem' % NM_DEVICE NM09_SETTINGS = NM_SERVICE NM09_SETTINGS_INT = '%s.Settings' % NM_INTFACE NM09_SETTINGS_OBJ = '%s/Settings' % NM_OBJPATH NM09_SETTINGS_CONNECTION = '%s.Connection' % NM09_SETTINGS_INT GCONF_PROFILES_BASE = '/system/networking/connections' NM_NETWORK_TYPE_MAP = { MM_ALLOWED_MODE_ANY: -1, MM_ALLOWED_MODE_2G_PREFERRED: 3, MM_ALLOWED_MODE_3G_PREFERRED: 2, MM_ALLOWED_MODE_2G_ONLY: 1, MM_ALLOWED_MODE_3G_ONLY: 0, } NM_NETWORK_TYPE_MAP_REV = revert_dict(NM_NETWORK_TYPE_MAP) def transpose_from_NM(oldprops): # call on read props = copy.deepcopy(oldprops) if 'gsm' in props: # map to Modem manager constants, default to ANY if not 'network-type' in props['gsm']: props['gsm']['network-type'] = MM_ALLOWED_MODE_ANY else: nm_val = props['gsm'].get('network-type') props['gsm']['network-type'] = NM_NETWORK_TYPE_MAP_REV[nm_val] # Note: password is never retrieved via plain props but we map it # anyway to be symmetric if 'password' in props['gsm']: props['gsm']['passwd'] = props['gsm']['password'] del props['gsm']['password'] if 'ipv4' in props: if 'dns' in props['ipv4']: props['ipv4']['ignore-auto-dns'] = (len(props['ipv4']['dns']) > 0) else: props['ipv4']['ignore-auto-dns'] = False # convert the integer format for key in ['addresses', 'dns', 'routes']: if key in props['ipv4']: vals = map(convert_uint32_to_int, props['ipv4'][key]) props['ipv4'][key] = vals return dict(props) def transpose_to_NM(oldprops, new=True): # call on write props = copy.deepcopy(oldprops) if 'gsm' in props: mm_val = props['gsm'].get('network-type', MM_ALLOWED_MODE_ANY) props['gsm']['network-type'] = NM_NETWORK_TYPE_MAP[mm_val] # filter out old single band settings, NM now uses a mask if 'band' in props['gsm']: del props['gsm']['band'] # Note: password is set via plain props if 'passwd' in props['gsm']: props['gsm']['password'] = props['gsm']['passwd'] del props['gsm']['passwd'] # NM doesn't like us setting these on update if not new: for key in ['connection', 'gsm', 'ppp', 'serial', 'ipv4']: if 'name' in props[key]: del props[key]['name'] if 'ipv4' in props: if not props['ipv4'].get('ignore-auto-dns'): props['ipv4']['dns'] = [] # convert the integer format for key in ['addresses', 'dns', 'routes']: if key in props['ipv4']: value = map(convert_int_to_uint32, props['ipv4'][key]) if key in ['dns']: props['ipv4'][key] = dbus.Array(value, signature='u') else: props['ipv4'][key] = dbus.Array(value, signature='au') return props class DummyKeyringManager(object): def __init__(self, get_cb, set_cb): self.get_cb = get_cb self.set_cb = set_cb def get_secrets(self, uuid): """Returns the secrets associated with ``uuid``""" return self.get_cb(uuid) def is_open(self): """Always open""" return True def update_secret(self, uuid, secrets, update=True): return self.set_cb(uuid, secrets) class DummySecrets(object): def __init__(self, connection, manager): self.uuid = connection.get_settings()['connection']['uuid'] self.manager = manager def get(self, ask=True): """Returns the secrets associated with the profile""" return self.manager.get_secrets(self.uuid) def is_open(self): return self.manager.is_open() def update(self, secrets, ask=True): """Updates the secrets associated with the profile""" return self.manager.update_secret(self.uuid, secrets) class GnomeKeyring(object): """I just wrap gnome-keyring""" def __init__(self): super(GnomeKeyring, self).__init__() self._is_new = False self.gk = None self.name = None self._setup_keyring() def _setup_keyring(self): # import it here so importing this backend on a non GNOME # system doesn't fails import gnomekeyring as gk self.gk = gk self.name = self.gk.get_default_keyring_sync() if not self.name: self._is_new = True self.name = 'login' # if keyring does not exist, create it try: self.gk.create_sync(self.name, None) except self.gk.AlreadyExistsError: pass self.gk.set_default_keyring_sync(self.name) def is_open(self): info = self.gk.get_info_sync(self.name) return not info.get_is_locked() def is_new(self): return self._is_new def open(self, password): """See :meth:`KeyringManager.open`""" if not self.is_open(): try: self.gk.unlock_sync(self.name, password) except (IOError, self.gk.DeniedError): raise KeyringInvalidPassword() def close(self): """See :meth:`KeyringManager.close`""" if self.is_open(): self.gk.lock_sync(self.name) else: raise KeyringIsClosed() def get(self, uuid): """See :meth:`KeyringManager.get_secrets`""" attrs = {'connection-uuid': str(uuid)} try: secrets = self.gk.find_items_sync( self.gk.ITEM_GENERIC_SECRET, attrs) return {'gsm': {'passwd': secrets[0].secret}} except self.gk.NoMatchError: msg = "No secrets for connection '%s'" raise KeyringNoMatchError(msg % str(uuid)) def update(self, uuid, conn_id, secrets, update=True): """See :meth:`KeyringManager.update_secret`""" attrs = {'connection-uuid': str(uuid), 'setting-name': 'gsm', 'setting-key': 'password'} password = secrets['gsm']['passwd'] text = 'Network secret for %s/%s/%s' % (conn_id, 'gsm', 'password') return self.gk.item_create_sync(self.name, self.gk.ITEM_GENERIC_SECRET, text, attrs, password, update) def delete(self, uuid): """See :meth:`KeyringManager.delete_secret`""" attrs = {'connection-uuid': str(uuid)} secrets = self.gk.find_items_sync(self.gk.ITEM_GENERIC_SECRET, attrs) # we find the secret, and we delete it return self.gk.item_delete_sync(self.name, secrets[0].item_id) class NMProfile(Profile): """I am a group of settings required to dial up""" def __init__(self, opath, nm_obj, props, manager): super(NMProfile, self).__init__(opath) self.nm_obj = nm_obj self.props = props self.manager = manager def _connect_to_signals(self): self.nm_obj.connect_to_signal("Removed", self._on_removed) self.nm_obj.connect_to_signal("Updated", self._on_updated) def get_secrets(self, tag, hints=None, ask=True): """ Returns the secrets associated with the profile :param tag: The section to use :param hints: what specific setting are we interested in :param ask: Should we ask the user if there is no secret? """ return self.secrets.get(ask) def get_settings(self): """Returns the profile settings""" return patch_list_signature(self.props) def get_timestamp(self): """Returns the last time this profile was used""" try: return self.get_settings()['connection']['timestamp'] except KeyError: return None def is_good(self): """Has this profile been successfully used?""" return bool(self.get_timestamp()) def on_open_keyring(self, tag): """Callback to be executed when the keyring has been opened""" secrets = self.secrets.get() if secrets: self.GetSecrets.reply(self, result=(secrets,)) else: self.KeyNeeded(self, tag) def set_secrets(self, tag, secrets): """ Sets or updates the secrets associated with the profile :param tag: The section to use :param secrets: The new secret to store """ self.secrets.update(secrets) self.GetSecrets.reply(self, result=(secrets,)) class NM08Profile(NMProfile): def __init__(self, opath, nm_obj, gpath, props, manager): super(NM08Profile, self).__init__(opath, nm_obj, props, manager) self.helper = GConfHelper() self.gpath = gpath from wader.common.backends import get_backend keyring = get_backend().get_keyring() self.secrets = ProfileSecrets(self, keyring) self._connect_to_signals() def _on_removed(self): log.msg("Profile %s has been removed externally" % self.opath) self.manager.remove_profile(self) def _on_updated(self, props): log.msg("Profile %s has been updated" % self.opath) self.update(props) def _write(self, props): self.props = props props = transpose_to_NM(props) for key, value in props.iteritems(): new_path = os.path.join(self.gpath, key) self.helper.set_value(new_path, value) self.helper.client.notify(self.gpath) self.helper.client.suggest_sync() def _load_info(self): props = {} if self.helper.client.dir_exists(self.gpath): self._load_dir(self.gpath, props) self.props = transpose_from_NM(props) def _load_dir(self, directory, info): for entry in self.helper.client.all_entries(directory): key = os.path.basename(entry.key) info[key] = self.helper.get_value(entry.value) for _dir in self.helper.client.all_dirs(directory): dirname = os.path.basename(_dir) info[dirname] = {} self._load_dir(_dir, info[dirname]) def update(self, props): """Updates the profile with settings ``props``""" self._write(props) self._load_info() self.Updated(patch_list_signature(self.props)) def remove(self): """Removes the profile""" from gconf import UNSET_INCLUDING_SCHEMA_NAMES self.helper.client.recursive_unset(self.gpath, UNSET_INCLUDING_SCHEMA_NAMES) # emit Removed and unexport from DBus self.Removed() self.remove_from_connection() class NM084Profile(NMProfile): def __init__(self, opath, nm_obj, props, manager): super(NM084Profile, self).__init__(opath, nm_obj, props, manager) self.secrets = DummySecrets(self, self.manager.keyring_manager) self._connect_to_signals() def _on_removed(self): log.msg("NM Connection profile %s has been removed" % self.opath) self.manager.remove_profile_cb(self) def _on_updated(self, props): log.msg("NM Connection profile %s has been updated" % self.opath) self.manager.update_profile_cb(self, props) def update(self, props): """Updates the profile with settings ``props``""" self.props = props # emit Updated self.Updated(patch_list_signature(self.props)) def remove(self): """Removes the profile""" # emit Removed and unexport from DBus self.Removed() self.remove_from_connection() class NM09Profile(NM084Profile): def _on_updated(self): log.msg("NM Connection profile %s has been updated" % self.opath) self.manager.update_profile_cb(self) class NMProfileManager(Object): """I manage profiles in the system""" implements(IProfileManagerBackend) def __init__(self): self.bus = dbus.SystemBus() bus_name = BusName(WADER_PROFILES_SERVICE, bus=self.bus) super(NMProfileManager, self).__init__(bus_name, WADER_PROFILES_OBJPATH) self.profiles = {} self.nm_profiles = {} self.nm_manager = None self.index = -1 self._init_nm_manager() def get_next_dbus_opath(self): self.index += 1 return os.path.join(MM_SYSTEM_SETTINGS_PATH, str(self.index)) def get_profile_by_object_path(self, opath): """Returns a :class:`Profile` out of its object path ``opath``""" for profile in self.profiles.values(): if profile.opath == opath: return profile raise ex.ProfileNotFoundError("No profile with object path %s" % opath) def get_profile_by_uuid(self, uuid): """ Returns the :class:`Profile` identified by ``uuid`` :param uuid: The uuid of the profile :raise ProfileNotFoundError: If no profile was found """ if not self.profiles: # initialise just in case self.get_profiles() try: return self.profiles[uuid] except KeyError: raise ex.ProfileNotFoundError("No profile with uuid %s" % uuid) @signal(dbus_interface=WADER_PROFILES_INTFACE, signature='o') def NewConnection(self, opath): pass @method(dbus_interface=WADER_PROFILES_INTFACE, in_signature='s', out_signature='o') def GetNMObjectPath(self, uuid): """Returns the object path of the connection referred by ``uuid``""" if uuid not in self.nm_profiles: msg = "Could not find profile %s in %s" raise KeyError(msg % (uuid, self.nm_profiles)) profile = self.nm_profiles[uuid] return profile.__dbus_object_path__ class NM08ProfileManager(NMProfileManager): """I manage profiles in the system""" def __init__(self): super(NM08ProfileManager, self).__init__() self.helper = GConfHelper() self.gpath = GCONF_PROFILES_BASE # connect to signals self._connect_to_signals() def _init_nm_manager(self): obj = self.bus.get_object(NM08_USER_SETTINGS, NM08_SYSTEM_SETTINGS_OBJ) self.nm_manager = dbus.Interface(obj, NM08_SYSTEM_SETTINGS) def _connect_to_signals(self): self.nm_manager.connect_to_signal("NewConnection", self._on_new_nm_profile, NM08_SYSTEM_SETTINGS) def _on_new_nm_profile(self, opath): obj = self.bus.get_object(NM08_USER_SETTINGS, opath) props = obj.GetSettings(dbus_interface=NM08_SYSTEM_SETTINGS_CONNECTION) # filter out non GSM profiles if props['connection']['type'] == 'gsm': self._add_nm_profile(obj, props) def _add_nm_profile(self, obj, props): uuid = props['connection']['uuid'] assert uuid not in self.nm_profiles, "Adding twice the same profile?" self.nm_profiles[uuid] = obj # handle when a NM profile has been externally added if uuid not in self.profiles: try: profile = self._get_profile_from_nm_connection(uuid) except ex.ProfileNotFoundError: log.msg("Removing non existing NM profile %s" % uuid) del self.nm_profiles[uuid] else: self.profiles[uuid] = profile self.NewConnection(profile.opath) def _get_next_free_gpath(self): """Returns the next unused slot of /system/networking/connections""" all_dirs = list(self.helper.client.all_dirs(self.gpath)) try: max_index = max(map(int, [d.split('/')[-1] for d in all_dirs])) except ValueError: # /system/networking/connections is empty max_index = -1 index = 0 if not all_dirs else max_index + 1 return os.path.join(self.gpath, str(index)) def _get_profile_from_nm_connection(self, uuid): for gpath in self.helper.client.all_dirs(self.gpath): # filter out wlan connections if self.helper.client.dir_exists(os.path.join(gpath, 'gsm')): path = os.path.join(gpath, 'connection', 'uuid') value = self.helper.client.get(path) if value and uuid == self.helper.get_value(value): return self._get_profile_from_gconf_path(gpath) msg = "NM profile identified by uuid %s could not be found" raise ex.ProfileNotFoundError(msg % uuid) def _get_profile_from_gconf_path(self, gconf_path): props = defaultdict(dict) for path in self.helper.client.all_dirs(gconf_path): for entry in self.helper.client.all_entries(path): section, key = entry.get_key().split('/')[-2:] value = entry.get_value() if value is not None: props[section][key] = self.helper.get_value(value) props = transpose_from_NM(props) uuid = props['connection']['uuid'] try: return NM08Profile(self.get_next_dbus_opath(), self.nm_profiles[uuid], gconf_path, props, self) except KeyError: raise ex.ProfileNotFoundError("Profile '%s' could not " "be found" % uuid) def _do_set_profile(self, path, props): props = transpose_to_NM(props) for key in props: for name in props[key]: value = props[key][name] _path = os.path.join(path, key, name) self.helper.set_value(_path, value) self.helper.client.notify(path) self.helper.client.suggest_sync() def add_profile(self, props): """Adds a profile with settings ``props``""" gconf_path = self._get_next_free_gpath() self._do_set_profile(gconf_path, props) # the rest will be handled by _on_new_nm_profile def get_profiles(self): """Returns all the profiles in the system""" if not self.nm_profiles: # cache existing profiles map(self._on_new_nm_profile, self.nm_manager.ListConnections()) if not self.profiles: for path in self.helper.client.all_dirs(self.gpath): # filter out wlan connections if self.helper.client.dir_exists(os.path.join(path, 'gsm')): # profile = self._get_profile_from_gconf_path(path) # uuid = profile.get_settings()['connection']['uuid'] # self.profiles[uuid] = profile try: profile = self._get_profile_from_gconf_path(path) uuid = profile.get_settings()['connection']['uuid'] self.profiles[uuid] = profile except ex.ProfileNotFoundError: pass return self.profiles.values() def remove_profile(self, profile): """Removes profile ``profile``""" uuid = profile.get_settings()['connection']['uuid'] assert uuid in self.profiles, "Removing a non-existent profile?" self.profiles[uuid].remove() del self.profiles[uuid] # as NetworkManager listens for GConf-DBus signals, we don't need # to manually sync it if uuid in self.nm_profiles: del self.nm_profiles[uuid] def update_profile(self, profile, props): """Updates ``profile`` with settings ``props``""" uuid = profile.get_settings()['connection']['uuid'] assert uuid in self.profiles, "Updating a non-existent profile?" _profile = self.profiles[uuid] _profile.update(props) props = transpose_to_NM(props, new=False) if uuid in self.nm_profiles: obj = self.nm_profiles[uuid] obj.Update(props, dbus_interface=NM08_SYSTEM_SETTINGS_CONNECTION) class NMLaterProfileManager(NMProfileManager): # XXX: Poor name but is intended to provide common stuff in 0.8.4 and 0.9, # to be subclassed only def __init__(self): super(NMLaterProfileManager, self).__init__() def _init_nm_manager(self): obj = self.bus.get_object(self.NM_SETTINGS, self.NM_SETTINGS_OBJ) self.nm_manager = dbus.Interface(obj, self.NM_SETTINGS_INT) def _connect_to_signals(self): self.nm_manager.connect_to_signal("NewConnection", self._on_new_nm_profile, self.NM_SETTINGS_INT) def _on_new_nm_profile(self, opath): log.msg("NM notified us of a new connection profile %s" % opath) self._store_new_profile(opath) def _get_profile_from_nm_connection(self, props): props = transpose_from_NM(props) uuid = props['connection']['uuid'] try: return self.NM_PROFILE_KLASS(self.get_next_dbus_opath(), self.nm_profiles[uuid], props, self) except KeyError: raise ex.ProfileNotFoundError("Profile '%s' could not " "be found" % uuid) def _keyring_set_callback(self, uuid, secrets): # Our UI clients expect that setting password secrets is a separate # operation to other profile activities when in fact we do it via the # same mechanism again, this means that two NM connection updates are # generated for every client profile save. passwd = secrets['gsm']['passwd'] if uuid in self.nm_profiles: obj = self.nm_profiles[uuid] # Need to merge in the password nm_props = obj.GetSettings(dbus_interface=self.NM_SETTINGS_CONNECTION) nm_props['gsm']['password'] = passwd obj.Update(nm_props, dbus_interface=self.NM_SETTINGS_CONNECTION) else: log.msg("NM connection profile does not exist for %s" % uuid) def _store_new_profile(self, opath): """ called: 1/ from signal handler when NM has a new connection 2/ by get_profiles to populate the profiles cache """ obj = self.bus.get_object(self.NM_SETTINGS, opath) props = obj.GetSettings(dbus_interface=self.NM_SETTINGS_CONNECTION) # filter out non GSM profiles if props['connection']['type'] != 'gsm': return uuid = props['connection']['uuid'] assert uuid not in self.nm_profiles, "Adding twice the same profile?" self.nm_profiles[uuid] = obj # handle when a NM profile has been externally added if uuid not in self.profiles: try: profile = self._get_profile_from_nm_connection(props) except ex.ProfileNotFoundError: log.msg("Adding non existing NM profile %s" % uuid) del self.nm_profiles[uuid] else: self.profiles[uuid] = profile self.NewConnection(profile.opath) def get_profiles(self): """Returns all the profiles in the system""" if not self.nm_profiles: # cache existing profiles map(self._store_new_profile, self.nm_manager.ListConnections()) if self.profiles is None: return [] return self.profiles.values() def remove_profile(self, profile): """ Removes profile ``profile`` Should initiate the NM connection removal, but the removal of our profile should be done by the signal handler """ uuid = profile.get_settings()['connection']['uuid'] if uuid in self.nm_profiles: obj = self.nm_profiles[uuid] obj.Delete(dbus_interface=self.NM_SETTINGS_CONNECTION) else: log.msg("NM connection profile does not exist for %s" % uuid) def remove_profile_cb(self, profile): """ Called by NMxxxProfile's Remove signal handler """ log.msg("NM notified us of a connection profile removal") uuid = profile.props['connection']['uuid'] if uuid in self.profiles: self.profiles[uuid].remove() del self.profiles[uuid] if uuid in self.nm_profiles: del self.nm_profiles[uuid] def update_profile(self, profile, props): """ Updates ``profile`` with settings ``props`` Should initiate the NM connection update, but the update of our profile should be done by the signal handler """ uuid = profile.get_settings()['connection']['uuid'] nm_props = transpose_to_NM(props, new=False) if uuid in self.nm_profiles: obj = self.nm_profiles[uuid] obj.Update(nm_props, dbus_interface=self.NM_SETTINGS_CONNECTION) else: log.msg("NM connection profile does not exist for %s" % uuid) def update_profile_cb(self, profile, nm_props=None): """ Called by NMxxxProfile's Updated signal handler Called twice per BCM profile change as passwd is updated separately via the keyring """ log.msg("NM notified us of a connection profile update") obj = profile.nm_obj if nm_props is None: # NM 0.9 doesn't signal the changed props so we have to retrieve them nm_props = obj.GetSettings(dbus_interface=self.NM_SETTINGS_CONNECTION) # NM 0.8.4 does signal the changed props but we still have to retrieve the # password and merge try: secrets = obj.GetSecrets('gsm', dbus_interface=self.NM_SETTINGS_CONNECTION) password = secrets['gsm']['password'] except (KeyError, dbus.exceptions.DBusException): pass else: nm_props['gsm']['password'] = password props = transpose_from_NM(nm_props) uuid = props['connection']['uuid'] if uuid in self.profiles: self.profiles[uuid].update(props) class NM084ProfileManager(NMLaterProfileManager): def __init__(self): super(NM084ProfileManager, self).__init__() self.keyring_manager = DummyKeyringManager(self._keyring_get_callback, self._keyring_set_callback) self.helper = GConfHelper() # profile class to create self.NM_PROFILE_KLASS = NM084Profile # connect to signals self._connect_to_signals() def _init_nm_manager(self): # define DBus details self.NM_SETTINGS = NM084_SETTINGS self.NM_SETTINGS_OBJ = NM084_SETTINGS_OBJ self.NM_SETTINGS_INT = NM084_SETTINGS_INT self.NM_SETTINGS_CONNECTION = NM084_SETTINGS_CONNECTION super(NM084ProfileManager, self)._init_nm_manager() def _get_secrets_gnome(self, uuid): # Absolutely the bare minimum wanted here, if the keyring doesn't exist # don't create it, and if we fail because we aren't on a gnome system or # the value doesn't exist then just return None attr = { 'setting-name': 'gsm', 'setting-key': 'password', 'connection-uuid': str(uuid) } try: import gnomekeyring as gk # Search all the keyrings for it, not just default items = gk.find_items_sync(gk.ITEM_GENERIC_SECRET, attr) return items[0].secret except (ImportError, IndexError, gk.IOError, gk.NoMatchError): return None def _keyring_get_callback(self, uuid): # Getting the secrets probably will need to be obtained from Gnome Keyring # directly on NM 0.8.4 as the DBus config usually restricts the secrets # access to root only try: secrets = self.nm_profiles[uuid].GetSecrets('gsm', ['password',], True, dbus_interface=NM084_SETTINGS_CONNECTION_SECRETS) return transpose_from_NM(secrets) except KeyError: # XXX: ideally we'd return None, but callers may expect return {u'gsm': {u'passwd': u'not found'}} except dbus.exceptions.DBusException, e: passwd = self._get_secrets_gnome(uuid) if passwd is not None: return {u'gsm': {u'passwd': unicode(passwd)}} # XXX: TODO, try KDE's keyring, kwallet # XXX: ideally we'd return None, but callers may expect return {u'gsm': {u'passwd': u'not found'}} def _get_next_free_gpath(self): """Returns the next unused slot of /system/networking/connections""" all_dirs = list(self.helper.client.all_dirs(GCONF_PROFILES_BASE)) try: max_index = max(map(int, [d.split('/')[-1] for d in all_dirs])) except ValueError: # /system/networking/connections is empty max_index = -1 index = 0 if not all_dirs else max_index + 1 return os.path.join(GCONF_PROFILES_BASE, str(index)) def _write_NM_connection_to_gconf(self, path, props): for key in props: for name in props[key]: value = props[key][name] _path = os.path.join(path, key, name) self.helper.set_value(_path, value) self.helper.client.notify(path) self.helper.client.suggest_sync() def add_profile(self, props): """Adds a profile with settings ``props``""" log.msg("Creating a new NM connection profile in GConf") props = transpose_to_NM(props) # AddConnection(props) never got implemented in NM 0.8x # So we need to write to gconf and wait for the applet to notice next_gpath = self._get_next_free_gpath() self._write_NM_connection_to_gconf(next_gpath, props) # The rest will be handled by _on_new_nm_profile when the signal # arrives class NM09ProfileManager(NMLaterProfileManager): def __init__(self): super(NM09ProfileManager, self).__init__() self.keyring_manager = DummyKeyringManager(self._keyring_get_callback, self._keyring_set_callback) # profile class to create self.NM_PROFILE_KLASS = NM09Profile # connect to signals self._connect_to_signals() def _init_nm_manager(self): # define DBus details self.NM_SETTINGS = NM09_SETTINGS self.NM_SETTINGS_OBJ = NM09_SETTINGS_OBJ self.NM_SETTINGS_INT = NM09_SETTINGS_INT self.NM_SETTINGS_CONNECTION = NM09_SETTINGS_CONNECTION super(NM09ProfileManager, self)._init_nm_manager() def _keyring_get_callback(self, uuid): # On NM0.9 we can get the secrets directly from the network manager # service try: secrets = self.nm_profiles[uuid].GetSecrets('gsm', dbus_interface=self.NM_SETTINGS_CONNECTION) except (KeyError, dbus.exceptions.DBusException): # XXX: ideally we'd return None, but callers may expect return {u'gsm': {u'passwd': u'not found'}} else: return transpose_from_NM(secrets) def add_profile(self, props): """Adds a profile with settings ``props``""" log.msg("Asking NM to create new connection profile") props = transpose_to_NM(props) # In NM 0.9 this finally got implemented self.nm_manager.AddConnection(props) # The rest will be handled by _on_new_nm_profile when the signal # arrives class NetworkManagerBackend(object): implements(IBackend) def __init__(self): self.bus = dbus.SystemBus() self._nm08_core_present = None self._nm08_applet_present = None self._nm084_present = None self._nm09_present = None self._profile_manager = None self._keyring_manager = None def _is_nm08_core_present(self): if self._nm08_core_present is None: try: obj = self.bus.get_object(NM_SERVICE, NM_OBJPATH) devices = obj.GetDevices() if len(devices): self._nm08_core_present = 'NetworkManager' in devices[0] else: self._nm08_core_present = False except dbus.DBusException: self._nm08_core_present = False return self._nm08_core_present def _is_nm08_applet_present(self): if self._nm08_applet_present is None: try: self.bus.get_object(NM08_USER_SETTINGS, NM08_SYSTEM_SETTINGS_OBJ) self._nm08_applet_present = True except dbus.DBusException: self._nm08_applet_present = False return self._nm08_applet_present def _is_nm08_present(self): return all([self._is_nm08_core_present(), self._is_nm08_applet_present()]) def _is_nm084_present(self): if self._nm084_present is None: try: # NM 0.8.4 core now provides a version property obj = self.bus.get_object(NM_SERVICE, NM_OBJPATH) iface = dbus.Interface(obj, "org.freedesktop.DBus.Properties") ver = iface.Get(NM_INTFACE, "Version") if ver.startswith('0.8.4') or ver.startswith('0.8.3.99'): self._nm084_present = True else: self._nm084_present = False except dbus.DBusException: self._nm084_present = False return self._nm084_present def _is_nm09_present(self): if self._nm09_present is None: try: # NM 0.9 core now provides a version property obj = self.bus.get_object(NM_SERVICE, NM_OBJPATH) iface = dbus.Interface(obj, "org.freedesktop.DBus.Properties") ver = iface.Get(NM_INTFACE, "Version") if ver.startswith('0.9') or ver.startswith('0.8.99'): self._nm09_present = True else: self._nm09_present = False except dbus.DBusException: self._nm09_present = False return self._nm09_present def _get_version(self): """ There may be some crossover between the _is_nmXX_present() functions. Using this function evaluates version in the correct order. """ if self._is_nm09_present(): return '09' if self._is_nm084_present(): return '084' if self._is_nm08_present(): return '08' return None def should_be_used(self): """ Returns True if: NM084 or NM09 is present or Both NM08 core and UI applet are present """ return self._get_version() is not None def get_dialer_klass(self, device): # Fake function to comply with interface. pass def get_keyring(self): # XXX: should be called get_keyring_manager if self._keyring_manager is None: if self._get_version() == '08': self._keyring_manager = KeyringManager(GnomeKeyring()) else: pm = self.get_profile_manager() self._keyring_manager = pm.keyring_manager return self._keyring_manager def get_profile_manager(self, arg=None): if self._profile_manager is None: if self._get_version() == '08': self._profile_manager = NM08ProfileManager() elif self._get_version() == '084': self._profile_manager = NM084ProfileManager() else: self._profile_manager = NM09ProfileManager() return self._profile_manager nm_backend = NetworkManagerBackend() wader-0.5.13/wader/common/backends/plain.py000066400000000000000000000243011257646610200205340ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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 with_statement import pickle from cStringIO import StringIO import os import dbus from dbus.service import signal, Object, BusName from zope.interface import implements from wader.common.aes import decryptData, encryptData from wader.common.consts import (WADER_PROFILES_SERVICE, WADER_PROFILES_INTFACE, WADER_PROFILES_OBJPATH, MM_SYSTEM_SETTINGS_PATH) import wader.common.exceptions as ex from wader.common.interfaces import IBackend, IProfileManagerBackend from wader.common.keyring import (KeyringManager, KeyringInvalidPassword, KeyringIsClosed, KeyringNoMatchError) from wader.common.profile import Profile from wader.common.secrets import ProfileSecrets from wader.common.utils import patch_list_signature class PlainProfile(Profile): """I am a group of settings required to dial up""" def __init__(self, opath, path, secrets_path, props=None): super(PlainProfile, self).__init__(opath) self.path = path self.secrets_path = secrets_path self.props = props self.secrets = None self._init() def _init(self): if self.props is None: # created with "from_path" self.props = pickle.load(open(self.path)) else: # regular constructor with properties self._write() from wader.common.backends import get_backend keyring = get_backend().get_keyring(self.secrets_path) self.secrets = ProfileSecrets(self, keyring) def _write(self): with open(self.path, 'w') as configfile: pickle.dump(self.props, configfile, pickle.HIGHEST_PROTOCOL) @classmethod def from_path(cls, opath, path, secrets_path): return cls(opath, path, secrets_path) def get_settings(self): """Returns the profile settings""" return patch_list_signature(self.props) def get_secrets(self, tag, hints=None, ask=True): """ Returns the secrets associated with the profile :param tag: The section to use :param hints: what specific setting are we interested in :param ask: Should we ask the user if there is no secret? """ return self.secrets.get() def get_timestamp(self): """Returns the last time this profile was used""" return self.props['connection'].get('timestamp', 0) def is_good(self): """Has this profile been successfully used?""" return bool(self.get_timestamp()) def on_open_keyring(self, tag): """Callback to be executed when the keyring has been opened""" secrets = self.secrets.get(tag) if secrets: self.GetSecrets.reply(self, result=(secrets,)) else: self.KeyNeeded(self, tag) def set_secrets(self, tag, secrets): """ Sets or updates the secrets associated with the profile :param tag: The section to use :param secrets: The new secret to store """ self.secrets.update(secrets) self.GetSecrets.reply(self, result=(secrets,)) def update(self, props): """Updates the profile with settings ``props``""" self.props = props self._write() # emit the signal self.Updated(patch_list_signature(props)) def remove(self): """Removes the profile""" os.unlink(self.path) # emit Removed and unexport from DBus self.Removed() self.remove_from_connection() class PlainProfileManager(Object): """I manage profiles in the system""" implements(IProfileManagerBackend) def __init__(self, base_path): self.bus = dbus.SystemBus() bus_name = BusName(WADER_PROFILES_SERVICE, bus=self.bus) super(PlainProfileManager, self).__init__(bus_name, WADER_PROFILES_OBJPATH) self.profiles = {} self.index = -1 self.base_path = base_path self.profiles_path = os.path.join(base_path, 'profiles') self.secrets_path = os.path.join(base_path, 'secrets') self._init() def _init(self): # check that the profiles path exists and create it otherwise if not os.path.exists(self.profiles_path): os.makedirs(self.profiles_path, mode=0700) # now load the profiles for uuid in os.listdir(self.profiles_path): path = os.path.join(self.profiles_path, uuid) profile = PlainProfile.from_path(self.get_next_dbus_opath(), path, self.secrets_path) self.profiles[uuid] = profile def get_next_dbus_opath(self): self.index += 1 return os.path.join(MM_SYSTEM_SETTINGS_PATH, str(self.index)) def add_profile(self, props): """Adds a profile with settings ``props``""" uuid = props['connection']['uuid'] path = os.path.join(self.profiles_path, uuid) if os.path.exists(path): raise ValueError("Profile with uuid %s already exists" % uuid) profile = PlainProfile(self.get_next_dbus_opath(), path, self.secrets_path, props=props) self.profiles[uuid] = profile self.NewConnection(profile.opath) def get_profile_by_uuid(self, uuid): """ Returns the :class:`Profile` identified by ``uuid`` :param uuid: The uuid of the profile :raise ProfileNotFoundError: If no profile was found """ try: return self.profiles[uuid] except KeyError: raise ex.ProfileNotFoundError("No profile with uuid %s" % uuid) def get_profile_by_object_path(self, opath): """Returns a :class:`Profile` out of its object path ``opath``""" for profile in self.profiles.values(): if profile.opath == opath: return profile raise ex.ProfileNotFoundError("No profile with opath %s" % opath) def get_profiles(self): """Returns all the profiles in the system""" return self.profiles.values() def remove_profile(self, profile): """Removes profile ``profile``""" uuid = profile.get_settings()['connection']['uuid'] profile = self.get_profile_by_uuid(uuid) self.profiles[uuid].remove() del self.profiles[uuid] def update_profile(self, profile, props): """Updates ``profile`` with settings ``props``""" uuid = profile.get_settings()['connection']['uuid'] profile = self.get_profile_by_uuid(uuid) profile.update(props) @signal(dbus_interface=WADER_PROFILES_INTFACE, signature='o') def NewConnection(self, opath): pass def transform_passwd(passwd): valid_lengths = [16, 24, 32] for l in valid_lengths: if l >= len(passwd): return passwd.zfill(l) msg = "Password '%s' is too long, max length is 32 chars" raise KeyringInvalidPassword(msg % passwd) class PlainKeyring(object): def __init__(self, secrets_path): self.secrets_path = secrets_path self.key = None self._is_open = False self._is_new = True self._init() def _init(self): if os.path.exists(self.secrets_path): self._is_new = False else: os.makedirs(self.secrets_path, 0700) def is_open(self): return self._is_open def is_new(self): return self._is_new def open(self, password): if not self.is_open(): self.key = transform_passwd(password) self._is_open = True def close(self): if not self.is_open(): raise KeyringIsClosed("Keyring is already closed") self.key = None self._is_open = False def get(self, uuid): try: data = open(os.path.join(self.secrets_path, uuid)).read() except IOError: raise KeyringNoMatchError("No secrets for uuid %s" % uuid) pickledobj = decryptData(self.key, data, testforpickle=True) try: return pickle.load(StringIO(pickledobj)) except: raise KeyringNoMatchError("bad password") def update(self, uuid, conn_id, secrets, update=True): path = os.path.join(self.secrets_path, uuid) with open(path, 'w') as f: data = encryptData(self.key, pickle.dumps(secrets)) f.write(data) def delete(self, uuid): path = os.path.join(self.secrets_path, uuid) if not os.path.exists(path): raise KeyringNoMatchError("No secrets for uuid %s" % uuid) os.unlink(path) self._is_new = False class PlainBackend(object): """Plain backend""" implements(IBackend) def __init__(self): self.bus = dbus.SystemBus() self._profile_manager = None self._keyring = None def should_be_used(self): # always used as a last resort return True def get_dialer_klass(self, device): # Fake function to comply with interface. pass def get_keyring(self, secrets_path): if self._keyring is None: self._keyring = KeyringManager(PlainKeyring(secrets_path)) return self._keyring def get_profile_manager(self, arg=None): if self._profile_manager is None: self._profile_manager = PlainProfileManager(arg) return self._profile_manager plain_backend = PlainBackend() wader-0.5.13/wader/common/config.py000066400000000000000000000037011257646610200171250ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """ GConf-powered config This module lives in wader.common so it can be used from applications that depend on python-wader. Do not try to use it in the core as it will fail. """ from os.path import join from wader.common._gconf import GConfHelper class WaderConfig(GConfHelper): """I manage Wader config""" def __init__(self, keys, base_path): # despite the fact that having default mutable types as # argument in python, keys will never be modified, only # read, so we are safe using it this way. super(WaderConfig, self).__init__() self.keys = keys self.base_path = base_path def get(self, section, option, default=None): """ Returns the value at ``section/option`` Will return ``default`` if undefined """ value = self.client.get(join(self.base_path, section, option)) if not value: return (default if default is not None else "") return self.get_value(value) def set(self, section, option, value): """Sets ``value`` at ``section/option``""" path = join(self.base_path, section, option) self.set_value(path, value) wader-0.5.13/wader/common/consts.py000066400000000000000000000166641257646610200172050ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2012 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Wader global variables""" from os import environ from os.path import join from dbus import UInt32 # app name APP_NAME = 'Wader' APP_SLUG_NAME = 'wader-core' APP_VERSION = '0.5.13' # DBus stuff WADER_SERVICE = 'org.freedesktop.ModemManager' WADER_OBJPATH = '/org/freedesktop/ModemManager' WADER_INTFACE = 'org.freedesktop.ModemManager' WADER_DIALUP_INTFACE = 'org.freedesktop.ModemManager.Dialup' WADER_DIALUP_SERVICE = 'org.freedesktop.ModemManager.Dialup' WADER_DIALUP_OBJECT = '/org/freedesktop/ModemManager/DialupManager' WADER_DIALUP_BASE = '/org/freedesktop/ModemManager/Connections/%d' WADER_PROFILES_SERVICE = 'org.freedesktop.ModemManager.Profiles' WADER_PROFILES_OBJPATH = '/org/freedesktop/ModemManager/Profiles' WADER_PROFILES_INTFACE = WADER_PROFILES_SERVICE WADER_KEYRING_SERVICE = 'org.freedesktop.ModemManager.Keyring' WADER_KEYRING_OBJPATH = '/org/freedesktop/ModemManager/Keyring' WADER_KEYRING_INTFACE = WADER_KEYRING_SERVICE MDM_INTFACE = 'org.freedesktop.ModemManager.Modem' SPL_INTFACE = 'org.freedesktop.ModemManager.Modem.Simple' SMS_INTFACE = 'org.freedesktop.ModemManager.Modem.Gsm.SMS' MMS_INTFACE = 'org.freedesktop.ModemManager.Modem.Gsm.Mms' CTS_INTFACE = 'org.freedesktop.ModemManager.Modem.Gsm.Contacts' NET_INTFACE = 'org.freedesktop.ModemManager.Modem.Gsm.Network' USD_INTFACE = 'org.freedesktop.ModemManager.Modem.Gsm.Ussd' CRD_INTFACE = 'org.freedesktop.ModemManager.Modem.Gsm.Card' HSO_INTFACE = 'org.freedesktop.ModemManager.Modem.Gsm.Hso' STATUS_IDLE, STATUS_HOME, STATUS_SEARCHING = 0, 1, 2 STATUS_DENIED, STATUS_UNKNOWN, STATUS_ROAMING = 3, 4, 5 MM_MODEM_STATE_UNKNOWN = UInt32(0) MM_MODEM_STATE_DISABLED = UInt32(10) MM_MODEM_STATE_DISABLING = UInt32(20) MM_MODEM_STATE_ENABLING = UInt32(30) MM_MODEM_STATE_ENABLED = UInt32(40) MM_MODEM_STATE_SEARCHING = UInt32(50) MM_MODEM_STATE_REGISTERED = UInt32(60) MM_MODEM_STATE_DISCONNECTING = UInt32(70) MM_MODEM_STATE_CONNECTING = UInt32(80) MM_MODEM_STATE_CONNECTED = UInt32(90) # Google DNS, used in the event that we have nothing else valid FALLBACK_DNS = ['8.8.8.8', '8.8.4.4'] MM_MODEM_TYPE = { UInt32(1): 'GSM', UInt32(2): 'CDMA', } MM_MODEM_TYPE_REV = dict(GSM=UInt32(1), CDMA=UInt32(2)) MM_IP_METHOD_PPP = UInt32(0) MM_IP_METHOD_STATIC = UInt32(1) MM_IP_METHOD_DHCP = UInt32(2) MM_GSM_ACCESS_TECH_UNKNOWN = UInt32(0) MM_GSM_ACCESS_TECH_GSM = UInt32(1) MM_GSM_ACCESS_TECH_GSM_COMPAT = UInt32(2) MM_GSM_ACCESS_TECH_GPRS = UInt32(3) MM_GSM_ACCESS_TECH_EDGE = UInt32(4) MM_GSM_ACCESS_TECH_UMTS = UInt32(5) MM_GSM_ACCESS_TECH_HSDPA = UInt32(6) MM_GSM_ACCESS_TECH_HSUPA = UInt32(7) MM_GSM_ACCESS_TECH_HSPA = UInt32(8) MM_GSM_ACCESS_TECH_HSPA_PLUS = UInt32(9) MM_GSM_ACCESS_TECH_LTE = UInt32(10) MM_GSM_ACCESS_TECHNOLOGIES = [ MM_GSM_ACCESS_TECH_UNKNOWN, MM_GSM_ACCESS_TECH_GSM, MM_GSM_ACCESS_TECH_GSM_COMPAT, MM_GSM_ACCESS_TECH_GPRS, MM_GSM_ACCESS_TECH_EDGE, MM_GSM_ACCESS_TECH_UMTS, MM_GSM_ACCESS_TECH_HSDPA, MM_GSM_ACCESS_TECH_HSUPA, MM_GSM_ACCESS_TECH_HSPA, MM_GSM_ACCESS_TECH_HSPA_PLUS, MM_GSM_ACCESS_TECH_LTE] # MM_NETWORK_MODE_* is deprecated # it will probably go away in NM 0.9/1.0 MM_NETWORK_MODE_UNKNOWN = 0x00000000 MM_NETWORK_MODE_ANY = 0x00000001 MM_NETWORK_MODE_GPRS = 0x00000002 MM_NETWORK_MODE_EDGE = 0x00000004 MM_NETWORK_MODE_UMTS = 0x00000008 MM_NETWORK_MODE_HSDPA = 0x00000010 MM_NETWORK_MODE_2G_PREFERRED = 0x00000020 MM_NETWORK_MODE_3G_PREFERRED = 0x00000040 MM_NETWORK_MODE_2G_ONLY = 0x00000080 MM_NETWORK_MODE_3G_ONLY = 0x00000100 MM_NETWORK_MODE_HSUPA = 0x00000200 MM_NETWORK_MODE_HSPA = 0x00000400 MM_NETWORK_MODE_LAST = MM_NETWORK_MODE_HSPA MM_NETWORK_MODES = [ MM_NETWORK_MODE_GPRS, MM_NETWORK_MODE_EDGE, MM_NETWORK_MODE_UMTS, MM_NETWORK_MODE_HSDPA, MM_NETWORK_MODE_2G_PREFERRED, MM_NETWORK_MODE_3G_PREFERRED, MM_NETWORK_MODE_2G_ONLY, MM_NETWORK_MODE_3G_ONLY, MM_NETWORK_MODE_HSUPA, MM_NETWORK_MODE_HSPA] MM_ALLOWED_AUTH_UNKNOWN = 0x0 # Unknown or invalid auth MM_ALLOWED_AUTH_NONE = 0x1 # Supports no authentication MM_ALLOWED_AUTH_PAP = 0x2 MM_ALLOWED_AUTH_CHAP = 0x4 MM_ALLOWED_AUTH_MSCHAP = 0x8 MM_ALLOWED_AUTH_MSCHAPV2 = 0x10 MM_ALLOWED_AUTH_EAP = 0x20 MM_ALLOWED_MODE_ANY = 0 MM_ALLOWED_MODE_2G_PREFERRED = 1 MM_ALLOWED_MODE_3G_PREFERRED = 2 MM_ALLOWED_MODE_2G_ONLY = 3 MM_ALLOWED_MODE_3G_ONLY = 4 MM_ALLOWED_MODES = [ MM_ALLOWED_MODE_ANY, MM_ALLOWED_MODE_2G_PREFERRED, MM_ALLOWED_MODE_3G_PREFERRED, MM_ALLOWED_MODE_2G_ONLY, MM_ALLOWED_MODE_3G_ONLY] MM_NETWORK_BAND_UNKNOWN = 0x0 # Unknown or invalid band MM_NETWORK_BAND_ANY = 0x1 # ANY MM_NETWORK_BAND_EGSM = 0x2 # 900 MHz MM_NETWORK_BAND_DCS = 0x4 # 1800 MHz MM_NETWORK_BAND_PCS = 0x8 # 1900 MHz MM_NETWORK_BAND_G850 = 0x10 # 850 MHz MM_NETWORK_BAND_U2100 = 0x20 # WCDMA 2100 MHz MM_NETWORK_BAND_U1800 = 0x40 # WCDMA 3GPP UMTS1800 MHz MM_NETWORK_BAND_U17IV = 0x80 # WCDMA 3GPP AWS 1700/2100 MHz MM_NETWORK_BAND_U800 = 0x100 # WCDMA 3GPP UMTS800 MHz MM_NETWORK_BAND_U850 = 0x200 # WCDMA 3GPP UMTS850 MHz MM_NETWORK_BAND_U900 = 0x400 # WCDMA 3GPP UMTS900 MHz MM_NETWORK_BAND_U17IX = 0x800 # WCDMA 3GPP UMTS MHz MM_NETWORK_BAND_U1900 = 0x1000 # WCDMA 3GPP UMTS MHz MM_NETWORK_BAND_LAST = MM_NETWORK_BAND_U1900 MM_NETWORK_BANDS = [ MM_NETWORK_BAND_EGSM, MM_NETWORK_BAND_DCS, MM_NETWORK_BAND_PCS, MM_NETWORK_BAND_G850, MM_NETWORK_BAND_U2100, MM_NETWORK_BAND_U1800, MM_NETWORK_BAND_U17IV, MM_NETWORK_BAND_U800, MM_NETWORK_BAND_U850, MM_NETWORK_BAND_U900, MM_NETWORK_BAND_U17IX, MM_NETWORK_BAND_U1900] MM_SYSTEM_SETTINGS_PATH = '/org/freedesktop/ModemManager/Settings' WADER_CONNTYPE_UNKNOWN = 0 WADER_CONNTYPE_PCMCIA = 1 WADER_CONNTYPE_EMBEDDED = 2 WADER_CONNTYPE_BLUETOOTH = 3 WADER_CONNTYPE_IRDA = 4 WADER_CONNTYPE_SERIAL = 5 WADER_CONNTYPE_USB = 6 WADER_CONNTYPE_WLAN = 7 # necessary for relocatable bundling on OSX BASE_DIR = environ.get('WADER_PREFIX', '/') DATA_DIR = join(BASE_DIR, 'usr', 'share', APP_SLUG_NAME) WADER_DOC = join(BASE_DIR, 'usr', 'share', 'doc', APP_SLUG_NAME, 'guide') # paths RESOURCES_DIR = join(DATA_DIR, 'resources') TEMPLATES_DIR = join(RESOURCES_DIR, 'config') EXTRA_DIR = join(RESOURCES_DIR, 'extra') # databases MBPI = '/usr/share/mobile-broadband-provider-info/serviceproviders.xml' NETWORKS_DB = join(DATA_DIR, 'networks.db') # plugins consts PLUGINS_DIR = join(DATA_DIR, 'plugins') PLUGINS_DIR = [PLUGINS_DIR, join(PLUGINS_DIR, 'oses'), join(PLUGINS_DIR, 'contacts'), join(PLUGINS_DIR, 'devices')] PID_PATH = join(BASE_DIR, 'var', 'run', 'wader.pid') # logger consts LOG_NAME = 'wader.log' LOG_DIR = join(BASE_DIR, 'var', 'log') LOG_NUMBER = 6 wader-0.5.13/wader/common/encoding.py000066400000000000000000000211571257646610200174530ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2011 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Helper methods for dealing with encoded strings""" import messaging.sms.gsm0338 # imports GSM7 codec CONTROL_0, CONTROL_1, LATIN_EX_A, LATIN_EX_B = range(4) ZIP_BASE64_MAGIC = '{ZIP+BASE64}' def pack_ucs2_bytes(s): """ Converts string ``s`` to UCS2 :rtype: str """ return to_u(s).encode('utf_16_be').encode('hex').upper() def unpack_ucs2_bytes(s): """ Unpacks string ``s`` from UCS2 :rtype: unicode """ return s.decode('hex').decode('utf_16_be', 'ignore') def unpack_ucs2_bytes_in_ts31101_80(s): """ Returns a string from ``s`` which is encoded in TS 31.101 (Annex A) type 80 Check out the function comments """ # Below is the detail from there, but we expect the first two hex # chars(80) to have been removed already. # If the first byte in the alpha string is '80', then the remaining # bytes are 16 bit UCS2 characters, with the more significant byte (MSB) # of the UCS2 character coded in the lower numbered byte of the alpha # field, and the less significant byte (LSB) of the UCS2 character is # coded in the higher numbered alpha field byte, i.e. byte 2 of the # alpha field contains the more significant byte (MSB) of the first UCS2 # character, and byte 3 of the alpha field contains the less significant # byte (LSB) of the first UCS2 character (as shown below). Unused bytes # shall be set to 'FF', and if the alpha field is an even number of bytes # in length, then the last (unusable) byte shall be set to 'FF'. # # example string '058300440586FF' vl = len(s) - len(s) % 4 vs = s[:vl] try: t = unpack_ucs2_bytes(vs) except: t = vs # show the invalid unicode return t def unpack_ucs2_bytes_in_ts31101_81(s): """ Returns a string from ``s`` which is encoded in TS 31.101 (Annex A) type 81 Check out the function comments """ # Below is the detail from there, but we expect the first two hex # chars(81) to have been removed already. # If the first byte of the alpha string is set to '81', then the second # byte contains a value indicating the number of characters in the string, # and the third byte contains an 8 bit number which defines bits 15 to 8 # of a 16 bit base pointer, where bit 16 is set to zero, and bits 7 to 1 # are also set to zero. These sixteen bits constitute a base pointer # to a "half-page" in the UCS2 code space, to be used with some or all of # the remaining bytes in the string. The fourth and subsequent bytes in # the string contain codings as follows; if bit 8 of the byte is set to # zero, the remaining 7 bits of the byte contain a GSM Default Alphabet # character, whereas if bit 8 of the byte is set to one, then the # remaining seven bits are an offset value added to the 16 bit base # pointer defined earlier, and the resultant 16 bit value is a UCS2 code # point, and completely defines a UCS2 character. # # example string '0602A46563746F72FF' num = ord(s[:2].decode('hex')) base = (ord(s[2:4].decode('hex')) & 0x7f) << 7 # bits 15..8 chars = s[4:4 + num * 2] t = '' for i in range(num): j = i * 2 c_hex = chars[j:j + 2] c_chr = c_hex.decode('hex') c_ord = ord(c_chr) if c_ord & 0x80 == 0: t += c_chr.decode('gsm0338', 'replace') else: t += unichr(base + (c_ord & 0x7f)) return t def unpack_ucs2_bytes_in_ts31101_82(s): """ Returns a string from ``s`` which is encoded in TS 31.101 (Annex A) type 82 Check out the function comments """ # Below is the detail from there, but we expect the first two hex # chars(82) to have been removed already. # If the first byte of the alpha string is set to '82', then the # second byte contains a value indicating the number of characters # in the string, and the third and fourth bytes contain a 16 bit number # which defines the complete 16 bit base pointer to a "half-page" in the # UCS2 code space, for use with some or all of the remaining bytes in the # string. The fifth and subsequent bytes in the string contain codings as # follows; if bit 8 of the byte is set to zero, the remaining 7 bits of # the byte contain a GSM Default Alphabet character, whereas if bit 8 of # the byte is set to one, the remaining seven bits are an offset value # added to the base pointer defined in bytes three and four, and the # resultant 16 bit value is a UCS2 code point, and defines a UCS2 character # # example string '0505302D82D32D31' num = ord(s[:2].decode('hex')) base = ord(s[2:4].decode('hex')) << 8 # bits 16..9 base += ord(s[4:6].decode('hex')) # bits 8..1 chars = s[6:6 + num * 2] t = '' for i in range(num): j = i * 2 c_hex = chars[j:j + 2] c_chr = c_hex.decode('hex') c_ord = ord(c_chr) if c_ord & 0x80 == 0: t += c_chr.decode('gsm0338', 'replace') else: t += unichr(base + (c_ord & 0x7f)) return t def from_8bit_in_gsm(s): """ Converts a string from ``s`` from GSM7 encoding using 8 bits. """ t = s.decode('hex') index = t.find('\xff') if index != -1: t = t[:index] return t.decode('gsm0338', 'replace') def from_8bit_in_gsm_or_ts31101(s): """ Returns a string from ``s`` to GSM7 or ts31101 encodings. It checks if first byte indicates ts31101_80, 81 or 82, if not it defaults to GSM7. """ encoding = s[:2] hexbytes = s[2:] if encoding == '80': return unpack_ucs2_bytes_in_ts31101_80(hexbytes).rstrip(u'\uffff') elif encoding == '81': return unpack_ucs2_bytes_in_ts31101_81(hexbytes) elif encoding == '82': return unpack_ucs2_bytes_in_ts31101_82(hexbytes) else: # default to GSM7 8bits, zero first bit. return from_8bit_in_gsm(s) def check_if_ucs2(text, limit=None): """ Test whether ``text`` is a UCS2 encoded string :rtype: bool """ # XXX: Returns false positives if text and isinstance(text, basestring) and (len(text) % 4 == 0): try: unpack_ucs2_bytes(text) except (UnicodeDecodeError, TypeError): return False else: if limit is None: return True s = text while len(s): val = int(s[:4], 16) if limit == LATIN_EX_B and val > 0x024F: return False if limit == LATIN_EX_A and val > 0x017F: return False if limit == CONTROL_1 and val > 0x00FF: return False if limit == CONTROL_0 and val > 0x007F: return False s = s[4:] return True return False def from_u(s): """ Encodes ``s`` to utf-8 if its not already encoded :rtype: str """ return (s.encode('utf8') if isinstance(s, unicode) else s) def from_ucs2(s): """ Converts ``s`` from UCS2 if not already converted :rtype: str """ return (unpack_ucs2_bytes(s) if check_if_ucs2(s) else s) def to_u(s): """ Converts ``s`` to unicode if not already converted :rtype: unicode """ return (s if isinstance(s, unicode) else unicode(s, 'utf8')) def pack_dbus_safe_string(s): """ Converts string ``s`` to BASE64 encoded ZIP compressed string :rtype: str """ return '%s\n%s' % (ZIP_BASE64_MAGIC, s.encode('zip').encode('base64')) def unpack_dbus_safe_string(s): """ Converts BASE64 encoded ZIP compressed string ``s`` to string :rtype: str """ if not s.startswith(ZIP_BASE64_MAGIC): raise ValueError try: return s[len(ZIP_BASE64_MAGIC):].decode('base64').decode('zip') except: raise ValueError wader-0.5.13/wader/common/exceptions.py000066400000000000000000000035071257646610200200450ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """All the exceptions in Wader""" class DeviceLockedError(Exception): """ Exception raised after an authentication mess ending up in a device locked """ class LimitedServiceNetworkError(Exception): """Exception raised when AT+COPS? replied 'Limited Service'""" class MalformedSMSError(Exception): """Exception raised when an error is received decodifying a SMS""" class MalformedUssdPduError(Exception): """Exception raised when an error is received decoding a USSD response""" class NetworkRegistrationError(Exception): """ Exception raised when an error occurred while registering with the network """ class PluginInitialisationError(Exception): """Exception raised when an error occurred while initialisating a plugin""" class ProfileNotFoundError(Exception): """Exception raised when a profile hasn't been found""" class UnknownPluginNameError(Exception): """ Exception raised when we don't have a plugin with the given remote name """ wader-0.5.13/wader/common/interfaces.py000066400000000000000000000161161257646610200200070ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Wader interfaces""" from zope.interface import Interface, Attribute class IContact(Interface): """Interface that all contact backends must implement""" name = Attribute("""Contact's name""") number = Attribute("""Contact's number""") index = Attribute("""Contact's index""") def to_csv(): """Returns a list with the name and number formatted for csv""" class IMessage(Interface): """Interface that all message backends must implement""" number = Attribute("""SMS sender""") text = Attribute("""SMS text""") index = Attribute("""Contact's index""") class IBackend(Interface): """Interface that all the integration backends must implement""" def get_dialer_klass(device): """Returns the dialer class that ``device`` will use for dialing up""" def get_profile_manager(arg=None): """ Returns this backend's profile manager ``arg`` is optional and is used in backends that require an initialization argument. """ def get_keyring(arg=None): """Returns an instance of the keyring manager for this platform""" def should_be_used(): """Returns True if this backend should be used""" class IProfileManagerBackend(Interface): """Interface that all the profile managers backends must implement""" def add_profile(opts): """Adds a profile with the given ``opts``""" def get_profile_by_uuid(uuid): """Returns the profile identified by ``uuid``""" def get_profile_by_object_path(opath): """Returns the profile with object path ``opath``""" def get_profile_options_from_network(network): """Returns the profile options out of ``network``""" def get_profiles(): """Returns all the profiles in the system""" def remove_profile(profile): """Removes ``profile`` from the system""" def update_profile(profile, opts): """Updates ``profile`` with ``opts``""" class IProfile(Interface): """Interface that all the profile objects must implement""" def get_settings(): """Returns a dictionary with all the settings""" def get_secrets(): """Returns the connection secrets""" def get_timestamp(): """Returns the last time the profile was used""" def is_good(): """Has this profile been successfully used?""" def update(props): """Updates the profile with ``props``""" def remove(): """Removes the profile""" class IDialer(Interface): def close(arg): """Frees internal dialer resources""" def configure(config, device): """Configures the dialer with `config` for `device`""" def connect(): """ Connects to Internet :rtype: `Deferred` """ def disconnect(): """ Disconnects from Internet :rtype: `Deferred` """ def stop(): """ Stops the connection attempt :rtype: `Deferred` """ class IWaderPlugin(Interface): """Base interface for all Wader plugins""" name = Attribute("""Plugin's name""") version = Attribute("""Plugin's version""") author = Attribute("""Plugin's author""") def initialize(init_obj): """ Initializes the plugin using ``init_obj`` :type init_obj: dict """ def close(): """Closes the plugin""" class IDevicePlugin(IWaderPlugin): """Interface that all device plugins should implement""" baudrate = Attribute("""At which speed should we talk with this guy""") custom = Attribute("""Container with all the device's customizations""") sim = Attribute("""SIM object""") sconn = Attribute("""Reference to the serial connection instance""") __properties__ = Attribute(""" pairs of properties that must be satisfied by DBus backend""") class IRemoteDevicePlugin(IDevicePlugin): """Interface that all remote device plugins should implent""" __remote_name__ = Attribute("""Response of an AT+CGMM command""") class IOSPlugin(IWaderPlugin): distrib_id = Attribute("""Name of the OS/Distro""") distrib_version = Attribute("""Version of the OS/Distro""") def add_default_route(iface): """Sets a default route for ``iface``""" def delete_default_route(iface): """Deletes default route for ``iface``""" def add_dns_info(dnsinfo, iface=None): """ Adds ``dnsinfo`` to ``iface`` type dnsinfo: tuple """ def delete_dns_info(dnsinfo, iface=None): """Deletes ``dnsinfo`` from ``iface``""" def configure_iface(iface, ip='', action='up'): """ Configures ``iface`` with ``ip`` and ``action`` ``action`` can be either 'up' or 'down'. If you bring down an iface, ip will be ignored. """ def get_iface_stats(iface): """Returns ``iface`` network statistics""" def is_valid(): """Returns True if we are on the given OS/Distro""" def update_dns_cache(): """Updates the OS DNS cache""" class IHardwareManager(Interface): def get_devices(): """ Returns a list with all the devices present in the system :rtype: `Deferred` """ def register_controller(controller): """ Registers ``controller`` as the driver class of this HW manager This reference will be used to emit Device{Add,Remov}ed signals upon hotplugging events. """ class IContactProvider(IWaderPlugin): def add_contact(data): """ Returns a subclass of :class:`~wader.common.contact.Contact` ``data`` has two required keys, `name` and `number` :type data: dict """ def edit_contact(contact): """ Edits ``contact`` with the new values :raises: NotImplementedError if the backend cannot edit contacts """ def find_contacts_by_name(name): """ Returns an iterator with all the contacts whose name match ``name`` """ def find_contacts_by_number(number): """ Returns an iterator with all the contacts whose number match ``number`` """ def list_contacts(): """Returns a generator with all the contacts in the backend""" def remove_contact(contact): """Removes ``contact``""" wader-0.5.13/wader/common/keyring.py000066400000000000000000000077521257646610200173420ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Keyring module for Wader""" import dbus from dbus.service import Object, BusName, signal from wader.common.consts import (WADER_KEYRING_SERVICE, WADER_KEYRING_OBJPATH, WADER_KEYRING_INTFACE) class KeyringNoMatchError(Exception): """Exception raised when there is no match for a keyring request""" class KeyringInvalidPassword(Exception): """Exception raised when the supplied password is invalid""" class KeyringIsClosed(Exception): """ Exception raised when an operation has been attempted on a closed keyring """ class KeyringManager(Object): """ I am the keyring manager I provide a uniform API over different keyrings """ def __init__(self, keyring): name = BusName(WADER_KEYRING_SERVICE, bus=dbus.SystemBus()) super(KeyringManager, self).__init__(bus_name=name, object_path=WADER_KEYRING_OBJPATH) self.keyring = keyring self.open_callbacks = set() def is_new(self): return self.keyring.is_new() def is_open(self): return self.keyring.is_open() def register_open_callback(self, callback): """Registers ``callback`` to be executed upon keyring unlock""" self.open_callbacks.add(callback) def delete_secret(self, uuid): """ Deletes the secret identified by ``uuid`` :raise KeyringIsClosed: When the underlying keyring is closed """ if self.is_open(): return self.keyring.delete(uuid) raise KeyringIsClosed() def update_secret(self, uuid, conn_id, secrets, update=True): """ Updates secret ``secrets`` in profile ``uuid`` :param uuid: The uuid of the profile to be updated :param conn_id: The id (name) of the profile to be updated :param secrets: The secrets :params update: Should existing secrets be updated? :raise KeyringIsClosed: When the underlying keyring is closed """ if self.is_open(): return self.keyring.update(uuid, conn_id, secrets, update) raise KeyringIsClosed() def get_secrets(self, uuid): """ Returns the secrets associated with ``uuid`` :param uuid: The UUID of the connection to use :raise KeyringIsClosed: When the underlying keyring is closed """ if self.is_open(): return self.keyring.get(uuid) raise KeyringIsClosed() def close(self): """ Cleans up the underlying backend and deletes the cached secrets :raise KeyringIsClosed: When the underlying keyring is closed """ if self.is_open(): return self.keyring.close() raise KeyringIsClosed() def open(self, password): """ Opens the keyring using ``password`` If successful, it will execute all the callbacks registered with :meth:`register_open_callback`. :raise KeyringIsClosed: When the underlying keyring is closed """ self.keyring.open(password) for callback in self.open_callbacks: callback() @signal(WADER_KEYRING_INTFACE, signature="o") def KeyNeeded(self, opath): pass wader-0.5.13/wader/common/profile.py000066400000000000000000000235031257646610200173220ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """ Profile-related classes A profile, also known as a connection in NM-lingo, is a group of settings to be used to dial up. This group of classes should be used from the user session, that is why they are defined and not instantiated. """ from __future__ import with_statement from contextlib import closing import socket import time from uuid import uuid1 import dbus from dbus.service import BusName, method, signal from zope.interface import implements from twisted.python import log from wader.common._dbus import DelayableDBusObject, delayable from wader.common.interfaces import IProfile from wader.common.consts import (WADER_PROFILES_SERVICE, WADER_PROFILES_INTFACE, MM_NETWORK_BAND_ANY, MM_ALLOWED_MODE_ANY) import wader.common.exceptions as ex from wader.common.provider import NetworkProvider from wader.common.utils import convert_ip_to_int class Profile(DelayableDBusObject): """I am a group of settings required to dial up""" implements(IProfile) def __init__(self, opath, props=None): self.bus = dbus.SystemBus() bus_name = BusName(WADER_PROFILES_SERVICE, bus=self.bus) DelayableDBusObject.__init__(self, bus_name, opath) self.opath = opath self.secrets = None self.props = {} if props is None else props def get_settings(self): """Returns the profile settings""" raise NotImplementedError("Implement in subclass") def get_secrets(self, tag, hints=None, ask=True): """ Returns the secrets associated with the profile :param tag: The section to use :param hints: what specific setting are we interested in :param ask: Should we ask the user if there is no secret? """ secrets = self.secrets.get(ask) if secrets: return secrets else: return {} def get_timestamp(self): """Returns the last time this profile was used""" raise NotImplementedError("Implement in subclass") def is_good(self): """Has this profile been successfully used?""" raise NotImplementedError("Implement in subclass") def on_open_keyring(self, tag): """Callback to be executed when the keyring has been opened""" raise NotImplementedError("Implement in subclass") def set_secrets(self, tag, secrets): """ Sets or updates the secrets associated with the profile :param tag: The section to use :param secrets: The new secret to store """ raise NotImplementedError("Implement in subclass") def update(self, props): """Updates the profile with settings ``props``""" raise NotImplementedError("Implement in subclass") def remove(self): """Removes the profile""" raise NotImplementedError("Implement in subclass") @method(dbus_interface=WADER_PROFILES_INTFACE, in_signature='', out_signature='a{sa{sv}}') def GetSettings(self): """See :meth:`get_settings`""" return self.get_settings() @signal(dbus_interface=WADER_PROFILES_INTFACE, signature='os') def KeyNeeded(self, conn_path, tag): msg = "KeyNeeded emitted for connection: %s tag: %s" log.msg(msg % (conn_path, tag)) @delayable @method(dbus_interface=WADER_PROFILES_INTFACE, in_signature='sasb', out_signature='a{sa{sv}}') def GetSecrets(self, tag, hints, ask): def ask_user(): self.GetSecrets.delay_reply() self.KeyNeeded(self, tag) if ask: ask_user() else: ask = not self.secrets.is_open() secrets = self.get_secrets(tag, hints, ask=ask) if secrets and tag in secrets: return {tag: secrets[tag]} elif self.secrets.is_open(): ask_user() else: self.secrets.register_open_callback( lambda: self.on_open_keyring(tag)) # will emit KeyNeeded if on_open_keyring does not return # sound secrets self.GetSecrets.delay_reply() @method(dbus_interface=WADER_PROFILES_INTFACE, in_signature='sa{sv}', out_signature='') def SetSecrets(self, tag, secrets): """See :meth:`set_secrets`""" self.set_secrets(tag, secrets) @method(dbus_interface=WADER_PROFILES_INTFACE, in_signature='a{sa{sv}}', out_signature='') def Update(self, options): """See :meth:`update`""" self.update(options) @method(dbus_interface=WADER_PROFILES_INTFACE, in_signature='', out_signature='') def Delete(self): """See :meth:`remove`""" log.msg("Delete received") self.remove() @signal(dbus_interface=WADER_PROFILES_INTFACE, signature='a{sa{sv}}') def Updated(self, options): log.msg("Updated emitted") @signal(dbus_interface=WADER_PROFILES_INTFACE, signature='') def Removed(self): log.msg("Removed emitted") class ProfileManager(object): """I manage profiles in the system""" def __init__(self, backend, arg): super(ProfileManager, self).__init__() self.backend = backend.get_profile_manager(arg) def add_profile(self, props): """Adds a profile with settings ``props``""" return self.backend.add_profile(props) def get_profile_by_uuid(self, uuid): """ Returns the :class:`Profile` identified by ``uuid`` :param uuid: The uuid of the profile :raise ProfileNotFoundError: If no profile was found """ profile = self.backend.get_profile_by_uuid(uuid) log.msg("INFO profile.py: (wader.common) - get_profile_by_uuid: %s" % profile) return self.backend.get_profile_by_uuid(uuid) def get_profile_by_object_path(self, opath): """Returns a :class:`Profile` out of its object path ``opath``""" return self.backend.get_profile_by_object_path(opath) def get_profile_options_from_imsi(self, imsi): """Generates a new :class:`Profile` from ``imsi``""" log.msg("INFO profile.py: (wader.common) - " "get_profile_options_from_imsi") with closing(NetworkProvider()) as provider: network = provider.get_network_by_id(imsi) if network: # XXX: use the first NetworkOperator object for now network = network[0] return self.get_profile_options_from_network(network) raise ex.ProfileNotFoundError("No profile for IMSI %s" % imsi) def get_profile_options_from_network(self, network): """Generates a new :class:`Profile` from ``network``""" log.msg("INFO profile.py: (wader.common) - " "get_profile_options_from_network") props = {} # gsm props['gsm'] = {'band': MM_NETWORK_BAND_ANY, 'username': network.username, 'password': network.password, 'network-type': MM_ALLOWED_MODE_ANY, 'number': '*99#', 'apn': network.apn, 'name': 'gsm'} # ppp props['ppp'] = dict(name='ppp') # set ppp auth to values we handle, else allow all types if network.auth is not None: if 'PAP' in network.auth: props['ppp']['refuse-eap'] = True props['ppp']['refuse-chap'] = True props['ppp']['refuse-mschap'] = True props['ppp']['refuse-mschapv2'] = True elif 'CHAP' in network.auth: props['ppp']['refuse-pap'] = True # serial props['serial'] = dict(baud=115200, name='serial') # connection props['connection'] = dict(id=network.name, autoconnect=False, timestamp=time.time(), type='gsm', name='connection', uuid=str(uuid1())) ignore_auto_dns = True try: dns = map(convert_ip_to_int, [network.dns1, network.dns2]) except (socket.error, TypeError): # if the DNS are None, this will raise TypeError ignore_auto_dns = False dns = [] props['ipv4'] = {'addresses': [], 'dns': dns, 'ignore-auto-dns': ignore_auto_dns, 'method': 'auto', 'name': 'ipv4', 'routes': []} return props def get_profiles(self): """Returns all the profiles in the system""" return self.backend.get_profiles() def remove_profile(self, profile): """Removes profile ``profile``""" return self.backend.remove_profile(profile) def update_profile(self, profile, props): """Updates ``profile`` with settings ``props``""" return self.backend.update_profile(profile, props) wader-0.5.13/wader/common/provider.py000066400000000000000000001127501257646610200175170ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Data providers""" from __future__ import with_statement import datetime import sqlite3 import os import sys from time import mktime from calendar import timegm from pytz import timezone, country_names from xml.dom.minidom import parse from wader.common.consts import EXTRA_DIR, MBPI, NETWORKS_DB from wader.common.sms import Message as _Message from wader.common.utils import (get_value_and_pop, get_tz_aware_now, get_tz_aware_mtime) if sqlite3.version_info >= (2, 4, 1): # Starting in 2.4.1, the str type is not accepted anymore, therefore, # we convert all str objects to Unicode sqlite3.register_adapter(str, lambda s: s.decode('utf-8')) SMS_SCHEMA = """ create table message ( id integer primary key autoincrement, date integer not null, number text not null, text text, flags integer, thread_id integer not null constraint fk_thread_id references thread(id) on delete cascade); create table thread ( id integer primary key autoincrement, date integer default 0, -- updated by trigger number text not null, -- specified in creation message_count integer default 0, -- updated by trigger snippet text, -- updated by trigger read integer default 0, -- updated by trigger folder_id integer not null constraint fk_folder_id references folder(id) on delete cascade); create table folder ( id integer primary key autoincrement, name text not null); create table version ( version integer default 1); create index message_flags_index on message(flags); create index message_thread_index on message(thread_id, id); create index thread_folder_index on thread(folder_id, id); -- delete on cascade thread -> message create trigger fkd_thread_message before delete on "thread" when exists (select 1 from "message" where old."id" == "thread_id") begin delete from "message" where "thread_id" = old."id"; end; -- delete on cascade folder -> thread create trigger fkd_folder_thread before delete on "folder" when exists (select 1 from "thread" where old."id" == "folder_id") begin delete from "thread" where "folder_id" = old."id"; end; -- prevent insertion of messages with an invalid thread_id create trigger fki_message_thread before insert on "message" when new."thread_id" is not null and not exists (select 1 from "thread" where new."thread_id" == "id") begin select raise(abort, 'constraint fki_message_thread failed: thread_id does not exist in thread table'); end; -- update snippet, message_count and date of thread upon message insertion create trigger fki_update_thread_values after insert on "message" when new."thread_id" is not null and exists (select 1 from "thread" where new."thread_id" == "id") begin update "thread" set snippet = substr(new."text", 0, 100), message_count = (select message_count from thread where id = new."thread_id") + 1, date = strftime('%s', 'now') where id = new."thread_id"; update "thread" set read = (select read from thread where id = new."thread_id") + 1 where id = new."thread_id" and msg_is_read(new."flags"); end; create trigger fku_mark_message_read after update on "message" when not msg_is_read(old."flags") and msg_is_read(new."flags") begin update "thread" set read = (select read from thread where id = new."thread_id") + 1 where id = new."thread_id"; end; create trigger fku_mark_message_unread after update on "message" when msg_is_read(old."flags") and not msg_is_read(new."flags") begin update "thread" set read = (select read from thread where id = new."thread_id") - 1 where id = new."thread_id"; end; -- prevent thread.id updates when there are messages associated to it create trigger fku_thread_message after update of id on "thread" when exists (select 1 from "message" where old."id" == "thread_id") begin select raise(abort, 'constraint fku_thread_message failed: can not update thread.id as there are messages associated to it'); end; -- prevent message.thread_id updates with non existing thread.id create trigger fku_message_thread before update of thread_id on "message" when new."thread_id" is not null and not exists (select 1 from "thread" where new."thread_id" == "id") begin select raise(abort, 'constraint fku_message_thread failed: can not update message.thread_id with non existing thread_id'); end; create trigger fki_thread_folder before insert on "thread" when new."folder_id" is not null and not exists (select 1 from "folder" where new."folder_id" == "id") begin select raise(abort, 'constraint fki_thread_folder failed: folder_id does not exist in folder table'); end; create trigger fku_folder_thread after update of id on "folder" when exists (select 1 from "thread" where old."id" == "folder_id") begin select raise(abort, 'constraint fku_folder_thread failed: can not update folder.id as there are threads associated to it'); end; create trigger fku_thread_folder before update of folder_id on "thread" when new."folder_id" is not null and not exists (select 1 from "folder" where new."folder_id" == "id") begin select raise(abort, 'constraint fku_thread_folder failed: can not update thread.folder_id as it does not exist'); end; -- decrease thread.message_count after deleting a message create trigger fkd_update_message_count after delete on "message" when old."thread_id" is not null begin update "thread" set message_count = (select message_count from thread where id = old.thread_id) - 1 where id = old."thread_id"; end; """ NETWORKS_SCHEMA = """ create table network_info( id integer primary key, name text, country text); create table apn( id integer primary key autoincrement, apn text not null, username text, password text, dns1 text, dns2 text, type text, auth text, smsc text, mmsc text, wap1 text, wap2 text, wap_apn text, wap_username text, wap_password text, wap_auth text, network_id integer not null constraint fk_mi_network_id references network_info(id) on delete cascade); create table sources_info ( objs integer default 0, mbpi integer default 0); create table version ( version integer not null); -- delete on cascade network_info -> apn create trigger fkd_network_info_apn before delete on "network_info" when exists (select 1 from "apn" where old."id" == "network_id") begin delete from "apn" where "network_id" = old."id"; end; -- prevent insert on apn with unknown network_id create trigger fki_apns_with_unknown_network_id before insert on "apn" when new."network_id" is not null and not exists (select 1 from "network_info" where new."network_id" == "id") begin select raise(abort, 'constraint failed'); end; -- prevent update of network_info_id if there are APNs associated to old id create trigger fku_prevent_apn_network_info_network_id_bad_update after update of id on "network_info" when exists (select 1 from "apn" where old."id" == "network_id") begin select raise(abort, 'constraint failed'); end; -- prevent update of apn.network_id if it does not exists create trigger fku_prevent_apn_network_id_bad_update before update of network_id on "apn" when new."network_id" is not null and not exists (select 1 from "network_info" where new."network_id" == "id") begin select raise(abort, 'constraint failed'); end; -- populate apn version insert into version values (%(version)d); """ USAGE_SCHEMA = """ create table usage( id integer primary key autoincrement, start_time datetime not null, end_time datetime not null, bytes_recv integer not null, bytes_sent integer not null, umts boolean); create table version ( version integer); -- populate usage version insert into version values (%(version)d); """ # constants INBOX, OUTBOX, DRAFTS = 1, 2, 3 UNREAD, READ = 0x01, 0x02 # GSM spec # 0 - Unread message that has been received # 1 - Read message that has been received # 2 - Unsent message that has been stored # 3 - Sent message that has been stored # 4 - Any message TYPE_CONTRACT, TYPE_PREPAID = 'Contract', 'Prepaid' def message_read(flags): """ Returns a bool indicating whether the message had been read or not """ # second bit is the "was read" flag return (int(flags) & READ) >> 1 def adapt_datetime(_datetime): if _datetime.tzinfo is None: # Naive object - Force to UTC, previous behaviour was to use mktime to # localize. We should raise an exception, but the problem # is that if done in the adapter, the error seen is very # obscure and I expect difficult to relate to the cause. return timegm(_datetime.timetuple()) return timegm(_datetime.astimezone(timezone('UTC')).timetuple()) def convert_datetime(ts): return datetime.datetime.fromtimestamp(float(ts), timezone('UTC')) sqlite3.register_adapter(datetime.datetime, adapt_datetime) sqlite3.register_converter("datetime", convert_datetime) def date_to_datetime(date, tz=None): # takes a date which is in local time (or optionally has timezone) and # returns a timezone aware datetime if tz is None: timestamp = mktime(date.timetuple()) return datetime.datetime.fromtimestamp(timestamp, timezone('UTC')) return tz.localize(datetime.datetime(date.year, date.month, date.day, 0, 0)) # common classes class DBError(Exception): """Base class for DB related errors""" class DBProvider(object): """Base class for the DB providers""" def __init__(self, path, schema, **kw): self.conn = sqlite3.connect(path, isolation_level=None, **kw) c = self.conn.cursor() try: c.executescript(schema) except sqlite3.OperationalError: # ignore error, the database already exists pass def close(self): """Closes the provider and frees resources""" try: self.conn.cursor().execute("vacuum") except sqlite3.OperationalError: pass self.conn.close() # data usage class UsageItem(object): """I represent data usage in the DB""" def __init__(self, index=None, bytes_recv=None, bytes_sent=None, end_time=None, start_time=None, umts=None): self.index = index self.bytes_recv = bytes_recv self.bytes_sent = bytes_sent self.end_time = end_time self.start_time = start_time self.umts = umts def __repr__(self): args = (str(self.end_time - self.start_time), self.bytes_recv, self.bytes_sent) return "" % args def __eq__(self, other): if other.index is not None and self.index is not None: return self.index == other.index raise ValueError("Cannot compare myself with %s" % other) def __ne__(self, other): return not self.__eq__(other) def total(self): return self.bytes_recv + self.bytes_sent def is_3g(self): return self.umts def is_gprs(self): return not self.umts @classmethod def from_row(cls, row): return cls(index=row[0], start_time=row[1], end_time=row[2], bytes_recv=int(row[3]), bytes_sent=int(row[4]), umts=bool(row[5])) class UsageProvider(DBProvider): """DB usage provider""" version = 1 def __init__(self, path): args = dict(version=self.version) super(UsageProvider, self).__init__(path, USAGE_SCHEMA % args, detect_types=sqlite3.PARSE_DECLTYPES) def add_usage_item(self, start, end, bytes_recv, bytes_sent, umts): c = self.conn.cursor() args = (None, start, end, bytes_recv, bytes_sent, umts) c.execute("insert into usage(id, start_time, end_time, bytes_recv," "bytes_sent, umts) values (?,?,?,?,?,?)", args) return UsageItem(umts=umts, start_time=start, end_time=end, bytes_recv=bytes_recv, bytes_sent=bytes_sent, index=c.lastrowid) def delete_usage_item(self, item): c = self.conn.cursor() c.execute("delete from usage where id=?", (item.index,)) def get_usage_for_day(self, day, tz=None): """ Returns all `UsageItem` for ``day`` :type day: ``datetime.date`` :type tz: ``pytz.tzfile`` """ c = self.conn.cursor() if not isinstance(day, datetime.date): raise ValueError("Don't know what to do with %s" % day) tomorrow = day + datetime.timedelta(days=1) args = (date_to_datetime(day, tz), date_to_datetime(tomorrow, tz)) c.execute("select * from usage where end_time >= ? and end_time < ?", args) return [UsageItem.from_row(row) for row in c.fetchall()] def get_usage_for_month(self, month, tz=None): """ Returns all ``UsageItem`` for ``month`` :type month: ``datetime.date`` """ c = self.conn.cursor() if not isinstance(month, datetime.date): raise ValueError("Don't know what to do with %s" % month) first_current_month_day = month.replace(day=1) if month.month < 12: _month = month.month + 1 _year = month.year else: _month = 1 _year = month.year + 1 first_next_month_day = month.replace(day=1, month=_month, year=_year) args = (date_to_datetime(first_current_month_day, tz), date_to_datetime(first_next_month_day, tz)) c.execute("select * from usage where end_time >= ? and end_time < ?", args) return [UsageItem.from_row(row) for row in c.fetchall()] def get_total_usage(self, month=None, tz=None): c = self.conn.cursor() if month is None: sql = "select * from usage" c.execute(sql) else: if not isinstance(month, datetime.date): raise ValueError("Don't know what to do with %s" % month) sql = "select * from usage where end_time >= ?" c.execute(sql, (date_to_datetime(month, tz),)) return [UsageItem.from_row(row) for row in c.fetchall()] # networks class NetworkOperator(object): """I represent a network operator in the DB""" def __init__(self, netid=None, apn=None, username=None, password=None, dns1=None, dns2=None, type=None, auth=None, smsc=None, mmsc=None, country=None, name=None, wap_apn=None, wap_username=None, wap_password=None, wap_auth=None, wap1=None, wap2=None,): self.netid = netid self.apn = apn self.username = username self.password = password self.dns1 = dns1 self.dns2 = dns2 self.type = type self.auth = auth self.wap_apn = wap_apn self.wap_username = wap_username self.wap_password = wap_password self.wap_auth = wap_auth self.wap1 = wap1 self.wap2 = wap2 self.smsc = smsc self.mmsc = mmsc self.country = country self.name = name def __repr__(self): args = (self.name.capitalize(), self.country.capitalize(), self.netid, str(self.type)) if self.wap2: args += (self.wap2,) return "" % repr(args) @classmethod def from_row(cls, row, netid): return cls(netid=[netid], name=row[0], country=row[1], apn=row[2], username=row[3], password=row[4], dns1=row[5], dns2=row[6], type=row[7], smsc=row[8], mmsc=row[9], wap1=row[10], wap2=row[11], wap_apn=row[12], wap_username=row[13], wap_password=row[14], auth=row[15], wap_auth=row[16]) class NetworkProvider(DBProvider): """DB network provider""" version = 2 def __init__(self, path=NETWORKS_DB): args = dict(version=self.version) super(NetworkProvider, self).__init__(path, NETWORKS_SCHEMA % args) def is_current(self): c = self.conn.cursor() try: c.execute("select version from version") version = c.fetchone()[0] if version < self.version: return False except (TypeError, sqlite3.OperationalError): # version table wasn't populated in old DB return False try: c.execute("select * from sources_info") row = c.fetchone() objs_mtime = convert_datetime(row[0]) mbpi_mtime = convert_datetime(row[1]) objs = os.path.join(EXTRA_DIR, 'networks.py') if ((objs_mtime != get_tz_aware_mtime(objs)) or (mbpi_mtime != get_tz_aware_mtime(MBPI))): return False except (TypeError, sqlite3.OperationalError): # it does not exist return False return True def get_network_by_id(self, imsi): """ Returns all the :class:`NetworkOperator` registered for ``imsi`` :rtype: list """ if not isinstance(imsi, basestring): raise TypeError("argument must be a string subclass") if len(imsi) < 14: raise ValueError("Pass the whole imsi") for n in [7, 6, 5]: result = self._get_network_by_id(imsi[:n]) if result: return result return [] def _get_network_by_id(self, netid): c = self.conn.cursor() c.execute("select n.name, n.country, a.apn, a.username," "a.password, a.dns1, a.dns2, a.type, a.smsc, " "a.mmsc, a.wap1, a.wap2, a.wap_apn, " "a.wap_username, a.wap_password, a.auth, a.wap_auth " "from network_info n inner join apn a on " "n.id = a.network_id where n.id=?", (netid,)) ret = [NetworkOperator.from_row(row, netid) for row in c.fetchall()] return ret def populate_networks(self): """ Populates the networks database using default methods (currently a list of network objects, and from the mobile-broadband-provider-info). It turns off autocommit during import for speed """ try: # only will succeed on development networks = __import__('resources/extra/networks') except ImportError: try: # this fails on feisty but not on gutsy networks = __import__(os.path.join(EXTRA_DIR, 'networks')) except ImportError: sys.path.insert(0, EXTRA_DIR) import networks def is_valid(item): return not item.startswith(("__", "Base", "NetworkOperator")) # turn off autocommit self.conn.isolation_level = 'DEFERRED' # Note: cascades over apn table too self.conn.cursor().execute("delete from network_info") self.populate_networks_from_objs([getattr(networks, item)() for item in dir(networks) if is_valid(item)]) try: self.populate_networks_from_mbpi() except TypeError: pass # MBPI v1.0 format # update timestamps objs = os.path.join(EXTRA_DIR, 'networks.py') args = (get_tz_aware_mtime(objs), get_tz_aware_mtime(MBPI)) self.conn.cursor().execute("delete from sources_info") self.conn.cursor().execute( "insert into sources_info values (?,?)", args) self.conn.commit() self.conn.isolation_level = None def populate_networks_from_objs(self, networks): """ Populate the network database with ``networks`` :param networks: NetworkOperator instances :type networks: iter """ c = self.conn.cursor() for network in networks: for snetid in network.netid: # DB field is int not string try: netid = int(snetid) except (ValueError, TypeError): continue # check if this network object exists in the database c.execute("select 1 from network_info where id=?", (netid,)) try: c.fetchone()[0] except TypeError: # it does not exist args = (netid, network.name, network.country) c.execute("insert into network_info values (?,?,?)", args) # check if the APN info exists in the database args = (network.apn, network.username, network.password, network.dns1, network.dns2, network.type, network.smsc, network.mmsc, network.wap1, network.wap2, network.wap_apn) c.execute("select id from apn where apn=? and username=? " "and password=? and dns1=? and dns2=? " "and type=? and smsc=? and mmsc=? " "and wap1=? and wap2=? and wap_apn=?", args) try: c.fetchone()[0] except TypeError: # it does not exist args = (None, network.apn, network.username, network.password, network.dns1, network.dns2, network.type, network.auth, network.smsc, network.mmsc, network.wap1, network.wap2, network.wap_apn, network.wap_username, network.wap_password, network.wap_auth, netid) c.execute("insert into apn values (?,?,?,?,?,?,?,?,?," "?,?,?,?,?,?,?,?)", args) def populate_networks_from_mbpi(self, xmlfile=MBPI): """ Populate the network database with info from``xmlfile`` :param xmlfile: the path to the mobile-broadband-provider-info xml file """ def getAttributeValue(element, name): if element is None or not hasattr(element, 'attributes'): return '' attr = element.attributes[name] if not attr: return '' return attr.value.strip() def getElementsByTagNameNoDescend(parent, name): ret = [] for node in parent.childNodes: if node.nodeType == node.ELEMENT_NODE and \ (name == "*" or node.tagName == name): ret.append(node) return ret def getValue(elementlist, index=0): if elementlist is None or index >= len(elementlist): return '' if not elementlist[index].firstChild: return '' return elementlist[index].firstChild.data.strip() root = parse(xmlfile) svcs = getElementsByTagNameNoDescend(root, "serviceproviders")[0] version = getAttributeValue(svcs, "format") if version != '2.0': raise(TypeError) c = self.conn.cursor() for gsm in svcs.getElementsByTagName("gsm"): provider = gsm.parentNode country = provider.parentNode countrycode = getAttributeValue(country, "code") if countrycode == '': continue elif countrycode == 'gb': # TZ DB is just plain wrong countryname = 'United Kingdom' else: countryname = country_names[countrycode] # we need to be more specific about path to 'provider/name' # as apn also has a subtag called 'name' tag = getElementsByTagNameNoDescend(provider, "name") provname = getValue(tag) for networkid in getElementsByTagNameNoDescend(gsm, "network-id"): mcc = getAttributeValue(networkid, "mcc") mnc = getAttributeValue(networkid, "mnc") # DB field is int not string try: netid = int(mcc + mnc) except (ValueError, TypeError): continue # check if this network object exists in the database c.execute("select 1 from network_info where id=?", (netid,)) try: c.fetchone()[0] except TypeError: # it does not exist args = (netid, provname, countryname) c.execute("insert into network_info values (?,?,?)", args) for apn in getElementsByTagNameNoDescend(gsm, "apn"): apnname = getAttributeValue(apn, "value") if apnname == '': continue tag = getElementsByTagNameNoDescend(apn, "name") _type = getValue(tag) if len(_type): typename = u'MBPI - ' + _type else: typename = u'MBPI - ' + provname tag = getElementsByTagNameNoDescend(apn, "username") username = getValue(tag) tag = getElementsByTagNameNoDescend(apn, "password") password = getValue(tag) tag = getElementsByTagNameNoDescend(apn, "dns") dns1 = getValue(tag, 0) dns2 = getValue(tag, 1) # check if info exists in the database for this network, # we are authorititive for VF Networks, and we don't want # duplicates of the others either args = (netid, apnname, username, password, dns1, dns2, typename) c.execute("select id from apn where network_id=? and " "(" "(not type like 'MBPI%')" " or " "(apn=? and username=? and password=? and" " dns1=? and dns2=? and type=?)" ")", args) try: c.fetchone()[0] except TypeError: # it does not exist args = (None, apnname, username, password, dns1, dns2, typename, None, None, None, None, None, None, None, None, None, netid) c.execute("insert into apn values (?,?,?,?,?,?,?,?," "?,?,?,?,?,?,?,?,?)", args) # SMS class Folder(object): """I am a container for threads and messages""" def __init__(self, name, index=None): self.name = name self.index = index def __repr__(self): return "" % (self.index, self.name) def __eq__(self, other): return self.index == other.index def __ne__(self, other): return not self.__eq__(other) @classmethod def from_row(cls, row): """Returns a :class:`Folder` out of ``row``""" return cls(row[1], row[0]) def to_row(self): """Returns a tuple object ready to be inserted in the DB""" return (self.index, self.name) inbox_folder = Folder("Inbox", 1) outbox_folder = Folder("Outbox", 2) drafts_folder = Folder("Drafts", 3) class Message(_Message): """I am a :class:`_Message` that belongs to a thread and has flags""" def __init__(self, *args, **kw): self.flags = get_value_and_pop(kw, 'flags', READ) self.thread = get_value_and_pop(kw, 'thread') super(Message, self).__init__(*args, **kw) def __repr__(self): args = (self.index, self.thread.index) return "" % args def to_row(self): """Returns a tuple ready to be added to the provider DB""" return (self.index, self.datetime, self.number, self.text, self.flags, self.thread.index) @classmethod def from_row(cls, row, thread=None): # XXX: calling converter manually as DB 'date' type is integer return cls(row[2], row[3], index=row[0], flags=row[4], _datetime=convert_datetime(row[1]), thread=thread) class Thread(object): """I represent an SMS thread in the DB""" def __init__(self, _datetime, number, index=None, message_count=1, snippet='', read=1, folder=None): super(Thread, self).__init__() self.datetime = _datetime self.number = number self.index = index self.message_count = message_count self.snippet = snippet self.read = read self.folder = folder def __repr__(self): return "" % self.index def __eq__(self, other): return self.index == other.index def __ne__(self, other): return not self.__eq__(other) @classmethod def from_row(cls, row, folder=None): """Returns a :class:`Thread` out of ``row``""" # XXX: calling converter manually as DB 'date' type is integer return cls(convert_datetime(row[1]), row[2], index=row[0], message_count=row[3], snippet=row[4], read=row[5], folder=folder) def to_row(self): """Returns a tuple ready to be inserted in the DB""" return (self.index, self.datetime, self.number, self.message_count, self.snippet, self.read, self.folder.index) class SmsProvider(DBProvider): """DB Sms provider""" def __init__(self, path): super(SmsProvider, self).__init__(path, SMS_SCHEMA) self.conn.create_function('msg_is_read', 1, message_read) def add_folder(self, folder): """ Adds ``folder`` to the DB and returns the object updated with index """ c = self.conn.cursor() c.execute("insert into folder values (?, ?)", folder.to_row()) folder.index = c.lastrowid return folder def add_thread(self, thread): """ Adds ``thread`` to the DB and returns the object updated with index """ c = self.conn.cursor() c.execute("insert into thread values (?, ?, ?, ?, ?, ?, ?)", thread.to_row()) thread.index = c.lastrowid return thread def add_sms(self, sms, folder=inbox_folder): """ Adds ``sms`` to the DB and returns the object updated with index :param folder: Folder where this SMS will be added """ c = self.conn.cursor() if sms.thread is None: # create a new thread for this sms thread = self.get_thread_by_number(sms.number, folder=folder) sms.thread = thread c.execute("insert into message values (?, ?, ?, ?, ?, ?)", sms.to_row()) sms.index = c.lastrowid return sms def delete_folder(self, folder): """Deletes ``folder`` from the DB""" # Folders 1, 2, 3 can not be deleted (Inbox, Outbox, Drafts) if folder.index <= 3: raise DBError("Folder %d can not be deleted" % folder.index) c = self.conn.cursor() c.execute("delete from folder where id=?", (folder.index,)) def delete_sms(self, sms): """Deletes ``sms`` from the DB""" c = self.conn.cursor() c.execute("delete from message where id=?", (sms.index,)) def delete_thread(self, thread): """Deletes ``thread`` from the DB""" c = self.conn.cursor() c.execute("delete from thread where id=?", (thread.index,)) def _get_folder_by_id(self, index): """Returns the :class:`Folder` identified by ``index``""" c = self.conn.cursor() c.execute("select * from folder where id=?", (index,)) try: return Folder.from_row(c.fetchone()) except TypeError: raise DBError("Folder %d does not exist" % index) def _get_thread_by_id(self, index): """Returns the :class:`Thread` identified by ``index``""" c = self.conn.cursor() c.execute("select * from thread where id=?", (index,)) try: row = c.fetchone() folder = self._get_folder_by_id(row[6]) return Thread.from_row(row, folder=folder) except TypeError: raise DBError("Thread %d does not exist" % index) def get_thread_by_number(self, number, folder=inbox_folder): """ Returns the :class:`Thread` that belongs to ``number`` under ``folder`` :rtype: :class:`Thread` """ c = self.conn.cursor() sql = "select * from thread where number like ? and folder_id=?" c.execute(sql, ("%%%s%%" % number, folder.index)) if c.rowcount == 1: # there already exists a thread for this number under folder return Thread.from_row(c.fetchone()) elif c.rowcount < 1: # create thread for this number thread = Thread(get_tz_aware_now(), number, folder=folder) return self.add_thread(thread) elif c.rowcount > 1: raise DBError("Too many threads associated to number %s" % number) raise DBError("No thread found for number %s" % number) def list_folders(self): """ List all the :class:`Folder` objects in the DB :rtype: iter """ c = self.conn.cursor() c.execute("select * from folder") return (Folder.from_row(row) for row in c.fetchall()) def list_from_folder(self, folder): """ List all the :class:`Thread` objects that belong to ``folder`` :rtype: iter """ c = self.conn.cursor() sql = "select * from thread where folder_id=? order by date desc" c.execute(sql, (folder.index,)) return (Thread.from_row(row, folder=folder) for row in c.fetchall()) def list_from_thread(self, thread): """ List all the :class:`Message` objects that belong to ``thread`` :rtype: iter """ c = self.conn.cursor() sql = "select * from message where thread_id=? order by date desc" c.execute(sql, (thread.index,)) return (Message.from_row(row, thread=thread) for row in c.fetchall()) def list_threads(self): """ List all the :class:`Thread` objects in the DB :rtype: iter """ c = self.conn.cursor() c.execute("select * from thread order by date desc") for row in c: folder = self._get_folder_by_id(row[6]) yield Thread.from_row(row, folder=folder) def list_sms(self): """ List all the :class:`Message` objects in the DB :rtype: iter """ c = self.conn.cursor() c.execute("select * from message order by date desc") for row in c: thread = self._get_thread_by_id(row[5]) yield Message.from_row(row, thread=thread) def move_to_folder(self, src, dst): """ Moves ``src`` to ``dst`` :return: The updated ``src`` object """ if not isinstance(dst, Folder): raise TypeError("dst must be a Folder instance") if isinstance(src, Thread): return self._move_thread_to_folder(src, dst) elif isinstance(src, Message): return self._move_sms_to_folder(src, dst) else: raise TypeError("src must be a Thread or a Folder instance") def update_sms_flags(self, sms, flags): """Updates ``sms`` with ``flags``""" c = self.conn.cursor() c.execute("update message set flags=? where id=?", (flags, sms.index)) sms.flags = flags return sms def _move_sms_to_folder(self, sms, folder): c = self.conn.cursor() sql = "select id from thread where number like ? and folder_id=?" c.execute(sql, ("%%%s%%" % sms.number, folder.index)) if c.rowcount == 1: # there already exists a thread for that number in folder thread = self._get_thread_by_id(c.fetchone()[0]) c.execute("update message set thread_id=? where id=?", (thread.index, sms.index)) sms.thread = thread return sms elif c.rowcount < 1: # create thread for this number thread = self.add_thread( Thread(sms.datetime, sms.number, folder=folder)) c.execute("update message set thread_id=? where id=?", (thread.index, sms.index)) sms.thread = thread return sms else: msg = "Too many threads associated to number %s" raise DBError(msg % sms.number) def _move_thread_to_folder(self, thread, folder): c = self.conn.cursor() c.execute("update thread set folder_id=? where id=?", (folder.index, thread.index)) thread.folder = folder return thread wader-0.5.13/wader/common/secrets.py000066400000000000000000000065611257646610200173370ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """ Classes that mediate access to the secrets. This is done through the :mod:`~wader.common.keyring` module. """ from wader.common import keyring class ProfileSecrets(object): """ I mediate access to the secrets associated with a profile I provide a uniform API to interact with the different keyrings. """ def __init__(self, connection, manager): self.connection = connection self.uuid = connection.get_settings()['connection']['uuid'] self.manager = manager self.temporal_secrets = {} def get(self, ask=True): """ Returns the secrets associated with the profile :param ask: Should we ask the user if the keyring is closed? """ if self.is_open(): try: return self.manager.get_secrets(self.uuid) except keyring.KeyringNoMatchError: # None signals that something went wrong if self.temporal_secrets: self.update(self.temporal_secrets, False) return self.temporal_secrets else: return {} else: if ask: self.manager.KeyNeeded(self.connection) return self.temporal_secrets def update(self, secrets, ask=False): """ Updates the secrets associated with the profile :param secrets: The new password to use :param ask: Should we ask the user if the keyring is closed? """ _id = self.connection.get_settings()['connection']['id'] if self.is_open(): self.manager.update_secret(self.uuid, _id, secrets) else: if ask: self.manager.KeyNeeded(self.connection) self.register_open_callback(lambda: self.manager.update_secret(self.uuid, _id, self.temporal_secrets)) self.temporal_secrets.update(secrets) def open(self, password): """Opens the keyring backend using ``password``""" self.manager.open(password) def clean(self): """Cleans up the profile secrets""" if self.is_open(): self.manager.delete_secret(self.uuid) self.manager.write() self.temporal_secrets = {} def is_open(self): return self.manager.is_open() def is_new(self): return self.manager.is_new() def register_open_callback(self, callback): """Registers ``callback`` to be executed when the keyring is open""" self.manager.register_open_callback(callback) wader-0.5.13/wader/common/signals.py000066400000000000000000000026711257646610200173250ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2008 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """ Wader signals Signals used internally in Wader. Some of them implement ModemManager API """ # SIGNALS SIG_CALL = 'CallReceived' SIG_CONNECTED = 'Connected' SIG_CREG = 'CregReceived' SIG_DEVICE_ADDED = 'DeviceAdded' SIG_DEVICE_REMOVED = 'DeviceRemoved' SIG_DIAL_STATS = 'DialStats' SIG_DISCONNECTED = 'Disconnected' SIG_INVALID_DNS = 'InvalidDNS' SIG_NETWORK_MODE = 'NetworkMode' SIG_REG_INFO = 'RegistrationInfo' SIG_RSSI = 'SignalQuality' SIG_SMS = 'SmsReceived' SIG_MMS = 'MMSReceived' SIG_SMS_COMP = 'Completed' SIG_SMS_DELV = 'Delivered' SIG_SMS_NOTIFY_ONLINE = 'SmsNotifyOnline' SIG_TIMEOUT = 'Timeout' wader-0.5.13/wader/common/sms.py000066400000000000000000000144441257646610200164700ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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. """Sms-related classes""" from datetime import datetime from operator import itemgetter from time import mktime from pytz import timezone from zope.interface import implements from messaging.sms import SmsSubmit, SmsDeliver from wader.common.interfaces import IMessage from wader.common.utils import get_tz_aware_now class Message(object): """I am a Message in the system""" implements(IMessage) def __init__(self, number=None, text=None, index=None, where=None, fmt=None, csca=None, _datetime=None, ref=None, cnt=None, seq=None): self.number = number self.index = index self.real_indexes = set() self.where = where self.csca = csca self.datetime = _datetime self.fmt = fmt self.ref = ref # Multipart SMS reference number self.cnt = cnt # Total number of fragments self.seq = seq # fragment number self.completed = False self.status_request = False self.status_references = [] self.status_reference = None self.type = None self._fragments = [] if text is not None: self.add_text_fragment(text) self.completed = True @property def text(self): return "".join(text for index, text in sorted(self._fragments, key=itemgetter(0))) def __repr__(self): import pprint import StringIO out = StringIO.StringIO() props = {'number': self.number, 'index': self.index, 'real_indexes': self.real_indexes, 'csca': self.csca, 'datetime': self.datetime, 'reference': self.ref, 'count': self.cnt, 'sequence': self.seq, 'completed': self.completed, 'fragments': self._fragments} pp = pprint.PrettyPrinter(indent=4, stream=out) pp.pprint(props) return out.getvalue() def __eq__(self, m): if all([self.index, m.index]): return self.index == m.index return self.number == m.number and self.text == m.text def __ne__(self, m): return not self.__eq__(m) @classmethod def from_dict(cls, d, tz=None): """ Converts ``d`` to a :class:`Message` :param d: The dict to be converted :param tz: The timezone of the datetime :rtype: ``Message`` """ m = cls(number=d['number']) m.index = d.get('index') m.where = d.get('where') m.csca = d.get('smsc') m.fmt = d.get('fmt') m.ref = d.get('ref') m.status_request = d.get('status_request', False) m.cnt = d.get('cnt', 0) m.seq = d.get('seq', 0) m.type = d.get('type') if 'text' in d: m.add_text_fragment(d['text']) m.completed = True if 'timestamp' in d: m.datetime = datetime.fromtimestamp(d['timestamp'], tz) return m @classmethod def from_pdu(cls, pdu): """ Converts ``pdu`` to a :class:`Message` object :param pdu: The PDU to convert :rtype: ``Message`` """ ret = SmsDeliver(pdu).data if 'date' not in ret: # XXX: Should we really fake a date? _datetime = get_tz_aware_now() else: # SmsDeliver dates are UTC but naive _datetime = ret['date'].replace(tzinfo=timezone('UTC')) m = cls(ret['number'], _datetime=_datetime, csca=ret['csca'], ref=ret.get('ref'), cnt=ret.get('cnt'), seq=ret.get('seq', 0), fmt=ret.get('fmt')) m.type = ret.get('type') m.add_text_fragment(ret['text'], ret.get('seq', 0)) return m def to_dict(self): """ Returns a dict ready to be sent via DBus :rtype: dict """ ret = dict(number=self.number, text=self.text) if self.where is not None: ret['where'] = self.where if self.index is not None: ret['index'] = self.index if self.datetime is not None: ret['timestamp'] = mktime(self.datetime.timetuple()) if self.csca is not None: ret['smsc'] = self.csca if self.status_request: ret['status_request'] = self.status_request return ret def to_pdu(self, store=False): """Returns the PDU representation of this message""" sms = SmsSubmit(self.number, self.text) sms.csca = self.csca sms.request_status = self.status_request if store: sms.validity = None return sms.to_pdu() def add_fragment(self, sms): self.add_text_fragment(sms.text, sms.seq) def add_text_fragment(self, text, pos=0): self._fragments.append((pos, text)) def append_sms(self, sms): """ Appends ``sms`` text internally :rtype bool :return Whether the sms was successfully assembled or not """ # quick filtering to rule out unwanted fragments if self.ref == sms.ref and self.cnt == sms.cnt: self.add_fragment(sms) self.real_indexes.add(sms.index) self.completed = len(self._fragments) == sms.cnt return self.completed else: error = "Cannot assembly SMS fragment with ref %d" raise ValueError(error % sms.ref) def is_status_report(self): if self.type is None: return False return bool(self.type & 0x03) wader-0.5.13/wader/common/utils.py000066400000000000000000000155711257646610200170300ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006-2010 Vodafone España, S.A. # Copyright (C) 2008-2009 Warp Networks, S.L. # Author: Pablo Martí # # 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.. """Misc utilities""" from __future__ import with_statement import os import re import socket import struct from dbus import Array from datetime import datetime from pytz import timezone from wader.common import consts def get_localzone_wader_implementation(fname='/etc/timezone'): try: return timezone(os.environ['TZ'].strip().replace(' ', '_')) except (KeyError, AttributeError, ValueError): pass with open(fname, "r") as f: l = f.readlines() try: return timezone(l[0].strip().replace(' ', '_')) except (KeyError, AttributeError, ValueError): pass return None try: from tzlocal import get_localzone except ImportError: get_localzone = get_localzone_wader_implementation def convert_network_mode_to_access_technology(mode): trans_table = { consts.MM_NETWORK_MODE_GPRS: consts.MM_GSM_ACCESS_TECH_GPRS, consts.MM_NETWORK_MODE_EDGE: consts.MM_GSM_ACCESS_TECH_EDGE, consts.MM_NETWORK_MODE_UMTS: consts.MM_GSM_ACCESS_TECH_UMTS, consts.MM_NETWORK_MODE_HSDPA: consts.MM_GSM_ACCESS_TECH_HSDPA, consts.MM_NETWORK_MODE_HSUPA: consts.MM_GSM_ACCESS_TECH_HSUPA, consts.MM_NETWORK_MODE_HSPA: consts.MM_GSM_ACCESS_TECH_HSPA, } return trans_table.get(mode, consts.MM_GSM_ACCESS_TECH_UNKNOWN) def convert_network_mode_to_allowed_mode(mode): trans_table = { consts.MM_NETWORK_MODE_ANY: consts.MM_ALLOWED_MODE_ANY, consts.MM_NETWORK_MODE_2G_PREFERRED: consts.MM_ALLOWED_MODE_2G_PREFERRED, consts.MM_NETWORK_MODE_3G_PREFERRED: consts.MM_ALLOWED_MODE_3G_PREFERRED, consts.MM_NETWORK_MODE_2G_ONLY: consts.MM_ALLOWED_MODE_2G_ONLY, consts.MM_NETWORK_MODE_3G_ONLY: consts.MM_ALLOWED_MODE_3G_ONLY, } return trans_table.get(mode) def get_bands(bitwised_band): """ Returns all the bitwised bands in ``bitwised_band`` :rtype: list """ return [band for band in consts.MM_NETWORK_BANDS if band & bitwised_band] def get_network_modes(bitwised_mode): """ Returns all the bitwised modes in ``bitwised_mode`` :rtype: list """ return [mode for mode in consts.MM_NETWORK_MODES if mode & bitwised_mode] def get_allowed_modes(bitwised_mode): """ Returns all the allowed_modes present in ``bitwised_mode`` :rtype: list """ ret = [] for mode in get_network_modes(bitwised_mode): ret.append(convert_network_mode_to_allowed_mode(mode)) return ret def rssi_to_percentage(rssi): """ Converts ``rssi`` to a percentage value :rtype: int """ return (rssi * 100) / 31 if rssi < 32 else 0 def convert_ip_to_int(ip): """ Converts ``ip`` to its integer representation :param ip: The IP to convert :type ip: str :rtype: int """ return struct.unpack('I', socket.inet_pton(socket.AF_INET, ip))[0] def convert_int_to_ip(i): """ Converts ``i`` to its IP representation :param i: The integer to convert :rtype: str """ # taken from ModemManager/test/test-mm.py ip = socket.ntohl(i) n1 = ip >> 24 & 0xFF n2 = ip >> 16 & 0xFF n3 = ip >> 8 & 0xFF n4 = ip & 0xFF return "%d.%d.%d.%d" % (n1, n2, n3, n4) def convert_int_to_uint32(i): """ Converts ``i`` to unsigned int32 We need to pass unsigned 32 bit ints over DBus, so we need to encode them as if they are signed. :rtype: unsigned int """ if i > 0xffffffff: raise OverflowError if i > 0x7fffffff: i = int(0x100000000 - i) if i < 2147483648: return - i else: return -2147483648 return i def convert_uint32_to_int(u32): """ Converts ``u32`` to python int Unsigned 32 bit ints are passed over DBus but get interpreted as signed so we need to convert them to standard Python ints. :rtype: int """ if u32 < 0: return u32 + 0xffffffff + 1 return u32 def patch_list_signature(props, signature='au'): """ Patches empty list signature in ``props`` with ``signature`` :param props: Dictionary with connection options :type props: dict :param signature: The signature to use in empty lists :rtype: dict """ for section in props: for key, val in props[section].iteritems(): if val == []: props[section][key] = Array(val, signature=signature) return props def flatten_list(x): """Flattens ``x`` into a single list""" result = [] for el in x: if hasattr(el, "__iter__") and not isinstance(el, basestring): result.extend(flatten_list(el)) else: result.append(el) return result def revert_dict(d): """ Returns a reverted copy of ``d`` :rtype: dict """ ret = {} for k, v in d.iteritems(): ret[v] = k return ret def natsort(l): """Naturally sort list ``l`` in place""" # extracted from http://nedbatchelder.com/blog/200712.html#e20071211T054956 convert = lambda text: int(text) if text.isdigit() else text l.sort(key=lambda key: map(convert, re.split('([0-9]+)', key))) def get_file_data(path): """ Returns the data of the file at ``path`` :param path: The file path :rtype: str """ with open(path) as f: return f.read() def save_file(path, data): """ Saves ``data`` in ``path`` :param path: The file path :param data: The data to be saved """ with open(path, 'w') as f: f.write(data) def is_bogus_ip(ip): """ Checks whether ``ip`` is a bogus IP :rtype: bool """ return ip in ["10.11.12.13", "10.11.12.14"] def get_value_and_pop(kw, name, d=None): """kw.pop[name] if name in kw, else d. d defaults to None""" return (kw.pop(name) if name in kw else d) def get_tz_aware_now(): return datetime.now(timezone('UTC')) def get_tz_aware_mtime(f): """ Gets the mtime on file ``f`` and returns a timezone aware datetime :rtype: datetime.datetime """ dt = datetime.utcfromtimestamp(os.stat(f).st_mtime) return dt.replace(tzinfo=timezone('UTC'))