Repository: babca/python-gsmmodem Branch: master Commit: ea7db39ade00 Files: 54 Total size: 502.4 KB Directory structure: gitextract_fex2qwud/ ├── .coveragerc ├── .flake8 ├── .github/ │ └── workflows/ │ └── publish.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .prettierrc.yaml ├── .travis.yml ├── AUTHORS ├── COPYING ├── ChangeLog ├── MANIFEST.in ├── README.rst ├── docs/ │ ├── Makefile │ ├── api.rst │ ├── conf.py │ ├── examples.rst │ ├── index.rst │ └── make.bat ├── examples/ │ ├── dial_callback_demo.py │ ├── dial_polling_demo.py │ ├── incoming_call_demo.py │ ├── own_number_demo.py │ ├── send_sms_demo.py │ ├── sms_handler_demo.py │ └── ussd_demo.py ├── gsmmodem/ │ ├── __init__.py │ ├── compat.py │ ├── exceptions.py │ ├── gprs.py │ ├── modem.py │ ├── pdu.py │ ├── serial_comms.py │ └── util.py ├── pyproject.toml ├── requirements.txt ├── setup.cfg ├── setup.py ├── test/ │ ├── __init__.py │ ├── compat.py │ ├── fakemodems.py │ ├── test_gsmterm.py │ ├── test_modem.py │ ├── test_pdu.py │ ├── test_serial_comms.py │ └── test_util.py └── tools/ ├── at_cmd_init_modem.txt ├── gsmterm.py ├── gsmtermlib/ │ ├── __init__.py │ ├── atcommands.py │ ├── posoptparse.py │ ├── terminal.py │ └── trie.py ├── identify-modem.py └── sendsms.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coveragerc ================================================ # .coveragerc to control coverage.py [run] branch = True source = gsmmodem/ tools/gsmtermlib omit = # Omit Python 2.6 and 3 compatibility wrappers gsmmodem/compat.py tools/gsmtermlib/posoptparse.py # Omit GSMTerm UI tools/gsmtermlib/terminal.py [report] # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about Python version checks and subsequent monkey-patching if sys.version_info if PYTHON_VERSION ================================================ FILE: .flake8 ================================================ [flake8] max-line-length = 88 extend-ignore = E203 # "Whitespace before ':'" - not PEP-8 compliant E501 # "Line too long (82 >= 79 characters)" per-file-ignores = __init__.py:F401 ================================================ FILE: .github/workflows/publish.yaml ================================================ # This workflow will upload a Python Package to PyPI when a release is published # For more information see: # - https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ # - https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: Upload Python Package on: push: tags: ["v*.*.*"] release: types: [published] jobs: build: name: Build package runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v2 with: python-version: "3.x" - name: Install pypa/build run: | python -m pip install --upgrade pip pip install --upgrade build - name: Build package run: | python -m build - name: Upload dist files uses: actions/upload-artifact@v2 with: name: dist-files path: dist/ if-no-files-found: error publish-test: name: Publish to Test PyPI if: github.event_name == 'push' needs: [build] runs-on: ubuntu-latest steps: - name: Download dist files uses: actions/download-artifact@v2 with: name: dist-files path: dist/ - name: Publish package to Test PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: repository_url: https://test.pypi.org/legacy/ user: __token__ password: ${{ secrets.TEST_PYPI_API_TOKEN }} publish: name: Publish to PyPI if: github.event_name == 'release' needs: [build] runs-on: ubuntu-latest steps: - name: Download dist files uses: actions/download-artifact@v2 with: name: dist-files path: dist/ - name: Publish package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} ================================================ FILE: .gitignore ================================================ *.py[cod] # Package-building stuff *.egg *.egg-info dist build docs/_build # Eclipse project info .project .pydevproject .settings # PyCharm project info .idea # Symlinks (if present) examples/gsmmodem test/gsmmodem test/gsmtermlib tools/gsmmodem tools/gsmtermlib/gsmmodem tools/init # Unit tests / coverage reports .coverage htmlcov # Working copy files *.swp .DS_Store ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.1.0 hooks: - id: check-case-conflict - id: check-merge-conflict - id: trailing-whitespace args: [--markdown-linebreak-ext=md] - id: end-of-file-fixer - id: check-json - id: check-toml - id: check-yaml - id: requirements-txt-fixer - repo: https://github.com/pre-commit/mirrors-prettier rev: v2.5.1 hooks: - id: prettier - repo: https://github.com/asottile/setup-cfg-fmt rev: v1.20.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/asottile/pyupgrade rev: v2.31.0 hooks: - id: pyupgrade args: [--py38-plus] ## <<< darker ## ## - repo: https://github.com/akaihola/darker ## rev: 1.4.0 ## hooks: ## - id: darker ## args: ["--isort"] # TODO: Move to pyproject.toml ## additional_dependencies: ## - isort ## === ## - repo: https://github.com/PyCQA/isort rev: 5.10.1 hooks: - id: isort - repo: https://github.com/psf/black rev: 22.1.0 hooks: - id: black # black-jupyter ## >>> black and isort ## - repo: https://github.com/PyCQA/bandit rev: 1.7.2 hooks: - id: bandit args: [--recursive, --quiet] - repo: https://gitlab.com/PyCQA/flake8 rev: 3.9.2 hooks: - id: flake8 # E***, W***, F*** additional_dependencies: - dlint # DUO*** - flake8-2020 # YTT*** - flake8-bugbear # B*** - flake8-builtins # A*** - flake8-comprehensions # C4** - flake8-deprecated # D*** - flake8-variables-names # VNE*** - mccabe # C9** - pep8-naming # N8** # - repo: https://github.com/pre-commit/mirrors-mypy # rev: v0.790 # hooks: # - id: mypy ================================================ FILE: .prettierrc.yaml ================================================ printWidth: 88 ================================================ FILE: .travis.yml ================================================ language: python python: - "3.6" - "3.5" - "3.4" - "3.3" - "2.7" install: # Install unittest2 on Python 2.6 - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi # Install coveralls (for coveralls.io integration) - pip install coveralls - pip install -r requirements.txt script: python setup.py coverage after_success: coveralls ================================================ FILE: AUTHORS ================================================ Francois Aucamp Thanks to the following people for patches/suggestions: davidphiliplee chakphanu Jonathan Endersby the01 Frederico Rosmaninho David Beitey BOOMER74 Cyril-Roques PeteLawler alex-eri tomchy bennyslbs epol rags22489664 fataevalex paolo-losi yuriykashin foXes68 babca ================================================ FILE: COPYING ================================================ GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser 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 Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. ================================================ FILE: ChangeLog ================================================ * Wed Mar 15 2017 babca - 0.12 – stable release - unit tests fixed after rapid merging – credits to: tomchy - python3.6 support added – message concatenation fixes and more * Thu Nov 10 2016 babca - 0.11 - added getter for SIM own number - added option for blocking incoming calls (GSMBUSY) - various python3 fixes * Thu Aug 18 2016 babca - 0.10 – Probably a new code maintainer for 2016 - All commits published for the last 3 years merged into a one branch – Compatibilty for python3 added, needs further testing! – experimental GPRS support – more: – change AT_CNMI command if needed – waitingForModemToStartInSeconds – timeouts increased – ability to check SMS encodings supported by modem - smsSupportedEncoding() – better modem specific support (incl. simcom) – TE SMS status reports handling support – option to disable requesting delivery reports – incoming DTMF support – todo: check AT+CMGD support for 1 or 2 params and use appropriate command format * Thu Jul 18 2013 Francois Aucamp - 0.9 - Added UDH support for SMS PDUs - Stored messages APIs made public - USSD support improved on different modem types - Vastly improved unit test coverage - Lots of bugfixes and stability improvements * Tue May 21 2013 Francois Aucamp - 0.8 - Support added for ZTE modems - Improved support for Huawei modems - Outgoing call status can now be tracked via polling (for unknown modems) - SMSC property added - Fixes for SMS sending and receiving on different modems - Added callback mechanism for outoging call status updates * Fri Apr 19 2013 Francois Aucamp - 0.7 - Support added for tracking SMS status reports - PIN unlock support - SMS API cleaned up - Bugfixes * Tue Apr 03 2013 Francois Aucamp - 0.6 - Added support for PDU mode SMS - Default SMS read/write mode is now PDU mode - Added identify-modem.py script to assist with debugging different modem types - Lots of bugfixes - Lots of tests added * Wed Mar 06 2013 Francois Aucamp - 0.5 - Many bugfixes and improvements, especially to USSD handling - Improved exceptions to allow more Pythonic error handling - Tests added for SMS API - Unit tests speeded up * Tue Mar 05 2013 Francois Aucamp - 0.4 - Support added for making voice calls - Library and utilities now supported under Python 2.6 - Support added for Wavecom modems - Tests expanded * Tue Feb 26 2013 Francois Aucamp - 0.3 - USSD functionality added - GsmModem class now exposed in main gsmmodem package - GsmModem test cases added for USSD functionality - Some fixes to GSMTerm tests * Mon Feb 18 2013 Francois Aucamp - 0.2 - Renamed "gsmterm" module to "gsmtermlib" to avoid conflict between startup script and module * Wed Feb 13 2013 Francois Aucamp - 0.1 - Initial 0.1 release to github - GsmModem class functionality more-or-less where I need it: handles incoming SMS messages and phone calls, can send SMS messages - GSMTerm essentially finished - SendSMS user script needs some polish ================================================ FILE: MANIFEST.in ================================================ include AUTHORS include ChangeLog include COPYING include requirements.txt include examples/*.py ================================================ FILE: README.rst ================================================ python-gsmmodem-new =================== *GSM modem module for Python* python-gsmmodem is a module that allows easy control of a GSM modem attached to the system. It also includes a couple of useful commandline utilities for interacting with a GSM modem. Its features include: - simple methods for sending SMS messages, checking signal level, etc - easy-to-use API for starting and responding to USSD sessions and making voice calls - handling incoming phone calls and received SMS messages via callback methods - support for SMS PDU and text mode - support for tracking SMS status reports - wraps AT command errors into Python exceptions by default - modular design; you easily issue your own AT commands to the modem (with error checking), or read/write directly from/to the modem if you prefer - comprehensive test suite Bundled utilities: - **GSMTerm**: an easy-to-use serial terminal for communicating with an attached GSM modem. It features command completion, built-in help for many AT commands, history, context-aware prompt, etc. - **sendsms.py**: a simple command line script to send SMS messages - **identify-modem.py**: simple utility to identify attached modem. Can also be used to provide debug information used for development of python-gsmmodem. How to use this package ----------------------- Go to `examples/` directory in this repo. Requirements ------------ - Python 3.3 or later - pySerial How to install this package --------------------------- There are multiple ways to install ``python-gsmmodem-new`` package: Automatic installation of the latest "stable" release from PyPI ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: pip install python-gsmmodem-new `pip `_ will automatically download and install all dependencies, as required. You can also utilise ``easy_install`` in the same manner as using ``pip`` above. If you are utilising ``python-gsmmodem-new`` as part of another project, add it to your ``install_requires`` section of your ``setup.py`` file and upon your project's installation, it will be pulled in automatically. Manual installation of the latest "stable" release from PyPI ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Download a ``python-gsmmodem-new`` archive from `PyPI `_, extract it and install the package with command:: python setup.py install Note that ``python-gsmmodem-new`` package relies on ``pySerial`` for serial communications: https://github.com/pyserial/pyserial Installation of the latest commit from GitHub ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Clone from GitHub:: git clone https://github.com/babca/python-gsmmodem.git cd python-gsmmodem/ python setup.py install Note that ``python-gsmmodem-new`` package relies on ``pySerial`` for serial communications: https://github.com/pyserial/pyserial Testing the package ------------------- .. |Build Status| image:: https://travis-ci.org/babca/python-gsmmodem.svg?branch=master .. _Build Status: https://travis-ci.org/babca/python-gsmmodem .. |Coverage Status| image:: https://coveralls.io/repos/github/babca/python-gsmmodem/badge.svg?branch=master .. _Coverage Status: https://coveralls.io/github/babca/python-gsmmodem?branch=master |Build Status|_ |Coverage Status|_ To run all unit tests, do:: python setup.py test Unit test code coverage information may be generated by using `coverage `_. You can execute it directly from setup.py by doing:: python setup.py coverage This will run all unit tests and report on code coverage statistics. Building documentation ---------------------- This package contains `Sphinx `_-based documentation. To manually build or test the documentation locally, do the following:: git clone https://github.com/babca/python-gsmmodem.git cd python-gsmmodem pip install .[doc] cd doc make html For true isolation, you may wish to run the above commands within a `virtualenv `_, which will help you manage this development installation. License information ------------------- Copyright (C) 2013 Francois Aucamp See AUTHORS for all authors and contact information. License: GNU Lesser General Public License, version 3 or later; see COPYING included in this archive for details. FAQ --- List all modem ports ~~~~~~~~~~~~~~~~~~~~ You can simply list all ttyUSB devices before and after pluging the modem in. ls /dev/ttyUSB* Device or resource busy error ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Check running processes. The device could be occupied by another program or another instance of gsmmodem which is still running in the background. Run ``sudo lsof | grep tty``, try to locate the problematic process and ``sudo kill ``. ================================================ FILE: docs/Makefile ================================================ # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-gsmmodem.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-gsmmodem.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/python-gsmmodem" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-gsmmodem" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." ================================================ FILE: docs/api.rst ================================================ API === GSM Modem --------- .. automodule:: gsmmodem.modem :members: Serial Communications --------------------- .. automodule:: gsmmodem.serial_comms :members: PDU --- .. automodule:: gsmmodem.pdu :members: Utilities --------- .. automodule:: gsmmodem.util :members: ================================================ FILE: docs/conf.py ================================================ # -*- coding: utf-8 -*- # # python-gsmmodem documentation build configuration file, created by # sphinx-quickstart on Sun Aug 11 20:50:25 2013. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # 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.doctest', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'python-gsmmodem' copyright = u'2013, Developers' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.9' # The full version, including alpha/beta/rc tags. release = '0.9' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # 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 = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # 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 = True # 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' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # 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 # 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_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'python-gsmmodemdoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'python-gsmmodem.tex', u'python-gsmmodem Documentation', u'Developers', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'python-gsmmodem', u'python-gsmmodem Documentation', [u'Developers'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'python-gsmmodem', u'python-gsmmodem Documentation', u'Developers', 'python-gsmmodem', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False ================================================ FILE: docs/examples.rst ================================================ Examples ======== Dial Callback ------------- .. literalinclude:: ../examples/dial_callback_demo.py :language: python Dial Polling ------------ .. literalinclude:: ../examples/dial_polling_demo.py :language: python Incoming Call Handling ---------------------- .. literalinclude:: ../examples/incoming_call_demo.py :language: python SMS Handling ------------ .. literalinclude:: ../examples/sms_handler_demo.py :language: python USSD Sessions ------------- .. literalinclude:: ../examples/ussd_demo.py :language: python ================================================ FILE: docs/index.rst ================================================ .. python-gsmmodem documentation master file, created by sphinx-quickstart on Sun Aug 11 20:50:25 2013. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to python-gsmmodem's documentation! =========================================== .. include:: ../README.rst .. automodule:: gsmmodem Examples and API ================ .. toctree:: :maxdepth: 3 examples.rst api.rst Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ================================================ FILE: docs/make.bat ================================================ @ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\python-gsmmodem.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\python-gsmmodem.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end ================================================ FILE: examples/dial_callback_demo.py ================================================ #!/usr/bin/env python """\ Demo: dial a number (using callbacks to track call status) Simple demo app that makes a voice call and plays sone DTMF tones (if supported by modem) when the call is answered, and hangs up the call. It uses the dial() methods callback mechanism to be informed when the call is answered and ended. Note: you need to modify the NUMBER variable for this to work """ from __future__ import print_function import sys, time, logging PORT = '/dev/ttyUSB2' BAUDRATE = 115200 NUMBER = '00000' # Number to dial - CHANGE THIS TO A REAL NUMBER PIN = None # SIM card PIN (if any) from gsmmodem.modem import GsmModem from gsmmodem.exceptions import InterruptedException, CommandError waitForCallback = True def callStatusCallback(call): global waitForCallback print('Call status update callback function called') if call.answered: print('Call has been answered; waiting a while...') # Wait for a bit - some older modems struggle to send DTMF tone immediately after answering a call time.sleep(3.0) print('Playing DTMF tones...') try: if call.active: # Call could have been ended by remote party while we waited in the time.sleep() call call.sendDtmfTone('9515999955951') except InterruptedException as e: # Call was ended during playback print('DTMF playback interrupted: {0} ({1} Error {2})'.format(e, e.cause.type, e.cause.code)) except CommandError as e: print('DTMF playback failed: {0}'.format(e)) finally: if call.active: # Call is still active print('Hanging up call...') call.hangup() waitForCallback = False else: # Call is no longer active (remote party ended it) print('Call has been ended by remote party') waitForCallback = False def main(): if NUMBER == None or NUMBER == '00000': print('Error: Please change the NUMBER variable\'s value before running this example.') sys.exit(1) print('Initializing modem...') logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG) modem = GsmModem(PORT, BAUDRATE) modem.connect(PIN) print('Waiting for network coverage...') modem.waitForNetworkCoverage(30) print('Dialing number: {0}'.format(NUMBER)) call = modem.dial(NUMBER, callStatusUpdateCallbackFunc=callStatusCallback) global waitForCallback while waitForCallback: time.sleep(0.1) print('Done') if __name__ == '__main__': main() ================================================ FILE: examples/dial_polling_demo.py ================================================ #!/usr/bin/env python """\ Demo: dial a number (simple example using polling to check call status) Simple demo app that makes a voice call and plays sone DTMF tones (if supported by modem) when the call is answered, and hangs up the call. It polls the call status to see if the call has been answered Note: you need to modify the NUMBER variable for this to work """ from __future__ import print_function import sys, time, logging PORT = '/dev/ttyUSB2' BAUDRATE = 115200 NUMBER = '00000' # Number to dial - CHANGE THIS TO A REAL NUMBER PIN = None # SIM card PIN (if any) from gsmmodem.modem import GsmModem from gsmmodem.exceptions import InterruptedException, CommandError def main(): if NUMBER == None or NUMBER == '00000': print('Error: Please change the NUMBER variable\'s value before running this example.') sys.exit(1) print('Initializing modem...') #logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG) modem = GsmModem(PORT, BAUDRATE) modem.connect(PIN) print('Waiting for network coverage...') modem.waitForNetworkCoverage(30) print('Dialing number: {0}'.format(NUMBER)) call = modem.dial(NUMBER) print('Waiting for call to be answered/rejected') wasAnswered = False while call.active: if call.answered: wasAnswered = True print('Call has been answered; waiting a while...') # Wait for a bit - some older modems struggle to send DTMF tone immediately after answering a call time.sleep(3.0) print('Playing DTMF tones...') try: if call.active: # Call could have been ended by remote party while we waited in the time.sleep() call call.sendDtmfTone('9515999955951') except InterruptedException as e: # Call was ended during playback print('DTMF playback interrupted: {0} ({1} Error {2})'.format(e, e.cause.type, e.cause.code)) except CommandError as e: print('DTMF playback failed: {0}'.format(e)) finally: if call.active: # Call is still active print('Hanging up call...') call.hangup() else: # Call is no longer active (remote party ended it) print('Call has been ended by remote party') else: # Wait a bit and check again time.sleep(0.5) if not wasAnswered: print('Call was not answered by remote party') print('Done.') modem.close() if __name__ == '__main__': main() ================================================ FILE: examples/incoming_call_demo.py ================================================ #!/usr/bin/env python """\ Demo: handle incoming calls Simple demo app that listens for incoming calls, displays the caller ID, optionally answers the call and plays sone DTMF tones (if supported by modem), and hangs up the call. """ from __future__ import print_function import time, logging PORT = '/dev/ttyUSB2' BAUDRATE = 115200 PIN = None # SIM card PIN (if any) from gsmmodem.modem import GsmModem from gsmmodem.exceptions import InterruptedException def handleIncomingCall(call): if call.ringCount == 1: print('Incoming call from:', call.number) elif call.ringCount >= 2: if call.dtmfSupport: print('Answering call and playing some DTMF tones...') call.answer() # Wait for a bit - some older modems struggle to send DTMF tone immediately after answering a call time.sleep(2.0) try: call.sendDtmfTone('9515999955951') except InterruptedException as e: # Call was ended during playback print('DTMF playback interrupted: {0} ({1} Error {2})'.format(e, e.cause.type, e.cause.code)) finally: if call.answered: print('Hanging up call.') call.hangup() else: print('Modem has no DTMF support - hanging up call.') call.hangup() else: print(' Call from {0} is still ringing...'.format(call.number)) def main(): print('Initializing modem...') #logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG) modem = GsmModem(PORT, BAUDRATE, incomingCallCallbackFunc=handleIncomingCall) modem.connect(PIN) print('Waiting for incoming calls...') try: modem.rxThread.join(2**31) # Specify a (huge) timeout so that it essentially blocks indefinitely, but still receives CTRL+C interrupt signal finally: modem.close() if __name__ == '__main__': main() ================================================ FILE: examples/own_number_demo.py ================================================ #!/usr/bin/env python """\ Demo: read own phone number """ from __future__ import print_function import logging PORT = '/dev/vmodem0' BAUDRATE = 115200 PIN = None # SIM card PIN (if any) from gsmmodem.modem import GsmModem def main(): print('Initializing modem...') # Uncomment the following line to see what the modem is doing: logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG) modem = GsmModem(PORT, BAUDRATE) modem.connect(PIN) number = modem.ownNumber print("The SIM card phone number is:") print(number) # Uncomment the following block to change your own number. # modem.ownNumber = "+000123456789" # lease empty for removing the phone entry altogether # number = modem.ownNumber # print("A new phone number is:") # print(number) # modem.close(); if __name__ == '__main__': main() ================================================ FILE: examples/send_sms_demo.py ================================================ #!/usr/bin/env python """ Demo: Send Simple SMS Demo Simple demo to send sms via gsmmodem package """ from __future__ import print_function import logging from gsmmodem.modem import GsmModem, SentSms # PORT = 'COM5' # ON WINDOWS, Port is from COM1 to COM9 , # We can check using the 'mode' command in cmd PORT = '/dev/ttyUSB2' BAUDRATE = 115200 SMS_TEXT = 'A good teacher is like a candle, it consumes itself to light the way for others.' SMS_DESTINATION = 'YOUR PHONE NUMBER HERE' PIN = None # SIM card PIN (if any) def main(): print('Initializing modem...') modem = GsmModem(PORT, BAUDRATE) modem.connect(PIN) modem.waitForNetworkCoverage(10) print('Sending SMS to: {0}'.format(SMS_DESTINATION)) response = modem.sendSms(SMS_DESTINATION, SMS_TEXT, True) if type(response) == SentSms: print('SMS Delivered.') else: print('SMS Could not be sent') modem.close() if __name__ == '__main__': main() ================================================ FILE: examples/sms_handler_demo.py ================================================ #!/usr/bin/env python """\ Demo: handle incoming SMS messages by replying to them Simple demo app that listens for incoming SMS messages, displays the sender's number and the messages, then replies to the SMS by saying "thank you" """ from __future__ import print_function import logging PORT = '/dev/ttyUSB2' BAUDRATE = 115200 PIN = None # SIM card PIN (if any) from gsmmodem.modem import GsmModem def handleSms(sms): print(u'== SMS message received ==\nFrom: {0}\nTime: {1}\nMessage:\n{2}\n'.format(sms.number, sms.time, sms.text)) print('Replying to SMS...') sms.reply(u'SMS received: "{0}{1}"'.format(sms.text[:20], '...' if len(sms.text) > 20 else '')) print('SMS sent.\n') def main(): print('Initializing modem...') # Uncomment the following line to see what the modem is doing: logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG) modem = GsmModem(PORT, BAUDRATE, smsReceivedCallbackFunc=handleSms) modem.smsTextMode = False modem.connect(PIN) print('Waiting for SMS message...') try: modem.rxThread.join(2**31) # Specify a (huge) timeout so that it essentially blocks indefinitely, but still receives CTRL+C interrupt signal finally: modem.close() if __name__ == '__main__': main() ================================================ FILE: examples/ussd_demo.py ================================================ #!/usr/bin/env python """\ Demo: Simple USSD example Simple demo app that initiates a USSD session, reads the string response and closes the session (if it wasn't closed by the network) Note: for this to work, a valid USSD string for your network must be used. """ from __future__ import print_function import logging PORT = '/dev/ttyUSB2' BAUDRATE = 115200 USSD_STRING = '*101#' PIN = None # SIM card PIN (if any) from gsmmodem.modem import GsmModem def main(): print('Initializing modem...') #logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG) modem = GsmModem(PORT, BAUDRATE) modem.connect(PIN) modem.waitForNetworkCoverage(10) print('Sending USSD string: {0}'.format(USSD_STRING)) response = modem.sendUssd(USSD_STRING) # response type: gsmmodem.modem.Ussd print('USSD reply received: {0}'.format(response.message)) if response.sessionActive: print('Closing USSD session.') # At this point, you could also reply to the USSD message by using response.reply() response.cancel() else: print('USSD session was ended by network.') modem.close() if __name__ == '__main__': main() ================================================ FILE: gsmmodem/__init__.py ================================================ """ Package that allows easy control of an attached GSM modem The main class for controlling a modem is GsmModem, which can be imported directly from this module. Other important and useful classes are: gsmmodem.modem.IncomingCall: wraps an incoming call and passed to the incoming call hanndler callback function gsmmodem.modem.ReceivedSms: wraps a received SMS message and passed to the sms received hanndler callback function gsmmodem.modem.SentSms: returned when sending SMS messages; used for tracking the status of the SMS message All python-gsmmodem-specific exceptions are defined in the gsmmodem.modem.exceptions package. @author: Francois Aucamp @license: LGPLv3+ """ from .modem import GsmModem ================================================ FILE: gsmmodem/compat.py ================================================ """ Contains monkey-patched equivalents for a few commonly-used Python 2.7-and-higher functions. Used to provide backwards-compatibility with Python 2.6 """ import sys if sys.version_info[0] == 2 and sys.version_info[1] < 7: import threading # threading.Event.wait() always returns None in Python < 2.7 so we need to patch it if hasattr(threading, '_Event'): # threading.Event is a function that return threading._Event # This is heavily Python-implementation-specific, so patch where we can, otherwise leave it def wrapWait(func): def newWait(self, timeout=None): func(self, timeout) return self.is_set() return newWait threading._Event.wait = wrapWait(threading._Event.wait) else: raise ImportError('Could not patch this version of Python 2.{0} for compatibility with python-gsmmodem.'.format(sys.version_info[1])) if sys.version_info[0] == 2: str = str else: str = lambda x: x ================================================ FILE: gsmmodem/exceptions.py ================================================ """ Module defines exceptions used by gsmmodem """ class GsmModemException(Exception): """ Base exception raised for error conditions when interacting with the GSM modem """ class TimeoutException(GsmModemException): """ Raised when a write command times out """ def __init__(self, data=None): """ @param data: Any data that was read was read before timeout occurred (if applicable) """ super(TimeoutException, self).__init__(data) self.data = data class InvalidStateException(GsmModemException): """ Raised when an API method call is invoked on an object that is in an incorrect state """ class InterruptedException(InvalidStateException): """ Raised when execution of an AT command is interrupt by a state change. May contain another exception that was the cause of the interruption """ def __init__(self, message, cause=None): """ @param cause: the exception that caused this interruption (usually a CmeError) """ super(InterruptedException, self).__init__(message) self.cause = cause class CommandError(GsmModemException): """ Raised if the modem returns an error in response to an AT command May optionally include an error type (CME or CMS) and -code (error-specific). """ _description = '' def __init__(self, command=None, type=None, code=None): self.command = command self.type = type self.code = code if type != None and code != None: super(CommandError, self).__init__('{0} {1}{2}'.format(type, code, ' ({0})'.format(self._description) if len(self._description) > 0 else '')) elif command != None: super(CommandError, self).__init__(command) else: super(CommandError, self).__init__() class CmeError(CommandError): """ ME error result code : +CME ERROR: Issued in response to an AT command """ def __new__(cls, *args, **kwargs): # Return a specialized version of this class if possible if len(args) >= 2: code = args[1] if code == 11: return PinRequiredError(args[0]) elif code == 16: return IncorrectPinError(args[0]) elif code == 12: return PukRequiredError(args[0]) return super(CmeError, cls).__new__(cls, *args, **kwargs) def __init__(self, command, code): super(CmeError, self).__init__(command, 'CME', code) class SecurityException(CmeError): """ Security-related CME error """ def __init__(self, command, code): super(SecurityException, self).__init__(command, code) class PinRequiredError(SecurityException): """ Raised if an operation failed because the SIM card's PIN has not been entered """ _description = 'SIM card PIN is required' def __init__(self, command, code=11): super(PinRequiredError, self).__init__(command, code) class IncorrectPinError(SecurityException): """ Raised if an incorrect PIN is entered """ _description = 'Incorrect PIN entered' def __init__(self, command, code=16): super(IncorrectPinError, self).__init__(command, code) class PukRequiredError(SecurityException): """ Raised an operation failed because the SIM card's PUK is required (SIM locked) """ _description = "PUK required (SIM locked)" def __init__(self, command, code=12): super(PukRequiredError, self).__init__(command, code) class CmsError(CommandError): """ Message service failure result code: +CMS ERROR : Issued in response to an AT command """ def __new__(cls, *args, **kwargs): # Return a specialized version of this class if possible if len(args) >= 2: code = args[1] if code == 330: return SmscNumberUnknownError(args[0]) return super(CmsError, cls).__new__(cls, *args, **kwargs) def __init__(self, command, code): super(CmsError, self).__init__(command, 'CMS', code) class SmscNumberUnknownError(CmsError): """ Raised if the SMSC (service centre) address is missing when trying to send an SMS message """ _description = 'SMSC number not set' def __init__(self, command, code=330): super(SmscNumberUnknownError, self).__init__(command, code) class EncodingError(GsmModemException): """ Raised if a decoding- or encoding operation failed """ ================================================ FILE: gsmmodem/gprs.py ================================================ # -*- coding: utf8 -*- """ GPRS/Data-specific classes BRANCH: mms PLEASE NOTE: *Everything* in this file (PdpContext, GprsModem class, etc) is experimental. This is NOT meant to be used in production in any way; the API is completely unstable, no unit tests will be written for this in the forseeable future, and stuff may generally break and cause riots. Please do not file bug reports against this branch unless you have a patch to go along with it, but even then: remember that this entire "mms" branch is exploratory; I simply want to see what the possibilities are with it. Use the "main" branch, and the GsmModem class if you want to build normal applications. """ import re from .util import allLinesMatchingPattern from .modem import GsmModem class PdpContext(object): """ Packet Data Protocol (PDP) context parameter values """ def __init__(self, cid, pdpType, apn, pdpAddress=None, dataCompression=0, headerCompression=0): """ Construct a new Packet Data Protocol context @param cid: PDP Context Identifier - specifies a particular PDP context definition @type cid: int @param pdpType: the type of packet data protocol (IP, PPP, IPV6, etc) @type pdpType: str @param apn: Access Point Name; logical name used to select the GGSN or external packet data network @type apn: str @param pdpAddress: identifies the MT in the address space applicable to the PDP. If None, a dynamic address may be requested. @type pdpAddress: str @param dataCompression: PDP data compression; 0 == off, 1 == on @type dataCompression: int @param headerCompression: PDP header compression; 0 == off, 1 == on @type headerCompression: int """ self.cid = cid self.pdpType = pdpType self.apn = apn self.pdpAddress = pdpAddress self.dataCompression = dataCompression self.headerCompression = headerCompression class GprsModem(GsmModem): """ EXPERIMENTAL: Specialized version of GsmModem that includes GPRS/data-specific commands """ @property def pdpContexts(self): """ Currently-defined Packet Data Protocol (PDP) context list PDP paramter values returned include PDP type (IP, IPV6, PPP, X.25 etc), APN, data compression, header compression, etc. @return: a list of currently-defined PDP contexts """ result = [] cgdContResult = self.write('AT+CGDCONT?') matches = allLinesMatchingPattern(re.compile(r'^\+CGDCONT:\s*(\d+),"([^"]+)","([^"]+)","([^"]+)",(\d+),(\d+)'), cgdContResult) for cgdContMatch in matches: cid, pdpType, apn, pdpAddress, dataCompression, headerCompression = cgdContMatch.groups() pdpContext = PdpContext(cid, pdpType, apn, pdpAddress, dataCompression, headerCompression) result.append(pdpContext) return result @property def defaultPdpContext(self): """ @return: the default PDP context, or None if not defined """ pdpContexts = self.pdpContexts return pdpContexts[0] if len(pdpContexts) > 0 else None @defaultPdpContext.setter def defaultPdpContext(self, pdpContext): """ Set the default PDP context (or clear it by setting it to None) """ self.write('AT+CGDCONT=,"{0}","{1}","{2}",{3},{4}'.format(pdpContext.pdpType, pdpContext.apn, pdpContext.pdpAddress or '', pdpContext.dataCompression, pdpContext.headerCompression)) def definePdpContext(self, pdpContext): """ Define a new Packet Data Protocol context, or overwrite an existing one @param pdpContext: The PDP context to define @type pdpContext: gsmmodem.gprs.PdpContext """ self.write('AT+CGDCONT={0},"{1}","{2}","{3}",{4},{5}'.format(pdpContext.cid or '', pdpContext.pdpType, pdpContext.apn, pdpContext.pdpAddress or '', pdpContext.dataCompression, pdpContext.headerCompression)) def initDataConnection(self, pdpCid=1): """ Initializes a packet data (GPRS) connection using the specified PDP Context ID """ # From this point on, we don't want the read thread interfering #self.log.debug('Stopping read thread') #self.alive = False #self.rxThread.join() self.log.debug('Init data connection') self.write('ATD*99#', expectedResponseTermSeq="CONNECT\r") self.log.debug('Data connection open; ready for PPP comms') # From here on we use PPP to communicate with the network ================================================ FILE: gsmmodem/modem.py ================================================ #!/usr/bin/env python """ High-level API classes for an attached GSM modem """ import sys, re, logging, weakref, time, threading, abc, codecs from datetime import datetime from time import sleep from .serial_comms import SerialComms from .exceptions import CommandError, InvalidStateException, CmeError, CmsError, InterruptedException, TimeoutException, PinRequiredError, IncorrectPinError, SmscNumberUnknownError from .pdu import encodeSmsSubmitPdu, decodeSmsPdu, encodeGsm7, encodeTextMode from .util import SimpleOffsetTzInfo, lineStartingWith, allLinesMatchingPattern, parseTextModeTimeStr, removeAtPrefix #from . import compat # For Python 2.6 compatibility from gsmmodem.util import lineMatching from gsmmodem.exceptions import EncodingError PYTHON_VERSION = sys.version_info[0] CTRLZ = '\x1a' TERMINATOR = '\r' if PYTHON_VERSION >= 3: xrange = range dictValuesIter = dict.values dictItemsIter = dict.items else: #pragma: no cover dictValuesIter = dict.itervalues dictItemsIter = dict.iteritems class Sms(object): """ Abstract SMS message base class """ __metaclass__ = abc.ABCMeta # Some constants to ease handling SMS statuses STATUS_RECEIVED_UNREAD = 0 STATUS_RECEIVED_READ = 1 STATUS_STORED_UNSENT = 2 STATUS_STORED_SENT = 3 STATUS_ALL = 4 # ...and a handy converter for text mode statuses TEXT_MODE_STATUS_MAP = {'REC UNREAD': STATUS_RECEIVED_UNREAD, 'REC READ': STATUS_RECEIVED_READ, 'STO UNSENT': STATUS_STORED_UNSENT, 'STO SENT': STATUS_STORED_SENT, 'ALL': STATUS_ALL} def __init__(self, number, text, smsc=None): self.number = number self.text = text self.smsc = smsc class ReceivedSms(Sms): """ An SMS message that has been received (MT) """ def __init__(self, gsmModem, status, number, time, text, smsc=None, udh=[], index=None): super(ReceivedSms, self).__init__(number, text, smsc) self._gsmModem = weakref.proxy(gsmModem) self.status = status self.time = time self.udh = udh self.index = index def reply(self, message): """ Convenience method that sends a reply SMS to the sender of this message """ return self._gsmModem.sendSms(self.number, message) def sendSms(self, dnumber, message): """ Convenience method that sends a SMS to someone else """ return self._gsmModem.sendSms(dnumber, message) def getModem(self): """ Convenience method that returns the gsm modem instance """ return self._gsmModem class SentSms(Sms): """ An SMS message that has been sent (MO) """ ENROUTE = 0 # Status indicating message is still enroute to destination DELIVERED = 1 # Status indicating message has been received by destination handset FAILED = 2 # Status indicating message delivery has failed def __init__(self, number, text, reference, smsc=None): super(SentSms, self).__init__(number, text, smsc) self.report = None # Status report for this SMS (StatusReport object) self.reference = reference @property def status(self): """ Status of this SMS. Can be ENROUTE, DELIVERED or FAILED The actual status report object may be accessed via the 'report' attribute if status is 'DELIVERED' or 'FAILED' """ if self.report == None: return SentSms.ENROUTE else: return SentSms.DELIVERED if self.report.deliveryStatus == StatusReport.DELIVERED else SentSms.FAILED class StatusReport(Sms): """ An SMS status/delivery report Note: the 'status' attribute of this class refers to this status report SM's status (whether it has been read, etc). To find the status of the message that caused this status report, use the 'deliveryStatus' attribute. """ DELIVERED = 0 # SMS delivery status: delivery successful FAILED = 68 # SMS delivery status: delivery failed def __init__(self, gsmModem, status, reference, number, timeSent, timeFinalized, deliveryStatus, smsc=None): super(StatusReport, self).__init__(number, None, smsc) self._gsmModem = weakref.proxy(gsmModem) self.status = status self.reference = reference self.timeSent = timeSent self.timeFinalized = timeFinalized self.deliveryStatus = deliveryStatus class GsmModem(SerialComms): """ Main class for interacting with an attached GSM modem """ log = logging.getLogger('gsmmodem.modem.GsmModem') # Used for parsing AT command errors CM_ERROR_REGEX = re.compile('^\+(CM[ES]) ERROR: (\d+)$') # Used for parsing signal strength query responses CSQ_REGEX = re.compile('^\+CSQ:\s*(\d+),') # Used for parsing caller ID announcements for incoming calls. Group 1 is the number CLIP_REGEX = re.compile('^\+CLIP:\s*"\+{0,1}(\d+)",(\d+).*$') # Used for parsing own number. Group 1 is the number CNUM_REGEX = re.compile('^\+CNUM:\s*".*?","(\+{0,1}\d+)",(\d+).*$') # Used for parsing new SMS message indications CMTI_REGEX = re.compile('^\+CMTI:\s*"([^"]+)",\s*(\d+)$') # Used for parsing SMS message reads (text mode) CMGR_SM_DELIVER_REGEX_TEXT = None # Used for parsing SMS status report message reads (text mode) CMGR_SM_REPORT_REGEXT_TEXT = None # Used for parsing SMS message reads (PDU mode) CMGR_REGEX_PDU = None # Used for parsing USSD event notifications CUSD_REGEX = re.compile('\+CUSD:\s*(\d),\s*"(.*?)",\s*(\d+)', re.DOTALL) # Used for parsing SMS status reports CDSI_REGEX = re.compile('\+CDSI:\s*"([^"]+)",(\d+)$') CDS_REGEX = re.compile('\+CDS:\s*([0-9]+)"$') def __init__(self, port, baudrate=115200, incomingCallCallbackFunc=None, smsReceivedCallbackFunc=None, smsStatusReportCallback=None, requestDelivery=True, AT_CNMI="", *a, **kw): super(GsmModem, self).__init__(port, baudrate, notifyCallbackFunc=self._handleModemNotification, *a, **kw) self.incomingCallCallback = incomingCallCallbackFunc or self._placeholderCallback self.smsReceivedCallback = smsReceivedCallbackFunc or self._placeholderCallback self.smsStatusReportCallback = smsStatusReportCallback or self._placeholderCallback self.requestDelivery = requestDelivery self.AT_CNMI = AT_CNMI or "2,1,0,2" # Flag indicating whether caller ID for incoming call notification has been set up self._callingLineIdentification = False # Flag indicating whether incoming call notifications have extended information self._extendedIncomingCallIndication = False # Current active calls (ringing and/or answered), key is the unique call ID (not the remote number) self.activeCalls = {} # Dict containing sent SMS messages (for auto-tracking their delivery status) self.sentSms = weakref.WeakValueDictionary() self._ussdSessionEvent = None # threading.Event self._ussdResponse = None # gsmmodem.modem.Ussd self._smsStatusReportEvent = None # threading.Event self._dialEvent = None # threading.Event self._dialResponse = None # gsmmodem.modem.Call self._waitForAtdResponse = True # Flag that controls if we should wait for an immediate response to ATD, or not self._waitForCallInitUpdate = True # Flag that controls if we should wait for a ATD "call initiated" message self._callStatusUpdates = [] # populated during connect() - contains regexes and handlers for detecting/handling call status updates self._mustPollCallStatus = False # whether or not the modem must be polled for outgoing call status updates self._pollCallStatusRegex = None # Regular expression used when polling outgoing call status self._writeWait = 0 # Time (in seconds to wait after writing a command (adjusted when 515 errors are detected) self._smsTextMode = False # Storage variable for the smsTextMode property self._gsmBusy = 0 # Storage variable for the GSMBUSY property self._smscNumber = None # Default SMSC number self._smsRef = 0 # Sent SMS reference counter self._smsMemReadDelete = None # Preferred message storage memory for reads/deletes ( parameter used for +CPMS) self._smsMemWrite = None # Preferred message storage memory for writes ( parameter used for +CPMS) self._smsReadSupported = True # Whether or not reading SMS messages is supported via AT commands self._smsEncoding = 'GSM' # Default SMS encoding self._smsSupportedEncodingNames = None # List of available encoding names self._commands = None # List of supported AT commands #Pool of detected DTMF self.dtmfpool = [] def connect(self, pin=None, waitingForModemToStartInSeconds=0): """ Opens the port and initializes the modem and SIM card :param pin: The SIM card PIN code, if any :type pin: str :raise PinRequiredError: if the SIM card requires a PIN but none was provided :raise IncorrectPinError: if the specified PIN is incorrect """ self.log.info('Connecting to modem on port %s at %dbps', self.port, self.baudrate) super(GsmModem, self).connect() if waitingForModemToStartInSeconds > 0: while waitingForModemToStartInSeconds > 0: try: self.write('AT', waitForResponse=True, timeout=0.5) break except TimeoutException: waitingForModemToStartInSeconds -= 0.5 # Send some initialization commands to the modem try: self.write('ATZ') # reset configuration except CommandError: # Some modems require a SIM PIN at this stage already; unlock it now # Attempt to enable detailed error messages (to catch incorrect PIN error) # but ignore if it fails self.write('AT+CMEE=1', parseError=False) self._unlockSim(pin) pinCheckComplete = True self.write('ATZ') # reset configuration else: pinCheckComplete = False self.write('ATE0') # echo off try: cfun = lineStartingWith('+CFUN:', self.write('AT+CFUN?'))[7:] # example response: +CFUN: 1 or +CFUN: 1,0 cfun = int(cfun.split(",")[0]) if cfun != 1: self.write('AT+CFUN=1') except CommandError: pass # just ignore if the +CFUN command isn't supported self.write('AT+CMEE=1') # enable detailed error messages (even if it has already been set - ATZ may reset this) if not pinCheckComplete: self._unlockSim(pin) # Get list of supported commands from modem commands = self.supportedCommands self._commands = commands # Device-specific settings callUpdateTableHint = 0 # unknown modem enableWind = False if commands != None: if '^CVOICE' in commands: self.write('AT^CVOICE=0', parseError=False) # Enable voice calls if '+VTS' in commands: # Check for DTMF sending support Call.dtmfSupport = True elif '^DTMF' in commands: # Huawei modems use ^DTMF to send DTMF tones callUpdateTableHint = 1 # Huawei if '^USSDMODE' in commands: # Enable Huawei text-mode USSD self.write('AT^USSDMODE=0', parseError=False) if '+WIND' in commands: callUpdateTableHint = 2 # Wavecom enableWind = True elif '+ZPAS' in commands: callUpdateTableHint = 3 # ZTE else: # Try to enable general notifications on Wavecom-like device enableWind = True if enableWind: try: wind = lineStartingWith('+WIND:', self.write('AT+WIND?')) # Check current WIND value; example response: +WIND: 63 except CommandError: # Modem does not support +WIND notifications. See if we can detect other known call update notifications pass else: # Enable notifications for call setup, hangup, etc if int(wind[7:]) != 50: self.write('AT+WIND=50') callUpdateTableHint = 2 # Wavecom # Attempt to identify modem type directly (if not already) - for outgoing call status updates if callUpdateTableHint == 0: if 'simcom' in self.manufacturer.lower() : #simcom modems support DTMF and don't support AT+CLAC Call.dtmfSupport = True try: self.write('AT+DDET=1') # enable detect incoming DTMF except CommandError: # simcom 7000E for example doesn't support the DDET command Call.dtmfSupport = False if self.manufacturer.lower() == 'huawei': callUpdateTableHint = 1 # huawei else: # See if this is a ZTE modem that has not yet been identified based on supported commands try: self.write('AT+ZPAS?') except CommandError: pass # Not a ZTE modem else: callUpdateTableHint = 3 # ZTE # Load outgoing call status updates based on identified modem features if callUpdateTableHint == 1: # Use Hauwei's ^NOTIFICATIONs self.log.info('Loading Huawei call state update table') self._callStatusUpdates = ((re.compile('^\^ORIG:(\d),(\d)$'), self._handleCallInitiated), (re.compile('^\^CONN:(\d),(\d)$'), self._handleCallAnswered), (re.compile('^\^CEND:(\d),(\d+),(\d)+,(\d)+$'), self._handleCallEnded)) self._mustPollCallStatus = False # Huawei modems use ^DTMF to send DTMF tones; use that instead Call.DTMF_COMMAND_BASE = '^DTMF={cid},' Call.dtmfSupport = True elif callUpdateTableHint == 2: # Wavecom modem: +WIND notifications supported self.log.info('Loading Wavecom call state update table') self._callStatusUpdates = ((re.compile('^\+WIND: 5,(\d)$'), self._handleCallInitiated), (re.compile('^OK$'), self._handleCallAnswered), (re.compile('^\+WIND: 6,(\d)$'), self._handleCallEnded)) self._waitForAtdResponse = False # Wavecom modems return OK only when the call is answered self._mustPollCallStatus = False if commands == None: # older modem, assume it has standard DTMF support Call.dtmfSupport = True elif callUpdateTableHint == 3: # ZTE # Use ZTE notifications ("CONNECT"/"HANGUP", but no "call initiated" notification) self.log.info('Loading ZTE call state update table') self._callStatusUpdates = ((re.compile('^CONNECT$'), self._handleCallAnswered), (re.compile('^HANGUP:\s*(\d+)$'), self._handleCallEnded), (re.compile('^OK$'), self._handleCallRejected)) self._waitForAtdResponse = False # ZTE modems do not return an immediate OK only when the call is answered self._mustPollCallStatus = False self._waitForCallInitUpdate = False # ZTE modems do not provide "call initiated" updates if commands == None: # ZTE uses standard +VTS for DTMF Call.dtmfSupport = True else: # Unknown modem - we do not know what its call updates look like. Use polling instead self.log.info('Unknown/generic modem type - will use polling for call state updates') self._mustPollCallStatus = True self._pollCallStatusRegex = re.compile('^\+CLCC:\s+(\d+),(\d),(\d),(\d),([^,]),"([^,]*)",(\d+)$') self._waitForAtdResponse = True # Most modems return OK immediately after issuing ATD # General meta-information setup self.write('AT+COPS=3,0', parseError=False) # Use long alphanumeric name format # SMS setup self.write('AT+CMGF={0}'.format(1 if self.smsTextMode else 0)) # Switch to text or PDU mode for SMS messages self._compileSmsRegexes() if self._smscNumber != None: self.write('AT+CSCA="{0}"'.format(self._smscNumber)) # Set default SMSC number currentSmscNumber = self._smscNumber else: currentSmscNumber = self.smsc # Some modems delete the SMSC number when setting text-mode SMS parameters; preserve it if needed if currentSmscNumber != None: self._smscNumber = None # clear cache if self.requestDelivery: self.write('AT+CSMP=49,167,0,0', parseError=False) # Enable delivery reports else: self.write('AT+CSMP=17,167,0,0', parseError=False) # Not enable delivery reports # ...check SMSC again to ensure it did not change if currentSmscNumber != None and self.smsc != currentSmscNumber: self.smsc = currentSmscNumber # Set message storage, but first check what the modem supports - example response: +CPMS: (("SM","BM","SR"),("SM")) try: cpmsLine = lineStartingWith('+CPMS', self.write('AT+CPMS=?')) except CommandError: # Modem does not support AT+CPMS; SMS reading unavailable self._smsReadSupported = False self.log.warning('SMS preferred message storage query not supported by modem. SMS reading unavailable.') else: cpmsSupport = cpmsLine.split(' ', 1)[1].split('),(') # Do a sanity check on the memory types returned - Nokia S60 devices return empty strings, for example for memItem in cpmsSupport: if len(memItem) == 0: # No support for reading stored SMS via AT commands - probably a Nokia S60 self._smsReadSupported = False self.log.warning('Invalid SMS message storage support returned by modem. SMS reading unavailable. Response was: "%s"', cpmsLine) break else: # Suppported memory types look fine, continue preferredMemoryTypes = ('"ME"', '"SM"', '"SR"') cpmsItems = [''] * len(cpmsSupport) for i in xrange(len(cpmsSupport)): for memType in preferredMemoryTypes: if memType in cpmsSupport[i]: if i == 0: self._smsMemReadDelete = memType cpmsItems[i] = memType break self.write('AT+CPMS={0}'.format(','.join(cpmsItems))) # Set message storage del cpmsSupport del cpmsLine if self._smsReadSupported and (self.smsReceivedCallback or self.smsStatusReportCallback): try: self.write('AT+CNMI=' + self.AT_CNMI) # Set message notifications except CommandError: try: self.write('AT+CNMI=2,1,0,1,0') # Set message notifications, using TE for delivery reports except CommandError: # Message notifications not supported self._smsReadSupported = False self.log.warning('Incoming SMS notifications not supported by modem. SMS receiving unavailable.') # Incoming call notification setup try: self.write('AT+CLIP=1') # Enable calling line identification presentation except CommandError as clipError: self._callingLineIdentification = False self.log.warning('Incoming call calling line identification (caller ID) not supported by modem. Error: {0}'.format(clipError)) else: self._callingLineIdentification = True try: self.write('AT+CRC=1') # Enable extended format of incoming indication (optional) except CommandError as crcError: self._extendedIncomingCallIndication = False self.log.warning('Extended format incoming call indication not supported by modem. Error: {0}'.format(crcError)) else: self._extendedIncomingCallIndication = True # Call control setup self.write('AT+CVHU=0', parseError=False) # Enable call hang-up with ATH command (ignore if command not supported) def _unlockSim(self, pin): """ Unlocks the SIM card using the specified PIN (if necessary, else does nothing) """ # Unlock the SIM card if needed try: cpinResponse = lineStartingWith('+CPIN', self.write('AT+CPIN?', timeout=15)) except TimeoutException as timeout: # Wavecom modems do not end +CPIN responses with "OK" (github issue #19) - see if just the +CPIN response was returned if timeout.data != None: cpinResponse = lineStartingWith('+CPIN', timeout.data) if cpinResponse == None: # No useful response read raise timeout else: # Nothing read (real timeout) raise timeout if cpinResponse != '+CPIN: READY': if pin != None: self.write('AT+CPIN="{0}"'.format(pin)) else: raise PinRequiredError('AT+CPIN') def write(self, data, waitForResponse=True, timeout=10, parseError=True, writeTerm=TERMINATOR, expectedResponseTermSeq=None): """ Write data to the modem. This method adds the ``\\r\\n`` end-of-line sequence to the data parameter, and writes it to the modem. :param data: Command/data to be written to the modem :type data: str :param waitForResponse: Whether this method should block and return the response from the modem or not :type waitForResponse: bool :param timeout: Maximum amount of time in seconds to wait for a response from the modem :type timeout: int :param parseError: If True, a CommandError is raised if the modem responds with an error (otherwise the response is returned as-is) :type parseError: bool :param writeTerm: The terminating sequence to append to the written data :type writeTerm: str :param expectedResponseTermSeq: The expected terminating sequence that marks the end of the modem's response (defaults to ``\\r\\n``) :type expectedResponseTermSeq: str :raise CommandError: if the command returns an error (only if parseError parameter is True) :raise TimeoutException: if no response to the command was received from the modem :return: A list containing the response lines from the modem, or None if waitForResponse is False :rtype: list """ self.log.debug('write: %s', data) responseLines = super(GsmModem, self).write(data + writeTerm, waitForResponse=waitForResponse, timeout=timeout, expectedResponseTermSeq=expectedResponseTermSeq) if self._writeWait > 0: # Sleep a bit if required (some older modems suffer under load) time.sleep(self._writeWait) if waitForResponse: cmdStatusLine = responseLines[-1] if parseError: if 'ERROR' in cmdStatusLine: cmErrorMatch = self.CM_ERROR_REGEX.match(cmdStatusLine) if cmErrorMatch: errorType = cmErrorMatch.group(1) errorCode = int(cmErrorMatch.group(2)) if errorCode == 515 or errorCode == 14: # 515 means: "Please wait, init or command processing in progress." # 14 means "SIM busy" self._writeWait += 0.2 # Increase waiting period temporarily # Retry the command after waiting a bit self.log.debug('Device/SIM busy error detected; self._writeWait adjusted to %fs', self._writeWait) time.sleep(self._writeWait) result = self.write(data, waitForResponse, timeout, parseError, writeTerm, expectedResponseTermSeq) self.log.debug('self_writeWait set to 0.1 because of recovering from device busy (515) error') if errorCode == 515: self._writeWait = 0.1 # Set this to something sane for further commands (slow modem) else: self._writeWait = 0 # The modem was just waiting for the SIM card return result if errorType == 'CME': raise CmeError(data, int(errorCode)) else: # CMS error raise CmsError(data, int(errorCode)) else: raise CommandError(data) elif cmdStatusLine == 'COMMAND NOT SUPPORT': # Some Huawei modems respond with this for unknown commands raise CommandError('{} ({})'.format(data,cmdStatusLine)) return responseLines @property def signalStrength(self): """ Checks the modem's cellular network signal strength :raise CommandError: if an error occurs :return: The network signal strength as an integer between 0 and 99, or -1 if it is unknown :rtype: int """ csq = self.CSQ_REGEX.match(self.write('AT+CSQ')[0]) if csq: ss = int(csq.group(1)) return ss if ss != 99 else -1 else: raise CommandError() @property def manufacturer(self): """ :return: The modem's manufacturer's name """ return self.write('AT+CGMI')[0] @property def model(self): """ :return: The modem's model name """ return self.write('AT+CGMM')[0] @property def revision(self): """ :return: The modem's software revision, or None if not known/supported """ try: return self.write('AT+CGMR')[0] except CommandError: return None @property def imei(self): """ :return: The modem's serial number (IMEI number) """ return self.write('AT+CGSN')[0] @property def imsi(self): """ :return: The IMSI (International Mobile Subscriber Identity) of the SIM card. The PIN may need to be entered before reading the IMSI """ return self.write('AT+CIMI')[0] @property def networkName(self): """ :return: the name of the GSM Network Operator to which the modem is connected """ copsMatch = lineMatching('^\+COPS: (\d),(\d),"(.+)",{0,1}\d*$', self.write('AT+COPS?')) # response format: +COPS: mode,format,"operator_name",x if copsMatch: return copsMatch.group(3) @property def supportedCommands(self): """ :return: list of AT commands supported by this modem (without the AT prefix). Returns None if not known """ try: # AT+CLAC responses differ between modems. Most respond with +CLAC: and then a comma-separated list of commands # while others simply return each command on a new line, with no +CLAC: prefix response = self.write('AT+CLAC', timeout=10) if len(response) == 2: # Single-line response, comma separated commands = response[0] if commands.startswith('+CLAC'): commands = commands[6:] # remove the +CLAC: prefix before splitting return commands.split(',') elif len(response) > 2: # Multi-line response return [removeAtPrefix(cmd.strip()) for cmd in response[:-1]] else: self.log.debug('Unhandled +CLAC response: {0}'.format(response)) return None except (TimeoutException, CommandError): # Try interactive command recognition commands = [] checkable_commands = ['^CVOICE', '+VTS', '^DTMF', '^USSDMODE', '+WIND', '+ZPAS', '+CSCS', '+CNUM'] # Check if modem is still alive try: response = self.write('AT') except: raise TimeoutException # Check all commands that will by considered for command in checkable_commands: try: # Compose AT command that will read values under specified function at_command='AT'+command+'=?' response = self.write(at_command) # If there are values inside response - add command to the list commands.append(command) except: continue # Return found commands if len(commands) == 0: return None else: return commands @property def smsTextMode(self): """ :return: True if the modem is set to use text mode for SMS, False if it is set to use PDU mode """ return self._smsTextMode @smsTextMode.setter def smsTextMode(self, textMode): """ Set to True for the modem to use text mode for SMS, or False for it to use PDU mode """ if textMode != self._smsTextMode: if self.alive: self.write('AT+CMGF={0}'.format(1 if textMode else 0)) self._smsTextMode = textMode self._compileSmsRegexes() @property def smsSupportedEncoding(self): """ :raise NotImplementedError: If an error occures during AT command response parsing. :return: List of supported encoding names. """ # Check if command is available if self._commands == None: self._commands = self.supportedCommands if self._commands == None: self._smsSupportedEncodingNames = [] return self._smsSupportedEncodingNames if not '+CSCS' in self._commands: self._smsSupportedEncodingNames = [] return self._smsSupportedEncodingNames # Get available encoding names response = self.write('AT+CSCS=?') # Check response length (should be 2 - list of options and command status) if len(response) != 2: self.log.debug('Unhandled +CSCS response: {0}'.format(response)) self._smsSupportedEncodingNames = [] raise NotImplementedError # Extract encoding names list try: enc_list = response[0] # Get the first line enc_list = enc_list[6:] # Remove '+CSCS: ' prefix # Extract AT list in format ("str", "str2", "str3") enc_list = enc_list.split('(')[1] enc_list = enc_list.split(')')[0] enc_list = enc_list.split(',') enc_list = [x.split('"')[1] for x in enc_list] except: self.log.debug('Unhandled +CSCS response: {0}'.format(response)) self._smsSupportedEncodingNames = [] raise NotImplementedError self._smsSupportedEncodingNames = enc_list return self._smsSupportedEncodingNames @property def smsEncoding(self): """ :return: Encoding name if encoding command is available, else GSM. """ if self._commands == None: self._commands = self.supportedCommands if self._commands == None: return self._smsEncoding if '+CSCS' in self._commands: response = self.write('AT+CSCS?') if len(response) == 2: encoding = response[0] if encoding.startswith('+CSCS'): encoding = encoding[6:].split('"') # remove the +CSCS: prefix before splitting if len(encoding) == 3: self._smsEncoding = encoding[1] else: self.log.debug('Unhandled +CSCS response: {0}'.format(response)) else: self.log.debug('Unhandled +CSCS response: {0}'.format(response)) return self._smsEncoding @smsEncoding.setter def smsEncoding(self, encoding): """ Set encoding for SMS inside PDU mode. :raise CommandError: if unable to set encoding :raise ValueError: if encoding is not supported by modem """ # Check if command is available if self._commands == None: self._commands = self.supportedCommands if self._commands == None: if encoding != self._smsEncoding: raise CommandError('Unable to set SMS encoding (no supported commands)') else: return if not '+CSCS' in self._commands: if encoding != self._smsEncoding: raise CommandError('Unable to set SMS encoding (+CSCS command not supported)') else: return # Check if command is available if self._smsSupportedEncodingNames == None: self.smsSupportedEncoding # Check if desired encoding is available if encoding in self._smsSupportedEncodingNames: # Set encoding response = self.write('AT+CSCS="{0}"'.format(encoding)) if len(response) == 1: if response[0].lower() == 'ok': self._smsEncoding = encoding return if encoding != self._smsEncoding: raise ValueError('Unable to set SMS encoding (enocoding {0} not supported)'.format(encoding)) else: return def _setSmsMemory(self, readDelete=None, write=None): """ Set the current SMS memory to use for read/delete/write operations """ # Switch to the correct memory type if required if write != None and write != self._smsMemWrite: readDel = readDelete or self._smsMemReadDelete self.write('AT+CPMS="{0}","{1}"'.format(readDel, write)) self._smsMemReadDelete = readDel self._smsMemWrite = write elif readDelete != None and readDelete != self._smsMemReadDelete: self.write('AT+CPMS="{0}"'.format(readDelete)) self._smsMemReadDelete = readDelete def _compileSmsRegexes(self): """ Compiles regular expression used for parsing SMS messages based on current mode """ if self.smsTextMode: if self.CMGR_SM_DELIVER_REGEX_TEXT == None: self.CMGR_SM_DELIVER_REGEX_TEXT = re.compile('^\+CMGR: "([^"]+)","([^"]+)",[^,]*,"([^"]+)"$') self.CMGR_SM_REPORT_REGEXT_TEXT = re.compile('^\+CMGR: ([^,]*),\d+,(\d+),"{0,1}([^"]*)"{0,1},\d*,"([^"]+)","([^"]+)",(\d+)$') elif self.CMGR_REGEX_PDU == None: self.CMGR_REGEX_PDU = re.compile('^\+CMGR:\s*(\d*),\s*"{0,1}([^"]*)"{0,1},\s*(\d+)$') @property def gsmBusy(self): """ :return: Current GSMBUSY state """ try: response = self.write('AT+GSMBUSY?') response = response[0] # Get the first line response = response[10] # Remove '+GSMBUSY: ' prefix self._gsmBusy = response except: pass # If error is related to ME funtionality: +CME ERROR: return self._gsmBusy @gsmBusy.setter def gsmBusy(self, gsmBusy): """ Sete GSMBUSY state """ if gsmBusy != self._gsmBusy: if self.alive: self.write('AT+GSMBUSY="{0}"'.format(gsmBusy)) self._gsmBusy = gsmBusy @property def smsc(self): """ :return: The default SMSC number stored on the SIM card """ if self._smscNumber == None: try: readSmsc = self.write('AT+CSCA?') except SmscNumberUnknownError: pass # Some modems return a CMS 330 error if the value isn't set else: cscaMatch = lineMatching('\+CSCA:\s*"([^,]+)",(\d+)$', readSmsc) if cscaMatch: self._smscNumber = cscaMatch.group(1) return self._smscNumber @smsc.setter def smsc(self, smscNumber): """ Set the default SMSC number to use when sending SMS messages """ if smscNumber != self._smscNumber: if self.alive: self.write('AT+CSCA="{0}"'.format(smscNumber)) self._smscNumber = smscNumber @property def ownNumber(self): """ Query subscriber phone number. It must be stored on SIM by operator. If is it not stored already, it usually is possible to store the number by user. :raise TimeoutException: if a timeout was specified and reached :return: Subscriber SIM phone number. Returns None if not known :rtype: int """ try: if "+CNUM" in self._commands: response = self.write('AT+CNUM') else: # temporarily switch to "own numbers" phonebook, read position 1 and than switch back response = self.write('AT+CPBS?') selected_phonebook = response[0][6:].split('"')[1] # first line, remove the +CSCS: prefix, split, first parameter if selected_phonebook is not "ON": self.write('AT+CPBS="ON"') response = self.write("AT+CPBR=1") self.write('AT+CPBS="{0}"'.format(selected_phonebook)) if response is "OK": # command is supported, but no number is set return None elif len(response) == 2: # OK and phone number. Actual number is in the first line, second parameter, and is placed inside quotation marks cnumLine = response[0] cnumMatch = self.CNUM_REGEX.match(cnumLine) if cnumMatch: return cnumMatch.group(1) else: self.log.debug('Error parse +CNUM response: {0}'.format(response)) return None elif len(response) > 2: # Multi-line response self.log.debug('Unhandled +CNUM/+CPBS response: {0}'.format(response)) return None except (TimeoutException, CommandError): raise @ownNumber.setter def ownNumber(self, phone_number): actual_phonebook = self.write('AT+CPBS?') if actual_phonebook is not "ON": self.write('AT+CPBS="ON"') self.write('AT+CPBW=1,"' + phone_number + '"') def waitForNetworkCoverage(self, timeout=None): """ Block until the modem has GSM network coverage. This method blocks until the modem is registered with the network and the signal strength is greater than 0, optionally timing out if a timeout was specified :param timeout: Maximum time to wait for network coverage, in seconds :type timeout: int or float :raise TimeoutException: if a timeout was specified and reached :raise InvalidStateException: if the modem is not going to receive network coverage (SIM blocked, etc) :return: the current signal strength """ block = [True] if timeout != None: # Set up a timeout mechanism def _cancelBlock(): block[0] = False t = threading.Timer(timeout, _cancelBlock) t.start() ss = -1 checkCreg = True while block[0]: if checkCreg: cregResult = lineMatching('^\+CREG:\s*(\d),(\d)(,[^,]*,[^,]*)?$', self.write('AT+CREG?', parseError=False)) # example result: +CREG: 0,1 if cregResult: status = int(cregResult.group(2)) if status in (1, 5): # 1: registered, home network, 5: registered, roaming # Now simply check and return network signal strength checkCreg = False elif status == 3: raise InvalidStateException('Network registration denied') elif status == 0: raise InvalidStateException('Device not searching for network operator') else: # Disable network registration check; only use signal strength self.log.info('+CREG check disabled due to invalid response or unsupported command') checkCreg = False else: # Check signal strength ss = self.signalStrength if ss > 0: return ss time.sleep(1) else: # If this is reached, the timer task has triggered raise TimeoutException() def sendSms(self, destination, text, waitForDeliveryReport=False, deliveryTimeout=15, sendFlash=False): """ Send an SMS text message :param destination: the recipient's phone number :type destination: str :param text: the message text :type text: str :param waitForDeliveryReport: if True, this method blocks until a delivery report is received for the sent message :type waitForDeliveryReport: boolean :param deliveryTimeout: the maximum time in seconds to wait for a delivery report (if "waitForDeliveryReport" is True) :type deliveryTimeout: int or float :raise CommandError: if an error occurs while attempting to send the message :raise TimeoutException: if the operation times out """ # Check input text to select appropriate mode (text or PDU) if self.smsTextMode: try: encodedText = encodeTextMode(text) except ValueError: self.smsTextMode = False if self.smsTextMode: # Send SMS via AT commands self.write('AT+CMGS="{0}"'.format(destination), timeout=5, expectedResponseTermSeq='> ') result = lineStartingWith('+CMGS:', self.write(text, timeout=35, writeTerm=CTRLZ)) else: # Check encoding try: encodedText = encodeGsm7(text) except ValueError: encodedText = None # Set GSM modem SMS encoding format # Encode message text and set data coding scheme based on text contents if encodedText == None: # Cannot encode text using GSM-7; use UCS2 instead self.smsEncoding = 'UCS2' else: self.smsEncoding = 'GSM' # Encode text into PDUs pdus = encodeSmsSubmitPdu(destination, text, reference=self._smsRef, sendFlash=sendFlash) # Send SMS PDUs via AT commands for pdu in pdus: self.write('AT+CMGS={0}'.format(pdu.tpduLength), timeout=5, expectedResponseTermSeq='> ') result = lineStartingWith('+CMGS:', self.write(str(pdu), timeout=35, writeTerm=CTRLZ)) # example: +CMGS: xx if result == None: raise CommandError('Modem did not respond with +CMGS response') # Keep SMS reference number in order to pair delivery reports with sent message reference = int(result[7:]) self._smsRef = reference + 1 if self._smsRef > 255: self._smsRef = 0 # Create sent SMS object for future delivery checks sms = SentSms(destination, text, reference) # Add a weak-referenced entry for this SMS (allows us to update the SMS state if a status report is received) self.sentSms[reference] = sms if waitForDeliveryReport: self._smsStatusReportEvent = threading.Event() if self._smsStatusReportEvent.wait(deliveryTimeout): self._smsStatusReportEvent = None else: # Response timed out self._smsStatusReportEvent = None raise TimeoutException() return sms def sendUssd(self, ussdString, responseTimeout=15): """ Starts a USSD session by dialing the the specified USSD string, or \ sends the specified string in the existing USSD session (if any) :param ussdString: The USSD access number to dial :param responseTimeout: Maximum time to wait a response, in seconds :raise TimeoutException: if no response is received in time :return: The USSD response message/session (as a Ussd object) :rtype: gsmmodem.modem.Ussd """ self._ussdSessionEvent = threading.Event() try: cusdResponse = self.write('AT+CUSD=1,"{0}",15'.format(ussdString), timeout=responseTimeout) # Should respond with "OK" except Exception: self._ussdSessionEvent = None # Cancel the thread sync lock raise # Some modems issue the +CUSD response before the acknowledgment "OK" - check for that if len(cusdResponse) > 1: cusdResponseFound = lineStartingWith('+CUSD', cusdResponse) != None if cusdResponseFound: self._ussdSessionEvent = None # Cancel thread sync lock return self._parseCusdResponse(cusdResponse) # Wait for the +CUSD notification message if self._ussdSessionEvent.wait(responseTimeout): self._ussdSessionEvent = None return self._ussdResponse else: # Response timed out self._ussdSessionEvent = None raise TimeoutException() def checkForwarding(self, querytype, responseTimeout=15): """ Check forwarding status: 0=Unconditional, 1=Busy, 2=NoReply, 3=NotReach, 4=AllFwd, 5=AllCondFwd :param querytype: The type of forwarding to check :return: Status :rtype: Boolean """ try: queryResponse = self.write('AT+CCFC={0},2'.format(querytype), timeout=responseTimeout) # Should respond with "OK" except Exception: raise print(queryResponse) return True def setForwarding(self, fwdType, fwdEnable, fwdNumber, responseTimeout=15): """ Check forwarding status: 0=Unconditional, 1=Busy, 2=NoReply, 3=NotReach, 4=AllFwd, 5=AllCondFwd :param fwdType: The type of forwarding to set :param fwdEnable: 1 to enable, 0 to disable, 2 to query, 3 to register, 4 to erase :param fwdNumber: Number to forward to :return: Success or not :rtype: Boolean """ try: queryResponse = self.write('AT+CCFC={0},{1},"{2}"'.format(fwdType, fwdEnable, fwdNumber), timeout=responseTimeout) # Should respond with "OK" except Exception: raise return False print(queryResponse) return queryResponse def dial(self, number, timeout=5, callStatusUpdateCallbackFunc=None): """ Calls the specified phone number using a voice phone call :param number: The phone number to dial :param timeout: Maximum time to wait for the call to be established :param callStatusUpdateCallbackFunc: Callback function that is executed if the call's status changes due to remote events (i.e. when it is answered, the call is ended by the remote party) :return: The outgoing call :rtype: gsmmodem.modem.Call """ if self._waitForCallInitUpdate: # Wait for the "call originated" notification message self._dialEvent = threading.Event() try: self.write('ATD{0};'.format(number), timeout=timeout, waitForResponse=self._waitForAtdResponse) except Exception: self._dialEvent = None # Cancel the thread sync lock raise else: # Don't wait for a call init update - base the call ID on the number of active calls self.write('ATD{0};'.format(number), timeout=timeout, waitForResponse=self._waitForAtdResponse) self.log.debug("Not waiting for outgoing call init update message") callId = len(self.activeCalls) + 1 callType = 0 # Assume voice call = Call(self, callId, callType, number, callStatusUpdateCallbackFunc) self.activeCalls[callId] = call return call if self._mustPollCallStatus: # Fake a call notification by polling call status until the status indicates that the call is being dialed threading.Thread(target=self._pollCallStatus, kwargs={'expectedState': 0, 'timeout': timeout}).start() if self._dialEvent.wait(timeout): self._dialEvent = None callId, callType = self._dialResponse call = Call(self, callId, callType, number, callStatusUpdateCallbackFunc) self.activeCalls[callId] = call return call else: # Call establishing timed out self._dialEvent = None raise TimeoutException() def processStoredSms(self, unreadOnly=False): """ Process all SMS messages currently stored on the device/SIM card. Reads all (or just unread) received SMS messages currently stored on the device/SIM card, initiates "SMS received" events for them, and removes them from the SIM card. This is useful if SMS messages were received during a period that python-gsmmodem was not running but the modem was powered on. :param unreadOnly: If True, only process unread SMS messages :type unreadOnly: boolean """ if self.smsReceivedCallback: states = [Sms.STATUS_RECEIVED_UNREAD] if not unreadOnly: states.insert(0, Sms.STATUS_RECEIVED_READ) for msgStatus in states: messages = self.listStoredSms(status=msgStatus, delete=True) for sms in messages: self.smsReceivedCallback(sms) else: raise ValueError('GsmModem.smsReceivedCallback not set') def listStoredSms(self, status=Sms.STATUS_ALL, memory=None, delete=False): """ Returns SMS messages currently stored on the device/SIM card. The messages are read from the memory set by the "memory" parameter. :param status: Filter messages based on this read status; must be 0-4 (see Sms class) :type status: int :param memory: The memory type to read from. If None, use the current default SMS read memory :type memory: str or None :param delete: If True, delete returned messages from the device/SIM card :type delete: bool :return: A list of Sms objects containing the messages read :rtype: list """ self._setSmsMemory(readDelete=memory) messages = [] delMessages = set() if self.smsTextMode: cmglRegex= re.compile('^\+CMGL: (\d+),"([^"]+)","([^"]+)",[^,]*,"([^"]+)"$') for key, val in dictItemsIter(Sms.TEXT_MODE_STATUS_MAP): if status == val: statusStr = key break else: raise ValueError('Invalid status value: {0}'.format(status)) result = self.write('AT+CMGL="{0}"'.format(statusStr)) msgLines = [] msgIndex = msgStatus = number = msgTime = None for line in result: cmglMatch = cmglRegex.match(line) if cmglMatch: # New message; save old one if applicable if msgIndex != None and len(msgLines) > 0: msgText = '\n'.join(msgLines) msgLines = [] messages.append(ReceivedSms(self, Sms.TEXT_MODE_STATUS_MAP[msgStatus], number, parseTextModeTimeStr(msgTime), msgText, None, [], msgIndex)) delMessages.add(int(msgIndex)) msgIndex, msgStatus, number, msgTime = cmglMatch.groups() msgLines = [] else: if line != 'OK': msgLines.append(line) if msgIndex != None and len(msgLines) > 0: msgText = '\n'.join(msgLines) msgLines = [] messages.append(ReceivedSms(self, Sms.TEXT_MODE_STATUS_MAP[msgStatus], number, parseTextModeTimeStr(msgTime), msgText, None, [], msgIndex)) delMessages.add(int(msgIndex)) else: cmglRegex = re.compile('^\+CMGL:\s*(\d+),\s*(\d+),.*$') readPdu = False result = self.write('AT+CMGL={0}'.format(status)) for line in result: if not readPdu: cmglMatch = cmglRegex.match(line) if cmglMatch: msgIndex = int(cmglMatch.group(1)) msgStat = int(cmglMatch.group(2)) readPdu = True else: try: smsDict = decodeSmsPdu(line) except EncodingError: self.log.debug('Discarding line from +CMGL response: %s', line) except: pass # dirty fix warning: https://github.com/yuriykashin/python-gsmmodem/issues/1 # todo: make better fix else: if smsDict['type'] == 'SMS-DELIVER': sms = ReceivedSms(self, int(msgStat), smsDict['number'], smsDict['time'], smsDict['text'], smsDict['smsc'], smsDict.get('udh', []), msgIndex) elif smsDict['type'] == 'SMS-STATUS-REPORT': sms = StatusReport(self, int(msgStat), smsDict['reference'], smsDict['number'], smsDict['time'], smsDict['discharge'], smsDict['status']) else: raise CommandError('Invalid PDU type for readStoredSms(): {0}'.format(smsDict['type'])) messages.append(sms) delMessages.add(msgIndex) readPdu = False if delete: if status == Sms.STATUS_ALL: # Delete all messages self.deleteMultipleStoredSms() else: for msgIndex in delMessages: self.deleteStoredSms(msgIndex) return messages def _handleModemNotification(self, lines): """ Handler for unsolicited notifications from the modem This method simply spawns a separate thread to handle the actual notification (in order to release the read thread so that the handlers are able to write back to the modem, etc) :param lines The lines that were read """ threading.Thread(target=self.__threadedHandleModemNotification, kwargs={'lines': lines}).start() def __threadedHandleModemNotification(self, lines): """ Implementation of _handleModemNotification() to be run in a separate thread :param lines The lines that were read """ next_line_is_te_statusreport = False for line in lines: if 'RING' in line: # Incoming call (or existing call is ringing) self._handleIncomingCall(lines) return elif line.startswith('+CMTI'): # New SMS message indication self._handleSmsReceived(line) return elif line.startswith('+CUSD'): # USSD notification - either a response or a MT-USSD ("push USSD") message self._handleUssd(lines) return elif line.startswith('+CDSI'): # SMS status report self._handleSmsStatusReport(line) return elif line.startswith('+CDS'): # SMS status report at next line next_line_is_te_statusreport = True cdsMatch = self.CDS_REGEX.match(line) if cdsMatch: next_line_is_te_statusreport_length = int(cdsMatch.group(1)) else: next_line_is_te_statusreport_length = -1 elif next_line_is_te_statusreport: self._handleSmsStatusReportTe(next_line_is_te_statusreport_length, line) return elif line.startswith('+DTMF'): # New incoming DTMF self._handleIncomingDTMF(line) return else: # Check for call status updates for updateRegex, handlerFunc in self._callStatusUpdates: match = updateRegex.match(line) if match: # Handle the update handlerFunc(match) return # If this is reached, the notification wasn't handled self.log.debug('Unhandled unsolicited modem notification: %s', lines) #Simcom modem able detect incoming DTMF def _handleIncomingDTMF(self,line): self.log.debug('Handling incoming DTMF') try: dtmf_num=line.split(':')[1].replace(" ","") self.dtmfpool.append(dtmf_num) self.log.debug('DTMF number is {0}'.format(dtmf_num)) except: self.log.debug('Error parse DTMF number on line {0}'.format(line)) def GetIncomingDTMF(self): if (len(self.dtmfpool)==0): return None else: return self.dtmfpool.pop(0) def _handleIncomingCall(self, lines): self.log.debug('Handling incoming call') ringLine = lines.pop(0) if self._extendedIncomingCallIndication: try: callType = ringLine.split(' ', 1)[1] except IndexError: # Some external 3G scripts modify incoming call indication settings (issue #18) self.log.debug('Extended incoming call indication format changed externally; re-enabling...') callType = None try: # Re-enable extended format of incoming indication (optional) self.write('AT+CRC=1') except CommandError: self.log.warning('Extended incoming call indication format changed externally; unable to re-enable') self._extendedIncomingCallIndication = False else: callType = None if self._callingLineIdentification and len(lines) > 0: clipLine = lines.pop(0) clipMatch = self.CLIP_REGEX.match(clipLine) if clipMatch: callerNumber = '+' + clipMatch.group(1) ton = clipMatch.group(2) #TODO: re-add support for this callerName = None #callerName = clipMatch.group(3) #if callerName != None and len(callerName) == 0: # callerName = None else: callerNumber = ton = callerName = None else: callerNumber = ton = callerName = None call = None for activeCall in dictValuesIter(self.activeCalls): if activeCall.number == callerNumber: call = activeCall call.ringCount += 1 if call == None: callId = len(self.activeCalls) + 1; call = IncomingCall(self, callerNumber, ton, callerName, callId, callType) self.activeCalls[callId] = call self.incomingCallCallback(call) def _handleCallInitiated(self, regexMatch, callId=None, callType=1): """ Handler for "outgoing call initiated" event notification line """ if self._dialEvent: if regexMatch: groups = regexMatch.groups() # Set self._dialReponse to (callId, callType) if len(groups) >= 2: self._dialResponse = (int(groups[0]) , int(groups[1])) else: self._dialResponse = (int(groups[0]), 1) # assume call type: VOICE else: self._dialResponse = callId, callType self._dialEvent.set() def _handleCallAnswered(self, regexMatch, callId=None): """ Handler for "outgoing call answered" event notification line """ if regexMatch: groups = regexMatch.groups() if len(groups) > 1: callId = int(groups[0]) self.activeCalls[callId].answered = True else: # Call ID not available for this notificition - check for the first outgoing call that has not been answered for call in dictValuesIter(self.activeCalls): if call.answered == False and type(call) == Call: call.answered = True return else: # Use supplied values self.activeCalls[callId].answered = True def _handleCallEnded(self, regexMatch, callId=None, filterUnanswered=False): if regexMatch: groups = regexMatch.groups() if len(groups) > 0: callId = int(groups[0]) else: # Call ID not available for this notification - check for the first outgoing call that is active for call in dictValuesIter(self.activeCalls): if type(call) == Call: if not filterUnanswered or (filterUnanswered == True and call.answered == False): callId = call.id break if callId and callId in self.activeCalls: self.activeCalls[callId].answered = False self.activeCalls[callId].active = False del self.activeCalls[callId] def _handleCallRejected(self, regexMatch, callId=None): """ Handler for rejected (unanswered calls being ended) Most modems use _handleCallEnded for handling both call rejections and remote hangups. This method does the same, but filters for unanswered calls only. """ return self._handleCallEnded(regexMatch, callId, True) def _handleSmsReceived(self, notificationLine): """ Handler for "new SMS" unsolicited notification line """ self.log.debug('SMS message received') if self.smsReceivedCallback is not None: cmtiMatch = self.CMTI_REGEX.match(notificationLine) if cmtiMatch: msgMemory = cmtiMatch.group(1) msgIndex = cmtiMatch.group(2) sms = self.readStoredSms(msgIndex, msgMemory) try: self.smsReceivedCallback(sms) except Exception: self.log.error('error in smsReceivedCallback', exc_info=True) else: self.deleteStoredSms(msgIndex) def _handleSmsStatusReport(self, notificationLine): """ Handler for SMS status reports """ self.log.debug('SMS status report received') cdsiMatch = self.CDSI_REGEX.match(notificationLine) if cdsiMatch: msgMemory = cdsiMatch.group(1) msgIndex = cdsiMatch.group(2) report = self.readStoredSms(msgIndex, msgMemory) self.deleteStoredSms(msgIndex) # Update sent SMS status if possible if report.reference in self.sentSms: self.sentSms[report.reference].report = report if self._smsStatusReportEvent: # A sendSms() call is waiting for this response - notify waiting thread self._smsStatusReportEvent.set() elif self.smsStatusReportCallback: # Nothing is waiting for this report directly - use callback try: self.smsStatusReportCallback(report) except Exception: self.log.error('error in smsStatusReportCallback', exc_info=True) def _handleSmsStatusReportTe(self, length, notificationLine): """ Handler for TE SMS status reports """ self.log.debug('TE SMS status report received') try: smsDict = decodeSmsPdu(notificationLine) except EncodingError: self.log.debug('Discarding notification line from +CDS response: %s', notificationLine) else: if smsDict['type'] == 'SMS-STATUS-REPORT': report = StatusReport(self, int(smsDict['status']), smsDict['reference'], smsDict['number'], smsDict['time'], smsDict['discharge'], smsDict['status']) else: raise CommandError('Invalid PDU type for readStoredSms(): {0}'.format(smsDict['type'])) # Update sent SMS status if possible if report.reference in self.sentSms: self.sentSms[report.reference].report = report if self._smsStatusReportEvent: # A sendSms() call is waiting for this response - notify waiting thread self._smsStatusReportEvent.set() else: # Nothing is waiting for this report directly - use callback try: self.smsStatusReportCallback(report) except Exception: self.log.error('error in smsStatusReportCallback', exc_info=True) def readStoredSms(self, index, memory=None): """ Reads and returns the SMS message at the specified index :param index: The index of the SMS message in the specified memory :type index: int :param memory: The memory type to read from. If None, use the current default SMS read memory :type memory: str or None :raise CommandError: if unable to read the stored message :return: The SMS message :rtype: subclass of gsmmodem.modem.Sms (either ReceivedSms or StatusReport) """ # Switch to the correct memory type if required self._setSmsMemory(readDelete=memory) msgData = self.write('AT+CMGR={0}'.format(index)) # Parse meta information if self.smsTextMode: cmgrMatch = self.CMGR_SM_DELIVER_REGEX_TEXT.match(msgData[0]) if cmgrMatch: msgStatus, number, msgTime = cmgrMatch.groups() msgText = '\n'.join(msgData[1:-1]) return ReceivedSms(self, Sms.TEXT_MODE_STATUS_MAP[msgStatus], number, parseTextModeTimeStr(msgTime), msgText) else: # Try parsing status report cmgrMatch = self.CMGR_SM_REPORT_REGEXT_TEXT.match(msgData[0]) if cmgrMatch: msgStatus, reference, number, sentTime, deliverTime, deliverStatus = cmgrMatch.groups() if msgStatus.startswith('"'): msgStatus = msgStatus[1:-1] if len(msgStatus) == 0: msgStatus = "REC UNREAD" return StatusReport(self, Sms.TEXT_MODE_STATUS_MAP[msgStatus], int(reference), number, parseTextModeTimeStr(sentTime), parseTextModeTimeStr(deliverTime), int(deliverStatus)) else: raise CommandError('Failed to parse text-mode SMS message +CMGR response: {0}'.format(msgData)) else: cmgrMatch = self.CMGR_REGEX_PDU.match(msgData[0]) if not cmgrMatch: raise CommandError('Failed to parse PDU-mode SMS message +CMGR response: {0}'.format(msgData)) stat, alpha, length = cmgrMatch.groups() try: stat = int(stat) except Exception: # Some modems (ZTE) do not always read return status - default to RECEIVED UNREAD stat = Sms.STATUS_RECEIVED_UNREAD pdu = msgData[1] smsDict = decodeSmsPdu(pdu) if smsDict['type'] == 'SMS-DELIVER': return ReceivedSms(self, int(stat), smsDict['number'], smsDict['time'], smsDict['text'], smsDict['smsc'], smsDict.get('udh', [])) elif smsDict['type'] == 'SMS-STATUS-REPORT': return StatusReport(self, int(stat), smsDict['reference'], smsDict['number'], smsDict['time'], smsDict['discharge'], smsDict['status']) else: raise CommandError('Invalid PDU type for readStoredSms(): {0}'.format(smsDict['type'])) def deleteStoredSms(self, index, memory=None): """ Deletes the SMS message stored at the specified index in modem/SIM card memory :param index: The index of the SMS message in the specified memory :type index: int :param memory: The memory type to delete from. If None, use the current default SMS read/delete memory :type memory: str or None :raise CommandError: if unable to delete the stored message """ self._setSmsMemory(readDelete=memory) self.write('AT+CMGD={0},0'.format(index)) # TODO: make a check how many params are supported by the modem and use the right command. For example, Siemens MC35, TC35 take only one parameter. #self.write('AT+CMGD={0}'.format(index)) def deleteMultipleStoredSms(self, delFlag=4, memory=None): """ Deletes all SMS messages that have the specified read status. The messages are read from the memory set by the "memory" parameter. The value of the "delFlag" paramater is the same as the "DelFlag" parameter of the +CMGD command: 1: Delete All READ messages 2: Delete All READ and SENT messages 3: Delete All READ, SENT and UNSENT messages 4: Delete All messages (this is the default) :param delFlag: Controls what type of messages to delete; see description above. :type delFlag: int :param memory: The memory type to delete from. If None, use the current default SMS read/delete memory :type memory: str or None :param delete: If True, delete returned messages from the device/SIM card :type delete: bool :raise ValueErrror: if "delFlag" is not in range [1,4] :raise CommandError: if unable to delete the stored messages """ if 0 < delFlag <= 4: self._setSmsMemory(readDelete=memory) self.write('AT+CMGD=1,{0}'.format(delFlag)) else: raise ValueError('"delFlag" must be in range [1,4]') def _handleUssd(self, lines): """ Handler for USSD event notification line(s) """ if self._ussdSessionEvent: # A sendUssd() call is waiting for this response - parse it self._ussdResponse = self._parseCusdResponse(lines) # Notify waiting thread self._ussdSessionEvent.set() def _parseCusdResponse(self, lines): """ Parses one or more +CUSD notification lines (for USSD) :return: USSD response object :rtype: gsmmodem.modem.Ussd """ if len(lines) > 1: # Issue #20: Some modem/network combinations use \r\n as in-message EOL indicators; # - join lines to compensate for that (thanks to davidjb for the fix) # Also, look for more than one +CUSD response because of certain modems' strange behaviour cusdMatches = list(self.CUSD_REGEX.finditer('\r\n'.join(lines))) else: # Single standard +CUSD response cusdMatches = [self.CUSD_REGEX.match(lines[0])] message = None sessionActive = True if len(cusdMatches) > 1: self.log.debug('Multiple +CUSD responses received; filtering...') # Some modems issue a non-standard "extra" +CUSD notification for releasing the session for cusdMatch in cusdMatches: if cusdMatch.group(1) == '2': # Set the session to inactive, but ignore the message self.log.debug('Ignoring "session release" message: %s', cusdMatch.group(2)) sessionActive = False else: # Not a "session release" message message = cusdMatch.group(2) if sessionActive and cusdMatch.group(1) != '1': sessionActive = False else: sessionActive = cusdMatches[0].group(1) == '1' message = cusdMatches[0].group(2) return Ussd(self, sessionActive, message) def _placeHolderCallback(self, *args): """ Does nothing """ self.log.debug('called with args: {0}'.format(args)) def _pollCallStatus(self, expectedState, callId=None, timeout=None): """ Poll the status of outgoing calls. This is used for modems that do not have a known set of call status update notifications. :param expectedState: The internal state we are waiting for. 0 == initiated, 1 == answered, 2 = hangup :type expectedState: int :raise TimeoutException: If a timeout was specified, and has occurred """ callDone = False timeLeft = timeout or 999999 while self.alive and not callDone and timeLeft > 0: time.sleep(0.5) if expectedState == 0: # Only call initializing can timeout timeLeft -= 0.5 try: clcc = self._pollCallStatusRegex.match(self.write('AT+CLCC')[0]) except TimeoutException as timeout: # Can happend if the call was ended during our time.sleep() call clcc = None if clcc: direction = int(clcc.group(2)) if direction == 0: # Outgoing call # Determine call state stat = int(clcc.group(3)) if expectedState == 0: # waiting for call initiated if stat == 2 or stat == 3: # Dialing or ringing ("alerting") callId = int(clcc.group(1)) callType = int(clcc.group(4)) self._handleCallInitiated(None, callId, callType) # if self_dialEvent is None, this does nothing expectedState = 1 # Now wait for call answer elif expectedState == 1: # waiting for call to be answered if stat == 0: # Call active callId = int(clcc.group(1)) self._handleCallAnswered(None, callId) expectedState = 2 # Now wait for call hangup elif expectedState == 2 : # waiting for remote hangup # Since there was no +CLCC response, the call is no longer active callDone = True self._handleCallEnded(None, callId=callId) elif expectedState == 1: # waiting for call to be answered # Call was rejected callDone = True self._handleCallRejected(None, callId=callId) if timeLeft <= 0: raise TimeoutException() class Call(object): """ A voice call """ DTMF_COMMAND_BASE = '+VTS=' dtmfSupport = False # Indicates whether or not DTMF tones can be sent in calls def __init__(self, gsmModem, callId, callType, number, callStatusUpdateCallbackFunc=None): """ :param gsmModem: GsmModem instance that created this object :param number: The number that is being called """ self._gsmModem = weakref.proxy(gsmModem) self._callStatusUpdateCallbackFunc = callStatusUpdateCallbackFunc # Unique ID of this call self.id = callId # Call type (VOICE == 0, etc) self.type = callType # The remote number of this call (destination or origin) self.number = number # Flag indicating whether the call has been answered or not (backing field for "answered" property) self._answered = False # Flag indicating whether or not the call is active # (meaning it may be ringing or answered, but not ended because of a hangup event) self.active = True @property def answered(self): return self._answered @answered.setter def answered(self, answered): self._answered = answered if self._callStatusUpdateCallbackFunc: self._callStatusUpdateCallbackFunc(self) def sendDtmfTone(self, tones): """ Send one or more DTMF tones to the remote party (only allowed for an answered call) Note: this is highly device-dependent, and might not work :param digits: A str containining one or more DTMF tones to play, e.g. "3" or "\*123#" :raise CommandError: if the command failed/is not supported :raise InvalidStateException: if the call has not been answered, or is ended while the command is still executing """ if self.answered: dtmfCommandBase = self.DTMF_COMMAND_BASE.format(cid=self.id) toneLen = len(tones) for tone in list(tones): try: self._gsmModem.write('AT{0}{1}'.format(dtmfCommandBase,tone), timeout=(5 + toneLen)) except CmeError as e: if e.code == 30: # No network service - can happen if call is ended during DTMF transmission (but also if DTMF is sent immediately after call is answered) raise InterruptedException('No network service', e) elif e.code == 3: # Operation not allowed - can happen if call is ended during DTMF transmission raise InterruptedException('Operation not allowed', e) else: raise e else: raise InvalidStateException('Call is not active (it has not yet been answered, or it has ended).') def hangup(self): """ End the phone call. Does nothing if the call is already inactive. """ if self.active: self._gsmModem.write('ATH') self.answered = False self.active = False if self.id in self._gsmModem.activeCalls: del self._gsmModem.activeCalls[self.id] class IncomingCall(Call): CALL_TYPE_MAP = {'VOICE': 0} """ Represents an incoming call, conveniently allowing access to call meta information and -control """ def __init__(self, gsmModem, number, ton, callerName, callId, callType): """ :param gsmModem: GsmModem instance that created this object :param number: Caller number :param ton: TON (type of number/address) in integer format :param callType: Type of the incoming call (VOICE, FAX, DATA, etc) """ if callType in self.CALL_TYPE_MAP: callType = self.CALL_TYPE_MAP[callType] super(IncomingCall, self).__init__(gsmModem, callId, callType, number) # Type attribute of the incoming call self.ton = ton self.callerName = callerName # Flag indicating whether the call is ringing or not self.ringing = True # Amount of times this call has rung (before answer/hangup) self.ringCount = 1 def answer(self): """ Answer the phone call. :return: self (for chaining method calls) """ if self.ringing: self._gsmModem.write('ATA') self.ringing = False self.answered = True return self def hangup(self): """ End the phone call. """ self.ringing = False super(IncomingCall, self).hangup() class Ussd(object): """ Unstructured Supplementary Service Data (USSD) message. This class contains convenient methods for replying to a USSD prompt and to cancel the USSD session """ def __init__(self, gsmModem, sessionActive, message): self._gsmModem = weakref.proxy(gsmModem) # Indicates if the session is active (True) or has been closed (False) self.sessionActive = sessionActive self.message = message def reply(self, message): """ Sends a reply to this USSD message in the same USSD session :raise InvalidStateException: if the USSD session is not active (i.e. it has ended) :return: The USSD response message/session (as a Ussd object) """ if self.sessionActive: return self._gsmModem.sendUssd(message) else: raise InvalidStateException('USSD session is inactive') def cancel(self): """ Terminates/cancels the USSD session (without sending a reply) Does nothing if the USSD session is inactive. """ if self.sessionActive: self._gsmModem.write('AT+CUSD=2') ================================================ FILE: gsmmodem/pdu.py ================================================ # -*- coding: utf8 -*- """ SMS PDU encoding methods """ from __future__ import unicode_literals import sys, codecs from datetime import datetime, timedelta, tzinfo from copy import copy from .exceptions import EncodingError # For Python 3 support PYTHON_VERSION = sys.version_info[0] if PYTHON_VERSION >= 3: MAX_INT = sys.maxsize dictItemsIter = dict.items xrange = range unichr = chr toByteArray = lambda x: bytearray(codecs.decode(x, 'hex_codec')) if type(x) == bytes else bytearray(codecs.decode(bytes(x, 'ascii'), 'hex_codec')) if type(x) == str else x rawStrToByteArray = lambda x: bytearray(bytes(x, 'latin-1')) else: #pragma: no cover MAX_INT = sys.maxint dictItemsIter = dict.iteritems toByteArray = lambda x: bytearray(x.decode('hex')) if type(x) in (str, unicode) else x rawStrToByteArray = bytearray TEXT_MODE = ('\n\r !\"#%&\'()*+,-./0123456789:;<=>?ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz') # TODO: Check if all of them are supported inside text mode # Tables can be found at: http://en.wikipedia.org/wiki/GSM_03.38#GSM_7_bit_default_alphabet_and_extension_table_of_3GPP_TS_23.038_.2F_GSM_03.38 GSM7_BASIC = ('@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\x1bÆæßÉ !\"#¤%&\'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑÜ`¿abcdefghijklmnopqrstuvwxyzäöñüà') GSM7_EXTENDED = {chr(0xFF): 0x0A, #CR2: chr(0x0D), '^': chr(0x14), #SS2: chr(0x1B), '{': chr(0x28), '}': chr(0x29), '\\': chr(0x2F), '[': chr(0x3C), '~': chr(0x3D), ']': chr(0x3E), '|': chr(0x40), '€': chr(0x65)} # Maximum message sizes for each data coding MAX_MESSAGE_LENGTH = {0x00: 160, # GSM-7 0x04: 140, # 8-bit 0x08: 70} # UCS2 # Maximum message sizes for each data coding for multipart messages MAX_MULTIPART_MESSAGE_LENGTH = {0x00: 153, # GSM-7 0x04: 133, # 8-bit TODO: Check this value! 0x08: 67} # UCS2 class SmsPduTzInfo(tzinfo): """ Simple implementation of datetime.tzinfo for handling timestamp GMT offsets specified in SMS PDUs """ def __init__(self, pduOffsetStr=None): """ :param pduOffset: 2 semi-octet timezone offset as specified by PDU (see GSM 03.40 spec) :type pduOffset: str Note: pduOffsetStr is optional in this constructor due to the special requirement for pickling mentioned in the Python docs. It should, however, be used (or otherwise pduOffsetStr must be manually set) """ self._offset = None if pduOffsetStr != None: self._setPduOffsetStr(pduOffsetStr) def _setPduOffsetStr(self, pduOffsetStr): # See if the timezone difference is positive/negative by checking MSB of first semi-octet tzHexVal = int(pduOffsetStr, 16) # In order to read time zone 'minute' shift: # - Remove MSB (sign) # - Read HEX value as decimal # - Multiply by 15 # See: https://en.wikipedia.org/wiki/GSM_03.40#Time_Format # Possible fix for #15 - convert invalid character to BCD-value if (tzHexVal & 0x0F) > 0x9: tzHexVal +=0x06 tzOffsetMinutes = int('{0:0>2X}'.format(tzHexVal & 0x7F)) * 15 if tzHexVal & 0x80 == 0: # positive self._offset = timedelta(minutes=(tzOffsetMinutes)) else: # negative self._offset = timedelta(minutes=(-tzOffsetMinutes)) def utcoffset(self, dt): return self._offset def dst(self, dt): """ We do not have enough info in the SMS PDU to implement daylight savings time """ return timedelta(0) class InformationElement(object): """ User Data Header (UDH) Information Element (IE) implementation This represents a single field ("information element") in the PDU's User Data Header. The UDH itself contains one or more of these information elements. If the IEI (IE identifier) is recognized, the class will automatically specialize into one of the subclasses of InformationElement, e.g. Concatenation or PortAddress, allowing the user to easily access the specific (and useful) attributes of these special cases. """ def __new__(cls, *args, **kwargs): #iei, ieLen, ieData): """ Causes a new InformationElement class, or subclass thereof, to be created. If the IEI is recognized, a specific subclass of InformationElement is returned """ if len(args) > 0: targetClass = IEI_CLASS_MAP.get(args[0], cls) elif 'iei' in kwargs: targetClass = IEI_CLASS_MAP.get(kwargs['iei'], cls) else: return super(InformationElement, cls).__new__(cls) return super(InformationElement, targetClass).__new__(targetClass) def __init__(self, iei, ieLen=0, ieData=None): self.id = iei # IEI self.dataLength = ieLen # IE Length self.data = ieData or [] # raw IE data @classmethod def decode(cls, byteIter): """ Decodes a single IE at the current position in the specified byte iterator :return: An InformationElement (or subclass) instance for the decoded IE :rtype: InformationElement, or subclass thereof """ iei = next(byteIter) ieLen = next(byteIter) ieData = [] for i in xrange(ieLen): ieData.append(next(byteIter)) return InformationElement(iei, ieLen, ieData) def encode(self): """ Encodes this IE and returns the resulting bytes """ result = bytearray() result.append(self.id) result.append(self.dataLength) result.extend(self.data) return result def __len__(self): """ Exposes the IE's total length (including the IEI and IE length octet) in octets """ return self.dataLength + 2 class Concatenation(InformationElement): """ IE that indicates SMS concatenation. This implementation handles both 8-bit and 16-bit concatenation indication, and exposes the specific useful details of this IE as instance variables. Exposes: reference CSMS reference number, must be same for all the SMS parts in the CSMS parts total number of parts. The value shall remain constant for every short message which makes up the concatenated short message. If the value is zero then the receiving entity shall ignore the whole information element number this part's number in the sequence. The value shall start at 1 and increment for every short message which makes up the concatenated short message """ def __init__(self, iei=0x00, ieLen=0, ieData=None): super(Concatenation, self).__init__(iei, ieLen, ieData) if ieData != None: if iei == 0x00: # 8-bit reference self.reference, self.parts, self.number = ieData else: # 0x08: 16-bit reference self.reference = ieData[0] << 8 | ieData[1] self.parts = ieData[2] self.number = ieData[3] def encode(self): if self.reference > 0xFF: self.id = 0x08 # 16-bit reference self.data = [self.reference >> 8, self.reference & 0xFF, self.parts, self.number] else: self.id = 0x00 # 8-bit reference self.data = [self.reference, self.parts, self.number] self.dataLength = len(self.data) return super(Concatenation, self).encode() class PortAddress(InformationElement): """ IE that indicates an Application Port Addressing Scheme. This implementation handles both 8-bit and 16-bit concatenation indication, and exposes the specific useful details of this IE as instance variables. Exposes: destination: The destination port number source: The source port number """ def __init__(self, iei=0x04, ieLen=0, ieData=None): super(PortAddress, self).__init__(iei, ieLen, ieData) if ieData != None: if iei == 0x04: # 8-bit port addressing scheme self.destination, self.source = ieData else: # 0x05: 16-bit port addressing scheme self.destination = ieData[0] << 8 | ieData[1] self.source = ieData[2] << 8 | ieData[3] def encode(self): if self.destination > 0xFF or self.source > 0xFF: self.id = 0x05 # 16-bit self.data = [self.destination >> 8, self.destination & 0xFF, self.source >> 8, self.source & 0xFF] else: self.id = 0x04 # 8-bit self.data = [self.destination, self.source] self.dataLength = len(self.data) return super(PortAddress, self).encode() # Map of recognized IEIs IEI_CLASS_MAP = {0x00: Concatenation, # Concatenated short messages, 8-bit reference number 0x08: Concatenation, # Concatenated short messages, 16-bit reference number 0x04: PortAddress, # Application port addressing scheme, 8 bit address 0x05: PortAddress # Application port addressing scheme, 16 bit address } class Pdu(object): """ Encoded SMS PDU. Contains raw PDU data and related meta-information """ def __init__(self, data, tpduLength): """ Constructor :param data: the raw PDU data (as bytes) :type data: bytearray :param tpduLength: Length (in bytes) of the TPDU :type tpduLength: int """ self.data = data self.tpduLength = tpduLength def __str__(self): global PYTHON_VERSION if PYTHON_VERSION < 3: return str(self.data).encode('hex').upper() else: #pragma: no cover return str(codecs.encode(self.data, 'hex_codec'), 'ascii').upper() def encodeSmsSubmitPdu(number, text, reference=0, validity=None, smsc=None, requestStatusReport=True, rejectDuplicates=False, sendFlash=False): """ Creates an SMS-SUBMIT PDU for sending a message with the specified text to the specified number :param number: the destination mobile number :type number: str :param text: the message text :type text: str :param reference: message reference number (see also: rejectDuplicates parameter) :type reference: int :param validity: message validity period (absolute or relative) :type validity: datetime.timedelta (relative) or datetime.datetime (absolute) :param smsc: SMSC number to use (leave None to use default) :type smsc: str :param rejectDuplicates: Flag that controls the TP-RD parameter (messages with same destination and reference may be rejected if True) :type rejectDuplicates: bool :return: A list of one or more tuples containing the SMS PDU (as a bytearray, and the length of the TPDU part :rtype: list of tuples """ if PYTHON_VERSION < 3: if type(text) == str: text = text.decode('UTF-8') tpduFirstOctet = 0x01 # SMS-SUBMIT PDU if validity != None: # Validity period format (TP-VPF) is stored in bits 4,3 of the first TPDU octet if type(validity) == timedelta: # Relative (TP-VP is integer) tpduFirstOctet |= 0x10 # bit4 == 1, bit3 == 0 validityPeriod = [_encodeRelativeValidityPeriod(validity)] elif type(validity) == datetime: # Absolute (TP-VP is semi-octet encoded date) tpduFirstOctet |= 0x18 # bit4 == 1, bit3 == 1 validityPeriod = _encodeTimestamp(validity) else: raise TypeError('"validity" must be of type datetime.timedelta (for relative value) or datetime.datetime (for absolute value)') else: validityPeriod = None if rejectDuplicates: tpduFirstOctet |= 0x04 # bit2 == 1 if requestStatusReport: tpduFirstOctet |= 0x20 # bit5 == 1 # Encode message text and set data coding scheme based on text contents try: encodedTextLength = len(encodeGsm7(text)) except ValueError: # Cannot encode text using GSM-7; use UCS2 instead encodedTextLength = len(text) alphabet = 0x08 # UCS2 else: alphabet = 0x00 # GSM-7 # Check if message should be concatenated if encodedTextLength > MAX_MESSAGE_LENGTH[alphabet]: # Text too long for single PDU - add "concatenation" User Data Header concatHeaderPrototype = Concatenation() concatHeaderPrototype.reference = reference # Devide whole text into parts if alphabet == 0x00: pduTextParts = divideTextGsm7(text) elif alphabet == 0x08: pduTextParts = divideTextUcs2(text) else: raise NotImplementedError pduCount = len(pduTextParts) concatHeaderPrototype.parts = pduCount tpduFirstOctet |= 0x40 else: concatHeaderPrototype = None pduCount = 1 # Construct required PDU(s) pdus = [] for i in xrange(pduCount): pdu = bytearray() if smsc: pdu.extend(_encodeAddressField(smsc, smscField=True)) else: pdu.append(0x00) # Don't supply an SMSC number - use the one configured in the device udh = bytearray() if concatHeaderPrototype != None: concatHeader = copy(concatHeaderPrototype) concatHeader.number = i + 1 pduText = pduTextParts[i] pduTextLength = len(pduText) udh.extend(concatHeader.encode()) else: pduText = text udhLen = len(udh) pdu.append(tpduFirstOctet) pdu.append(reference) # message reference # Add destination number pdu.extend(_encodeAddressField(number)) pdu.append(0x00) # Protocol identifier - no higher-level protocol pdu.append(alphabet if not sendFlash else (0x10 if alphabet == 0x00 else 0x18)) if validityPeriod: pdu.extend(validityPeriod) if alphabet == 0x00: # GSM-7 encodedText = encodeGsm7(pduText) userDataLength = len(encodedText) # Payload size in septets/characters if udhLen > 0: shift = ((udhLen + 1) * 8) % 7 # "fill bits" needed to make the UDH end on a septet boundary userData = packSeptets(encodedText, padBits=shift) if shift > 0: userDataLength += 1 # take padding bits into account else: userData = packSeptets(encodedText) elif alphabet == 0x08: # UCS2 userData = encodeUcs2(pduText) userDataLength = len(userData) if udhLen > 0: userDataLength += udhLen + 1 # +1 for the UDH length indicator byte pdu.append(userDataLength) pdu.append(udhLen) pdu.extend(udh) # UDH else: pdu.append(userDataLength) pdu.extend(userData) # User Data (message payload) tpdu_length = len(pdu) - 1 pdus.append(Pdu(pdu, tpdu_length)) return pdus def decodeSmsPdu(pdu): """ Decodes SMS pdu data and returns a tuple in format (number, text) :param pdu: PDU data as a hex string, or a bytearray containing PDU octects :type pdu: str or bytearray :raise EncodingError: If the specified PDU data cannot be decoded :return: The decoded SMS data as a dictionary :rtype: dict """ try: pdu = toByteArray(pdu) except Exception as e: # Python 2 raises TypeError, Python 3 raises binascii.Error raise EncodingError(e) result = {} pduIter = iter(pdu) smscNumber, smscBytesRead = _decodeAddressField(pduIter, smscField=True) result['smsc'] = smscNumber result['tpdu_length'] = len(pdu) - smscBytesRead tpduFirstOctet = next(pduIter) pduType = tpduFirstOctet & 0x03 # bits 1-0 if pduType == 0x00: # SMS-DELIVER or SMS-DELIVER REPORT result['type'] = 'SMS-DELIVER' result['number'] = _decodeAddressField(pduIter)[0] result['protocol_id'] = next(pduIter) dataCoding = _decodeDataCoding(next(pduIter)) result['time'] = _decodeTimestamp(pduIter) userDataLen = next(pduIter) udhPresent = (tpduFirstOctet & 0x40) != 0 ud = _decodeUserData(pduIter, userDataLen, dataCoding, udhPresent) result.update(ud) elif pduType == 0x01: # SMS-SUBMIT or SMS-SUBMIT-REPORT result['type'] = 'SMS-SUBMIT' result['reference'] = next(pduIter) # message reference - we don't really use this result['number'] = _decodeAddressField(pduIter)[0] result['protocol_id'] = next(pduIter) dataCoding = _decodeDataCoding(next(pduIter)) validityPeriodFormat = (tpduFirstOctet & 0x18) >> 3 # bits 4,3 if validityPeriodFormat == 0x02: # TP-VP field present and integer represented (relative) result['validity'] = _decodeRelativeValidityPeriod(next(pduIter)) elif validityPeriodFormat == 0x03: # TP-VP field present and semi-octet represented (absolute) result['validity'] = _decodeTimestamp(pduIter) userDataLen = next(pduIter) udhPresent = (tpduFirstOctet & 0x40) != 0 ud = _decodeUserData(pduIter, userDataLen, dataCoding, udhPresent) result.update(ud) elif pduType == 0x02: # SMS-STATUS-REPORT or SMS-COMMAND result['type'] = 'SMS-STATUS-REPORT' result['reference'] = next(pduIter) result['number'] = _decodeAddressField(pduIter)[0] result['time'] = _decodeTimestamp(pduIter) result['discharge'] = _decodeTimestamp(pduIter) result['status'] = next(pduIter) else: raise EncodingError('Unknown SMS message type: {0}. First TPDU octet was: {1}'.format(pduType, tpduFirstOctet)) return result def _decodeUserData(byteIter, userDataLen, dataCoding, udhPresent): """ Decodes PDU user data (UDHI (if present) and message text) """ result = {} if udhPresent: # User Data Header is present result['udh'] = [] udhLen = next(byteIter) ieLenRead = 0 # Parse and store UDH fields while ieLenRead < udhLen: ie = InformationElement.decode(byteIter) ieLenRead += len(ie) result['udh'].append(ie) del ieLenRead if dataCoding == 0x00: # GSM-7 # Since we are using 7-bit data, "fill bits" may have been added to make the UDH end on a septet boundary shift = ((udhLen + 1) * 8) % 7 # "fill bits" needed to make the UDH end on a septet boundary # Simulate another "shift" in the unpackSeptets algorithm in order to ignore the fill bits prevOctet = next(byteIter) shift += 1 if dataCoding == 0x00: # GSM-7 if udhPresent: userDataSeptets = unpackSeptets(byteIter, userDataLen, prevOctet, shift) else: userDataSeptets = unpackSeptets(byteIter, userDataLen) result['text'] = decodeGsm7(userDataSeptets) elif dataCoding == 0x02: # UCS2 result['text'] = decodeUcs2(byteIter, userDataLen) else: # 8-bit (data) userData = [] for b in byteIter: userData.append(unichr(b)) result['text'] = ''.join(userData) return result def _decodeRelativeValidityPeriod(tpVp): """ Calculates the relative SMS validity period (based on the table in section 9.2.3.12 of GSM 03.40) :rtype: datetime.timedelta """ if tpVp <= 143: return timedelta(minutes=((tpVp + 1) * 5)) elif 144 <= tpVp <= 167: return timedelta(hours=12, minutes=((tpVp - 143) * 30)) elif 168 <= tpVp <= 196: return timedelta(days=(tpVp - 166)) elif 197 <= tpVp <= 255: return timedelta(weeks=(tpVp - 192)) else: raise ValueError('tpVp must be in range [0, 255]') def _encodeRelativeValidityPeriod(validityPeriod): """ Encodes the specified relative validity period timedelta into an integer for use in an SMS PDU (based on the table in section 9.2.3.12 of GSM 03.40) :param validityPeriod: The validity period to encode :type validityPeriod: datetime.timedelta :rtype: int """ # Python 2.6 does not have timedelta.total_seconds(), so compute it manually #seconds = validityPeriod.total_seconds() seconds = validityPeriod.seconds + (validityPeriod.days * 24 * 3600) if seconds <= 43200: # 12 hours tpVp = int(seconds / 300) - 1 # divide by 5 minutes, subtract 1 elif seconds <= 86400: # 24 hours tpVp = int((seconds - 43200) / 1800) + 143 # subtract 12 hours, divide by 30 minutes. add 143 elif validityPeriod.days <= 30: # 30 days tpVp = validityPeriod.days + 166 # amount of days + 166 elif validityPeriod.days <= 441: # max value of tpVp is 255 tpVp = int(validityPeriod.days / 7) + 192 # amount of weeks + 192 else: raise ValueError('Validity period too long; tpVp limited to 1 octet (max value: 255)') return tpVp def _decodeTimestamp(byteIter): """ Decodes a 7-octet timestamp """ dateStr = decodeSemiOctets(byteIter, 7) timeZoneStr = dateStr[-2:] return datetime.strptime(dateStr[:-2], '%y%m%d%H%M%S').replace(tzinfo=SmsPduTzInfo(timeZoneStr)) def _encodeTimestamp(timestamp): """ Encodes a 7-octet timestamp from the specified date Note: the specified timestamp must have a UTC offset set; you can use gsmmodem.util.SimpleOffsetTzInfo for simple cases :param timestamp: The timestamp to encode :type timestamp: datetime.datetime :return: The encoded timestamp :rtype: bytearray """ if timestamp.tzinfo == None: raise ValueError('Please specify time zone information for the timestamp (e.g. by using gsmmodem.util.SimpleOffsetTzInfo)') # See if the timezone difference is positive/negative tzDelta = timestamp.utcoffset() if tzDelta.days >= 0: tzValStr = '{0:0>2}'.format(int(tzDelta.seconds / 60 / 15)) else: # negative tzVal = int((tzDelta.days * -3600 * 24 - tzDelta.seconds) / 60 / 15) # calculate offset in 0.25 hours # Cast as literal hex value and set MSB of first semi-octet of timezone to 1 to indicate negative value tzVal = int('{0:0>2}'.format(tzVal), 16) | 0x80 tzValStr = '{0:0>2X}'.format(tzVal) dateStr = timestamp.strftime('%y%m%d%H%M%S') + tzValStr return encodeSemiOctets(dateStr) def _decodeDataCoding(octet): if octet & 0xC0 == 0: #compressed = octect & 0x20 alphabet = (octet & 0x0C) >> 2 return alphabet # 0x00 == GSM-7, 0x01 == 8-bit data, 0x02 == UCS2 # We ignore other coding groups return 0 def nibble2octet(addressLen): return int((addressLen + 1) / 2) def _decodeAddressField(byteIter, smscField=False, log=False): """ Decodes the address field at the current position of the bytearray iterator :param byteIter: Iterator over bytearray :type byteIter: iter(bytearray) :return: Tuple containing the address value and amount of bytes read (value is or None if it is empty (zero-length)) :rtype: tuple """ addressLen = next(byteIter) if addressLen > 0: toa = next(byteIter) ton = (toa & 0x70) # bits 6,5,4 of type-of-address == type-of-number if ton == 0x50: # Alphanumberic number addressLen = nibble2octet(addressLen) septets = unpackSeptets(byteIter, addressLen) addressValue = decodeGsm7(septets) return (addressValue, (addressLen + 2)) else: # ton == 0x00: Unknown (might be international, local, etc) - leave as is # ton == 0x20: National number if smscField: addressValue = decodeSemiOctets(byteIter, addressLen-1) else: addressLen = nibble2octet(addressLen) addressValue = decodeSemiOctets(byteIter, addressLen) addressLen += 1 # for the return value, add the toa byte if ton == 0x10: # International number addressValue = '+' + addressValue return (addressValue, (addressLen + 1)) else: return (None, 1) def _encodeAddressField(address, smscField=False): """ Encodes the address into an address field :param address: The address to encode (phone number or alphanumeric) :type byteIter: str :return: Encoded SMS PDU address field :rtype: bytearray """ # First, see if this is a number or an alphanumeric string toa = 0x80 | 0x00 | 0x01 # Type-of-address start | Unknown type-of-number | ISDN/tel numbering plan alphaNumeric = False if address.isalnum(): # Might just be a local number if address.isdigit(): # Local number toa |= 0x20 else: # Alphanumeric address toa |= 0x50 toa &= 0xFE # switch to "unknown" numbering plan alphaNumeric = True else: if address[0] == '+' and address[1:].isdigit(): # International number toa |= 0x10 # Remove the '+' prefix address = address[1:] else: # Alphanumeric address toa |= 0x50 toa &= 0xFE # switch to "unknown" numbering plan alphaNumeric = True if alphaNumeric: addressValue = packSeptets(encodeGsm7(address, False)) addressLen = len(addressValue) * 2 else: addressValue = encodeSemiOctets(address) if smscField: addressLen = len(addressValue) + 1 else: addressLen = len(address) result = bytearray() result.append(addressLen) result.append(toa) result.extend(addressValue) return result def encodeSemiOctets(number): """ Semi-octet encoding algorithm (e.g. for phone numbers) :return: bytearray containing the encoded octets :rtype: bytearray """ if len(number) % 2 == 1: number = number + 'F' # append the "end" indicator octets = [int(number[i+1] + number[i], 16) for i in xrange(0, len(number), 2)] return bytearray(octets) def decodeSemiOctets(encodedNumber, numberOfOctets=None): """ Semi-octet decoding algorithm(e.g. for phone numbers) :param encodedNumber: The semi-octet-encoded telephone number (in bytearray format or hex string) :type encodedNumber: bytearray, str or iter(bytearray) :param numberOfOctets: The expected amount of octets after decoding (i.e. when to stop) :type numberOfOctets: int :return: decoded telephone number :rtype: string """ number = [] if type(encodedNumber) in (str, bytes): encodedNumber = bytearray(codecs.decode(encodedNumber, 'hex_codec')) i = 0 for octet in encodedNumber: hexVal = hex(octet)[2:].zfill(2) number.append(hexVal[1]) if hexVal[0] != 'f': number.append(hexVal[0]) else: break if numberOfOctets != None: i += 1 if i == numberOfOctets: break return ''.join(number) def encodeTextMode(plaintext): """ Text mode checker Tests whther SMS could be sent in text mode :param text: the text string to encode :raise ValueError: if the text string cannot be sent in text mode :return: Passed string :rtype: str """ if PYTHON_VERSION >= 3: plaintext = str(plaintext) elif type(plaintext) == str: plaintext = plaintext.decode('UTF-8') for char in plaintext: idx = TEXT_MODE.find(char) if idx != -1: continue else: raise ValueError('Cannot encode char "{0}" inside text mode'.format(char)) if len(plaintext) > MAX_MESSAGE_LENGTH[0x00]: raise ValueError('Message is too long for text mode (maximum {0} characters)'.format(MAX_MESSAGE_LENGTH[0x00])) return plaintext def encodeGsm7(plaintext, discardInvalid=False): """ GSM-7 text encoding algorithm Encodes the specified text string into GSM-7 octets (characters). This method does not pack the characters into septets. :param text: the text string to encode :param discardInvalid: if True, characters that cannot be encoded will be silently discarded :raise ValueError: if the text string cannot be encoded using GSM-7 encoding (unless discardInvalid == True) :return: A bytearray containing the string encoded in GSM-7 encoding :rtype: bytearray """ result = bytearray() if PYTHON_VERSION >= 3: plaintext = str(plaintext) elif type(plaintext) == str: plaintext = plaintext.decode('UTF-8') for char in plaintext: idx = GSM7_BASIC.find(char) if idx != -1: result.append(idx) elif char in GSM7_EXTENDED: result.append(0x1B) # ESC - switch to extended table result.append(ord(GSM7_EXTENDED[char])) elif not discardInvalid: raise ValueError('Cannot encode char "{0}" using GSM-7 encoding'.format(char)) return result def decodeGsm7(encodedText): """ GSM-7 text decoding algorithm Decodes the specified GSM-7-encoded string into a plaintext string. :param encodedText: the text string to encode :type encodedText: bytearray or str :return: A string containing the decoded text :rtype: str """ result = [] if type(encodedText) == str: encodedText = rawStrToByteArray(encodedText) #bytearray(encodedText) iterEncoded = iter(encodedText) for b in iterEncoded: if b == 0x1B: # ESC - switch to extended table c = chr(next(iterEncoded)) for char, value in dictItemsIter(GSM7_EXTENDED): if c == value: result.append(char) break else: result.append(GSM7_BASIC[b]) return ''.join(result) def divideTextGsm7(plainText): """ GSM7 message dividing algorithm Divides text into list of chunks that could be stored in a single, GSM7-encoded SMS message. :param plainText: the text string to divide :type plainText: str :return: A list of strings :rtype: list of str """ result = [] plainStartPtr = 0 plainStopPtr = 0 chunkByteSize = 0 if PYTHON_VERSION >= 3: plainText = str(plainText) while plainStopPtr < len(plainText): char = plainText[plainStopPtr] idx = GSM7_BASIC.find(char) if idx != -1: chunkByteSize = chunkByteSize + 1; elif char in GSM7_EXTENDED: chunkByteSize = chunkByteSize + 2; else: raise ValueError('Cannot encode char "{0}" using GSM-7 encoding'.format(char)) plainStopPtr = plainStopPtr + 1 if chunkByteSize > MAX_MULTIPART_MESSAGE_LENGTH[0x00]: plainStopPtr = plainStopPtr - 1 if chunkByteSize >= MAX_MULTIPART_MESSAGE_LENGTH[0x00]: result.append(plainText[plainStartPtr:plainStopPtr]) plainStartPtr = plainStopPtr chunkByteSize = 0 if chunkByteSize > 0: result.append(plainText[plainStartPtr:]) return result def packSeptets(octets, padBits=0): """ Packs the specified octets into septets Typically the output of encodeGsm7 would be used as input to this function. The resulting bytearray contains the original GSM-7 characters packed into septets ready for transmission. :rtype: bytearray """ result = bytearray() if type(octets) == str: octets = iter(rawStrToByteArray(octets)) elif type(octets) == bytearray: octets = iter(octets) shift = padBits if padBits == 0: try: prevSeptet = next(octets) except StopIteration: return result else: prevSeptet = 0x00 for octet in octets: septet = octet & 0x7f; if shift == 7: # prevSeptet has already been fully added to result shift = 0 prevSeptet = septet continue b = ((septet << (7 - shift)) & 0xFF) | (prevSeptet >> shift) prevSeptet = septet shift += 1 result.append(b) if shift != 7: # There is a bit "left over" from prevSeptet result.append(prevSeptet >> shift) return result def unpackSeptets(septets, numberOfSeptets=None, prevOctet=None, shift=7): """ Unpacks the specified septets into octets :param septets: Iterator or iterable containing the septets packed into octets :type septets: iter(bytearray), bytearray or str :param numberOfSeptets: The amount of septets to unpack (or None for all remaining in "septets") :type numberOfSeptets: int or None :return: The septets unpacked into octets :rtype: bytearray """ result = bytearray() if type(septets) == str: septets = iter(rawStrToByteArray(septets)) elif type(septets) == bytearray: septets = iter(septets) if numberOfSeptets == None: numberOfSeptets = MAX_INT # Loop until StopIteration if numberOfSeptets == 0: return result i = 0 for octet in septets: i += 1 if shift == 7: shift = 1 if prevOctet != None: result.append(prevOctet >> 1) if i <= numberOfSeptets: result.append(octet & 0x7F) prevOctet = octet if i == numberOfSeptets: break else: continue b = ((octet << shift) & 0x7F) | (prevOctet >> (8 - shift)) prevOctet = octet result.append(b) shift += 1 if i == numberOfSeptets: break if shift == 7 and prevOctet: b = prevOctet >> (8 - shift) if b: # The final septet value still needs to be unpacked result.append(b) return result def decodeUcs2(byteIter, numBytes): """ Decodes UCS2-encoded text from the specified byte iterator, up to a maximum of numBytes """ userData = [] i = 0 try: while i < numBytes: userData.append(unichr((next(byteIter) << 8) | next(byteIter))) i += 2 except StopIteration: # Not enough bytes in iterator to reach numBytes; return what we have pass return ''.join(userData) def encodeUcs2(text): """ UCS2 text encoding algorithm Encodes the specified text string into UCS2-encoded bytes. :param text: the text string to encode :return: A bytearray containing the string encoded in UCS2 encoding :rtype: bytearray """ result = bytearray() for b in map(ord, text): result.append(b >> 8) result.append(b & 0xFF) return result def divideTextUcs2(plainText): """ UCS-2 message dividing algorithm Divides text into list of chunks that could be stored in a single, UCS-2 -encoded SMS message. :param plainText: the text string to divide :type plainText: str :return: A list of strings :rtype: list of str """ result = [] resultLength = 0 fullChunksCount = int(len(plainText) / MAX_MULTIPART_MESSAGE_LENGTH[0x08]) for i in range(fullChunksCount): result.append(plainText[i * MAX_MULTIPART_MESSAGE_LENGTH[0x08] : (i + 1) * MAX_MULTIPART_MESSAGE_LENGTH[0x08]]) resultLength = resultLength + MAX_MULTIPART_MESSAGE_LENGTH[0x08] # Add last, not fully filled chunk if resultLength < len(plainText): result.append(plainText[resultLength:]) return result ================================================ FILE: gsmmodem/serial_comms.py ================================================ #!/usr/bin/env python """ Low-level serial communications handling """ import sys, threading, logging import re import serial # pyserial: http://pyserial.sourceforge.net from .exceptions import TimeoutException from . import compat # For Python 2.6 compatibility class SerialComms(object): """ Wraps all low-level serial communications (actual read/write operations) """ log = logging.getLogger('gsmmodem.serial_comms.SerialComms') # End-of-line read terminator RX_EOL_SEQ = b'\r\n' # End-of-response terminator RESPONSE_TERM = re.compile('^OK|ERROR|(\+CM[ES] ERROR: \d+)|(COMMAND NOT SUPPORT)$') # Default timeout for serial port reads (in seconds) timeout = 1 def __init__(self, port, baudrate=115200, notifyCallbackFunc=None, fatalErrorCallbackFunc=None, *args, **kwargs): """ Constructor :param fatalErrorCallbackFunc: function to call if a fatal error occurs in the serial device reading thread :type fatalErrorCallbackFunc: func """ self.alive = False self.port = port self.baudrate = baudrate self._responseEvent = None # threading.Event() self._expectResponseTermSeq = None # expected response terminator sequence self._response = None # Buffer containing response to a written command self._notification = [] # Buffer containing lines from an unsolicited notification from the modem # Reentrant lock for managing concurrent write access to the underlying serial port self._txLock = threading.RLock() self.notifyCallback = notifyCallbackFunc or self._placeholderCallback self.fatalErrorCallback = fatalErrorCallbackFunc or self._placeholderCallback self.com_args = args self.com_kwargs = kwargs def connect(self): """ Connects to the device and starts the read thread """ self.serial = serial.Serial(dsrdtr=True, rtscts=True, port=self.port, baudrate=self.baudrate, timeout=self.timeout,*self.com_args,**self.com_kwargs) # Start read thread self.alive = True self.rxThread = threading.Thread(target=self._readLoop) self.rxThread.daemon = True self.rxThread.start() def close(self): """ Stops the read thread, waits for it to exit cleanly, then closes the underlying serial port """ self.alive = False self.rxThread.join() self.serial.close() def _handleLineRead(self, line, checkForResponseTerm=True): #print 'sc.hlineread:',line if self._responseEvent and not self._responseEvent.is_set(): # A response event has been set up (another thread is waiting for this response) self._response.append(line) if not checkForResponseTerm or self.RESPONSE_TERM.match(line): # End of response reached; notify waiting thread #print 'response:', self._response self.log.debug('response: %s', self._response) self._responseEvent.set() else: # Nothing was waiting for this - treat it as a notification self._notification.append(line) if self.serial.inWaiting() == 0: # No more chars on the way for this notification - notify higher-level callback #print 'notification:', self._notification self.log.debug('notification: %s', self._notification) self.notifyCallback(self._notification) self._notification = [] def _placeholderCallback(self, *args, **kwargs): """ Placeholder callback function (does nothing) """ def _readLoop(self): """ Read thread main loop Reads lines from the connected device """ try: readTermSeq = bytearray(self.RX_EOL_SEQ) readTermLen = len(readTermSeq) rxBuffer = bytearray() while self.alive: data = self.serial.read(1) if len(data) != 0: # check for timeout #print >> sys.stderr, ' RX:', data,'({0})'.format(ord(data)) rxBuffer.append(ord(data)) if rxBuffer[-readTermLen:] == readTermSeq: # A line (or other logical segment) has been read line = rxBuffer[:-readTermLen].decode() rxBuffer = bytearray() if len(line) > 0: #print 'calling handler' self._handleLineRead(line) elif self._expectResponseTermSeq: if rxBuffer[-len(self._expectResponseTermSeq):] == self._expectResponseTermSeq: line = rxBuffer.decode() rxBuffer = bytearray() self._handleLineRead(line, checkForResponseTerm=False) #else: #' ' except serial.SerialException as e: self.alive = False try: self.serial.close() except Exception: #pragma: no cover pass # Notify the fatal error handler self.fatalErrorCallback(e) def write(self, data, waitForResponse=True, timeout=5, expectedResponseTermSeq=None): data = data.encode() with self._txLock: if waitForResponse: if expectedResponseTermSeq: self._expectResponseTermSeq = bytearray(expectedResponseTermSeq.encode()) self._response = [] self._responseEvent = threading.Event() self.serial.write(data) if self._responseEvent.wait(timeout): self._responseEvent = None self._expectResponseTermSeq = False return self._response else: # Response timed out self._responseEvent = None self._expectResponseTermSeq = False if len(self._response) > 0: # Add the partial response to the timeout exception raise TimeoutException(self._response) else: raise TimeoutException() else: self.serial.write(data) ================================================ FILE: gsmmodem/util.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- """ Some common utility classes used by tests """ from datetime import datetime, timedelta, tzinfo import re class SimpleOffsetTzInfo(tzinfo): """ Very simple implementation of datetime.tzinfo offering set timezone offset for datetime instances """ def __init__(self, offsetInHours=None): """ Constructs a new tzinfo instance using an amount of hours as an offset :param offsetInHours: The timezone offset, in hours (may be negative) :type offsetInHours: int or float """ if offsetInHours != None: #pragma: no cover self.offsetInHours = offsetInHours def utcoffset(self, dt): return timedelta(hours=self.offsetInHours) def dst(self, dt): return timedelta(0) def __repr__(self): return 'gsmmodem.util.SimpleOffsetTzInfo({0})'.format(self.offsetInHours) def parseTextModeTimeStr(timeStr): """ Parses the specified SMS text mode time string The time stamp format is "yy/MM/dd,hh:mm:ss±zz" (yy = year, MM = month, dd = day, hh = hour, mm = minute, ss = second, zz = time zone [Note: the unit of time zone is a quarter of an hour]) :param timeStr: The time string to parse :type timeStr: str :return: datetime object representing the specified time string :rtype: datetime.datetime """ msgTime = timeStr[:-3] tzOffsetHours = int(int(timeStr[-3:]) * 0.25) return datetime.strptime(msgTime, '%y/%m/%d,%H:%M:%S').replace(tzinfo=SimpleOffsetTzInfo(tzOffsetHours)) def lineStartingWith(string, lines): """ Searches through the specified list of strings and returns the first line starting with the specified search string, or None if not found """ for line in lines: if line.startswith(string): return line else: return None def lineMatching(regexStr, lines): """ Searches through the specified list of strings and returns the regular expression match for the first line that matches the specified regex string, or None if no match was found Note: if you have a pre-compiled regex pattern, use lineMatchingPattern() instead :type regexStr: Regular expression string to use :type lines: List of lines to search :return: the regular expression match for the first line that matches the specified regex, or None if no match was found :rtype: re.Match """ regex = re.compile(regexStr) for line in lines: m = regex.match(line) if m: return m else: return None def lineMatchingPattern(pattern, lines): """ Searches through the specified list of strings and returns the regular expression match for the first line that matches the specified pre-compiled regex pattern, or None if no match was found Note: if you are using a regex pattern string (i.e. not already compiled), use lineMatching() instead :type pattern: Compiled regular expression pattern to use :type lines: List of lines to search :return: the regular expression match for the first line that matches the specified regex, or None if no match was found :rtype: re.Match """ for line in lines: m = pattern.match(line) if m: return m else: return None def allLinesMatchingPattern(pattern, lines): """ Like lineMatchingPattern, but returns all lines that match the specified pattern :type pattern: Compiled regular expression pattern to use :type lines: List of lines to search :return: list of re.Match objects for each line matched, or an empty list if none matched :rtype: list """ result = [] for line in lines: m = pattern.match(line) if m: result.append(m) return result def removeAtPrefix(string): """ Remove AT prefix from a specified string. :param string: An original string :type string: str :return: A string with AT prefix removed :rtype: str """ if string.startswith('AT'): return string[2:] return string ================================================ FILE: pyproject.toml ================================================ [build-system] requires = [ "setuptools>=45", "setuptools_scm[toml]>=6.2", "wheel", ] build-backend = "setuptools.build_meta" [tool.setuptools_scm] [tool.black] target-version = ["py38", "py39", "py310"] [tool.isort] profile = "black" ================================================ FILE: requirements.txt ================================================ . ================================================ FILE: setup.cfg ================================================ [metadata] name = python-gsmmodem-new description = Control an attached GSM modem: send/receive SMS messages, handle calls, etc license = LGPLv3+ author = Francois Aucamp author_email = francois.aucamp@gmail.com url = https://github.com/babca/python-gsmmodem long_description = file: README.rst long_description_content_type = text/x-rst classifiers = Development Status :: 4 - Beta Environment :: Console Intended Audience :: Developers Intended Audience :: Telecommunications Industry License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+) Operating System :: OS Independent Programming Language :: Python :: 3 Topic :: Communications :: Telephony Topic :: Home Automation Topic :: Software Development :: Libraries :: Python Modules Topic :: System :: Hardware Topic :: Terminals :: Serial Topic :: Utilities keywords = gsm sms modem mobile phone usb serial [options] packages = gsmmodem gsmtermlib package_dir = gsmtermlib=tools/gsmtermlib scripts = tools/gsmterm.py tools/sendsms.py tools/identify-modem.py install_requires = pyserial>=3.1.1 tests_require = [options.extras_require] docs = sphinx ================================================ FILE: setup.py ================================================ #!/usr/bin/env python """ python-gsmmodem installation script """ import sys from distutils.core import Command from setuptools import setup test_command = [sys.executable, '-m', 'unittest', 'discover'] coverage_command = ['coverage', 'run', '-m', 'unittest', 'discover'] VERSION = "0.12" class RunUnitTests(Command): """ run unit tests """ user_options = [] description = __doc__[1:] def initialize_options(self): pass def finalize_options(self): pass def run(self): import subprocess errno = subprocess.call(test_command) raise SystemExit(errno) class RunUnitTestsCoverage(Command): """ run unit tests and report on code coverage using the 'coverage' tool """ user_options = [] description = __doc__[1:] def initialize_options(self): pass def finalize_options(self): pass def run(self): import subprocess errno = subprocess.call(coverage_command) if errno == 0: subprocess.call(['coverage', 'report']) raise SystemExit(errno) setup(use_scm_version=True, cmdclass = {'test': RunUnitTests, 'coverage': RunUnitTestsCoverage}) ================================================ FILE: test/__init__.py ================================================ """ Tests for python-gsmmodem """ ================================================ FILE: test/compat.py ================================================ """ Contains equivalents for a few commonly-used Python 2.7-and-higher test functions. Used to provide backwards-compatibility with Python 2.6 """ import sys if sys.version_info[0] == 2 and sys.version_info[1] < 7: import unittest def assertGreater(self, a, b, msg=None): """ Drop-in replacement for Python 2.7's method of the same name """ return self.assertTrue(a > b, msg) def assertGreaterEqual(self, a, b, msg=None): """ Drop-in replacement for Python 2.7's method of the same name """ return self.assertTrue(a >= b, msg) def assertIsInstance(self, a, b, msg=None): """ Drop-in replacement for Python 2.7's method of the same name """ return self.assertTrue(isinstance(a, b), msg) def assertListEqual(self, a, b, msg=None): """ Drop-in replacement for Python 2.7's method of the same name """ if len(a) != len(b): raise self.failureException(msg or 'List length differs') else: for i in xrange(len(a)): if a[i] != b[i]: raise self.failureException(msg or 'List differs: {0} != {1}'.format(a[i], b[i])) def assertIn(self, a, b, msg=None): """ Drop-in replacement for Python 2.7's method of the same name """ return self.assertTrue(a in b, msg) def assertNotIn(self, a, b, msg=None): """ Drop-in replacement for Python 2.7's method of the same name """ return self.assertTrue(a not in b, msg) def assertIs(self, a, b, msg=None): """ Drop-in replacement for Python 2.7's method of the same name """ return self.assertTrue(a is b, msg) # Monkey-patch our compatibility methods into unittest.TestCase unittest.TestCase.assertGreater = assertGreater unittest.TestCase.assertGreaterEqual = assertGreaterEqual unittest.TestCase.assertIsInstance = assertIsInstance unittest.TestCase.assertListEqual = assertListEqual unittest.TestCase.assertIn = assertIn unittest.TestCase.assertNotIn = assertNotIn unittest.TestCase.assertIs = assertIs if sys.version_info[0] == 2: str = str bytearrayToStr = str else: str = lambda x: x bytearrayToStr = lambda x: x.decode('latin-1') ================================================ FILE: test/fakemodems.py ================================================ """ Module containing fake modem descriptors, for testing """ import abc from copy import copy class FakeModem(object): """ Abstract base class for fake modem descriptors """ __metaclass__ = abc.ABCMeta def __init__(self): self.responses = {} self.commandsNoPinRequired = [] self.commandsSimBusy = [] # Commands that may trigger "SIM busy" errors self.pinLock = False self.defaultResponse = ['OK\r\n'] self.pinRequiredErrorResponse = ['+CME ERROR: 11\r\n'] self.smscNumber = None self.simBusyErrorCounter = 0 # Number of times to issue a "SIM busy" error self.deviceBusyErrorCounter = 0 # Number of times to issue a "Device busy" error self.cfun = 1 # +CFUN value to report back self.dtmfCommandBase = '+VTS=' def getResponse(self, cmd): if type(cmd) == bytes: cmd = cmd.decode() if self.deviceBusyErrorCounter > 0: self.deviceBusyErrorCounter -= 1 return ['+CME ERROR: 515\r\n'] if self._pinLock and not cmd.startswith('AT+CPIN'): if cmd not in self.commandsNoPinRequired: return copy(self.pinRequiredErrorResponse) if cmd.startswith('AT+CPIN="'): self.pinLock = False elif self.simBusyErrorCounter > 0 and cmd in self.commandsSimBusy: self.simBusyErrorCounter -= 1 return ['+CME ERROR: 14\r\n'] if cmd == 'AT+CFUN?\r' and self.cfun != -1: return ['+CFUN: {0}\r\n'.format(self.cfun), 'OK\r\n'] elif cmd == 'AT+CSCA?\r': if self.smscNumber != None: return ['+CSCA: "{0}",145\r\n'.format(self.smscNumber), 'OK\r\n'] else: return ['OK\r\n'] if cmd in self.responses: return copy(self.responses[cmd]) else: return copy(self.defaultResponse) @property def pinLock(self): return self._pinLock @pinLock.setter def pinLock(self, pinLock): self._pinLock = pinLock if self._pinLock == True: self.responses['AT+CPIN?\r'] = ['+CPIN: SIM PIN\r\n', 'OK\r\n'] else: self.responses['AT+CPIN?\r'] = ['+CPIN: READY\r\n', 'OK\r\n'] @abc.abstractmethod def getAtdResponse(self, number): return [] @abc.abstractmethod def getPreCallInitWaitSequence(self): return [0.1] @abc.abstractmethod def getCallInitNotification(self, callId, callType): return ['+WIND: 5,1\r\n', '+WIND: 2\r\n'] @abc.abstractmethod def getRemoteAnsweredNotification(self, callId, callType): return ['OK\r\n'] @abc.abstractmethod def getRemoteHangupNotification(self, callId, callType): return ['NO CARRIER\r\n', '+WIND: 6,1\r\n'] def getRemoteRejectCallNotification(self, callId, callType): # For a lot of modems, this is the same as a hangup notification - override this if necessary! return self.getRemoteHangupNotification(callId, callType) @abc.abstractmethod def getIncomingCallNotification(self, callerNumber, callType='VOICE', ton=145): return ['RING\r\n'] class GenericTestModem(FakeModem): """ Not based on a real modem - simply used for general tests. Uses polling for call status updates """ def __init__(self): super(GenericTestModem, self).__init__() self._callState = 2 self._callNumber = None self._callId = None self.commandsNoPinRequired = ['ATZ\r', 'ATE0\r', 'AT+CFUN?\r', 'AT+CFUN=1\r', 'AT+CMEE=1\r'] self.responses = {'AT+CPMS=?\r': ['+CPMS: ("ME","MT","SM","SR"),("ME","MT","SM","SR"),("ME","MT","SM","SR")\r\n', 'OK\r\n'], 'AT+CLAC=?\r': ['ERROR\r\n'], 'AT+CLAC\r': ['ERROR\r\n'], 'AT+WIND=?\r': ['ERROR\r\n'], 'AT+WIND?\r': ['ERROR\r\n'], 'AT+WIND=50\r': ['ERROR\r\n'], 'AT+ZPAS=?\r': ['ERROR\r\n'], 'AT+ZPAS?\r': ['ERROR\r\n'], 'AT+CSCS=?\r': ['+CSCS: ("GSM","UCS2")\r\n', 'OK\r\n'], 'AT+CPIN?\r': ['+CPIN: READY\r\n', 'OK\r\n'], 'AT\r': ['OK\r\n']} def getResponse(self, cmd): if type(cmd) == bytes: cmd = cmd.decode() if not self._pinLock and cmd == 'AT+CLCC\r': if self._callNumber: if self._callState == 0: return ['+CLCC: 1,0,2,0,0,"{0}",129\r\n'.format(self._callNumber), 'OK\r\n'] elif self._callState == 1: return ['+CLCC: 1,0,0,0,0,"{0}",129\r\n'.format(self._callNumber), 'OK\r\n'] else: return ['OK\r\n'] else: return super(GenericTestModem, self).getResponse(cmd) else: return super(GenericTestModem, self).getResponse(cmd) def getAtdResponse(self, number): self._callNumber = number self._callState = 0 return ['OK\r\n'] def getPreCallInitWaitSequence(self): return [0.1] def getCallInitNotification(self, callId, callType): return [] def getRemoteAnsweredNotification(self, callId, callType): self._callState = 1 return [] def getRemoteHangupNotification(self, callId, callType): self._callState = 2 self._callNumber = None return [] def getIncomingCallNotification(self, callerNumber, callType='VOICE', ton=145): return ['+CRING: {0}\r\n'.format(callType), '+CLIP: "{1}",{2},,,,0\r\n'.format(callType, callerNumber, ton)] class WavecomMultiband900E1800(FakeModem): """ Family of old Wavecom serial modems User franciumlin also submitted the following improvements to this profile: +CPIN replies are not ended with "OK" """ def __init__(self): super(WavecomMultiband900E1800, self).__init__() self.responses = {'AT+CGMI\r': [' WAVECOM MODEM\r\n', 'OK\r\n'], 'AT+CGMM\r': [' MULTIBAND 900E 1800\r\n', 'OK\r\n'], 'AT+CGMR\r': ['ERROR\r\n'], 'AT+CIMI\r': ['111111111111111\r\n', 'OK\r\n'], 'AT+CGSN\r': ['111111111111111\r\n', 'OK\r\n'], 'AT+CLAC\r': ['ERROR\r\n'], 'AT+WIND?\r': ['+WIND: 0\r\n', 'OK\r\n'], 'AT+WIND=50\r': ['OK\r\n'], 'AT+ZPAS?\r': ['ERROR\r\n'], 'AT+CPMS="SM","SM","SR"\r': ['ERROR\r\n'], 'AT+CPMS=?\r': ['+CPMS: (("SM","BM","SR"),("SM"))\r\n', 'OK\r\n'], 'AT+CPMS="SM","SM"\r': ['+CPMS: 14,50,14,50\r\n', 'OK\r\n'], 'AT+CNMI=2,1,0,2\r': ['OK\r\n'], 'AT+CVHU=0\r': ['ERROR\r\n'], 'AT+CPIN?\r': ['+CPIN: READY\r\n']} # <---- note: missing 'OK\r\n' self.commandsNoPinRequired = ['ATZ\r', 'ATE0\r', 'AT+CFUN?\r', 'AT+CFUN=1\r', 'AT+CMEE=1\r'] def getResponse(self, cmd): if type(cmd) == bytes: cmd = cmd.decode() if cmd == 'AT+CFUN=1\r': self.deviceBusyErrorCounter = 2 # This modem takes quite a while to recover from this return ['OK\r\n'] return super(WavecomMultiband900E1800, self).getResponse(cmd) @property def pinLock(self): return self._pinLock @pinLock.setter def pinLock(self, pinLock): self._pinLock = pinLock if self._pinLock == True: self.responses['AT+CPIN?\r'] = ['+CPIN: SIM PIN\r\n'] # missing OK else: self.responses['AT+CPIN?\r'] = ['+CPIN: READY\r\n'] # missing OK def getAtdResponse(self, number): return [] def getPreCallInitWaitSequence(self): return [0.1] def getCallInitNotification(self, callId, callType): # +WIND: 5 == indication of call # +WIND: 2 == remote party is ringing return ['+WIND: 5,1\r\n', '+WIND: 2\r\n'] def getRemoteAnsweredNotification(self, callId, callType): return ['OK\r\n'] def getRemoteHangupNotification(self, callId, callType): return ['NO CARRIER\r\n', '+WIND: 6,1\r\n'] def getIncomingCallNotification(self, callerNumber, callType='VOICE', ton=145): return ['+CRING: {0}\r\n'.format(callType), '+CLIP: "{1}",{2}\r\n'.format(callType, callerNumber, ton)] def __str__(self): return 'WAVECOM MODEM MULTIBAND 900E 1800' class HuaweiK3715(FakeModem): """ Huawei K3715 modem (commonly used by Vodafone) """ def __init__(self): super(HuaweiK3715, self).__init__() self.responses = {'AT+CGMI\r': ['huawei\r\n', 'OK\r\n'], 'AT+CGMM\r': ['K3715\r\n', 'OK\r\n'], 'AT+CGMR\r': ['11.104.05.00.00\r\n', 'OK\r\n'], 'AT+CIMI\r': ['111111111111111\r\n', 'OK\r\n'], 'AT+CGSN\r': ['111111111111111\r\n', 'OK\r\n'], 'AT+CPMS=?\r': ['+CPMS: ("ME","MT","SM","SR"),("ME","MT","SM","SR"),("ME","MT","SM","SR")\r\n', 'OK\r\n'], 'AT+WIND?\r': ['ERROR\r\n'], 'AT+WIND=50\r': ['ERROR\r\n'], 'AT+ZPAS?\r': ['ERROR\r\n'], 'AT+CLAC\r': ['+CLAC:&C,&D,&E,&F,&S,&V,&W,E,I,L,M,Q,V,X,Z,T,P,\S,\V,\ %V,D,A,H,O,S0,S2,S3,S4,S5,S6,S7,S8,S9,S10,S11,S30,S103,S104,+FCLASS,+ICF,+IFC,+IPR,+GMI,\ +GMM,+GMR,+GCAP,+GSN,+DR,+DS,+WS46,+CLAC,+CCLK,+CBST,+CRLP,+CV120,+CHSN,+CSSN,+CREG,+CGREG,\ +CFUN,+GCAP,+CSCS,+CSTA,+CR,+CEER,+CRC,+CMEE,+CGDCONT,+CGDSCONT,+CGTFT,+CGEQREQ,+CGEQMIN,\ +CGQREQ,+CGQMIN,+CGEQNEG,+CGEREP,+CGPADDR,+CGCLASS,+CGSMS,+CSMS,+CMGF,+CSAS,+CRES,+CSCA,\ +CSMP,+CSDH,+CSCB,+FDD,+FAR,+FCL,+FIT,+ES,+ESA,+CMOD,+CVHU,+CGDATA,+CSQ,+CBC,+CPAS,+CPIN,\ +CMEC,+CGATT,+CGACT,+CGCMOD,+CPBS,+CPBR,+CPBF,+CPBW,+CPMS,+CNMI,+CMGL,+CMGR,+CMGS,+CMSS,\ +CMGW,+CMGD,+CMGC,+CNMA,+CMMS,+FTS,+FRS,+FTH,+FRH,+FTM,+FRM,+CHUP,+CCFC,+CCUG,+COPS,+CLCK,\ +CPWD,+CUSD,+CAOC,+CACM,+CAMM,+CPUC,+CCWA,+CHLD,+CIMI,+CGMI,+CGMM,+CGMR,+CGSN,+CNUM,+CSIM,\ +CRSM,+CCLK,+CLVL,+CMUT,+CLCC,+COPN,+CPOL,+CPLS,+CTZR,+CTZU,+CLAC,+CLIP,+COLP,+CDIP,+CTFR,\ +CLIR,$QCSIMSTAT,$QCCNMI,$QCCLR,$QCDMG,$QCDMR,$QCDNSP,$QCDNSS,$QCTER,$QCSLOT,$QCPINSTAT,$QCPDPP,\ $QCPDPLT,$QCPWRDN,$QCDGEN,$BREW,$QCSYSMODE,^CVOICE,^DDSETEX,^pcmrecord,^SYSINFO,^SYSCFG,^IMSICHG,\ ^HS,^DTMF,^EARST,^CDUR,^LIGHT,^CPBR,^CPBW,^HWVER,^HVER,^DSFLOWCLR,^DSFLOWQRY,^DSFLOWRPT,^SPN,\ ^PORTSEL,^CPIN,^PNN,^OPL,^CPNN,^SN,^CARDLOCK,^BOOT,^FHVER,^CURC,^FREQLOCK,^HSDPA,^HSUPA,^CARDMODE,\ ^U2DIAG,^CELLMODE,^HSPA,^SCSIOVERTIME,^SETPID,^ADCTEMP,^OPWORD,^CPWORD,^DISLOG,^ANQUERY,^RSCPCFG,^ECIOCFG,\r\n', 'OK\r\n'], 'AT+CPIN?\r': ['+CPIN: READY\r\n', 'OK\r\n']} self.commandsNoPinRequired = ['ATZ\r', 'ATE0\r', 'AT+CFUN?\r', 'AT+CFUN=1\r', 'AT+CMEE=1\r'] self.dtmfCommandBase = '^DTMF={cid},' def getAtdResponse(self, number): return ['OK\r\n'] def getPreCallInitWaitSequence(self): return [0.1] def getCallInitNotification(self, callId, callType): return ['^ORIG:{0},{1}\r\n'.format(callId, callType), 0.2, '^CONF:{0}\r\n'.format(callId)] def getRemoteAnsweredNotification(self, callId, callType): return ['^CONN:{0},{1}\r\n'.format(callId, callType)] def getRemoteHangupNotification(self, callId, callType): return ['^CEND:{0},5,29,16\r\n'.format(callId)] def getIncomingCallNotification(self, callerNumber, callType='VOICE', ton=145): return ['+CRING: {0}\r\n'.format(callType), '+CLIP: "{1}",{2},,,,0\r\n'.format(callType, callerNumber, ton)] def __str__(self): return 'Huawei K3715' class HuaweiE1752(FakeModem): """ Huawei E1752 modem (used by Cell C in South Africa) This modem issues "COMMAND NOT SUPPORT" non-standard error messages """ def __init__(self): super(HuaweiE1752, self).__init__() # This modem uses AT^USSDMODE to control text/PDU mode USSD self._ussdMode = 1 self.responses = {'AT+CGMI\r': ['huawei\r\n', 'OK\r\n'], 'AT+CGMM\r': ['E1752\r\n', 'OK\r\n'], 'AT+CGMR\r': ['11.126.13.00.00\r\n', 'OK\r\n'], 'AT+CIMI\r': ['111111111111111\r\n', 'OK\r\n'], 'AT+CGSN\r': ['111111111111111\r\n', 'OK\r\n'], 'AT+CPMS=?\r': ['+CPMS: ("ME","MT","SM","SR"),("ME","MT","SM","SR"),("ME","MT","SM","SR")\r\n', 'OK\r\n'], # Note the non-standard "COMMAND NOT SUPPORT" error message 'AT+WIND?\r': ['COMMAND NOT SUPPORT\r\n'], 'AT+WIND=50\r': ['COMMAND NOT SUPPORT\r\n'], 'AT+ZPAS?\r': ['COMMAND NOT SUPPORT\r\n'], # Modem has non-standard +CLAC response (does not start with +CLAC:, and extra \r added to each line (i.e. as part of the command name) 'AT+CLAC\r': ['&C\r\r\n', '&D\r\r\n', '&F\r\r\n', '&V\r\r\n', 'E\r\r\n', 'I\r\r\n', 'L\r\r\n', 'M\r\r\n', 'Q\r\r\n', 'V\r\r\n', 'X\r\r\n', 'Z\r\r\n', 'T\r\r\n', 'P\r\r\n', 'D\r\r\n', 'A\r\r\n', 'H\r\r\n', 'O\r\r\n', 'S0\r\r\n', 'S2\r\r\n', 'S3\r\r\n', 'S4\r\r\n', 'S5\r\r\n', 'S6\r\r\n', 'S7\r\r\n', 'S8\r\r\n', 'S9\r\r\n', 'S10\r\r\n', 'S11\r\r\n', 'S30\r\r\n', 'S103\r\r\n', 'S104\r\r\n', '+FCLASS\r\r\n', '+ICF\r\r\n', '+IFC\r\r\n', '+IPR\r\r\n', '+GMI\r\r\n', '+GMM\r\r\n', '+GMR\r\r\n', '+GCAP\r\r\n', '+GSN\r\r\n', '+DR\r\r\n', '+DS\r\r\n', '+WS46\r\r\n', '+CLAC\r\r\n', '+CCLK\r\r\n', '+CBST\r\r\n', '+CRLP\r\r\n', '+CV120\r\r\n', '+CHSN\r\r\n', '+CSSN\r\r\n', '+CREG\r\r\n', '+CGREG\r\r\n', '+CFUN\r\r\n', '+GCAP\r\r\n', '+CSCS\r\r\n', '+CSTA\r\r\n', '+CR\r\r\n', '+CEER\r\r\n', '+CRC\r\r\n', '+CMEE\r\r\n', '+CGDCONT\r\r\n', '+CGDSCONT\r\r\n', '+CGTFT\r\r\n', '+CGEQREQ\r\r\n', '+CGEQMIN\r\r\n', '+CGQREQ\r\r\n', '+CGQMIN\r\r\n', '+CGEQNEG\r\r\n', '+CGEREP\r\r\n', '+CGPADDR\r\r\n', '+CGCLASS\r\r\n', '+CGSMS\r\r\n', '+CSMS\r\r\n', '+CMGF\r\r\n', '+CSAS\r\r\n', '+CRES\r\r\n', '+CSCA\r\r\n', '+CSMP\r\r\n', '+CSDH\r\r\n', '+CSCB\r\r\n', '+FDD\r\r\n', '+FAR\r\r\n', '+FCL\r\r\n', '+FIT\r\r\n', '+ES\r\r\n', '+ESA\r\r\n', '+CMOD\r\r\n', '+CVHU\r\r\n', '+CGDATA\r\r\n', '+CSQ\r\r\n', '+CBC\r\r\n', '+CPAS\r\r\n', '+CPIN\r\r\n', '+CMEC\r\r\n', '+CKPD\r\r\n', '+CIND\r\r\n', '+CMER\r\r\n', '+CGATT\r\r\n', '+CGACT\r\r\n', '+CGCMOD\r\r\n', '+CPBS\r\r\n', '+CPBR\r\r\n', '+CPBF\r\r\n', '+CPBW\r\r\n', '+CPMS\r\r\n', '+CNMI\r\r\n', '+CMGL\r\r\n', '+CMGR\r\r\n', '+CMGS\r\r\n', '+CMSS\r\r\n', '+CMGW\r\r\n', '+CMGD\r\r\n', '+CMGC\r\r\n', '+CNMA\r\r\n', '+CMMS\r\r\n', '+FTS\r\r\n', '+FRS\r\r\n', '+FTH\r\r\n', '+FRH\r\r\n', '+FTM\r\r\n', '+FRM\r\r\n', '+CHUP\r\r\n', '+CCFC\r\r\n', '+CCUG\r\r\n', '+COPS\r\r\n', '+CLCK\r\r\n', '+CPWD\r\r\n', '+CUSD\r\r\n', '+CAOC\r\r\n', '+CACM\r\r\n', '+CAMM\r\r\n', '+CPUC\r\r\n', '+CCWA\r\r\n', '+CHLD\r\r\n', '+CIMI\r\r\n', '+CGMI\r\r\n', '+CGMM\r\r\n', '+CGMR\r\r\n', '+CGSN\r\r\n', '+CNUM\r\r\n', '+CSIM\r\r\n', '+CRSM\r\r\n', '+CCLK\r\r\n', '+CLVL\r\r\n', '+CMUT\r\r\n', '+CLCC\r\r\n', '+COPN\r\r\n', '+CPOL\r\r\n', '+CPLS\r\r\n', '+CTZR\r\r\n', '+CTZU\r\r\n', '+CLAC\r\r\n', '+CLIP\r\r\n', '+COLP\r\r\n', '+CDIP\r\r\n', '+CTFR\r\r\n', '+CLIR\r\r\n', '$QCSIMSTAT\r\r\n', '$QCCNMI\r\r\n', '$QCCLR\r\r\n', '$QCDMG\r\r\n', '$QCDMR\r\r\n', '$QCDNSP\r\r\n', '$QCDNSS\r\r\n', '$QCTER\r\r\n', '$QCSLOT\r\r\n', '$QCPINSTAT\r\r\n', '$QCPDPP\r\r\n', '$QCPDPLT\r\r\n', '$QCPWRDN\r\r\n', '$QCDGEN\r\r\n', '$BREW\r\r\n', '$QCSYSMODE\r\r\n', '$QCCTM\r\r\n', '^RFSWITCH\r\r\n', '^SOFTSWITCH\r\r\n', '^FLIGHTMODESAVE\r\r\n', '^IMSICHG\r\r\n', '^STSF\r\r\n', '^STGI\r\r\n', '^STGR\r\r\n', '^CELLMODE\r\r\n', '^SYSINFO\r\r\n', '^DIALMODE\r\r\n', '^SYSCFG\r\r\n', '^SYSCONFIG\r\r\n', '^HS\r\r\n', '^DTMF\r\r\n', '^CPBR\r\r\n', '^CPBW\r\r\n', '^HWVER\r\r\n', '^HVER\r\r\n', '^DSFLOWCLR\r\r\n', '^DSFLOWQRY\r\r\n', '^DSFLOWRPT\r\r\n', '^SPN\r\r\n', '^PORTSEL\r\r\n', '^CPIN\r\r\n', '^SN\r\r\n', '^EARST\r\r\n', '^CARDLOCK\r\r\n', '^CARDUNLOCK\r\r\n', '^ATRECORD\r\r\n', '^CDUR\r\r\n', '^BOOT\r\r\n', '^FHVER\r\r\n', '^CURC\r\r\n', '^FREQLOCK\r\r\n', '^FREQPREF\r\r\n', '^HSPA\r\r\n', '^HSUPA\r\r\n', '^GPSTYPE\r\r\n', '^HSDPA\r\r\n', '^GLASTERR\r\r\n', '^CARDMODE\r\r\n', '^U2DIAG\r\r\n', '^RSTRIGGER\r\r\n', '^SETPID\r\r\n', '^SCSITIMEOUT\r\r\n', '^CQI\r\r\n', '^GETPORTMODE\r\r\n', '^CVOICE\r\r\n', '^DDSETEX\r\r\n', '^pcmrecord\r\r\n', '^CSNR\r\r\n', '^CMSR\r\r\n', '^CMMT\r\r\n', '^CMGI\r\r\n', '^RDCUST\r\r\n', '^OPWORD\r\r\n', '^CPWORD\r\r\n', '^DISLOG\r\r\n', '^FPLMN\r\r\n', '^FPLMNCTRL\r\r\n', '^ANQUERY\r\r\n', '^RSCPCFG\r\r\n', '^ECIOCFG\r\r\n', '^IMSICHECK\r\r\n', '^USSDMODE\r\r\n', '^SLOTCFG\r\r\n', '^YJCX\r\r\n', '^NDISDUP\r\r\n', '^DHCP\r\r\n', '^AUTHDATA\r\r\n', '^CRPN\r\r\n', '^ICCID\r\r\n', '^NVMBN\r\r\n', '^RXDIV\r\r\n', '^DNSP\r\r\n', '^DNSS\r\r\n', '^WPDST\r\r\n', '^WPDOM\r\r\n', '^WPDFR\r\r\n', '^WPQOS\r\r\n', '^WPDSC\r\r\n', '^WPDGP\r\r\n', '^WPEND\r\r\n', '^WNICT\r\r\n', '^SOCKETCONT\r\r\n', '^WPURL\r\r\n', '^WMOLR\r\r\n', '^SECTIME\r\r\n', '^WPDNP\r\r\n', '^WPDDL\r\r\n', '^WPDCP\r\r\n', 'OK\r\n'], 'AT+CPIN?\r': ['+CPIN: READY\r\n', 'OK\r\n']} self.commandsNoPinRequired = ['ATZ\r', 'ATE0\r', 'AT+CFUN?\r', 'AT+CFUN=1\r', 'AT+CMEE=1\r'] self.dtmfCommandBase = '^DTMF={cid},' def getResponse(self, cmd): if type(cmd) == bytes: cmd = cmd.decode() # Device defaults to ^USSDMODE == 1 if cmd.startswith('AT+CUSD=1') and self._ussdMode == 1: return ['ERROR\r\n'] elif cmd.startswith('AT^USSDMODE='): self._ussdMode = int(cmd[12]) return super(HuaweiE1752, self).getResponse(cmd) else: return super(HuaweiE1752, self).getResponse(cmd) def getAtdResponse(self, number): return ['OK\r\n'] def getPreCallInitWaitSequence(self): return [0.1] def getCallInitNotification(self, callId, callType): return ['^ORIG:{0},{1}\r\n'.format(callId, callType), 0.2, '^CONF:{0}\r\n'.format(callId)] def getRemoteAnsweredNotification(self, callId, callType): return ['^CONN:{0},{1}\r\n'.format(callId, callType)] def getRemoteHangupNotification(self, callId, callType): return ['^CEND:{0},5,29,16\r\n'.format(callId)] def getIncomingCallNotification(self, callerNumber, callType='VOICE', ton=145): return ['+CRING: {0}\r\n'.format(callType), '+CLIP: "{1}",{2},,,,0\r\n'.format(callType, callerNumber, ton)] def __str__(self): return 'Huawei E1752' class QualcommM6280(FakeModem): """ Qualcomm/ZTE modem information provided by davidphiliplee on github """ def __init__(self): super(QualcommM6280, self).__init__() self._callState = 2 self._callNumber = None self._callId = None self.commandsNoPinRequired = [] # This modem requires the CPIN command to be issued first self.commandsSimBusy = ['AT+CSCA?\r'] # Issue #10 on github self.responses = {'AT+CGMI\r': ['QUALCOMM INCORPORATED\r\n', 'OK\r\n'], 'AT+CGMM\r': ['M6280\r\n', 'OK\r\n'], 'AT+CGMR\r': ['M6280_V1.0.0 M6280_V1.0.0 1 [Sep 4 2008 12:00:00]\r\n', 'OK\r\n'], 'AT+CIMI\r': ['111111111111111\r\n', 'OK\r\n'], 'AT+CGSN\r': ['111111111111111\r\n', 'OK\r\n'], 'AT+CLAC=?\r': ['ERROR\r\n'], 'AT+CLAC\r': ['ERROR\r\n'], 'AT+WIND=?\r': ['ERROR\r\n'], 'AT+WIND?\r': ['ERROR\r\n'], 'AT+WIND=50\r': ['ERROR\r\n'], 'AT+ZPAS?\r': ['+BEARTYPE: "UMTS","CS_PS"\r\n', 'OK\r\n'], 'AT+CPMS=?\r': ['+CPMS: ("ME","MT","SM","SR"),("ME","MT","SM","SR"),("ME","MT","SM","SR")\r\n', 'OK\r\n'], 'AT+CVHU=0\r': ['+CVHU: (0-1)\r\n', 'OK\r\n'], 'AT+CPIN?\r': ['+CPIN: READY\r\n', 'OK\r\n']} def getResponse(self, cmd): if type(cmd) == bytes: cmd = cmd.decode() if not self._pinLock: if cmd.startswith('AT+CSMP='): # Clear the SMSC number (this behaviour was reported in issue #8 on github) self.smscNumber = None elif cmd == 'AT+CLCC\r': if self._callNumber: if self._callState == 0: return ['+CLCC: 1,0,2,0,0,"{0}",129\r\n'.format(self._callNumber), 'OK\r\n'] elif self._callState == 1: return ['+CLCC: 1,0,0,0,0,"{0}",129\r\n'.format(self._callNumber), 'OK\r\n'] else: return ['OK\r\n'] return super(QualcommM6280, self).getResponse(cmd) else: return super(QualcommM6280, self).getResponse(cmd) def getAtdResponse(self, number): self._callNumber = number self._callState = 0 return [] def getPreCallInitWaitSequence(self): return [0.1] def getCallInitNotification(self, callId, callType): return [] def getRemoteAnsweredNotification(self, callId, callType): return ['CONNECT\r\n'] def getRemoteHangupNotification(self, callId, callType): self._callState = 2 self._callNumber = None return ['HANGUP: {0}\r\n'.format(callId)] def getIncomingCallNotification(self, callerNumber, callType='VOICE', ton=145): return ['+CRING: {0}\r\n'.format(callType), '+CLIP: "{1}",{2},,,,0\r\n'.format(callType, callerNumber, ton)] def __str__(self): return 'QUALCOMM M6280 (ZTE modem)' class ZteK3565Z(FakeModem): """ ZTE K3565-Z (Vodafone branded) """ def __init__(self): super(ZteK3565Z, self).__init__() self._callState = 2 self._callNumber = None self._callId = None self.commandsNoPinRequired = [] # This modem requires the CPIN command to be issued first self.responses = {'AT+CGMI\r': ['ZTE INCORPORATED\r\n', 'OK\r\n'], 'AT+CGMM\r': ['K3565-Z\r\n', 'OK\r\n'], 'AT+CGMR\r': ['BD_P673A2V1.0.0B09\r\n', 'OK\r\n'], 'AT+CFUN?\r': ['+CFUN: (0-1,4-7),(0-1)\r\n', 'OK\r\n'], 'AT+CIMI\r': ['111111111111111\r\n', 'OK\r\n'], 'AT+CGSN\r': ['111111111111111\r\n', 'OK\r\n'], # Note that AT+CLAC does NOT respond in the standard "+CLAC:" format 'AT+CLAC\r': ['&C\r\n', '&D\r\n', '&E\r\n', '&F\r\n', '&S\r\n', '&V\r\n', '&W\r\n', 'E\r\n', 'I\r\n', 'L\r\n', 'M\r\n', 'Q\r\n', 'V\r\n', 'X\r\n', 'Z\r\n', 'T\r\n', 'P\r\n', '\\Q\r\n', '\\S\r\n', '\\V\r\n', '%V\r\n', 'D\r\n', 'A\r\n', 'H\r\n', 'O\r\n', 'S0\r\n', 'S2\r\n', 'S3\r\n', 'S4\r\n', 'S5\r\n', 'S6\r\n', 'S7\r\n', 'S8\r\n', 'S9\r\n', 'S10\r\n', 'S11\r\n', 'S30\r\n', 'S103\r\n', 'S104\r\n', '+FCLASS\r\n', '+ICF\r\n', '+IFC\r\n', '+IPR\r\n', '+GMI\r\n', '+GMM\r\n', '+GMR\r\n', '+GCAP\r\n', '+GSN\r\n', '+DR\r\n', '+DS\r\n', '+WS46\r\n', '+CBST\r\n', '+CRLP\r\n', '+CV120\r\n', '+CHSN\r\n', '+CSSN\r\n', '+CREG\r\n', '+CGREG\r\n', '+CFUN\r\n', '+GCAP\r\n', '+CSCS\r\n', '+CSTA\r\n', '+CR\r\n', '+CEER\r\n', '+CRC\r\n', '+CMEE\r\n', '+CGDCONT\r\n', '+CGDSCONT\r\n', '+CGTFT\r\n', '+CGEQREQ\r\n', '+CGEQMIN\r\n', '+CGQREQ\r\n', '+CGQMIN\r\n', '+CGEREP\r\n', '+CGPADDR\r\n', '+CGDATA\r\n', '+CGCLASS\r\n', '+CGSMS\r\n', '+CSMS\r\n', '+CMGF\r\n', '+CSAS\r\n', '+CRES\r\n', '+CSCA\r\n', '+CSMP\r\n', '+CSDH\r\n', '+CSCB\r\n', '+FDD\r\n', '+FAR\r\n', '+FCL\r\n', '+FIT\r\n', '+ES\r\n', '+ESA\r\n', '+CMOD\r\n', '+CVHU\r\n', '+CSQ\r\n', '+ZRSSI\r\n', '+CBC\r\n', '+CPAS\r\n', '+CPIN\r\n', '+CMEC\r\n', '+CKPD\r\n', '+CGATT\r\n', '+CGACT\r\n', '+CGCMOD\r\n', '+CPBS\r\n', '+CPBR\r\n', '+ZCPBR\r\n', '+ZUSIM\r\n', '+CPBF\r\n', '+CPBW\r\n', '+ZCPBW\r\n', '+CPMS\r\n', '+CNMI\r\n', '+CMGL\r\n', '+CMGR\r\n', '+CMGS\r\n', '+CMSS\r\n', '+CMGW\r\n', '+CMGD\r\n', '+CMGC\r\n', '+CNMA\r\n', '+CMMS\r\n', '+CHUP\r\n', '+CCFC\r\n', '+CCUG\r\n', '+COPS\r\n', '+CLCK\r\n', '+CPWD\r\n', '+CUSD\r\n', '+CAOC\r\n', '+CACM\r\n', '+CAMM\r\n', '+CPUC\r\n', '+CCWA\r\n', '+CHLD\r\n', '+CIMI\r\n', '+CGMI\r\n', '+CGMM\r\n', '+CGMR\r\n', '+CGSN\r\n', '+CNUM\r\n', '+CSIM\r\n', '+CRSM\r\n', '+CCLK\r\n', '+CLVL\r\n', '+CMUT\r\n', '+CLCC\r\n', '+COPN\r\n', '+CPOL\r\n', '+CPLS\r\n', '+CTZR\r\n', '+CTZU\r\n', '+CLAC\r\n', '+CLIP\r\n', '+COLP\r\n', '+CDIP\r\n', '+CTFR\r\n', '+CLIR\r\n', '$QCSIMSTAT\r\n', '$QCCNMI\r\n', '$QCCLR\r\n', '$QCDMG\r\n', '$QCDMR\r\n', '$QCDNSP\r\n', '$QCDNSS\r\n', '$QCTER\r\n', '$QCSLOT\r\n', '$QCPINSTAT\r\n', '$QCPDPP\r\n', '$QCPDPLT\r\n', '$QCPWRDN\r\n', '$QCDGEN\r\n', '$BREW\r\n', '$QCSYSMODE\r\n', 'OK\r\n'], 'AT+WIND?\r': ['ERROR\r\n'], 'AT+WIND=50\r': ['ERROR\r\n'], 'AT+ZPAS?\r': ['+BEARTYPE: "UMTS","CS_PS"\r\n', 'OK\r\n'], 'AT+CPMS=?\r': ['+CPMS: ("ME","MT","SM","SR"),("ME","MT","SM","SR"),("ME","MT","SM","SR")\r\n', 'OK\r\n'], 'AT+CVHU=0\r': ['+CVHU: (0-1)\r\n', 'OK\r\n'], 'AT+CPIN?\r': ['+CPIN: READY\r\n', 'OK\r\n']} def getResponse(self, cmd): if type(cmd) == bytes: cmd = cmd.decode() if not self._pinLock: if cmd.startswith('AT+CSMP='): # Clear the SMSC number (this behaviour was reported in issue #8 on github) self.smscNumber = None elif cmd == 'AT+CLCC\r': if self._callNumber: if self._callState == 0: return ['+CLCC: 1,0,2,0,0,"{0}",129\r\n'.format(self._callNumber), 'OK\r\n'] elif self._callState == 1: return ['+CLCC: 1,0,0,0,0,"{0}",129\r\n'.format(self._callNumber), 'OK\r\n'] else: return ['OK\r\n'] return super(ZteK3565Z, self).getResponse(cmd) else: return super(ZteK3565Z, self).getResponse(cmd) def getAtdResponse(self, number): self._callNumber = number self._callState = 0 return [] def getPreCallInitWaitSequence(self): return [0.1] def getCallInitNotification(self, callId, callType): return [] def getRemoteAnsweredNotification(self, callId, callType): return ['CONNECT\r\n'] def getRemoteHangupNotification(self, callId, callType): self._callState = 2 self._callNumber = None return ['HANGUP: {0}\r\n'.format(callId)] def getRemoteRejectCallNotification(self, callId, callType): self._callState = 2 self._callNumber = None return ["OK\r\n"] def getIncomingCallNotification(self, callerNumber, callType='VOICE', ton=145): return ['+CRING: {0}\r\n'.format(callType), '+CLIP: "{1}",{2},,,,0\r\n'.format(callType, callerNumber, ton)] def __str__(self): return 'ZTE K3565-Z' class NokiaN79(GenericTestModem): """ Nokia Symbian S60-based modem (details taken from a Nokia N79) and also from issue 15: https://github.com/faucamp/python-gsmmodem/issues/15 (Nokia N95) SMS reading is not supported on these devices via AT commands; thus commands like AT+CNMI are not supported. """ def __init__(self): super(NokiaN79, self).__init__() self.responses = {'AT+CGMI\r': ['Nokia\r\n', 'OK\r\n'], 'AT+CGMM\r': ['Nokia N79\r\n', 'OK\r\n'], 'AT+CGMR\r': ['V ICPR72_08w44.1\r\n', '24-11-08\r\n', 'RM-348\r\n', '(c) Nokia\r\n', '11.049\r\n', 'OK\r\n'], 'AT+CIMI\r': ['111111111111111\r\n', 'OK\r\n'], 'AT+CGSN\r': ['111111111111111\r\n', 'OK\r\n'], 'AT+CNMI=?\r': ['ERROR\r\n'], # SMS reading and notifications not supported 'AT+CNMI=2,1,0,2\r': ['ERROR\r\n'], # SMS reading and notifications not supported 'AT+CLAC=?\r': ['ERROR\r\n'], 'AT+CLAC\r': ['ERROR\r\n'], 'AT+WIND=?\r': ['ERROR\r\n'], 'AT+WIND?\r': ['ERROR\r\n'], 'AT+WIND=50\r': ['ERROR\r\n'], 'AT+ZPAS=?\r': ['ERROR\r\n'], 'AT+ZPAS?\r': ['ERROR\r\n'], 'AT+CPMS="SM","SM","SR"\r': ['ERROR\r\n'], 'AT+CPMS=?\r': ['+CPMS: (),(),()\r\n', 'OK\r\n'], # not supported 'AT+CPMS?\r': ['+CPMS: ,,,,,,,,\r\n', 'OK\r\n'], # not supported 'AT+CPMS=,,\r': ['ERROR\r\n'], 'AT+CPMS="SM","SM"\r': ['ERROR\r\n'], # not supported 'AT+CSMP?\r': ['+CSMP: 49,167,0,0\r\n', 'OK\r\n'], 'AT+GCAP\r': ['+GCAP: +CGSM,+DS,+W\r\n', 'OK\r\n'], 'AT+CNMI=2,1,0,2\r': ['ERROR\r\n'], # not supported 'AT+CVHU=0\r': ['OK\r\n'], 'AT+CPIN?\r': ['+CPIN: READY\r\n', 'OK\r\n']} self.commandsNoPinRequired = ['ATZ\r', 'ATE0\r', 'AT+CFUN?\r', 'AT+CFUN=1\r', 'AT+CMEE=1\r'] def __str__(self): return 'Nokia N79' modemClasses = [HuaweiK3715, HuaweiE1752, WavecomMultiband900E1800, QualcommM6280, ZteK3565Z, NokiaN79] def createModems(): return [modem() for modem in modemClasses] ================================================ FILE: test/test_gsmterm.py ================================================ #!/usr/bin/env python """ Test suite for GsmTerm """ import sys, unittest from . import compat # For Python 2.6 compatibility try: import gsmtermlib.trie except ImportError: # "python -m unittest discover" run from project root sys.path.insert(0, 'tools') import gsmtermlib.trie class TestTrie(unittest.TestCase): """ Tests the trie implementation used by GsmTerm """ def setUp(self): self.trie = gsmtermlib.trie.Trie() self.keyValuePairs = (('abc', 'def'), ('hallo', 'daar'), ('hoe gaan', 'dit met jou'), ('sbzd', '123'), ('abcde', '234627sdg'), ('ab', 'asdk;jgdjsagkl')) def test_storeSingle(self): """ Tests single key/value pair storage """ self.trie['hallo'] = 'daar' self.assertEqual(self.trie['hallo'], 'daar') self.assertEqual(len(self.trie), 1) self.assertRaises(KeyError, self.trie.__getitem__, 'abc') # Set/get None key self.assertRaises(ValueError, self.trie.__setitem__, None, 'someValue') self.assertRaises(ValueError, self.trie.__getitem__, None) # Store zero-length key self.assertRaises(KeyError, self.trie.__getitem__, '') self.trie[''] = '123' self.assertEqual(self.trie[''], '123') self.assertEqual(len(self.trie), 2) def test_deleteSingle(self): """ Tests deleting single key/value pair """ self.trie['hallo'] = 'daar' self.assertEqual(self.trie['hallo'], 'daar') self.assertEqual(len(self.trie), 1) del self.trie['hallo'] self.assertRaises(KeyError, self.trie.__getitem__, 'hallo') self.assertEqual(len(self.trie), 0) # Delete None key self.assertRaises(ValueError, self.trie.__delitem__, None) # Delete unknown key self.assertRaises(KeyError, self.trie.__delitem__, 'unknown key') # Delete zero-length unknown key self.assertRaises(KeyError, self.trie.__delitem__, '') # Delete zero-lenght known key self.trie[''] = '123' self.assertEqual(len(self.trie), 1) del self.trie[''] self.assertEqual(len(self.trie), 0) def test_storeRetrieveMultiple(self): n = 0 for key, value in self.keyValuePairs: n += 1 self.trie[key] = value self.assertEqual(self.trie[key], value) # Make sure nothing was lost for oldKey, oldValue in self.keyValuePairs[:n-1]: self.assertEqual(self.trie[oldKey], oldValue) def test_storeDeleteMultiple(self): self.assertEqual(len(self.trie), 0) for key, value in self.keyValuePairs: self.trie[key] = value self.assertEqual(len(self.trie), len(self.keyValuePairs)) n = len(self.trie) for key, value in self.keyValuePairs: n -= 1 del self.trie[key] self.assertEqual(len(self.trie), n) self.assertEqual(len(self.trie), 0) def test_len(self): n = 0 for key, value in self.keyValuePairs: n += 1 self.trie[key] = value self.assertEqual(len(self.trie), n, 'Incorrect trie length. Expected {0}, got {1}. Last entry: {2}: {3}'.format(n, len(self.trie), key, value)) def test_contains(self): for key, value in self.keyValuePairs: self.assertFalse(key in self.trie) self.trie[key] = value self.assertTrue(key in self.trie) def test_getMethod(self): # Test invalid None key self.assertRaises(ValueError, self.trie.get, None) # Test unknown key self.assertEqual(self.trie.get('abc'), None) # no default self.assertEqual(self.trie.get('abc', default='def'), 'def') # with default self.trie['abc'] = '123' self.assertEqual(self.trie.get('abc'), '123') # no default self.assertEqual(self.trie.get('abc', default='def'), '123') # with default def test_keys(self): """ Test the "keys" method of the trie """ localKeys = [] for key, value in self.keyValuePairs: localKeys.append(key) self.trie[key] = value # The trie has no concept of ordering, so we can't simply compare keys with == trieKeys = self.trie.keys() self.assertEqual(len(trieKeys), len(localKeys)) for key in localKeys: self.assertTrue(key in trieKeys) def test_overWrite(self): # Fill up trie with some values for key, value in self.keyValuePairs: self.trie[key] = value key, oldValue = self.keyValuePairs[0] length = len(self.keyValuePairs) self.assertEqual(self.trie[key], oldValue) self.assertEqual(len(self.trie), length) # Overwrite value newValue = oldValue + '12345' self.assertNotEqual(oldValue, newValue) self.trie[key] = newValue # Read it back self.assertEqual(self.trie[key], newValue) # Check trie length is unchanged self.assertEqual(len(self.trie), length) def test_filteredKeys(self): """ Test the "matching keys" functionality of the trie """ keys = ('a', 'ab', 'abc', 'abcd0000', 'abcd1111', 'abcd2222', 'abcd3333', 'b000', 'b1111', 'zzz123', 'zzzz1234', 'xyz123', 'AT+CSCS') prefixMatches = (('abc', [key for key in keys if key.startswith('abc')]), ('b', [key for key in keys if key.startswith('b')]), ('bc', [key for key in keys if key.startswith('bc')]), ('zzz', [key for key in keys if key.startswith('zzz')]), ('x', [key for key in keys if key.startswith('x')]), ('xy', [key for key in keys if key.startswith('xy')]), ('qwerty', [key for key in keys if key.startswith('qwerty')]), ('AT+CSCS=', [key for key in keys if key.startswith('AT+CSCS=')])) for key in keys: self.trie[key] = 1 for prefix, matchingKeys in prefixMatches: trieKeys = self.trie.keys(prefix) self.assertEqual(len(trieKeys), len(matchingKeys), 'Filtered keys length failed. Prefix: {0}, expected len: {1}, items: {2}, got len {3}, items: {4}'.format(prefix, len(matchingKeys), matchingKeys, len(trieKeys), trieKeys)) for key in matchingKeys: self.assertTrue(key in trieKeys, 'Key not in trie keys: {0}. Trie keys: {1}'.format(key, trieKeys)) def test_longestCommonPrefix(self): """ Test the "get longest common prefix" functionality of the trie """ keys = ('abcDEF', 'abc123', 'abcASFDDSFDSF', 'abc@#$@#$', 'abcDxxx') for key in keys: self.trie[key] = 1 self.assertEqual(self.trie.longestCommonPrefix(), 'abc') self.assertEqual(self.trie.longestCommonPrefix('a'), 'abc') self.assertEqual(self.trie.longestCommonPrefix('ab'), 'abc') self.assertEqual(self.trie.longestCommonPrefix('abc'), 'abc') self.assertEqual(self.trie.longestCommonPrefix('abcD'), 'abcD') self.assertEqual(self.trie.longestCommonPrefix('abcDz'), '') self.assertEqual(self.trie.longestCommonPrefix('abcDE'), 'abcDEF') self.assertEqual(self.trie.longestCommonPrefix('abcDEF'), 'abcDEF') self.assertEqual(self.trie.longestCommonPrefix('abcDEz'), '') keys = ('ATD', 'ATDL') for key in keys: self.trie[key] = 1 self.assertEqual(self.trie.longestCommonPrefix(), '') self.assertEqual(self.trie.longestCommonPrefix('A'), 'ATD') self.assertEqual(self.trie.longestCommonPrefix('AT'), 'ATD') self.assertEqual(self.trie.longestCommonPrefix('ATD'), 'ATD') def test_iter(self): localKeys = [] for key, value in self.keyValuePairs: localKeys.append(key) self.trie[key] = value n = 0 a = iter(self.trie) while True: try: self.assertIn(next(a), localKeys) except StopIteration: break else: n += 1 self.assertEqual(n, len(self.trie)) class TestAtCommands(unittest.TestCase): """ Test suite for the AT Commands data structure """ def test_loadAtCommands(self): """ Check that the AT commands can be loaded correctly, and they are correctly formatted """ from gsmtermlib.atcommands import ATCOMMANDS, CATEGORIES for command, help in ATCOMMANDS: self.assertNotEqual(command, None) self.assertGreater(len(command), 0) self.assertEqual(command.strip(), command, 'Command has leading and/or trailing spaces: {0}'.format(command)) self.assertNotEqual(help, None, 'Command\'s help tuple is None: {0}'.format(command)) self.assertGreaterEqual(len(help), 2) self.assertTrue(help[0] in CATEGORIES) if len(help) > 2: if help[2] != None: self.assertIsInstance(help[2], tuple) self.assertGreater(len(help[2]), 0) for item in help[2]: self.assertEqual(len(item), 2, 'Input value item tuple length should be 2, got {0}. Command: {1}, item: {2}'.format(len(item), command, item)) if help[3] != None: self.assertIsInstance(help[3], tuple) self.assertGreater(len(help[3]), 0) for item in help[3]: self.assertEqual(len(item), 2, 'Output value item tuple length should be 2, got {0}. Command: {1}, item: {2}'.format(len(item), command, item)) self.assertIsInstance(help[4], str) if __name__ == "__main__": unittest.main() ================================================ FILE: test/test_modem.py ================================================ #!/usr/bin/env python # -*- coding: utf8 -*- """ Test suite for gsmmodem.modem """ from __future__ import print_function import sys, time, unittest, logging, codecs from datetime import datetime from copy import copy from . import compat # For Python 2.6 compatibility from gsmmodem.exceptions import PinRequiredError, CommandError, InvalidStateException, TimeoutException,\ CmsError, CmeError, EncodingError from gsmmodem.modem import StatusReport, Sms, ReceivedSms PYTHON_VERSION = sys.version_info[0] import gsmmodem.serial_comms import gsmmodem.modem import gsmmodem.pdu from gsmmodem.util import SimpleOffsetTzInfo from . import fakemodems # Silence logging exceptions logging.raiseExceptions = False if sys.version_info[0] == 3 and sys.version_info[1] >= 1: logging.getLogger('gsmmodem').addHandler(logging.NullHandler()) # The fake modem to use (if any) FAKE_MODEM = None # Write callback to use during Serial.__init__() - usually None, but useful for setting write callbacks during modem.connect() SERIAL_WRITE_CALLBACK_FUNC = None class MockSerialPackage(object): """ Fake serial package for the GsmModem/SerialComms classes to import during tests """ class Serial(): _REPONSE_TIME = 0.02 """ Mock serial object for use by the GsmModem class during tests """ def __init__(self, *args, **kwargs): # The default value to read/"return" if responseSequence isn't set up, or None for nothing #self.defaultResponse = 'OK\r\n' self.responseSequence = [] self.flushResponseSequence = True self.writeQueue = [] self._alive = True self._readQueue = [] global SERIAL_WRITE_CALLBACK_FUNC self.writeCallbackFunc = SERIAL_WRITE_CALLBACK_FUNC global FAKE_MODEM # Pre-determined responses to specific commands - used for imitating specific modems if FAKE_MODEM != None: self.modem = copy(FAKE_MODEM) else: self.modem = fakemodems.GenericTestModem() def read(self, timeout=None): if len(self._readQueue) > 0: return self._readQueue.pop(0) elif len(self.writeQueue) > 0: self._setupReadValue(self.writeQueue.pop(0)) if len(self._readQueue) > 0: return self._readQueue.pop(0) elif self.flushResponseSequence and len(self.responseSequence) > 0: self._setupReadValue(None) if timeout != None: time.sleep(0.001) return '' else: while self._alive: if len(self.writeQueue) > 0: self._setupReadValue(self.writeQueue.pop(0)) if len(self._readQueue) > 0: return self._readQueue.pop(0) time.sleep(0.05) def _setupReadValue(self, command): if len(self._readQueue) == 0: if len(self.responseSequence) > 0: value = self.responseSequence.pop(0) if type(value) in (float, int): time.sleep(value) if len(self.responseSequence) > 0: self._setupReadValue(command) else: self._readQueue = list(value) else: self.responseSequence = self.modem.getResponse(command) if len(self.responseSequence) > 0: self._setupReadValue(command) #elif command in self.modem.responses: # self.responseSequence = self.modem.responses[command] # if len(self.responseSequence) > 0: # self._setupReadValue(command) #elif self.defaultResponse != None: # self._readQueue = list(self.defaultResponse) def write(self, data): if self.writeCallbackFunc != None: if type(data) == bytes: data = data.decode() self.writeCallbackFunc(data) self.writeQueue.append(data) def close(self): pass def inWaiting(self): rqLen = len(self._readQueue) for item in self.responseSequence: if type(item) in (int, float): break else: rqLen += len(item) return rqLen class SerialException(Exception): """ Mock Serial Exception """ class TestGsmModemGeneralApi(unittest.TestCase): """ Tests the API of GsmModem class (excluding connect/close) """ def setUp(self): # Override the pyserial import self.mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = self.mockSerial self.modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --') self.modem.connect() def tearDown(self): self.modem.close() def test_manufacturer(self): def writeCallbackFunc(data): self.assertEqual('AT+CGMI\r', data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CGMI\r', data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc tests = ['huawei', 'ABCDefgh1235', 'Some Random Manufacturer'] for test in tests: self.modem.serial.responseSequence = ['{0}\r\n'.format(test), 'OK\r\n'] self.assertEqual(test, self.modem.manufacturer) def test_model(self): def writeCallbackFunc(data): self.assertEqual('AT+CGMM\r', data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CGMM\r', data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc tests = ['K3715', '1324-Qwerty', 'Some Random Model'] for test in tests: self.modem.serial.responseSequence = ['{0}\r\n'.format(test), 'OK\r\n'] self.assertEqual(test, self.modem.model) def test_revision(self): def writeCallbackFunc(data): self.assertEqual('AT+CGMR\r', data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CGMR\r', data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc tests = ['1', '1324-56768-23414', 'r987'] for test in tests: self.modem.serial.responseSequence = ['{0}\r\n'.format(test), 'OK\r\n'] self.assertEqual(test, self.modem.revision) # Fake a modem that does not support this command self.modem.serial.modem.defaultResponse = ['ERROR\r\n'] self.assertEqual(None, self.modem.revision) def test_imei(self): def writeCallbackFunc(data): self.assertEqual('AT+CGSN\r', data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CGSN\r', data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc tests = ['012345678912345'] for test in tests: self.modem.serial.responseSequence = ['{0}\r\n'.format(test), 'OK\r\n'] self.assertEqual(test, self.modem.imei) def test_imsi(self): def writeCallbackFunc(data): self.assertEqual('AT+CIMI\r', data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CIMI\r', data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc tests = ['987654321012345'] for test in tests: self.modem.serial.responseSequence = ['{0}\r\n'.format(test), 'OK\r\n'] self.assertEqual(test, self.modem.imsi) def test_networkName(self): def writeCallbackFunc(data): self.assertEqual('AT+COPS?\r', data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+COPS', data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc tests = [('MTN', '+COPS: 0,0,"MTN",2'), ('I OMNITEL', '+COPS: 0,0,"I OMNITEL"'), (None, 'SOME RANDOM RESPONSE')] for name, toWrite in tests: self.modem.serial.responseSequence = ['{0}\r\n'.format(toWrite), 'OK\r\n'] self.assertEqual(name, self.modem.networkName) def test_supportedCommands(self): def writeCallbackFunc(data): if data == 'AT\r': # Handle keep-alive AT command return self.assertEqual('AT+CLAC\r', data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CLAC\r', data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc tests = ((['+CLAC:&C,D,E,\S,+CGMM,^DTMF\r\n', 'OK\r\n'], ['&C', 'D', 'E', '\S', '+CGMM', '^DTMF']), (['+CLAC:Z\r\n', 'OK\r\n'], ['Z']), (['FGH,RTY,UIO\r\n', 'OK\r\n'], ['FGH', 'RTY', 'UIO']), # nasty, but possible # ZTE-like response: do not start with +CLAC, and use multiple lines (['A\r\n', 'BCD\r\n', 'EFGH\r\n', 'OK\r\n'], ['A', 'BCD', 'EFGH']), # Teleofis response: like ZTE but each command has AT prefix (['AT&F\r\n', 'AT&V\r\n', 'AT&W\r\n', 'AT+CACM\r\n', 'OK\r\n'], ['&F', '&V', '&W', '+CACM']), # Some Huawei modems have a ZTE-like response, but add an addition \r character at the end of each listed command (['Q\r\r\n', 'QWERTY\r\r\n', '^DTMF\r\r\n', 'OK\r\n'], ['Q', 'QWERTY', '^DTMF'])) for responseSequence, expected in tests: self.modem.serial.responseSequence = responseSequence commands = self.modem.supportedCommands self.assertEqual(commands, expected) # Fake a modem that does not support this command self.modem.serial.responseSequence = ['ERROR\r\n'] commands = self.modem.supportedCommands self.assertEqual(commands, None) # Test unhandled response format self.modem.serial.responseSequence = ['OK\r\n'] commands = self.modem.supportedCommands self.assertEqual(commands, None) def test_smsc(self): """ Tests reading and writing the SMSC number from the SIM card """ def writeCallbackFunc1(data): self.assertEqual('AT+CSCA?\r', data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CSCA?', data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc1 tests = [None, '+12345678'] for test in tests: if test: self.modem.serial.responseSequence = ['+CSCA: "{0}",145\r\n'.format(test), 'OK\r\n'] else: self.modem.serial.responseSequence = ['OK\r\n'] self.assertEqual(test, self.modem.smsc) # Reset SMSC number internally self.modem._smscNumber = None self.assertEqual(self.modem.smsc, None) # Now test setting the SMSC number for test in tests: if not test: continue def writeCallbackFunc2(data): self.assertEqual('AT+CSCA="{0}"\r'.format(test), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CSCA="{0}"'.format(test), data)) def writeCallbackFunc3(data): # This method should not be called - it merely exists to make sure nothing is written to the modem self.fail("Nothing should have been written to modem, but got: {0}".format(data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc2 self.modem.smsc = test self.assertEqual(test, self.modem.smsc) # Now see if the SMSC value was cached properly self.modem.serial.writeCallbackFunc = writeCallbackFunc3 self.assertEqual(test, self.modem.smsc) self.modem.smsc = test # Shouldn't do anything # Check response if modem returns a +CMS ERROR: 330 (SMSC number unknown) on querying the SMSC self.modem._smscNumber = None self.modem.serial.responseSequence = ['+CMS ERROR: 330\r\n'] self.modem.serial.writeCallbackFunc = writeCallbackFunc1 self.assertEqual(self.modem.smsc, None) # Should just return None def test_signalStrength(self): """ Tests reading signal strength from the modem """ def writeCallbackFunc(data): self.assertEqual('AT+CSQ\r', data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CSQ', data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc tests = (('+CSQ: 18,99', 18), ('+CSQ:4,0', 4), ('+CSQ: 99,99', -1)) for response, expected in tests: self.modem.serial.responseSequence = ['{0}\r\n'.format(response), 'OK\r\n'] self.assertEqual(expected, self.modem.signalStrength) # Test error condition (unparseable response) self.modem.serial.responseSequence = ['OK\r\n'] try: self.modem.signalStrength except CommandError: pass else: self.fail('CommandError not raised on error condition') def test_waitForNetorkCoverageNoCreg(self): """ Tests waiting for network coverage (no AT+CREG support) """ tests = ((82,), (99, 99, 47),) for seq in tests: items = iter(seq) def writeCallbackFunc(data): if data == 'AT+CSQ\r': try: self.modem.serial.responseSequence = ['+CSQ: {0},99\r\n'.format(next(items)), 'OK\r\n'] except StopIteration: self.fail("Too many AT+CSQ writes issued") self.modem.serial.writeCallbackFunc = writeCallbackFunc signalStrength = self.modem.waitForNetworkCoverage() self.assertNotEqual(signalStrength, -1, '"Unknown" signal strength returned - should still have blocked') self.assertEqual(seq[-1], signalStrength, 'Incorrect signal strength returned') def test_waitForNetorkCoverage(self): """ Tests waiting for network coverage (normal) """ tests = (('0,2', '0,2', '0,1', 82), ('0,5', 47),) for seq in tests: items = iter(seq) def writeCallbackFunc(data): if data == 'AT+CSQ\r': try: self.modem.serial.responseSequence = ['+CSQ: {0},99\r\n'.format(next(items)), 'OK\r\n'] except StopIteration: self.fail("Too many writes issued") elif data == 'AT+CREG?\r': try: self.modem.serial.responseSequence = ['+CREG: {0}\r\n'.format(next(items)), 'OK\r\n'] except StopIteration: self.fail("Too many writes issued") self.modem.serial.writeCallbackFunc = writeCallbackFunc signalStrength = self.modem.waitForNetworkCoverage() self.assertNotEqual(signalStrength, -1, '"Unknown" signal strength returned - should still have blocked') self.assertEqual(seq[-1], signalStrength, 'Incorrect signal strength returned') # Test InvalidStateException tests = ('0,3', '0,0') # 0,3: network registration denied. 0,0: SIM not searching for network for result in tests: def writeCallbackFunc(data): if data == 'AT+CREG?\r': self.modem.serial.responseSequence = ['+CREG: {0}\r\n'.format(result), 'OK\r\n'] self.modem.serial.writeCallbackFunc = writeCallbackFunc self.assertRaises(InvalidStateException, self.modem.waitForNetworkCoverage) # Test TimeoutException def writeCallbackFunc2(data): self.modem.serial.responseSequence = ['+CREG: 0,1\r\n'.format(result), 'OK\r\n'] self.modem.serial.writeCallbackFunc = writeCallbackFunc2 self.assertRaises(TimeoutException, self.modem.waitForNetworkCoverage, timeout=1) def test_errorTypes(self): """ Tests error type detection- and handling by throwing random errors to commands """ # Throw unnamed error self.modem.serial.responseSequence = ['ERROR\r\n'] try: self.modem.write('AT') except CommandError as e: self.assertIsInstance(e, CommandError) self.assertEqual(e.command, 'AT') self.assertEqual(e.type, None) self.assertEqual(e.code, None) # Throw CME error self.modem.serial.responseSequence = ['+CME ERROR: 22\r\n'] try: self.modem.write('AT+ZZZ') except CommandError as e: self.assertIsInstance(e, CmeError) self.assertEqual(e.command, 'AT+ZZZ') self.assertEqual(e.type, 'CME') self.assertEqual(e.code, 22) # Throw CMS error self.modem.serial.responseSequence = ['+CMS ERROR: 310\r\n'] try: self.modem.write('AT+XYZ') except CommandError as e: self.assertIsInstance(e, CmsError) self.assertEqual(e.command, 'AT+XYZ') self.assertEqual(e.type, 'CMS') self.assertEqual(e.code, 310) def test_smsEncoding(self): def writeCallbackFunc(data): if type(data) == bytes: data = data.decode() self.assertEqual('AT+CSCS?\r', data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CSCS?', data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc tests = [('UNKNOWN', '+CSCSERROR'), ('UCS2', '+CSCS: "UCS2"'), ('ISO', '+CSCS:"ISO"'), ('IRA', '+CSCS: "IRA"'), ('UNKNOWN', '+CSCS: ("GSM", "UCS2")'), ('UNKNOWN', 'OK'),] for name, toWrite in tests: self.modem._smsEncoding = 'UNKNOWN' self.modem.serial.responseSequence = ['{0}\r\n'.format(toWrite), 'OK\r\n'] self.assertEqual(name, self.modem.smsEncoding) def test_smsSupportedEncoding(self): def writeCallbackFunc(data): if type(data) == bytes: data = data.decode() self.assertEqual('AT+CSCS=?\r', data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CSCS?', data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc tests = [(['GSM'], '+CSCS: ("GSM")'), (['GSM', 'UCS2'], '+CSCS: ("GSM", "UCS2")'), (['GSM', 'UCS2'], '+CSCS:("GSM","UCS2")'), (['GSM', 'UCS2'], '+CSCS: ( "GSM" , "UCS2" )'), (['GSM'], '+CSCS: ("GSM" "UCS2")'),] for name, toWrite in tests: self.modem._smsSupportedEncodingNames = None self.modem.serial.responseSequence = ['{0}\r\n'.format(toWrite), 'OK\r\n'] self.assertEqual(name, self.modem.smsSupportedEncoding) class TestUssd(unittest.TestCase): """ Tests USSD session handling """ def setUp(self): #logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG) self.tests = tests = [('*101#', 'AT+CUSD=1,"*101#",15\r', '+CUSD: 0,"Available Balance: R 96.45 .",15\r\n', 'Available Balance: R 96.45 .', False), ('*120*500#', 'AT+CUSD=1,"*120*500#",15\r', '+CUSD: 1,"Hallo daar",15\r\n', 'Hallo daar', True), ('*130*111#', 'AT+CUSD=1,"*130*111#",15\r', '+CUSD: 2,"Totsiens",15\r\n', 'Totsiens', False), ('*111*502#', 'AT+CUSD=1,"*111*502#",15\r', '+CUSD: 2,"You have the following remaining balances:\n0 free minutes\n20 MORE Weekend minutes ",15\r\n', 'You have the following remaining balances:\n0 free minutes\n20 MORE Weekend minutes ', False), ('#100#', 'AT+CUSD=1,"#100#",15\r', '+CUSD: 1,"Bal:$100.00 *\r\nExp 01 Jan 2013\r\n1. Recharge\r\n2. Balance\r\n3. My Offer\r\n4. PlusPacks\r\n5. Tones&Extras\r\n6. History\r\n7. CredMe2U\r\n8. Hlp\r\n00. Home\r\n*charges can take 48hrs",15\r\n', 'Bal:$100.00 *\r\nExp 01 Jan 2013\r\n1. Recharge\r\n2. Balance\r\n3. My Offer\r\n4. PlusPacks\r\n5. Tones&Extras\r\n6. History\r\n7. CredMe2U\r\n8. Hlp\r\n00. Home\r\n*charges can take 48hrs', True)] # Override the pyserial import self.mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = self.mockSerial self.modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --') self.modem.connect() def tearDown(self): self.modem.close() def test_sendUssd(self): """ Standard USSD tests """ # tests tuple format: (USSD_STRING_TO_WRITE, MODEM_WRITE, MODEM_RESPONSE, USSD_MESSAGE, USSD_SESSION_ACTIVE) for test in self.tests: def writeCallbackFunc(data): self.assertEqual(test[1], data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format(test[1], data)) self.modem.serial.responseSequence = ['OK\r\n', test[2]] self.modem.serial.writeCallbackFunc = writeCallbackFunc ussd = self.modem.sendUssd(test[0]) self.assertIsInstance(ussd, gsmmodem.modem.Ussd) self.assertEqual(ussd.sessionActive, test[4], 'Session state is invalid for test case: {0}'.format(test)) self.assertEqual(ussd.message, test[3]) if ussd.sessionActive: def writeCallbackFunc2(data): self.assertEqual('AT+CUSD=2\r', data, 'Invalid data written to modem; expected "AT+CUSD=2", got: "{0}"'.format(data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc2 ussd.cancel() else: ussd.cancel() # This call shouldn't do anything del ussd def test_sendUssd_differentModems(self): """ Tests sendUssd functionality with different modem behaviours (some modems require mode switching) """ tests = [('*101#', 'Testing 123')] global FAKE_MODEM for ussdStr, ussdResponse in tests: for fakeModem in fakemodems.createModems(): fakeModem.responses['AT+CUSD=1,"{0}",15\r'.format(ussdStr)] = ['+CUSD: 2,"{0}",15\r\n'.format(ussdResponse), 'OK\r\n'] # Init modem and preload SMSC number FAKE_MODEM = fakeModem mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = mockSerial modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --') modem.connect() response = modem.sendUssd(ussdStr) self.assertEqual(ussdResponse, response.message) modem.close() FAKE_MODEM = None def test_sendUssdReply(self): """ Test replying in a USSD session via Ussd.reply() """ test = ('First menu. Reply with 1 for blah blah blah...', 'Second menu') self.modem.serial.responseSequence = ['+CUSD: 1,"{0}",15\r\n'.format(test[0]), 'OK\r\n'] ussd = self.modem.sendUssd('*101#') self.assertIsInstance(ussd, gsmmodem.modem.Ussd) self.assertTrue(ussd.sessionActive, 'Session should be active') self.assertEqual(ussd.message, test[0]) # Reply to this active session self.modem.serial.responseSequence = ['+CUSD: 2,"{0}",15\r\n'.format(test[1]), 'OK\r\n'] ussd = ussd.reply('1') self.assertIsInstance(ussd, gsmmodem.modem.Ussd) self.assertFalse(ussd.sessionActive, 'Session should be inactive') self.assertEqual(ussd.message, test[1]) # Reply to inactive session self.assertRaises(gsmmodem.exceptions.InvalidStateException, ussd.reply, '2') def test_sendUssdResponseBeforeOk(self): """ Tests +CUSD responses that arrive before the +CUSD command's OK is issued (non-standard behaviour) - reported by user """ # tests tuple format: (USSD_STRING_TO_WRITE, MODEM_WRITE, MODEM_RESPONSE, USSD_MESSAGE, USSD_SESSION_ACTIVE) for test in self.tests: def writeCallbackFunc(data): self.assertEqual(test[1], data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format(test[1], data)) # Note: The +CUSD response will now be sent before the command is acknowledged self.modem.serial.responseSequence = [test[2], 'OK\r\n'] self.modem.serial.writeCallbackFunc = writeCallbackFunc ussd = self.modem.sendUssd(test[0]) self.assertIsInstance(ussd, gsmmodem.modem.Ussd) self.assertEqual(ussd.sessionActive, test[4], 'Session state is invalid for test case: {0}'.format(test)) self.assertEqual(ussd.message, test[3]) if ussd.sessionActive: def writeCallbackFunc2(data): self.assertEqual('AT+CUSD=2\r', data, 'Invalid data written to modem; expected "AT+CUSD=2", got: "{0}"'.format(data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc2 ussd.cancel() else: ussd.cancel() # This call shouldn't do anything del ussd def test_sendUssdExtraRelease(self): """ Some modems send an extra +CUSD: 2 message when the USSD session is released - see issue #14 on github """ tests = (('*100#', 'Wrong order test message', ['+CUSD: 2,"Initiating Release",15\r\n', '+CUSD: 0,"Wrong order test message",15\r\n', 'OK\r\n']), ('*101#', 'Notifications test', ['OK\r\n', '+CUSD: 2,"Initiating Release",15\r\n', '+CUSD: 0,"Notifications test",15\r\n']), ('*101#', 'Test2', ['OK\r\n', '+CUSD: 3,"Other local client responded",15\r\n', '+CUSD: 0,"Test2",15\r\n'])) for test in tests: self.modem.serial.responseSequence = test[2] ussd = self.modem.sendUssd(test[0]) self.assertIsInstance(ussd, gsmmodem.modem.Ussd) self.assertEqual(ussd.message, test[1], 'Invalid message received; expected "{0}", got "{1}"'.format(test[1], ussd.message)) self.assertEqual(ussd.sessionActive, False, 'Invalid session state - should be inactive') # Make sure the next call does not include any of the USSD extras atResponse = self.modem.write('AT') self.assertEqual(len(atResponse), 1) self.assertEqual(atResponse[0], 'OK') def test_sendUssdError(self): """ Test error handling in a USSD session """ self.modem.serial.responseSequence = ['+CME ERROR: 30\r\n'] self.assertRaises(gsmmodem.exceptions.CmeError, self.modem.sendUssd, '*101#') self.modem.serial.responseSequence = ['+CMS ERROR: 500\r\n'] self.assertRaises(gsmmodem.exceptions.CmsError, self.modem.sendUssd, '*101#') self.modem.serial.responseSequence = ['ERROR\r\n'] self.assertRaises(gsmmodem.exceptions.CommandError, self.modem.sendUssd, '*101#') def test_sendUssdExtraLinesInResponse(self): """ Test parsing USSD response if it contains extra unsolicited notifications """ tests = (('Notification appended', ['OK\r\n', 0.1, '+CUSD: 2,"Notification appended",15\r\n', 'Some random notification!\r\n']), ('Notification prepended', ['OK\r\n', 0.1, 'Another random notification!\r\n', '+CUSD: 2,"Notification prepended",15\r\n']), ('Notification before OK', ['Yet another random notification!\r\n', 'OK\r\n', 0.1, '+CUSD: 2,"Notification before OK",15\r\n'])) for message, responseSeq in tests: self.modem.serial.responseSequence = responseSeq ussd = self.modem.sendUssd('*101#') self.assertIsInstance(ussd, gsmmodem.modem.Ussd) self.assertEqual(ussd.message, message) def test_sendUssd_responseTimeout(self): """ Test sendUssd() response timeout event """ # The following should timeout very quickly due to no +CUSD update being issued self.assertRaises(gsmmodem.exceptions.TimeoutException, self.modem.sendUssd, **{'ussdString': '*101#', 'responseTimeout': 0.05}) class TestEdgeCases(unittest.TestCase): """ Edge-case testing; some modems do funny things during seemingly normal operations """ def test_smscPreloaded(self): """ Tests reading the SMSC number if it was pre-loaded on the SIM (some modems delete the number during connect()) """ tests = [None, '+12345678'] global FAKE_MODEM for test in tests: for fakeModem in fakemodems.createModems(): # Init modem and preload SMSC number fakeModem.smscNumber = test fakeModem.simBusyErrorCounter = 3 # Enable "SIM busy" errors for modem for more accurate testing FAKE_MODEM = fakeModem mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = mockSerial modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --') modem.connect() # Make sure SMSC number was prevented from being deleted (some modems do this when setting text-mode paramters AT+CSMP) self.assertEqual(test, modem.smsc, 'SMSC number was changed/deleted during connect()') modem.close() FAKE_MODEM = None def test_cfun0(self): """ Tests case where a modem's functionality setting is 0 at startup """ global FAKE_MODEM for fakeModem in fakemodems.createModems(): fakeModem.cfun = 0 FAKE_MODEM = fakeModem # This should pass without any problem, and AT+CFUN=1 should be set during connect() cfunWritten = [False] def writeCallbackFunc(data): if data == 'AT+CFUN=1\r': cfunWritten[0] = True global SERIAL_WRITE_CALLBACK_FUNC SERIAL_WRITE_CALLBACK_FUNC = writeCallbackFunc mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = mockSerial modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --') modem.connect() SERIAL_WRITE_CALLBACK_FUNC = None self.assertTrue(cfunWritten[0], 'Modem CFUN setting not set to 1 during connect()') modem.close() FAKE_MODEM = None def test_cfunNotSupported(self): """ Tests case where a modem does not support the AT+CFUN command """ global FAKE_MODEM FAKE_MODEM = copy(fakemodems.GenericTestModem()) FAKE_MODEM.cfun = -1 # disable FAKE_MODEM.responses['AT+CFUN?\r'] = ['ERROR\r\n'] FAKE_MODEM.responses['AT+CFUN=1\r'] = ['ERROR\r\n'] # This should pass without any problem, and AT+CFUN? should at least have been checked during connect() cfunWritten = [False] def writeCallbackFunc(data): if data == 'AT+CFUN?\r': cfunWritten[0] = True global SERIAL_WRITE_CALLBACK_FUNC SERIAL_WRITE_CALLBACK_FUNC = writeCallbackFunc mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = mockSerial modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --') modem.connect() SERIAL_WRITE_CALLBACK_FUNC = None self.assertTrue(cfunWritten[0], 'Modem CFUN setting not set to 1 during connect()') modem.close() FAKE_MODEM = None def test_commandNotSupported(self): """ Some Huawei modems response with "COMMAND NOT SUPPORT" instead of "ERROR" or "OK"; ensure we detect this """ global FAKE_MODEM FAKE_MODEM = copy(fakemodems.GenericTestModem()) FAKE_MODEM.responses['AT+WIND?\r'] = ['COMMAND NOT SUPPORT\r\n'] mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = mockSerial modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --') modem.connect() self.assertRaises(CommandError, modem.write, 'AT+WIND?') modem.close() FAKE_MODEM = None def test_wavecomConnectSpecifics(self): """ Wavecom-specific test cases that might not be covered by the modem profiles in fakemodems.py - this is mostly to attain 100% code coverage in tests """ global FAKE_MODEM FAKE_MODEM = copy(fakemodems.WavecomMultiband900E1800()) # Test the case where AT+CLAC returns a response for Wavecom devices, and it includes +WIND and +VTS FAKE_MODEM.responses['AT+CLAC\r'] = ['+CLAC: D,+CUSD,+WIND,+VTS\r\n', 'OK\r\n'] # Test the case where the +WIND setting is already what we want it to be FAKE_MODEM.responses['AT+WIND?\r'] = ['+WIND: 50\r\n', 'OK\r\n'] mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = mockSerial modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --') modem.connect() self.assertTrue(gsmmodem.modem.Call.dtmfSupport, '+VTS in AT+CLAC response should have indicated DTMF support') modem.close() FAKE_MODEM = None def test_zteConnectSpecifics(self): """ ZTE-specific test cases that might not be covered by the modem profiles in fakemodems.py - this is mostly to attain 100% code coverage in tests """ global FAKE_MODEM FAKE_MODEM = copy(fakemodems.ZteK3565Z()) # Test the case where AT+CLAC returns a response for ZTE devices, and it includes +ZPAS and +VTS FAKE_MODEM.responses['AT+CLAC\r'][-1] = '+ZPAS\r\n' FAKE_MODEM.responses['AT+CLAC\r'].append('OK\r\n') mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = mockSerial modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --') modem.connect() self.assertTrue(gsmmodem.modem.Call.dtmfSupport, '+VTS in AT+CLAC response should have indicated DTMF support') modem.close() FAKE_MODEM = None def test_huaweiConnectSpecifics(self): """ Huawei-specific test cases that might not be covered by the modem profiles in fakemodems.py - this is mostly to attain 100% code coverage in tests """ global FAKE_MODEM FAKE_MODEM = copy(fakemodems.HuaweiK3715()) # Test the case where AT+CLAC returns no response for Huawei devices; causing the need for other methods to detect phone type FAKE_MODEM.responses['AT+CLAC\r'] = ['ERROR\r\n'] mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = mockSerial modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --') modem.connect() # Huawei modems should have DTMF support self.assertTrue(gsmmodem.modem.Call.dtmfSupport, 'Huawei modems should have DTMF support') modem.close() FAKE_MODEM = None def test_smscSpecifiedBeforeConnect(self): """ Tests connect() operation when an SMSC number is set before connect() is called """ smscNumber = '123454321' global FAKE_MODEM FAKE_MODEM = copy(fakemodems.GenericTestModem()) FAKE_MODEM.smsc = None mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = mockSerial modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --') # Look for the AT+CSCA write cscaWritten = [False] def writeCallbackFunc(data): if data == 'AT+CSCA="{0}"\r'.format(smscNumber): cscaWritten[0] = True global SERIAL_WRITE_CALLBACK_FUNC SERIAL_WRITE_CALLBACK_FUNC = writeCallbackFunc # Set the SMSC number before calling connect() modem.smsc = smscNumber self.assertFalse(cscaWritten[0]) modem.connect() self.assertTrue(cscaWritten[0], 'Preset SMSC value not written to modem during connect()') self.assertEqual(modem.smsc, smscNumber, 'Pre-set SMSC not stored correctly during connect()') modem.close() FAKE_MODEM = None def test_cpmsNotSupported(self): """ Tests case where a modem does not support the AT+CPMS command """ global FAKE_MODEM FAKE_MODEM = copy(fakemodems.GenericTestModem()) FAKE_MODEM.responses['AT+CPMS=?\r'] = ['+CMS ERROR: 302\r\n'] # This should pass without any problem, and AT+CPMS=? should at least have been checked during connect() cpmsWritten = [False] def writeCallbackFunc(data): if data == 'AT+CPMS=?\r': cpmsWritten[0] = True global SERIAL_WRITE_CALLBACK_FUNC SERIAL_WRITE_CALLBACK_FUNC = writeCallbackFunc mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = mockSerial modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --') modem.connect() SERIAL_WRITE_CALLBACK_FUNC = None self.assertTrue(cpmsWritten[0], 'Modem CPMS allowed values not checked during connect()') modem.close() FAKE_MODEM = None def test_cnmiNotSupported(self): """ Tests case where a modem does not support the AT+CNMI command (but does support other SMS-related commands) """ global FAKE_MODEM FAKE_MODEM = copy(fakemodems.GenericTestModem()) FAKE_MODEM.responses['AT+CNMI=2,1,0,2\r'] = ['ERROR\r\n'] FAKE_MODEM.responses['AT+CNMI=2,1,0,1,0\r'] = ['ERROR\r\n'] # This should pass without any problem, and AT+CNMI=2,1,0,2 should at least have been attempted during connect() cnmiWritten = [False] def writeCallbackFunc(data): if data == 'AT+CNMI=2,1,0,2\r': cnmiWritten[0] = True global SERIAL_WRITE_CALLBACK_FUNC SERIAL_WRITE_CALLBACK_FUNC = writeCallbackFunc mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = mockSerial modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --') modem.connect() SERIAL_WRITE_CALLBACK_FUNC = None self.assertTrue(cnmiWritten[0], 'AT+CNMI setting not written to modem during connect()') self.assertFalse(modem._smsReadSupported, 'Modem\'s internal SMS read support flag should be False if AT+CNMI is not supported') modem.close() FAKE_MODEM = None def test_clipNotSupported(self): """ Tests case where a modem does not support the AT+CLIP command """ global FAKE_MODEM FAKE_MODEM = copy(fakemodems.GenericTestModem()) FAKE_MODEM.responses['AT+CLIP=1\r'] = ['ERROR\r\n'] # This should pass without any problem, and AT+CLIP=1 should at least have been attempted during connect() clipWritten = [False] crcWritten = [False] def writeCallbackFunc(data): if data == 'AT+CLIP=1\r': clipWritten[0] = True elif data == 'AT+CRC=1\r': crcWritten[0] = True global SERIAL_WRITE_CALLBACK_FUNC SERIAL_WRITE_CALLBACK_FUNC = writeCallbackFunc mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = mockSerial modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --') modem.connect() SERIAL_WRITE_CALLBACK_FUNC = None self.assertTrue(clipWritten[0], 'AT+CLIP=1 not written to modem during connect()') self.assertFalse(crcWritten[0], 'AT+CRC=1 should not be attempted if AT+CLIP is not supported') self.assertFalse(modem._callingLineIdentification, 'Modem\'s internal calling line identification flag should be False if AT+CLIP is not supported') self.assertFalse(modem._extendedIncomingCallIndication, 'Modem\'s internal extended calling line identification information flag should be False if AT+CLIP is not supported') modem.close() FAKE_MODEM = None def test_crcNotSupported(self): """ Tests case where a modem does not support the AT+CRC command """ global FAKE_MODEM FAKE_MODEM = copy(fakemodems.GenericTestModem()) FAKE_MODEM.responses['AT+CRC=1\r'] = ['ERROR\r\n'] # This should pass without any problem, and AT+CRC=1 should at least have been attempted during connect() clipWritten = [False] crcWritten = [False] def writeCallbackFunc(data): if data == 'AT+CLIP=1\r': clipWritten[0] = True elif data == 'AT+CRC=1\r': crcWritten[0] = True global SERIAL_WRITE_CALLBACK_FUNC SERIAL_WRITE_CALLBACK_FUNC = writeCallbackFunc mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = mockSerial modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --') modem.connect() SERIAL_WRITE_CALLBACK_FUNC = None self.assertTrue(clipWritten[0], 'AT+CLIP=1 not written to modem during connect()') self.assertTrue(crcWritten[0], 'AT+CRC=1 not written to modem during connect()') self.assertTrue(modem._callingLineIdentification, 'Modem\'s internal calling line identification flag should be True if AT+CLIP is supported') self.assertFalse(modem._extendedIncomingCallIndication, 'Modem\'s internal extended calling line identification information flag should be False if AT+CRC is not supported') modem.close() FAKE_MODEM = None class TestGsmModemDial(unittest.TestCase): def tearDown(self): self.modem.close() global FAKE_MODEM FAKE_MODEM = None def init_modem(self, modem): global FAKE_MODEM FAKE_MODEM = modem self.mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = self.mockSerial self.modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --') self.modem.connect() def test_dial(self): """ Tests dialing without specifying a callback function """ tests = (['0123456789', '1', '0'],) global MODEMS testModems = fakemodems.createModems() testModems.append(fakemodems.GenericTestModem()) # Test polling only for fakeModem in testModems: self.init_modem(fakeModem) modem = self.modem.serial.modem # load the copy()-ed modem instance for number, callId, callType in tests: def writeCallbackFunc(data): if self.modem._mustPollCallStatus and data.startswith('AT+CLCC'): return # Can happen due to polling self.assertEqual('ATD{0};\r'.format(number), data, 'Invalid data written to modem; expected "{0}", got: "{1}". Modem: {2}'.format('ATD{0};'.format(number), data[:-1] if data[-1] == '\r' else data, modem)) self.modem.serial.writeCallbackFunc = None self.modem.serial.writeCallbackFunc = writeCallbackFunc self.modem.serial.responseSequence = modem.getAtdResponse(number) self.modem.serial.responseSequence.extend(modem.getPreCallInitWaitSequence()) # Fake call initiated notification self.modem.serial.responseSequence.extend(modem.getCallInitNotification(callId, callType)) call = self.modem.dial(number) # Wait for the read buffer to clear while len(self.modem.serial._readQueue) > 0 or len(self.modem.serial.responseSequence) > 0: time.sleep(0.05) if self.modem._mustPollCallStatus: time.sleep(0.6) self.assertIsInstance(call, gsmmodem.modem.Call) self.assertIs(call.number, number) # Check status self.assertTrue(call.active, 'Call state invalid: should be active. Modem: {0}'.format(modem)) self.assertFalse(call.answered, 'Call state invalid: should not yet be answered. Modem: {0}'.format(modem)) self.assertIn(call.id, self.modem.activeCalls) self.assertEqual(len(self.modem.activeCalls), 1) # Fake an answer self.modem.serial.responseSequence = modem.getRemoteAnsweredNotification(callId, callType) # Wait a bit for the event to be picked up while len(self.modem.serial._readQueue) > 0 or len(self.modem.serial.responseSequence) > 0: time.sleep(0.05) if self.modem._mustPollCallStatus: time.sleep(0.6) # Ensure polling picks up event elif not self.modem._waitForCallInitUpdate: time.sleep(0.1) # Ensure event is picked up self.assertTrue(call.answered, 'Remote call answer was not detected. Modem: {0}'.format(modem)) self.assertTrue(call.active, 'Call state invalid: should be active. Modem: {0}'.format(modem)) def hangupCallback(data): if self.modem._mustPollCallStatus and data.startswith('AT+CLCC'): return # Can happen due to polling self.assertEqual('ATH\r'.format(number), data, 'Invalid data written to modem; expected "{0}", got: "{1}". Modem: {2}'.format('ATH'.format(number), data[:-1] if data[-1] == '\r' else data, modem)) self.modem.serial.writeCallbackFunc = hangupCallback call.hangup() self.assertFalse(call.answered, 'Hangup call did not change answered state. Modem: {0}'.format(modem)) self.assertFalse(call.active, 'Call state invalid: should not be active (local hangup). Modem: {0}'.format(modem)) self.assertNotIn(call.id, self.modem.activeCalls) self.assertEqual(len(self.modem.activeCalls), 0) ############## Check remote hangup detection ############### self.modem.serial.writeCallbackFunc = writeCallbackFunc self.modem.serial.responseSequence = modem.getAtdResponse(number) self.modem.serial.responseSequence.extend(modem.getPreCallInitWaitSequence()) # Fake call initiated notification self.modem.serial.responseSequence.extend(modem.getCallInitNotification(callId, callType)) call = self.modem.dial(number) self.assertTrue(call.active, 'Call state invalid: should be active. Modem: {0}'.format(modem)) # Wait a bit for the event to be picked up while len(self.modem.serial._readQueue) > 0 or len(self.modem.serial.responseSequence) > 0: time.sleep(0.05) if self.modem._mustPollCallStatus: time.sleep(0.6) # Ensure polling picks up event # Fake remote answer self.modem.serial.responseSequence = modem.getRemoteAnsweredNotification(callId, callType) while len(self.modem.serial._readQueue) > 0 or len(self.modem.serial.responseSequence) > 0: time.sleep(0.05) if self.modem._mustPollCallStatus: time.sleep(0.5) # Ensure polling picks up event elif not self.modem._waitForCallInitUpdate: time.sleep(0.1) # Ensure event is picked up self.assertTrue(call.answered, 'Remote call answer was not detected. Modem: {0}'.format(modem)) self.assertIn(call.id, self.modem.activeCalls) self.assertEqual(len(self.modem.activeCalls), 1) # Now fake a remote hangup self.modem.serial.responseSequence = modem.getRemoteHangupNotification(callId, callType) # Wait a bit for the event to be picked up while len(self.modem.serial._readQueue) > 0 or len(self.modem.serial.responseSequence) > 0: time.sleep(0.05) if self.modem._mustPollCallStatus: time.sleep(0.6) # Ensure polling picks up event self.assertFalse(call.answered, 'Remote hangup was not detected. Modem: {0}'.format(modem)) self.assertFalse(call.active, 'Call state invalid: should not be active (remote hangup). Modem: {0}'.format(modem)) self.assertNotIn(call.id, self.modem.activeCalls) self.assertEqual(len(self.modem.activeCalls), 0) ############## Check remote call rejection (hangup before answering) ############### self.modem.serial.writeCallbackFunc = writeCallbackFunc self.modem.serial.responseSequence = modem.getAtdResponse(number) self.modem.serial.responseSequence.extend(modem.getPreCallInitWaitSequence()) # Fake call initiated notification self.modem.serial.responseSequence.extend(modem.getCallInitNotification(callId, callType)) call = self.modem.dial(number) self.assertTrue(call.active, 'Call state invalid: should be active. Modem: {0}'.format(modem)) # Wait a bit for the event to be picked up while len(self.modem.serial._readQueue) > 0 or len(self.modem.serial.responseSequence) > 0: time.sleep(0.05) if self.modem._mustPollCallStatus: time.sleep(0.6) # Ensure polling picks up event self.assertFalse(call.answered, 'Call should not have been in "answered" state. Modem: {0}'.format(modem)) self.assertIn(call.id, self.modem.activeCalls) self.assertEqual(len(self.modem.activeCalls), 1) # Now reject the call self.modem.serial.responseSequence = modem.getRemoteRejectCallNotification(callId, callType) # Wait a bit for the event to be picked up while len(self.modem.serial._readQueue) > 0 or len(self.modem.serial.responseSequence) > 0: time.sleep(0.05) if self.modem._mustPollCallStatus: time.sleep(0.6) # Ensure polling picks up event time.sleep(0.05) self.assertFalse(call.answered, 'Call state invalid: should not be answered (remote call rejection). Modem: {0}'.format(modem)) self.assertFalse(call.active, 'Call state invalid: should not be active (remote rejection). Modem: {0}'.format(modem)) self.assertNotIn(call.id, self.modem.activeCalls) self.assertEqual(len(self.modem.activeCalls), 0) self.modem.close() def test_dialCallback(self): """ Tests the dial method's callback mechanism """ tests = (['12345678', '1', '0'],) global MODEMS testModems = fakemodems.createModems() testModems.append(fakemodems.GenericTestModem()) # Test polling only for fakeModem in testModems: self.init_modem(fakeModem) modem = self.modem.serial.modem # load the copy()-ed modem instance for number, callId, callType in tests: callbackVars = [None, False, 0] def callUpdateCallbackFunc1(call): self.assertIsInstance(call, gsmmodem.modem.Call) self.assertEqual(call, callbackVars[0]) # Check call status if callbackVars[2] == 0: # Expected "answer" event self.assertTrue(call.active, 'Call state invalid: should be active. Modem: {0}'.format(modem)) self.assertTrue(call.answered, 'Call state invalid: should have been answered. Modem: {0}'.format(modem)) elif callbackVars[2] == 1: # Expected "hangup" event self.assertFalse(call.answered, 'Call state invalid: "answered" should be false after hangup. Modem: {0}'.format(modem)) self.assertFalse(call.active, 'Call state invalid: should be inactive. Modem: {0}'.format(modem)) callbackVars[1] = True # set "callback called" flag call = self.modem.dial(number, callStatusUpdateCallbackFunc=callUpdateCallbackFunc1) self.assertIsInstance(call, gsmmodem.modem.Call) callbackVars[0] = call self.assertTrue(call.active, 'Call state invalid: should be active. Modem: {0}'.format(modem)) self.assertFalse(call.answered, 'Call state invalid: should not yet be answered. Modem: {0}'.format(modem)) # Fake an answer... self.modem.serial.responseSequence = modem.getRemoteAnsweredNotification(callId, callType) # ...and wait for the callback to be called while not callbackVars[1]: time.sleep(0.05) # Double check local call variable self.assertTrue(call.active, 'Call state invalid: should be active. Modem: {0}'.format(modem)) self.assertTrue(call.answered, 'Call state invalid: should have been answered. Modem: {0}'.format(modem)) # Fake remote hangup... callbackVars[1] = False callbackVars[2] = 1 self.modem.serial.responseSequence = modem.getRemoteAnsweredNotification(callId, callType) # ...and wait for the callback to be called while not callbackVars[1]: time.sleep(0.05) # Double check local call variable self.assertFalse(call.answered, 'Call state invalid: "answered" should be false after hangup. Modem: {0}'.format(modem)) self.assertFalse(call.active, 'Call state invalid: should be inactive. Modem: {0}'.format(modem)) self.modem.close() def test_dialError(self): """ Test error handling when dialing """ self.init_modem(fakemodems.HuaweiK3715()) # Use a modem that supports call update notifications self.modem.serial.responseSequence = ['+CME ERROR: 30\r\n'] self.assertRaises(gsmmodem.exceptions.CmeError, self.modem.dial, '123') self.modem.serial.responseSequence = ['+CMS ERROR: 500\r\n'] self.assertRaises(gsmmodem.exceptions.CmsError, self.modem.dial, '123') self.modem.serial.responseSequence = ['ERROR\r\n'] self.assertRaises(gsmmodem.exceptions.CommandError, self.modem.dial, '123') def test_dial_callInitEventTimeout(self): """ Test dial() timeout event: call initiated event never occurs """ self.init_modem(fakemodems.HuaweiK3715()) # Use a modem that supports call update notifications # The following should timeout very quickly - ATD does not timeout, but no call is established self.assertRaises(gsmmodem.exceptions.TimeoutException, self.modem.dial, **{'number': '123', 'timeout': 0.05}) def test_dial_atdTimeout(self): """ Test dial() timeout event: ATD command timeout """ self.init_modem(fakemodems.GenericTestModem()) # Disable ATD response self.modem.serial.modem.responses['ATD123;\r'] = [] # The following should timeout very quickly - no ATD command response received self.assertRaises(gsmmodem.exceptions.TimeoutException, self.modem.dial, **{'number': '123', 'timeout': 0.05}) class TestGsmModemPinConnect(unittest.TestCase): """ Tests PIN unlocking and connect() method of GsmModem class (excluding connect/close) """ def tearDown(self): global FAKE_MODEM FAKE_MODEM = None def init_modem(self, modem): global FAKE_MODEM FAKE_MODEM = modem self.mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = self.mockSerial self.modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --') def test_connectPinLockedNoPin(self): """ Test connecting to the modem with a SIM PIN code - no PIN specified""" testModems = fakemodems.createModems() for modem in testModems: modem.pinLock = True self.init_modem(modem) self.assertRaises(PinRequiredError, self.modem.connect) self.modem.close() def test_connectPinLockedWithPin(self): """ Test connecting to the modem with a SIM PIN code - PIN specified""" testModems = fakemodems.createModems() # Also test a modem that allows only CMEE commands before PIN is entered edgeCaseModem = fakemodems.GenericTestModem() edgeCaseModem.commandsNoPinRequired = ['AT+CMEE=1\r'] testModems.append(edgeCaseModem) for modem in testModems: modem.pinLock = True self.init_modem(modem) # This should succeed try: self.modem.connect(pin='1234') except PinRequiredError: self.fail("Pin required exception thrown for modem {0}".format(modem)) finally: self.modem.close() def test_connectPin_incorrect(self): """ Test connecting to the modem with a SIM PIN code - incorrect PIN specified """ def writeCallbackFunc(data): if data.startswith('AT+CPIN="'): # Fake "incorrect PIN" response self.modem.serial.responseSequence = ['+CME ERROR: 16\r\n'] global SERIAL_WRITE_CALLBACK_FUNC SERIAL_WRITE_CALLBACK_FUNC = writeCallbackFunc fakeModem = fakemodems.GenericTestModem() fakeModem.pinLock = True self.init_modem(fakeModem) self.assertRaises(gsmmodem.exceptions.IncorrectPinError, self.modem.connect, **{'pin': '1234'}) self.modem.close() SERIAL_WRITE_CALLBACK_FUNC = None def test_connectPin_pukRequired(self): """ Test connecting to the modem with a SIM PIN code - SIM locked; PUK required """ def writeCallbackFunc(data): if data.startswith('AT+CPIN="'): # Fake "PUK required" response self.modem.serial.responseSequence = ['+CME ERROR: 12\r\n'] global SERIAL_WRITE_CALLBACK_FUNC SERIAL_WRITE_CALLBACK_FUNC = writeCallbackFunc fakeModem = fakemodems.GenericTestModem() fakeModem.pinLock = True self.init_modem(fakeModem) self.assertRaises(gsmmodem.exceptions.PukRequiredError, self.modem.connect, **{'pin': '1234'}) self.modem.close() SERIAL_WRITE_CALLBACK_FUNC = None def test_connectPin_timeoutEvents(self): """ Test different TimeoutException scenarios when checking PIN status (github issue #19) """ tests = (([0.05], True), (['+CPIN: READY\r\n'], False), (['FIRST LINE\r\n', 'SECOND LINE\r\n'], True)) for response, shouldTimeout in tests: def writeCallbackFunc(data): if data.startswith('AT+CPIN?'): # Fake "incorrect PIN" response self.modem.serial.responseSequence = response global SERIAL_WRITE_CALLBACK_FUNC SERIAL_WRITE_CALLBACK_FUNC = writeCallbackFunc fakeModem = fakemodems.GenericTestModem() fakeModem.pinLock = False self.init_modem(fakeModem) if shouldTimeout: self.assertRaises(gsmmodem.exceptions.TimeoutException, self.modem.connect) else: self.modem.connect() # should run fine self.modem.close() SERIAL_WRITE_CALLBACK_FUNC = None class TestIncomingCall(unittest.TestCase): def tearDown(self): global FAKE_MODEM FAKE_MODEM = None self.modem.close() def init_modem(self, modem, incomingCallCallbackFunc): global FAKE_MODEM FAKE_MODEM = modem self.mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = self.mockSerial self.modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --', incomingCallCallbackFunc=incomingCallCallbackFunc) self.modem.connect() def test_incomingCallAnswer(self): for modem in fakemodems.createModems(): callReceived = [False, 'VOICE', ''] def incomingCallCallbackFunc(call): try: self.assertIsInstance(call, gsmmodem.modem.IncomingCall) self.assertIn(call.id, self.modem.activeCalls) self.assertEqual(len(self.modem.activeCalls), 1) self.assertEqual(call.number, callReceived[2], 'Caller ID (caller number) incorrect. Expected: "{0}", got: "{1}". Modem: {2}'.format(callReceived[2], call.number, modem)) self.assertFalse(call.answered, 'Call state invalid: should not yet be answered. Modem: {0}'.format(modem)) self.assertIsInstance(call.type, int) self.assertEqual(call.type, callReceived[1], 'Invalid call type; expected "{0}", got "{1}". Modem: {2}'.format(callReceived[1], call.type, modem)) def writeCallbackFunc1(data): self.assertEqual('ATA\r', data, 'Invalid data written to modem; expected "{0}", got: "{1}". Modem: {2}'.format('ATA\r', data, modem)) self.modem.serial.writeCallbackFunc = writeCallbackFunc1 call.answer() self.assertTrue(call.answered, 'Call state invalid: should be answered. Modem: {0}'.format(modem)) # Call answer() again - shouldn't do anything def writeCallbackShouldNotBeCalled(data): self.fail('Nothing should have been written to modem, but got: {0}'.format(data)) self.modem.serial.writeCallbackFunc = writeCallbackShouldNotBeCalled call.answer() # Hang up def writeCallbackFunc2(data): self.assertEqual('ATH\r', data, 'Invalid data written to modem; expected "{0}", got: "{1}". Modem: {2}'.format('ATH\r', data, modem)) self.modem.serial.writeCallbackFunc = writeCallbackFunc2 call.hangup() self.assertFalse(call.answered, 'Call state invalid: hangup did not change call state. Modem: {0}'.format(modem)) self.assertNotIn(call.id, self.modem.activeCalls) self.assertEqual(len(self.modem.activeCalls), 0) # Call hangup() again - shouldn't do anything self.modem.serial.writeCallbackFunc = writeCallbackShouldNotBeCalled call.hangup() finally: callReceived[0] = True self.init_modem(modem, incomingCallCallbackFunc) tests = (('+27820001234', 'VOICE', 0),) for number, cringParam, callType in tests: callReceived[0] = False callReceived[1] = callType callReceived[2] = number # Fake incoming voice call self.modem.serial.responseSequence = modem.getIncomingCallNotification(number, cringParam) # Wait for the handler function to finish while callReceived[0] == False: time.sleep(0.05) self.modem.close() def test_incomingCallCrcNotSupported(self): """ Tests handling incoming calls without +CRC support """ callReceived = [False] def callbackFunc(call): self.assertIsInstance(call, gsmmodem.modem.IncomingCall) self.assertEqual(call.type, None, 'Invalid call type; expected "{0}", got "{1}".'.format(None, call.type)) callReceived[0] = True testModem = copy(fakemodems.GenericTestModem()) testModem.responses['AT+CRC?\r'] = ['ERROR\r\n'] testModem.responses['AT+CRC=1\r'] = ['ERROR\r\n'] self.init_modem(testModem, incomingCallCallbackFunc=callbackFunc) # Ensure extended incoming call indications are active self.assertFalse(self.modem._extendedIncomingCallIndication, 'Extended incoming call indicator flag should be False') # Fake incoming voice call using basic incoming call indication format self.modem.serial.responseSequence = ['RING\r\n', '+CLIP: "+27821231234",145,,,,0\r\n'] # Wait for the handler function to finish while callReceived[0] == False: time.sleep(0.1) self.assertFalse(self.modem._extendedIncomingCallIndication, 'Extended incoming call indicator flag should be False') def test_incomingCallCrcChangedExternally(self): """ Tests handling incoming call notifications when the +CRC setting \ was modfied by some external program (issue #18) """ callReceived = [False] def callbackFunc(call): self.assertIsInstance(call, gsmmodem.modem.IncomingCall) callReceived[0] = True self.init_modem(None, incomingCallCallbackFunc=callbackFunc) # Ensure extended incoming call indications are active self.assertTrue(self.modem._extendedIncomingCallIndication, 'Extended incoming call indicator flag should be True') # Fake incoming voice call using extended incoming call indication format self.modem.serial.responseSequence = ['+CRING: VOICE\r\n', '+CLIP: "+27821231234",145,,,,0\r\n'] # Wait for the handler function to finish while callReceived[0] == False: time.sleep(0.1) callReceived[0] = False # Now fake incoming call using basic incoming call indication format (without informing GsmModem class about change) self.modem.serial.responseSequence = ['RING\r\n', '+CLIP: "+27821231234",145,,,,0\r\n'] # Wait for the handler function to finish while callReceived[0] == False: time.sleep(0.05) # Ensure extended incoming call indications have been re-enabled self.assertTrue(self.modem._extendedIncomingCallIndication, 'Extended incoming call indicator flag should be True') # Now repeat the test, but cause re-enabling the +CRC setting to fail self.modem.serial.modem.responses['AT+CRC=1\r'] = ['ERROR\r\n'] callReceived[0] = False # Basic incoming call indication format (without informing GsmModem class about change) self.modem.serial.responseSequence = ['RING\r\n', '+CLIP: "+27821231234",145,,,,0\r\n'] # Wait for the handler function to finish while callReceived[0] == False: time.sleep(0.05) # Since re-enabling the extended format failed, extended incoming call indications flag should be False self.assertFalse(self.modem._extendedIncomingCallIndication, 'Extended incoming call indicator flag should be False because AT+CRC=1 failed') class TestCall(unittest.TestCase): """ Tests Call object APIs that are not covered by TestIncomingCall and TestGsmModemDial """ def init_modem(self, modem): global FAKE_MODEM FAKE_MODEM = modem gsmmodem.serial_comms.serial = MockSerialPackage() self.modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --') self.modem.connect() FAKE_MODEM = None def testDtmf(self): """ Tests sending DTMF tones in a phone call """ originalBaseDtmfCommand = gsmmodem.modem.Call.DTMF_COMMAND_BASE for fakeModem in fakemodems.createModems(): gsmmodem.modem.Call.DTMF_COMMAND_BASE = originalBaseDtmfCommand self.init_modem(fakeModem) # Make sure everything is set up correctly during connect() self.assertEqual(gsmmodem.modem.Call.DTMF_COMMAND_BASE, fakeModem.dtmfCommandBase, 'Invalid base DTMF command for modem: {0}; expected "{1}", got "{2}"'.format(fakeModem, fakeModem.dtmfCommandBase, gsmmodem.modem.Call.DTMF_COMMAND_BASE)) # Test sending DTMF tones in a call call = gsmmodem.modem.Call(self.modem, 1, 1, '+270000000') call.answered = True tests = (('3', 'AT{0}3\r'.format(fakeModem.dtmfCommandBase.format(cid=call.id))), ('1234', 'AT{0}1;{0}2;{0}3;{0}4\r'.format(fakeModem.dtmfCommandBase.format(cid=call.id))), ('#0*', 'AT{0}#;{0}0;{0}*\r'.format(fakeModem.dtmfCommandBase.format(cid=call.id)))) for tones, expectedCommand in tests: def writeCallbackFunc(data): expectedCommand = 'AT{0}{1}\r'.format(fakeModem.dtmfCommandBase.format(cid=call.id), tones[self.currentTone]) self.currentTone += 1; self.assertEqual(expectedCommand, data, 'Invalid data written to modem for tones: "{0}"; expected "{1}", got: "{2}". Modem: {3}'.format(tones, expectedCommand[:-1].format(cid=self.id), data[:-1] if data[-1] == '\r' else data, fakeModem)) self.modem.serial.writeCallbackFunc = writeCallbackFunc self.currentTone = 0; # Now attempt to send DTMF tones in an inactive call self.modem.serial.writeCallbackFunc = None call.hangup() self.assertRaises(gsmmodem.exceptions.InvalidStateException, call.sendDtmfTone, '1') self.modem.close() gsmmodem.modem.Call.DTMF_COMMAND_BASE = originalBaseDtmfCommand def testDtmfInterrupted(self): """ Tests interrupting the playback of DTMF tones """ self.init_modem(fakemodems.GenericTestModem()) call = gsmmodem.modem.Call(self.modem, 1, 1, '+270000000') call.answered = True # Fake an interruption - no network service self.modem.serial.responseSequence = [0.1, '+CME ERROR: 30\r\n'] self.assertRaises(gsmmodem.exceptions.InterruptedException, call.sendDtmfTone, '5') # Fake an interruption - operation not allowed self.modem.serial.responseSequence = [0.1, '+CME ERROR: 3\r\n'] self.assertRaises(gsmmodem.exceptions.InterruptedException, call.sendDtmfTone, '5') # Fake some other CME error self.modem.serial.responseSequence = [0.1, '+CME ERROR: 1234\r\n'] self.assertRaises(gsmmodem.exceptions.CmeError, call.sendDtmfTone, '5') self.modem.close() def testCallAnsweredCallback(self): """ Tests Call object's "call answered" callback mechanism """ self.init_modem(fakemodems.GenericTestModem()) callbackCalled = [False] def callbackFunc(callObj): self.assertEqual(callObj, call) callbackCalled[0] = True call = gsmmodem.modem.Call(self.modem, 1, 1, '+270000000', callStatusUpdateCallbackFunc=callbackFunc) # Answer the call "remotely" - this should trigger the callback call.answered = True self.assertTrue(callbackCalled[0], "Call status update callback not called for answer event") self.modem.close() class TestSms(unittest.TestCase): """ Tests the SMS API of GsmModem class """ def setUp(self): self.tests = (('+0123456789', 'Hello world!', 1, datetime(2013, 3, 8, 15, 2, 16, tzinfo=SimpleOffsetTzInfo(2)), '+2782913593', '06917228195339040A9110325476980000313080512061800CC8329BFD06DDDF72363904', 29, 142, 'SM'), ('+9876543210', 'Hallo\nhoe gaan dit?', 4, datetime(2013, 3, 8, 15, 2, 16, tzinfo=SimpleOffsetTzInfo(2)), '+2782913593', '06917228195339040A91896745230100003130805120618013C8309BFD56A0DF65D0391C7683C869FA0F', 35, 33, 'SM'), ('+353870000000', 'My message', 13, datetime(2013, 4, 20, 20, 22, 27, tzinfo=SimpleOffsetTzInfo(4)), None, None, 0, 0, 'ME'), ) # address_text data to use for tests when testing PDU mode self.testsPduAddressText = ('', '"abc123"', '""', 'Test User 123', '9876543231') def initModem(self, smsReceivedCallbackFunc): # Override the pyserial import self.mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = self.mockSerial self.modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --', smsReceivedCallbackFunc=smsReceivedCallbackFunc) self.modem.connect() def test_sendSmsLeaveTextModeOnInvalidCharacter(self): """ Tests sending SMS messages in text mode """ self.initModem(None) self.modem.smsTextMode = True # Set modem to text mode self.assertTrue(self.modem.smsTextMode) # PDUs checked on https://www.diafaan.com/sms-tutorials/gsm-modem-tutorial/online-sms-pdu-decoder/ tests = (('+0123456789', 'Helló worłd!', 1, datetime(2013, 3, 8, 15, 2, 16, tzinfo=SimpleOffsetTzInfo(2)), '+2782913593', [('00218D0A91103254769800081800480065006C006C00F300200077006F0072014200640021', 36, 141)], 'SM', 'UCS2'), ('+0123456789', 'Hellò wor£d!', 2, datetime(2013, 3, 8, 15, 2, 16, tzinfo=SimpleOffsetTzInfo(2)), '82913593', [('00218E0A91103254769800000CC8329B8D00DDDFF2003904', 23, 142)], 'SM', 'GSM'), ('+0123456789', '12345-010 12345-020 12345-030 12345-040 12345-050 12345-060 12345-070 12345-080 12345-090 12345-100 12345-110 12345-120 12345-130 12345-140 12345-150 12345-160-Hellò wor£d!', 3, datetime(2013, 3, 8, 15, 2, 16, tzinfo=SimpleOffsetTzInfo(2)), '82913593', [('00618F0A9110325476980000A00500038F020162B219ADD682C560A0986C46ABB560321828269BD16A2DD80C068AC966B45A0B46838162B219ADD682D560A0986C46ABB560361828269BD16A2DD80D068AC966B45A0B86838162B219ADD682E560A0986C46ABB562301828269BD16AAD580C068AC966B45A2B26838162B219ADD68ACD60A0986C46ABB562341828269BD16AAD580D068AC966', 152, 143), ('00618F0A91103254769800001A0500038F020268B556CC066B21CB6C3602747FCB03E410', 35, 143)], 'SM', 'GSM'), ('+0123456789', 'Hello world!\n Hello world!\n Hello world!\n Hello world!\n-> Helló worłd! ', 4, datetime(2013, 3, 8, 15, 2, 16, tzinfo=SimpleOffsetTzInfo(2)), '+2782913593', [('0061900A91103254769800088C05000390020100480065006C006C006F00200077006F0072006C00640021000A002000480065006C006C006F00200077006F0072006C00640021000A002000480065006C006C006F00200077006F0072006C00640021000A002000480065006C006C006F00200077006F0072006C00640021000A002D003E002000480065006C006C00F300200077006F0072', 152, 144), ('0061900A91103254769800080E0500039002020142006400210020', 26, 144)], 'SM', 'UCS2'), ('+0123456789', '12345-010 12345-020 12345-030 12345-040 12345-050 12345-060 12345-070 12345-080 12345-090 12345-100 12345-110 12345-120 12345-130 12345-140 12345-150 12345-160-Hello word!', 5, datetime(2013, 3, 8, 15, 2, 16, tzinfo=SimpleOffsetTzInfo(2)), '82913593', [('0061910A9110325476980000A005000391020162B219ADD682C560A0986C46ABB560321828269BD16A2DD80C068AC966B45A0B46838162B219ADD682D560A0986C46ABB560361828269BD16A2DD80D068AC966B45A0B86838162B219ADD682E560A0986C46ABB562301828269BD16AAD580C068AC966B45A2B26838162B219ADD68ACD60A0986C46ABB562341828269BD16AAD580D068AC966', 152, 145), ('0061910A91103254769800001905000391020268B556CC066B21CB6CF61B747FCBC921', 34, 145)], 'SM', 'GSM'),) for number, message, index, smsTime, smsc, pdus, mem, encoding in tests: def writeCallbackFunc(data): def writeCallbackFunc2(data): # Second step - get available encoding schemes self.assertEqual('AT+CSCS=?\r', data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CSCS=?', data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc3 def writeCallbackFunc3(data): # Third step - set encoding self.assertEqual('AT+CSCS="{0}"\r'.format(encoding), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CSCS="{0}"\r'.format(encoding), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc4 def writeCallbackFunc4(data): # Fourth step - send PDU length tpdu_length = pdus[self.currentPdu][1] ref = pdus[self.currentPdu][2] self.assertEqual('AT+CMGS={0}\r'.format(tpdu_length), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGS={0}'.format(tpdu_length), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc5 self.modem.serial.flushResponseSequence = False self.modem.serial.responseSequence = ['> \r\n', '+CMGS: {0}\r\n'.format(ref), 'OK\r\n'] def writeCallbackFuncRaiseError(data): self.assertEqual(self.currentPdu, len(pdus) - 1, 'Invalid data written to modem; expected {0} PDUs, got {1} PDU'.format(len(pdus), self.currentPdu + 1)) def writeCallbackFunc5(data): # Fifth step - send SMS PDU pdu = pdus[self.currentPdu][0] self.assertEqual('{0}{1}'.format(pdu, chr(26)), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('{0}{1}'.format(pdu, chr(26)), data)) self.modem.serial.flushResponseSequence = True self.currentPdu += 1 if len(pdus) > self.currentPdu: self.modem.serial.writeCallbackFunc = writeCallbackFunc4 else: self.modem.serial.writeCallbackFunc = writeCallbackFuncRaiseError # First step - change to PDU mode self.assertEqual('AT+CMGF=0\r', data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGF=0', data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc2 self.currentPdu = 0 self.modem._smsRef = pdus[self.currentPdu][2] self.modem.serial.writeCallbackFunc = writeCallbackFunc self.modem.serial.flushResponseSequence = True sms = self.modem.sendSms(number, message) self.assertFalse(self.modem.smsTextMode) self.assertEqual(self.modem._smsEncoding, encoding, 'Modem uses invalid encoding. Expected "{0}", got "{1}"'.format(encoding, self.modem._smsEncoding)) self.assertIsInstance(sms, gsmmodem.modem.SentSms) self.assertEqual(sms.number, number, 'Sent SMS has invalid number. Expected "{0}", got "{1}"'.format(number, sms.number)) self.assertEqual(sms.text, message, 'Sent SMS has invalid text. Expected "{0}", got "{1}"'.format(message, sms.text)) self.assertIsInstance(sms.reference, int, 'Sent SMS reference type incorrect. Expected "{0}", got "{1}"'.format(int, type(sms.reference))) ref = pdus[0][2] # All refference numbers should be equal self.assertEqual(sms.reference, ref, 'Sent SMS reference incorrect. Expected "{0}", got "{1}"'.format(ref, sms.reference)) self.assertEqual(sms.status, gsmmodem.modem.SentSms.ENROUTE, 'Sent SMS status should have been {0} ("ENROUTE"), but is: {1}'.format(gsmmodem.modem.SentSms.ENROUTE, sms.status)) # Reset mode and encoding self.modem._smsTextMode = True # Set modem to text mode self.modem._smsEncoding = "GSM" # Set encoding to GSM-7 self.modem._smsSupportedEncodingNames = None # Force modem to ask about possible encoding names self.modem.close() def test_sendSmsTextMode(self): """ Tests sending SMS messages in text mode """ self.initModem(None) self.modem.smsTextMode = True # Set modem to text mode self.assertTrue(self.modem.smsTextMode) for number, message, index, smsTime, smsc, pdu, tpdu_length, ref, mem in self.tests: self.modem._smsRef = ref def writeCallbackFunc(data): def writeCallbackFunc2(data): self.assertEqual('{0}{1}'.format(message, chr(26)), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('{0}{1}'.format(message, chr(26)), data)) self.modem.serial.flushResponseSequence = True self.assertEqual('AT+CMGS="{0}"\r'.format(number), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGS="{0}"'.format(number), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc2 self.modem.serial.writeCallbackFunc = writeCallbackFunc self.modem.serial.flushResponseSequence = False self.modem.serial.responseSequence = ['> \r\n', '+CMGS: {0}\r\n'.format(ref), 'OK\r\n'] sms = self.modem.sendSms(number, message) self.assertIsInstance(sms, gsmmodem.modem.SentSms) self.assertEqual(sms.number, number, 'Sent SMS has invalid number. Expected "{0}", got "{1}"'.format(number, sms.number)) self.assertEqual(sms.text, message, 'Sent SMS has invalid text. Expected "{0}", got "{1}"'.format(message, sms.text)) self.assertIsInstance(sms.reference, int, 'Sent SMS reference type incorrect. Expected "{0}", got "{1}"'.format(int, type(sms.reference))) self.assertEqual(sms.reference, ref, 'Sent SMS reference incorrect. Expected "{0}", got "{1}"'.format(ref, sms.reference)) self.assertEqual(sms.status, gsmmodem.modem.SentSms.ENROUTE, 'Sent SMS status should have been {0} ("ENROUTE"), but is: {1}'.format(gsmmodem.modem.SentSms.ENROUTE, sms.status)) self.modem.close() def test_sendSmsPduMode(self): """ Tests sending a SMS messages in PDU mode """ self.initModem(None) self.modem.smsTextMode = False # Set modem to PDU mode self.modem._smsEncoding = "GSM" self.assertFalse(self.modem.smsTextMode) self.firstSMS = True for number, message, index, smsTime, smsc, pdu, sms_deliver_tpdu_length, ref, mem in self.tests: self.modem._smsRef = ref calcPdu = gsmmodem.pdu.encodeSmsSubmitPdu(number, message, ref)[0] pduHex = codecs.encode(compat.str(calcPdu.data), 'hex_codec').upper() if PYTHON_VERSION >= 3: pduHex = str(pduHex, 'ascii') def writeCallbackFunc(data): def writeCallbackFuncReadCSCS(data): self.assertEqual('AT+CSCS=?\r', data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CSCS=?', data)) self.firstSMS = False def writeCallbackFunc2(data): self.assertEqual('AT+CMGS={0}\r'.format(calcPdu.tpduLength), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGS={0}'.format(calcPdu.tpduLength), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc3 self.modem.serial.flushResponseSequence = False self.modem.serial.responseSequence = ['> \r\n', '+CMGS: {0}\r\n'.format(ref), 'OK\r\n'] def writeCallbackFunc3(data): self.assertEqual('{0}{1}'.format(pduHex, chr(26)), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('{0}{1}'.format(pduHex, chr(26)), data)) self.modem.serial.flushResponseSequence = True if self.firstSMS: return writeCallbackFuncReadCSCS(data) self.assertEqual('AT+CSCS="{0}"\r'.format(self.modem._smsEncoding), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CSCS="{0}"'.format(self.modem._smsEncoding), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc2 self.modem.serial.writeCallbackFunc = writeCallbackFunc sms = self.modem.sendSms(number, message) self.assertIsInstance(sms, gsmmodem.modem.SentSms) self.assertEqual(sms.number, number, 'Sent SMS has invalid number. Expected "{0}", got "{1}"'.format(number, sms.number)) self.assertEqual(sms.text, message, 'Sent SMS has invalid text. Expected "{0}", got "{1}"'.format(message, sms.text)) self.assertIsInstance(sms.reference, int, 'Sent SMS reference type incorrect. Expected "{0}", got "{1}"'.format(int, type(sms.reference))) self.assertEqual(sms.reference, ref, 'Sent SMS reference incorrect. Expected "{0}", got "{1}"'.format(ref, sms.reference)) self.assertEqual(sms.status, gsmmodem.modem.SentSms.ENROUTE, 'Sent SMS status should have been {0} ("ENROUTE"), but is: {1}'.format(gsmmodem.modem.SentSms.ENROUTE, sms.status)) self.modem.close() def test_sendSmsResponseMixedWithUnsolictedMessages(self): """ Tests sending a SMS messages (PDU mode), but with unsolicted messages mixed into the modem responses - the only difference here is that the modem's responseSequence contains unsolicted messages taken from github issue #11 """ self.initModem(None) self.modem.smsTextMode = False # Set modem to PDU mode self.modem._smsEncoding = "GSM" self.firstSMS = True for number, message, index, smsTime, smsc, pdu, sms_deliver_tpdu_length, ref, mem in self.tests: self.modem._smsRef = ref calcPdu = gsmmodem.pdu.encodeSmsSubmitPdu(number, message, ref)[0] pduHex = codecs.encode(compat.str(calcPdu.data), 'hex_codec').upper() if PYTHON_VERSION >= 3: pduHex = str(pduHex, 'ascii') def writeCallbackFunc(data): def writeCallbackFuncReadCSCS(data): self.assertEqual('AT+CSCS=?\r', data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CSCS=?', data)) self.firstSMS = False def writeCallbackFunc2(data): self.assertEqual('AT+CMGS={0}\r'.format(calcPdu.tpduLength), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGS={0}'.format(calcPdu.tpduLength), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc3 self.modem.serial.flushResponseSequence = True # Note thee +ZDONR and +ZPASR unsolicted messages in the "response" self.modem.serial.responseSequence = ['+ZDONR: "METEOR",272,3,"CS_ONLY","ROAM_OFF"\r\n', '+ZPASR: "UMTS"\r\n', '> \r\n'] def writeCallbackFunc3(data): self.assertEqual('{0}{1}'.format(pduHex, chr(26)), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('{0}{1}'.format(pduHex, chr(26)), data)) # Note thee +ZDONR and +ZPASR unsolicted messages in the "response" self.modem.serial.responseSequence = ['+ZDONR: "METEOR",272,3,"CS_ONLY","ROAM_OFF"\r\n', '+ZPASR: "UMTS"\r\n', '+ZDONR: "METEOR",272,3,"CS_PS","ROAM_OFF"\r\n', '+ZPASR: "UMTS"\r\n', '+CMGS: {0}\r\n'.format(ref), 'OK\r\n'] if self.firstSMS: return writeCallbackFuncReadCSCS(data) self.assertEqual('AT+CSCS="{0}"\r'.format(self.modem._smsEncoding), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CSCS="{0}"'.format(self.modem._smsEncoding), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc2 self.modem.serial.writeCallbackFunc = writeCallbackFunc sms = self.modem.sendSms(number, message) self.assertIsInstance(sms, gsmmodem.modem.SentSms) self.assertEqual(sms.number, number, 'Sent SMS has invalid number. Expected "{0}", got "{1}"'.format(number, sms.number)) self.assertEqual(sms.text, message, 'Sent SMS has invalid text. Expected "{0}", got "{1}"'.format(message, sms.text)) self.assertIsInstance(sms.reference, int, 'Sent SMS reference type incorrect. Expected "{0}", got "{1}"'.format(int, type(sms.reference))) self.assertEqual(sms.reference, ref, 'Sent SMS reference incorrect. Expected "{0}", got "{1}"'.format(ref, sms.reference)) self.modem.close() def test_receiveSmsTextMode(self): """ Tests receiving SMS messages in text mode """ callbackInfo = [False, '', '', -1, None, '', None] def smsReceivedCallbackFuncText(sms): try: self.assertIsInstance(sms, gsmmodem.modem.ReceivedSms) self.assertEqual(sms.number, callbackInfo[1], 'SMS sender number incorrect. Expected: "{0}", got: "{1}"'.format(callbackInfo[1], sms.number)) self.assertEqual(sms.text, callbackInfo[2], 'SMS text incorrect. Expected: "{0}", got: "{1}"'.format(callbackInfo[2], sms.text)) self.assertIsInstance(sms.time, datetime, 'SMS received time type invalid. Expected: datetime.datetime, got: {0}"'.format(type(sms.time))) self.assertEqual(sms.time, callbackInfo[4], 'SMS received time incorrect. Expected: "{0}", got: "{1}"'.format(callbackInfo[4], sms.time)) self.assertEqual(sms.status, gsmmodem.modem.Sms.STATUS_RECEIVED_UNREAD) self.assertEqual(sms.smsc, None, 'Text-mode SMS should not have any SMSC information') finally: callbackInfo[0] = True self.initModem(smsReceivedCallbackFunc=smsReceivedCallbackFuncText) self.modem.smsTextMode = True # Set modem to text mode self.assertTrue(self.modem.smsTextMode) for number, message, index, smsTime, smsc, pdu, tpdu_length, ref, mem in self.tests: # Wait for the handler function to finish callbackInfo[0] = False # "done" flag callbackInfo[1] = number callbackInfo[2] = message callbackInfo[3] = index callbackInfo[4] = smsTime # Time string as returned by modem in text modem tzDelta = smsTime.utcoffset() if tzDelta.days >= 0: tzValStr = '+{0:0>2}'.format(int(tzDelta.seconds / 60 / 15)) # calculate offset in 0.25 hours if tzDelta.days < 0: # negative tzValStr = '-{0:0>2}'.format(int((tzDelta.days * -3600 * 24 - tzDelta.seconds) / 60 / 15)) textModeStr = smsTime.strftime('%y/%m/%d,%H:%M:%S') + tzValStr def writeCallbackFunc(data): """ Intercept the "read stored message" command """ def writeCallbackFunc2(data): self.assertEqual('AT+CMGR={0}\r'.format(index), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGR={0}'.format(index), data)) self.modem.serial.responseSequence = ['+CMGR: "REC UNREAD","{0}",,"{1}"\r\n'.format(number, textModeStr), '{0}\r\n'.format(message), 'OK\r\n'] def writeCallbackFunc3(data): self.assertEqual('AT+CMGD={0},0\r'.format(index), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGD={0}'.format(index), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc3 if self.modem._smsMemReadDelete != mem: self.assertEqual('AT+CPMS="{0}"\r'.format(mem), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CPMS="{0}"'.format(mem), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc2 else: # Modem does not need to change read memory writeCallbackFunc2(data) self.modem.serial.writeCallbackFunc = writeCallbackFunc # Fake a "new message" notification self.modem.serial.responseSequence = ['+CMTI: "{0}",{1}\r\n'.format(mem, index)] # Wait for the handler function to finish while callbackInfo[0] == False: time.sleep(0.1) self.modem.close() def test_receiveSmsPduMode(self): """ Tests receiving SMS messages in PDU mode """ callbackInfo = [False, '', '', -1, None, '', None] def smsReceivedCallbackFuncPdu(sms): try: self.assertIsInstance(sms, gsmmodem.modem.ReceivedSms) self.assertEqual(sms.number, callbackInfo[1], 'SMS sender number incorrect. Expected: "{0}", got: "{1}"'.format(callbackInfo[1], sms.number)) self.assertEqual(sms.text, callbackInfo[2], 'SMS text incorrect. Expected: "{0}", got: "{1}"'.format(callbackInfo[2], sms.text)) self.assertIsInstance(sms.time, datetime, 'SMS received time type invalid. Expected: datetime.datetime, got: {0}"'.format(type(sms.time))) self.assertEqual(sms.time, callbackInfo[4], 'SMS received time incorrect. Expected: "{0}", got: "{1}"'.format(callbackInfo[4], sms.time)) self.assertEqual(sms.status, gsmmodem.modem.Sms.STATUS_RECEIVED_UNREAD) self.assertEqual(sms.smsc, callbackInfo[5], 'PDU-mode SMS SMSC number incorrect. Expected: "{0}", got: "{1}"'.format(callbackInfo[5], sms.smsc)) finally: callbackInfo[0] = True self.initModem(smsReceivedCallbackFunc=smsReceivedCallbackFuncPdu) self.modem.smsTextMode = False # Set modem to PDU mode self.assertFalse(self.modem.smsTextMode) for pduAddressText in self.testsPduAddressText: for number, message, index, smsTime, smsc, pdu, tpdu_length, ref, mem in self.tests: if smsc == None or pdu == None: continue # not enough info for a PDU test, skip it # Wait for the handler function to finish callbackInfo[0] = False # "done" flag callbackInfo[1] = number callbackInfo[2] = message callbackInfo[3] = index callbackInfo[4] = smsTime callbackInfo[5] = smsc def writeCallbackFunc(data): def writeCallbackFunc2(data): """ Intercept the "read stored message" command """ self.assertEqual('AT+CMGR={0}\r'.format(index), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGR={0}'.format(index), data)) self.modem.serial.responseSequence = ['+CMGR: 0,{0},{1}\r\n'.format(pduAddressText, tpdu_length), '{0}\r\n'.format(pdu), 'OK\r\n'] def writeCallbackFunc3(data): self.assertEqual('AT+CMGD={0},0\r'.format(index), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGD={0}'.format(index), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc3 if self.modem._smsMemReadDelete != mem: self.assertEqual('AT+CPMS="{0}"\r'.format(mem), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CPMS="{0}"'.format(mem), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc2 else: # Modem does not need to change read memory writeCallbackFunc2(data) self.modem.serial.writeCallbackFunc = writeCallbackFunc # Fake a "new message" notification self.modem.serial.responseSequence = ['+CMTI: "SM",{0}\r\n'.format(index)] # Wait for the handler function to finish while callbackInfo[0] == False: time.sleep(0.1) self.modem.close() def test_sendSms_refCount(self): """ Test the SMS reference counter operation when sending SMSs """ self.initModem(None) ref = 0 def writeCallbackFunc(data): if data.startswith('AT+CMGS'): self.modem.serial.flushResponseSequence = False self.modem.serial.responseSequence = ['> \r\n', '+CMGS: {0}\r\n'.format(ref), 'OK\r\n'] else: self.modem.serial.flushResponseSequence = True self.modem.serial.writeCallbackFunc = writeCallbackFunc ref = 0 sms = self.modem.sendSms("+27820000000", 'Test message') firstRef = sms.reference self.assertEqual(firstRef, 0) # Ensure the reference counter is incremented each time an SMS is sent ref = 1 sms = self.modem.sendSms("+27820000000", 'Test message 2') reference = sms.reference self.assertEqual(sms.reference, firstRef + 1) # Ensure the reference counter rolls over once 255 is reached ref = 255 self.modem._smsRef = 255 sms = self.modem.sendSms("+27820000000", 'Test message 3') ref = 0 self.assertEqual(sms.reference, 255) sms = self.modem.sendSms("+27820000000", 'Test message 4') self.assertEqual(sms.reference, 0) self.modem.close() def test_sendSms_waitForDeliveryReport(self): """ Test waiting for the status report when sending SMSs """ self.initModem(None) causeTimeout = [False] def writeCallbackFunc(data): if data.startswith('AT+CMGS'): self.modem.serial.flushResponseSequence = False if causeTimeout[0]: self.modem.serial.responseSequence = ['> \r\n', '+CMGS: 183\r\n', 'OK\r\n'] else: # Fake a delivery report notification after sending SMS self.modem.serial.responseSequence = ['> \r\n', '+CMGS: 183\r\n', 'OK\r\n', 0.1, '+CDSI: "SM",3\r\n'] elif data.startswith('AT+CMGR'): # Provide a fake status report - these are tested by the TestSmsStatusReports class self.modem.serial.responseSequence = ['+CMGR: 0,,24\r\n', '07917248014000F506B70AA18092020000317071518590803170715185418000\r\n', 'OK\r\n'] else: self.modem.serial.flushResponseSequence = True self.modem.serial.writeCallbackFunc = writeCallbackFunc # Prepare send SMS response as well as "delivered" notification self.modem._smsRef = 183 sms = self.modem.sendSms('0829200000', 'Test message', waitForDeliveryReport=True) self.assertIsInstance(sms, gsmmodem.modem.SentSms) self.assertNotEqual(sms.report, None, 'Sent SMS\'s "report" attribute should not be None') self.assertIsInstance(sms.report, gsmmodem.modem.StatusReport) self.assertEqual(sms.status, gsmmodem.modem.SentSms.DELIVERED, 'Sent SMS status should have been {0} ("DELIVERED"), but is: {1}'.format(gsmmodem.modem.SentSms.DELIVERED, sms.status)) # Now test timeout event when waiting for delivery report causeTimeout[0] = True self.modem._smsRef = 183 # Set deliveryTimeout to 0.05 - should timeout very quickly self.assertRaises(gsmmodem.exceptions.TimeoutException, self.modem.sendSms, **{'destination': '0829200000', 'text': 'Test message', 'waitForDeliveryReport': True, 'deliveryTimeout': 0.05}) self.modem.close() def test_sendSms_reply(self): """ Test the reply() method of the ReceivedSms class """ self.initModem(None) def writeCallbackFunc(data): if data.startswith('AT+CMGS'): self.modem.serial.flushResponseSequence = False self.modem.serial.responseSequence = ['> \r\n', '+CMGS: 0\r\n', 'OK\r\n'] else: self.modem.serial.flushResponseSequence = True self.modem.serial.writeCallbackFunc = writeCallbackFunc receivedSms = gsmmodem.modem.ReceivedSms(self.modem, gsmmodem.modem.ReceivedSms.STATUS_RECEIVED_READ, '+27820000000', datetime(2013, 3, 8, 15, 2, 16, tzinfo=SimpleOffsetTzInfo(2)), 'Text message', '+9876543210') sms = receivedSms.reply('This is the reply') self.assertIsInstance(sms, gsmmodem.modem.SentSms) self.assertEqual(sms.number, receivedSms.number) self.assertEqual(sms.text, 'This is the reply') self.modem.close() def test_sendSms_noCgmsResponse(self): """ Test GsmModem.sendSms() but issue an invalid response from the modem """ self.initModem(None) # Modem is just going to respond with "OK" to the send SMS command self.assertRaises(gsmmodem.exceptions.CommandError, self.modem.sendSms, '+27820000000', 'Test message') self.modem.close() class TestStoredSms(unittest.TestCase): """ Tests processing/accessing SMS messages stored on the SIM card """ def initModem(self, textMode, smsReceivedCallbackFunc): global FAKE_MODEM # Override the pyserial import mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = mockSerial self.modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --', smsReceivedCallbackFunc=smsReceivedCallbackFunc) self.modem.smsTextMode = textMode self.modem.connect() FAKE_MODEM = None def setUp(self): self.modem = None def tearDown(self): if self.modem != None: self.modem.close() def initFakeModemResponses(self, textMode): global FAKE_MODEM FAKE_MODEM = copy(fakemodems.GenericTestModem()) modem = gsmmodem.modem.GsmModem('--weak ref object--') self.expectedMessages = [ReceivedSms(modem, Sms.STATUS_RECEIVED_UNREAD, '+27748577604', datetime(2013, 1, 28, 14, 51, 42, tzinfo=SimpleOffsetTzInfo(2)), 'Hello raspberry pi', None), ReceivedSms(modem, Sms.STATUS_RECEIVED_READ, '+2784000153099999', datetime(2013, 2, 7, 1, 31, 44, tzinfo=SimpleOffsetTzInfo(2)), 'New and here to stay! Don\'t just recharge SUPACHARGE and get your recharged airtime+FREE CellC to CellC mins & SMSs+Free data to use anytime. T&C apply. Cell C', None), ReceivedSms(modem, Sms.STATUS_RECEIVED_READ, '+27840001463', datetime(2013, 2, 7, 6, 24, 2, tzinfo=SimpleOffsetTzInfo(2)), 'Standard Bank: Your accounts are no longer FICA compliant. Please bring ID & proof of residence to any branch to reactivate your accounts. Queries? 0860003422.')] if textMode: FAKE_MODEM.responses['AT+CMGL="REC UNREAD"\r'] = ['+CMGL: 0,"REC UNREAD","+27748577604",,"13/01/28,14:51:42+08"\r\n', 'Hello raspberry pi\r\n', 'OK\r\n'] FAKE_MODEM.responses['AT+CMGL="REC READ"\r'] = ['+CMGL: 1,"REC READ","+2784000153099999",,"13/02/07,01:31:44+08"\r\n', 'New and here to stay! Don\'t just recharge SUPACHARGE and get your recharged airtime+FREE CellC to CellC mins & SMSs+Free data to use anytime. T&C apply. Cell C\r\n', '+CMGL: 2,"REC READ","+27840001463",,"13/02/07,06:24:02+08"\r\n', 'Standard Bank: Your accounts are no longer FICA compliant. Please bring ID & proof of residence to any branch to reactivate your accounts. Queries? 0860003422.\r\n', 'OK\r\n'] allMessages = FAKE_MODEM.responses['AT+CMGL="REC UNREAD"\r'][:-1] allMessages.extend(FAKE_MODEM.responses['AT+CMGL="REC READ"\r']) FAKE_MODEM.responses['AT+CMGL="ALL"\r'] = allMessages FAKE_MODEM.responses['AT+CMGL="STO UNSENT"\r'] = FAKE_MODEM.responses['AT+CMGL="STO SENT"\r'] = ['OK\r\n'] FAKE_MODEM.responses['AT+CMGL=0\r'] = FAKE_MODEM.responses['AT+CMGL=1\r'] = FAKE_MODEM.responses['AT+CMGL=2\r'] = FAKE_MODEM.responses['AT+CMGL=3\r'] = FAKE_MODEM.responses['AT+CMGL=4\r'] = ['ERROR\r\n'] else: FAKE_MODEM.responses['AT+CMGL=0\r'] = ['+CMGL: 0,0,,35\r\n', '07917248014000F3240B917247587706F400003110824115248012C8329BFD06C9C373B8B82C97E741F034\r\n', 'OK\r\n'] FAKE_MODEM.responses['AT+CMGL=1\r'] = ['+CMGL: 1,1,,161\r\n', '07917248010080F020109172480010359099990000312070101344809FCEF21D14769341E8B2BC0CA2BF41737A381F0211DFEE131DA4AECFE92079798C0ECBCF65D0B40A0D0E9141E9B1080ABBC9A073990ECABFEB7290BC3C4687E5E73219144ECBE9E976796594168BA06199CD1E82E86FD0B0CC660F41EDB47B0E3281A6CDE97C659497CB2072981E06D1DFA0FABC0C0ABBF3F474BBEC02514D4350180E67E75DA06199CD060D01\r\n', '+CMGL: 2,1,,159\r\n', '07917248010080F0240B917248001064F30000312070604220809F537AD84D0ECBC92061D8BDD681B2EFBA1C141E8FDF75377D0E0ACBCB20F71BC47EBBCF6539C8981C0641E3771BCE4E87DD741708CA2E87E76590589E769F414922C80482CBDF6F33E86D06C9CBF334B9EC1E9741F43728ECCE83C4F2B07B8C06D1DF2079393CA6A7ED617A19947FD7E5A0F078FCAEBBE97317285A2FCBD3E5F90F04C3D96030D88C2693B900\r\n', 'OK\r\n'] allMessages = FAKE_MODEM.responses['AT+CMGL=0\r'][:-1] allMessages.extend(FAKE_MODEM.responses['AT+CMGL=1\r']) FAKE_MODEM.responses['AT+CMGL=4\r'] = allMessages FAKE_MODEM.responses['AT+CMGL=2\r'] = FAKE_MODEM.responses['AT+CMGL=3\r'] = ['OK\r\n'] FAKE_MODEM.responses['AT+CMGL="REC UNREAD"\r'] = FAKE_MODEM.responses['AT+CMGL="REC READ"\r'] = FAKE_MODEM.responses['AT+CMGL="STO UNSENT"\r'] = FAKE_MODEM.responses['AT+CMGL="STO SENT"\r'] = FAKE_MODEM.responses['AT+CMGL="ALL"\r'] = ['ERROR\r\n'] FAKE_MODEM.responses['AT+CMGR=0\r'] = ['+CMGR: 0,,35\r\n', '07917248014000F3240B917247587706F400003110824115248012C8329BFD06C9C373B8B82C97E741F034\r\n', 'OK\r\n'] def test_listStoredSms_pdu(self): """ Tests listing/reading SMSs that are currently stored on the SIM card (PDU mode) """ self.initFakeModemResponses(textMode=False) self.initModem(False, None) # Test getting all messages def writeCallbackFunc(data): self.assertEqual('AT+CMGL=4\r', data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGL=4', data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc messages = self.modem.listStoredSms() self.assertIsInstance(messages, list) self.assertEqual(len(messages), 3, 'Invalid number of messages returned; expected 3, got {0}'.format(len(messages))) for i in range(len(messages)): message = messages[i] expected = self.expectedMessages[i] self.assertIsInstance(message, expected.__class__) self.assertEqual(message.number, expected.number) self.assertEqual(message.status, expected.status) self.assertEqual(message.text, expected.text) self.assertEqual(message.time, expected.time) del messages # Test filtering tests = ((Sms.STATUS_RECEIVED_UNREAD, 1), (Sms.STATUS_RECEIVED_READ, 2), (Sms.STATUS_STORED_SENT, 0), (Sms.STATUS_STORED_UNSENT, 0)) for status, numberOfMessages in tests: def writeCallbackFunc2(data): self.assertEqual('AT+CMGL={0}\r'.format(status), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGL={0}'.format(status), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc2 messages = self.modem.listStoredSms(status=status) self.assertIsInstance(messages, list) self.assertEqual(len(messages), numberOfMessages, 'Invalid number of messages returned for status: {0}; expected {1}, got {2}'.format(status, numberOfMessages, len(messages))) del messages # Test deleting messages after retrieval # Test deleting all messages expectedFilter = [4, ['1,4']] delCount = [0] def writeCallbackFunc3(data): self.assertEqual('AT+CMGL={0}\r'.format(expectedFilter[0]), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGL={0}'.format(expectedFilter[0]), data)) def writeCallbackFunc4(data): self.assertEqual('AT+CMGD={0}\r'.format(expectedFilter[1][delCount[0]]), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGD={0}'.format(expectedFilter[1][delCount[0]]), data)) delCount[0] += 1 self.modem.serial.writeCallbackFunc = writeCallbackFunc4 self.modem.serial.writeCallbackFunc = writeCallbackFunc3 messages = self.modem.listStoredSms(status=Sms.STATUS_ALL, delete=True) self.assertIsInstance(messages, list) self.assertEqual(len(messages), 3, 'Invalid number of messages returned; expected 3, got {0}'.format(len(messages))) # Test deleting filtered messages expectedFilter[0] = 1 expectedFilter[1] = ['1,0', '2,0'] delCount[0] = 0 self.modem.serial.writeCallbackFunc = writeCallbackFunc3 messages = self.modem.listStoredSms(status=Sms.STATUS_RECEIVED_READ, delete=True) # Test error handling if an invalid line is added between PDU data (line should be ignored) self.modem.serial.writeCallbackFunc = None self.modem.serial.modem.responses['AT+CMGL=4\r'].insert(1, 'AFSDLF SDKFJSKDLFJLKSDJF SJDLKFSKLDJFKSDFS\r\n') messages = self.modem.listStoredSms() self.assertIsInstance(messages, list) self.assertEqual(len(messages), 3, 'Invalid number of messages returned; expected 3, got {0}'.format(len(messages))) def test_listStoredSms_text(self): """ Tests listing/reading SMSs that are currently stored on the SIM card (text mode) """ self.initFakeModemResponses(textMode=True) self.initModem(True, None) # Test getting all messages def writeCallbackFunc(data): self.assertEqual('AT+CMGL="ALL"\r', data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGL="ALL"', data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc messages = self.modem.listStoredSms() self.assertIsInstance(messages, list) self.assertEqual(len(messages), 3, 'Invalid number of messages returned; expected 3, got {0}'.format(len(messages))) for i in range(len(messages)): message = messages[i] expected = self.expectedMessages[i] self.assertIsInstance(message, expected.__class__) self.assertEqual(message.number, expected.number) self.assertEqual(message.status, expected.status) self.assertEqual(message.text, expected.text) self.assertEqual(message.time, expected.time) del messages # Test filtering tests = ((Sms.STATUS_RECEIVED_UNREAD, 'REC UNREAD', 1), (Sms.STATUS_RECEIVED_READ, 'REC READ', 2), (Sms.STATUS_STORED_SENT, 'STO SENT', 0), (Sms.STATUS_STORED_UNSENT, 'STO UNSENT', 0)) for status, statusStr, numberOfMessages in tests: def writeCallbackFunc2(data): self.assertEqual('AT+CMGL="{0}"\r'.format(statusStr), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGL="{0}"'.format(statusStr), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc2 messages = self.modem.listStoredSms(status=status) self.assertIsInstance(messages, list) self.assertEqual(len(messages), numberOfMessages, 'Invalid number of messages returned for status: {0}; expected {1}, got {2}'.format(status, numberOfMessages, len(messages))) del messages # Test deleting messages after retrieval # Test deleting all messages expectedFilter = ['ALL', ['1,4']] delCount = [0] def writeCallbackFunc3(data): self.assertEqual('AT+CMGL="{0}"\r'.format(expectedFilter[0]), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGL="{0}"'.format(expectedFilter[0]), data)) def writeCallbackFunc4(data): self.assertEqual('AT+CMGD={0}\r'.format(expectedFilter[1][delCount[0]]), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGD={0}'.format(expectedFilter[1][delCount[0]]), data)) delCount[0] += 1 self.modem.serial.writeCallbackFunc = writeCallbackFunc4 self.modem.serial.writeCallbackFunc = writeCallbackFunc3 messages = self.modem.listStoredSms(status=Sms.STATUS_ALL, delete=True) self.assertIsInstance(messages, list) self.assertEqual(len(messages), 3, 'Invalid number of messages returned; expected 3, got {0}'.format(len(messages))) # Test deleting filtered messages expectedFilter[0] = 'REC READ' expectedFilter[1] = ['1,0', '2,0'] delCount[0] = 0 self.modem.serial.writeCallbackFunc = writeCallbackFunc3 messages = self.modem.listStoredSms(status=Sms.STATUS_RECEIVED_READ, delete=True) # Test error handling when specifying an invalid SMS status value self.modem.serial.writeCallbackFunc = None self.assertRaises(ValueError, self.modem.listStoredSms, **{'status': 99}) def test_processStoredSms(self): """ Tests processing and then "receiving" SMSs that are currently stored on the SIM card """ self.initFakeModemResponses(textMode=False) expectedMessages = copy(self.expectedMessages) unread = expectedMessages.pop(0) expectedMessages.append(unread) i = [0] def smsCallbackFunc(sms): expected = expectedMessages[i[0]] self.assertIsInstance(sms, ReceivedSms) self.assertEqual(sms.number, expected.number) self.assertEqual(sms.status, expected.status) self.assertEqual(sms.text, expected.text) self.assertEqual(sms.time, expected.time) i[0] += 1 self.initModem(False, smsCallbackFunc) commandsWritten = [False, False] def writeCallbackFunc(data): if data.startswith('AT+CMGL'): commandsWritten[0] = True elif data.startswith('AT+CMGD'): commandsWritten[1] = True self.modem.serial.writeCallbackFunc = writeCallbackFunc self.modem.processStoredSms() self.assertTrue(commandsWritten[0], 'AT+CMGL command not written to modem') self.assertTrue(commandsWritten[1], 'AT+CMGD command not written to modem') self.assertEqual(i[0], 3, 'Message received callback count incorrect; expected 3, got {0}'.format(i[0])) # Test unread only commandsWritten[0] = commandsWritten[1] = False i[0] = 0 expectedMessages = [unread] self.modem.processStoredSms(unreadOnly=True) self.assertTrue(commandsWritten[0], 'AT+CMGL command not written to modem') self.assertTrue(commandsWritten[1], 'AT+CMGD command not written to modem') self.assertEqual(i[0], 1, 'Message received callback count incorrect; expected 1, got {0}'.format(i[0])) def test_deleteStoredSms(self): self.initFakeModemResponses(textMode=True) self.initModem(True, None) tests = (1,2,3) for index in tests: def writeCallbackFunc(data): self.assertEqual('AT+CMGD={0},0\r'.format(index), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGD={0},0'.format(index), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc self.modem.deleteStoredSms(index) # Test switching SMS memory tests = ((5, 'TEST1'), (32, 'ME')) for index, mem in tests: def writeCallbackFunc(data): self.assertEqual('AT+CPMS="{0}"\r'.format(mem), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CPMS="{0}"'.format(mem), data)) def writeCallbackFunc2(data): self.assertEqual('AT+CMGD={0},0\r'.format(index), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGD={0},0'.format(index), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc2 self.modem.serial.writeCallbackFunc = writeCallbackFunc self.modem.deleteStoredSms(index, memory=mem) def test_deleteMultipleStoredSms(self): self.initFakeModemResponses(textMode=True) self.initModem(True, None) tests = (4,3,2,1) for delFlag in tests: # Test getting all messages def writeCallbackFunc(data): self.assertEqual('AT+CMGD=1,{0}\r'.format(delFlag), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGD=1,{0}'.format(delFlag), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc self.modem.deleteMultipleStoredSms(delFlag) # Test switching SMS memory tests = ((4, 'TEST1'), (4, 'ME')) for delFlag, mem in tests: def writeCallbackFunc(data): self.assertEqual('AT+CPMS="{0}"\r'.format(mem), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CPMS="{0}"'.format(mem), data)) def writeCallbackFunc2(data): self.assertEqual('AT+CMGD=1,{0}\r'.format(delFlag), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGD=1,{0}'.format(delFlag), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc2 self.modem.serial.writeCallbackFunc = writeCallbackFunc self.modem.deleteMultipleStoredSms(delFlag, memory=mem) # Test default delFlag value delFlag = 4 def writeCallbackFunc3(data): self.assertEqual('AT+CMGD=1,{0}\r'.format(delFlag), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGD=1,{0}'.format(delFlag), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc3 self.modem.deleteMultipleStoredSms() # Test invalid delFlag values tests = (0, 5, -3) for delFlag in tests: self.assertRaises(ValueError, self.modem.deleteMultipleStoredSms, **{'delFlag': delFlag}) def test_readStoredSms_pdu(self): """ Tests reading stored SMS messages (PDU mode) """ self.initFakeModemResponses(textMode=False) self.initModem(False, None) # Test basic reading index = 0 def writeCallbackFunc(data): self.assertEqual('AT+CMGR={0}\r'.format(index), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGR={0}'.format(index), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc message = self.modem.readStoredSms(index) expected = self.expectedMessages[index] self.assertIsInstance(message, expected.__class__) self.assertEqual(message.number, expected.number) self.assertEqual(message.status, expected.status) self.assertEqual(message.text, expected.text) self.assertEqual(message.time, expected.time) # Test switching SMS memory tests = ((0, 'TEST1'), (0, 'ME')) for index, mem in tests: def writeCallbackFunc(data): self.assertEqual('AT+CPMS="{0}"\r'.format(mem), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CPMS="{0}"'.format(mem), data)) def writeCallbackFunc2(data): self.assertEqual('AT+CMGR={0}\r'.format(index), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGR={0}'.format(index), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc2 self.modem.serial.writeCallbackFunc = writeCallbackFunc self.modem.readStoredSms(index, memory=mem) expected = self.expectedMessages[index] self.assertIsInstance(message, expected.__class__) self.assertEqual(message.number, expected.number) self.assertEqual(message.status, expected.status) self.assertEqual(message.text, expected.text) self.assertEqual(message.time, expected.time) class TestSmsStatusReports(unittest.TestCase): """ Tests receiving SMS status reports """ def initModem(self, smsStatusReportCallback): # Override the pyserial import self.mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = self.mockSerial self.modem = gsmmodem.modem.GsmModem('-- PORT IGNORED DURING TESTS --', smsStatusReportCallback=smsStatusReportCallback) self.modem.connect() def test_receiveStatusReportTextMode(self): """ Tests receiving SMS status reports in text mode """ tests = ((57, 'SR', '+CMGR: ,6,20,"0870000000",129,"13/04/29,19:58:00+04","13/04/29,19:59:00+04",0', Sms.STATUS_RECEIVED_UNREAD, # message read status '0870000000', # number 20, # reference datetime(2013, 4, 29, 19, 58, 0, tzinfo=SimpleOffsetTzInfo(1)), # sentTime datetime(2013, 4, 29, 19, 59, 0, tzinfo=SimpleOffsetTzInfo(1)), # deliverTime StatusReport.DELIVERED), # delivery status ) callbackDone = [False] for index, mem, notification, msgStatus, number, reference, sentTime, deliverTime, deliveryStatus in tests: def smsStatusReportCallbackFuncText(sms): try: self.assertIsInstance(sms, gsmmodem.modem.StatusReport) self.assertEqual(sms.status, msgStatus, 'Status report read status incorrect. Expected: "{0}", got: "{1}"'.format(msgStatus, sms.status)) self.assertEqual(sms.number, number, 'SMS sender number incorrect. Expected: "{0}", got: "{1}"'.format(number, sms.number)) self.assertEqual(sms.reference, reference, 'Status report SMS reference number incorrect. Expected: "{0}", got: "{1}"'.format(reference, sms.reference)) self.assertIsInstance(sms.timeSent, datetime, 'SMS sent time type invalid. Expected: datetime.datetime, got: {0}"'.format(type(sms.timeSent))) self.assertEqual(sms.timeSent, sentTime, 'SMS sent time incorrect. Expected: "{0}", got: "{1}"'.format(sentTime, sms.timeSent)) self.assertIsInstance(sms.timeFinalized, datetime, 'SMS finalized time type invalid. Expected: datetime.datetime, got: {0}"'.format(type(sms.timeFinalized))) self.assertEqual(sms.timeFinalized, deliverTime, 'SMS finalized time incorrect. Expected: "{0}", got: "{1}"'.format(deliverTime, sms.timeFinalized)) self.assertEqual(sms.deliveryStatus, deliveryStatus, 'SMS delivery status incorrect. Expected: "{0}", got: "{1}"'.format(deliveryStatus, sms.deliveryStatus)) self.assertEqual(sms.smsc, None, 'Text-mode SMS should not have any SMSC information') finally: callbackDone[0] = True self.initModem(smsStatusReportCallback=smsStatusReportCallbackFuncText) self.modem.smsTextMode = True def writeCallbackFunc(data): def writeCallbackFunc2(data): self.assertEqual('AT+CMGR={0}\r'.format(index), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGR={0}'.format(index), data)) self.modem.serial.responseSequence = ['{0}\r\n'.format(notification), 'OK\r\n'] def writeCallbackFunc3(data): self.assertEqual('AT+CMGD={0},0\r'.format(index), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGD={0}'.format(index), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc3 if self.modem._smsMemReadDelete != mem: self.assertEqual('AT+CPMS="{0}"\r'.format(mem), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CPMS="{0}"'.format(mem), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc2 else: # Modem does not need to change read memory writeCallbackFunc2(data) self.modem.serial.writeCallbackFunc = writeCallbackFunc # Fake a "new status report" notification self.modem.serial.responseSequence = ['+CDSI: "{0}",{1}\r\n'.format(mem, index)] # Wait for the handler function to finish while callbackDone[0] == False: time.sleep(0.1) self.modem.close() def test_receiveSmsPduMode_problemCases(self): """ Test receiving PDU-mode SMS using data captured from failed operations/bug reports """ # AT+CMGR response from ZTE modem breaks incoming message read - simply test that we can parse it properly zteResponse = ['+CMGR: ,,27\r\n', '0297F1061C0F910B487228297020F5317062419272803170624192138000\r\n', 'OK\r\n'] callbackInfo = [False, '', '', -1, None, '', None] def smsCallbackFunc1(sms): try: self.assertIsInstance(sms, gsmmodem.modem.StatusReport) # Since the +CMGR response did not include the SMS's status, see if the default fallback was loaded correctly self.assertEqual(sms.status, gsmmodem.modem.Sms.STATUS_RECEIVED_UNREAD) finally: callbackInfo[0] = True def writeCallback1(data): if data.startswith('AT+CMGR'): self.modem.serial.flushResponseSequence = True self.modem.serial.responseSequence = zteResponse self.initModem(smsStatusReportCallback=smsCallbackFunc1) # Fake a "new message" notification self.modem.serial.writeCallbackFunc = writeCallback1 self.modem.serial.responseSequence = ['+CDSI: "SM",1\r\n'] # Wait for the handler function to finish while callbackInfo[0] == False: time.sleep(0.1) def test_receiveStatusReportPduMode(self): """ Tests receiving SMS status reports in PDU mode """ tests = ((3, 'SM', ['+CMGR: 0,,24\r\n', '07917248014000F506B70AA18092020000317071518590803170715185418000\r\n', 'OK\r\n'], Sms.STATUS_RECEIVED_UNREAD, # message read status '0829200000', # number 183, # reference datetime(2013, 7, 17, 15, 58, 9, tzinfo=SimpleOffsetTzInfo(2)), # sentTime datetime(2013, 7, 17, 15, 58, 14, tzinfo=SimpleOffsetTzInfo(2)), # deliverTime StatusReport.DELIVERED), # delivery status (1, 'SM', # This output was captured from a ZTE modem that seems to be broken (PDU is semi-invalid (SMSC length incorrect), and +CMGR output missing status) ['+CMGR: ,,27\r\n', '0297F1061C0F910B487228297020F5317062419272803170624192138000\r\n', 'OK\r\n'], Sms.STATUS_RECEIVED_UNREAD, '+b08427829207025', # <-- note the broken number 28, datetime(2013, 7, 26, 14, 29, 27, tzinfo=SimpleOffsetTzInfo(2)), # sentTime datetime(2013, 7, 26, 14, 29, 31, tzinfo=SimpleOffsetTzInfo(2)), # deliverTime StatusReport.DELIVERED), ) callbackDone = [False] for index, mem, responseSeq, msgStatus, number, reference, sentTime, deliverTime, deliveryStatus in tests: callbackDone[0] = False def smsStatusReportCallbackFuncText(sms): try: self.assertIsInstance(sms, gsmmodem.modem.StatusReport) self.assertEqual(sms.status, msgStatus, 'Status report read status incorrect. Expected: "{0}", got: "{1}"'.format(msgStatus, sms.status)) self.assertEqual(sms.number, number, 'SMS sender number incorrect. Expected: "{0}", got: "{1}"'.format(number, sms.number)) self.assertEqual(sms.reference, reference, 'Status report SMS reference number incorrect. Expected: "{0}", got: "{1}"'.format(reference, sms.reference)) self.assertIsInstance(sms.timeSent, datetime, 'SMS sent time type invalid. Expected: datetime.datetime, got: {0}"'.format(type(sms.timeSent))) self.assertEqual(sms.timeSent, sentTime, 'SMS sent time incorrect. Expected: "{0}", got: "{1}"'.format(sentTime, sms.timeSent)) self.assertIsInstance(sms.timeFinalized, datetime, 'SMS finalized time type invalid. Expected: datetime.datetime, got: {0}"'.format(type(sms.timeFinalized))) self.assertEqual(sms.timeFinalized, deliverTime, 'SMS finalized time incorrect. Expected: "{0}", got: "{1}"'.format(deliverTime, sms.timeFinalized)) self.assertEqual(sms.deliveryStatus, deliveryStatus, 'SMS delivery status incorrect. Expected: "{0}", got: "{1}"'.format(deliveryStatus, sms.deliveryStatus)) self.assertEqual(sms.smsc, None, 'Text-mode SMS should not have any SMSC information') finally: callbackDone[0] = True self.initModem(smsStatusReportCallback=smsStatusReportCallbackFuncText) self.modem.smsTextMode = False def writeCallbackFunc(data): def writeCallbackFunc2(data): self.assertEqual('AT+CMGR={0}\r'.format(index), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGR={0}'.format(index), data)) self.modem.serial.responseSequence = responseSeq def writeCallbackFunc3(data): self.assertEqual('AT+CMGD={0},0\r'.format(index), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CMGD={0}'.format(index), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc3 if self.modem._smsMemReadDelete != mem: self.assertEqual('AT+CPMS="{0}"\r'.format(mem), data, 'Invalid data written to modem; expected "{0}", got: "{1}"'.format('AT+CPMS="{0}"'.format(mem), data)) self.modem.serial.writeCallbackFunc = writeCallbackFunc2 else: # Modem does not need to change read memory writeCallbackFunc2(data) self.modem.serial.writeCallbackFunc = writeCallbackFunc # Fake a "new status report" notification self.modem.serial.responseSequence = ['+CDSI: "{0}",{1}\r\n'.format(mem, index)] # Wait for the handler function to finish while callbackDone[0] == False: time.sleep(0.1) self.modem.close() def test_receiveSmsPduMode_invalidPDUsRecordedFromModems(self): """ Test receiving PDU-mode SMS using data captured from failed operations/bug reports """ tests = ((['+CMGR: 1,,26\r\n', '0006230E9126983575169498610103409544C26101034095448200\r\n', 'OK\r\n'], # see: babca/python-gsmmodem#15 Sms.STATUS_RECEIVED_READ, # message read status '+62895357614989', # number 35, # reference datetime(2016, 10, 30, 4, 59, 44, tzinfo=SimpleOffsetTzInfo(8)), # sentTime datetime(2016, 10, 30, 4, 59, 44, tzinfo=SimpleOffsetTzInfo(7)), # deliverTime StatusReport.DELIVERED), # delivery status ) callbackDone = [False] for modemResponse, msgStatus, number, reference, sentTime, deliverTime, deliveryStatus in tests: def smsCallbackFunc1(sms): try: self.assertIsInstance(sms, gsmmodem.modem.StatusReport) self.assertEqual(sms.status, msgStatus, 'Status report read status incorrect. Expected: "{0}", got: "{1}"'.format(msgStatus, sms.status)) self.assertEqual(sms.number, number, 'SMS sender number incorrect. Expected: "{0}", got: "{1}"'.format(number, sms.number)) self.assertEqual(sms.reference, reference, 'Status report SMS reference number incorrect. Expected: "{0}", got: "{1}"'.format(reference, sms.reference)) self.assertIsInstance(sms.timeSent, datetime, 'SMS sent time type invalid. Expected: datetime.datetime, got: {0}"'.format(type(sms.timeSent))) self.assertEqual(sms.timeSent, sentTime, 'SMS sent time incorrect. Expected: "{0}", got: "{1}"'.format(sentTime, sms.timeSent)) self.assertIsInstance(sms.timeFinalized, datetime, 'SMS finalized time type invalid. Expected: datetime.datetime, got: {0}"'.format(type(sms.timeFinalized))) self.assertEqual(sms.timeFinalized, deliverTime, 'SMS finalized time incorrect. Expected: "{0}", got: "{1}"'.format(deliverTime, sms.timeFinalized)) self.assertEqual(sms.deliveryStatus, deliveryStatus, 'SMS delivery status incorrect. Expected: "{0}", got: "{1}"'.format(deliveryStatus, sms.deliveryStatus)) self.assertEqual(sms.smsc, None, 'This SMS should not have any SMSC information') finally: callbackDone[0] = True def writeCallback1(data): if data.startswith('AT+CMGR'): self.modem.serial.flushResponseSequence = True self.modem.serial.responseSequence = modemResponse self.initModem(smsStatusReportCallback=smsCallbackFunc1) # Fake a "new message" notification self.modem.serial.writeCallbackFunc = writeCallback1 self.modem.serial.flushResponseSequence = True self.modem.serial.responseSequence = ['+CDSI: "SM",1\r\n'] # Wait for the handler function to finish while callbackDone[0] == False: time.sleep(0.1) if __name__ == "__main__": logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG) unittest.main() ================================================ FILE: test/test_pdu.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- """ Test suite for SMS PDU encoding/decoding algorithms """ from __future__ import unicode_literals import sys, unittest, random, codecs from datetime import datetime, timedelta from . import compat # For Python 2.6, 3.0-2 compatibility import gsmmodem.pdu from gsmmodem.util import SimpleOffsetTzInfo class TestSemiOctets(unittest.TestCase): """ Tests the semi-octet encoder/decoder """ def setUp(self): self.tests = (('15125551234', bytearray([0x51, 0x21, 0x55, 0x15, 0x32, 0xf4])), ('123', bytearray([0x21, 0xf3])), ('1234', bytearray([0x21, 0x43]))) def test_encode(self): """ Tests the semi-octet encoding algorithm """ for plaintext, encoded in self.tests: result = gsmmodem.pdu.encodeSemiOctets(plaintext) self.assertEqual(result, encoded, 'Failed to encode plaintext string: "{0}". Expected: "{1}", got: "{2}"'.format(plaintext, [b for b in encoded], [b for b in result])) def test_decode(self): """ Tests the semi-octet decoding algorithm """ for plaintext, encoded in self.tests: # Test different parameter types: bytearray, str for param in (encoded, codecs.encode(compat.str(encoded), 'hex_codec')): result = gsmmodem.pdu.decodeSemiOctets(param) self.assertEqual(result, plaintext, 'Failed to decode data. Expected: "{0}", got: "{1}"'.format(plaintext, result)) def test_decodeIter(self): """ Tests semi-octet decoding when using a bytearray iterator and number of octets as input argument """ iterTests = (('0123456789', 9, iter(bytearray(codecs.decode(b'1032547698', 'hex_codec')))),) for plaintext, numberOfOctets, byteIter in iterTests: result = gsmmodem.pdu.decodeSemiOctets(byteIter, numberOfOctets) self.assertEqual(result, plaintext, 'Failed to decode data iter. Expected: "{0}", got: "{1}"'.format(plaintext, result)) class TestGsm7(unittest.TestCase): """ Tests the GSM-7 encoding/decoding algorithms """ def setUp(self): self.tests = (('', bytearray(b''), bytearray([])), ('123', bytearray(b'123'), bytearray([49, 217, 12])), ('12345678', bytearray(b'12345678'), bytearray([49, 217, 140, 86, 179, 221, 112])), ('123456789', bytearray(b'123456789'), bytearray([49, 217, 140, 86, 179, 221, 112, 57])), ('Hello World!', bytearray([0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21]), bytearray([200, 50, 155, 253, 6, 93, 223, 114, 54, 57, 4])), ('[{abc}]~', bytearray([0x1B, 0x3C, 0x1B, 0x28, 0x61, 0x62, 0x63, 0x1B, 0x29, 0x1B, 0x3E, 0x1B, 0x3D]), bytearray([27, 222, 6, 21, 22, 143, 55, 169, 141, 111, 211, 3])), ('123456789012345678901234567890', bytearray([49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48]), bytearray([49, 217, 140, 86, 179, 221, 112, 57, 88, 76, 54, 163, 213, 108, 55, 92, 14, 22, 147, 205, 104, 53, 219, 13, 151, 131, 1])), ('{åΦΓΛΩΠΨΣΘ€}', bytearray([27, 40, 15, 18, 19, 20, 21, 22, 23, 24, 25, 27, 101, 27, 41]), bytearray([27, 212, 67, 50, 161, 84, 44, 23, 76, 102, 83, 222, 164, 0])), ('a[]{}€', bytearray([97, 27, 60, 27, 62, 27, 40, 27, 41, 27, 101]), bytearray([225, 13, 111, 227, 219, 160, 54, 169, 77, 25])), ) def test_encode(self): """ Tests GSM-7 encoding algorithm """ for plaintext, encoded, septets in self.tests: result = gsmmodem.pdu.encodeGsm7(plaintext) self.assertEqual(result, encoded, 'Failed to GSM-7 encode plaintext string: "{0}". Expected: "{1}", got: "{2}"'.format(plaintext, [b for b in encoded], [b for b in result])) def test_decode(self): """ Tests GSM-7 decoding algorithm """ for plaintext, encoded, septets in self.tests: # Test different parameter types: bytearray, str for param in (encoded, compat.bytearrayToStr(encoded)): result = gsmmodem.pdu.decodeGsm7(param) self.assertEqual(result, plaintext, 'Failed to decode GSM-7 string: "{0}". Expected: "{1}", got: "{2}"'.format([b for b in encoded], plaintext, result)) def test_packSeptets(self): """ Tests the septet-packing alogrithm for GSM-7-encoded strings """ for plaintext, encoded, septets in self.tests: # Test different parameter types: bytearray, str, iter(bytearray) i = 0 for param in (encoded, compat.bytearrayToStr(encoded), iter(encoded)): result = gsmmodem.pdu.packSeptets(param) self.assertEqual(result, septets, 'Failed to pack GSM-7 octets into septets for string: "{0}" using parameter type: {1}. Expected: "{2}", got: "{3}"'.format(plaintext, type(param), [b for b in septets], [b for b in result])) i+=1 def test_unpackSeptets_no_limits(self): """ Tests the septet-unpacking alogrithm for GSM-7-encoded strings (no maximum number of septets specified) """ for plaintext, encoded, septets in self.tests: # Test different parameter types: bytearray, str, iter(bytearray) for param in (septets, compat.bytearrayToStr(septets), iter(septets)): result = gsmmodem.pdu.unpackSeptets(param) self.assertEqual(result, encoded, 'Failed to unpack GSM-7 septets into octets for string: "{0}". Expected: "{1}", got: "{2}"'.format(plaintext, [b for b in encoded], [b for b in result])) def test_unpackSeptets_with_limits(self): """ Tests the septet-unpacking alogrithm for GSM-7-encoded strings (max number of septets specified) """ for plaintext, encoded, septets in self.tests: limit = len(septets) septets.extend([random.randint(0,255), random.randint(0,255), random.randint(0,255), random.randint(0,255)]) # add some garbage data (should be ignored due to numberOfSeptets being set) result = gsmmodem.pdu.unpackSeptets(septets, limit) self.assertEqual(result, encoded, 'Failed to unpack GSM-7 septets into {0} octets for string: "{1}". Expected: "{2}", got: "{3}"'.format(len(encoded), plaintext, [b for b in encoded], [b for b in result])) def test_encodeInvalid(self): """ Test encoding a string that cannot be encoded with GSM-7 """ tests = ('世界您好!',) for invalidStr in tests: self.assertRaises(ValueError, gsmmodem.pdu.encodeGsm7, invalidStr, discardInvalid=False) def test_encodeInvalidDiscard(self): """ Tests encoding a string containing invalid GSM-7 characters when set to discard them """ tests = (('a世界b您c好!', bytearray([97, 98, 99])),) for invalidStr, encoded in tests: result = gsmmodem.pdu.encodeGsm7(invalidStr, discardInvalid=True) self.assertEqual(result, encoded, 'Failed to GSM-7 encode invalid plaintext string: "{0}". Expected: "{1}", got: "{2}"'.format(invalidStr, [b for b in encoded], [b for b in result])) class TestUcs2(unittest.TestCase): """ Tests the UCS2 encoding/decoding algorithms """ def setUp(self): self.tests = (('あ叶葉', bytearray([0x30, 0x42, 0x53, 0xF6, 0x84, 0x49])), ('はい', bytearray([0x30, 0x6F, 0x30, 0x44]))) def test_encode(self): """ Tests GSM-7 encoding algorithm """ for plaintext, encoded in self.tests: result = gsmmodem.pdu.encodeUcs2(plaintext) self.assertEqual(result, encoded, 'Failed to UCS-2 encode plaintext string: "{0}". Expected: "{1}", got: "{2}"'.format(plaintext, [b for b in encoded], [b for b in result])) def test_decode(self): """ Tests GSM-7 decoding algorithm """ for plaintext, encoded in self.tests: result = gsmmodem.pdu.decodeUcs2(iter(encoded), len(encoded)) self.assertEqual(result, plaintext, 'Failed to decode UCS-2 string: "{0}". Expected: "{1}", got: "{2}"'.format([b for b in encoded], plaintext, result)) class TestSmsPduAddressFields(unittest.TestCase): """ Tests for SMS PDU address fields (these methods are not meant to be public) """ def setUp(self): self.tests = (('+9876543210', 7, b'0A918967452301', b'0A918967452301'), ('+9876543210', 7, b'0A918967452301000000', b'0A918967452301'), # same as above, but checking read limits ('+987654321', 7, b'099189674523F1000000', b'099189674523F1'), ('+27829135934', 8, b'0B917228195339F4', b'0B917228195339F4'), ('abc', 5, b'06D061F118', b'06D061F118'), ('abc', 5, b'06D061F118D3F1FF0032', b'06D061F118'), # same as above, but checking read limits ('FRANCOIS', 9, b'0ED04669D0397C26A7', b'0ED04669D0397C26A7'), ('a[]{}€', 12, b'14D0E10D6FE3DBA036A94D19', b'14D0E10D6FE3DBA036A94D19'), ('0129998765', 7, b'0AA11092997856', b'0AA11092997856') # local number ) def test_decodeAddressField(self): for plaintext, bytesRead, hexEncoded, realHexEncoded in self.tests: byteIter = iter(bytearray(codecs.decode(hexEncoded, 'hex_codec'))) resultValue, resultNumBytesRead = gsmmodem.pdu._decodeAddressField(byteIter, log=True) self.assertEqual(resultValue, plaintext, 'Failed to decode address field data "{0}". Expected: "{1}", got: "{2}"'.format(hexEncoded, plaintext, resultValue)) self.assertEqual(resultNumBytesRead, bytesRead, 'Incorrect "number of bytes read" returned for data "{0}". Expected: "{1}", got: "{2}"'.format(hexEncoded, bytesRead, resultNumBytesRead)) def test_encodeAddressField(self): for plaintext, bytesRead, hexEncoded, realHexEncoded in self.tests: expected = bytearray(codecs.decode(realHexEncoded, 'hex_codec')) result = gsmmodem.pdu._encodeAddressField(plaintext) self.assertEqual(result, expected, 'Failed to encode address field data "{0}". Expected: "{1}", got: "{2}"'.format(plaintext, realHexEncoded, codecs.encode(compat.str(result), 'hex_codec').upper())) class TestSmsPduSmscFields(unittest.TestCase): """ Tests for SMS PDU SMSC-specific address fields (these methods are not meant to be public) Note: SMSC fields are encoded *slightly* differently from "normal" address fields (the length indicator is different) """ def setUp(self): self.tests = (('+9876543210', 7, b'06918967452301', b'06918967452301'), ('+9876543210', 7, b'06918967452301000000', b'06918967452301'), # same as above, but checking read limits ('+987654321', 7, b'069189674523F1000000', b'069189674523F1'), ('+2782913593', 7, b'06917228195339', b'06917228195339')) def test_decodeSmscField(self): for plaintext, bytesRead, hexEncoded, realHexEncoded in self.tests: byteIter = iter(bytearray(codecs.decode(hexEncoded, 'hex_codec'))) resultValue, resultNumBytesRead = gsmmodem.pdu._decodeAddressField(byteIter, smscField=True) self.assertEqual(resultValue, plaintext, 'Failed to decode SMSC address field data "{0}". Expected: "{1}", got: "{2}"'.format(hexEncoded, plaintext, resultValue)) self.assertEqual(resultNumBytesRead, bytesRead, 'Incorrect "number of bytes read" returned for data "{0}". Expected: "{1}", got: "{2}"'.format(hexEncoded, bytesRead, resultNumBytesRead)) def test_encodeSmscField(self): for plaintext, bytesRead, hexEncoded, realHexEncoded in self.tests: expected = bytearray(codecs.decode(realHexEncoded, 'hex_codec')) result = gsmmodem.pdu._encodeAddressField(plaintext, smscField=True) self.assertEqual(result, expected, 'Failed to encode SMSC address field data "{0}". Expected: "{1}", got: "{2}"'.format(plaintext, realHexEncoded, codecs.encode(compat.str(result), 'hex_codec').upper())) class TestRelativeValidityPeriod(unittest.TestCase): """ Tests for SMS PDU relative validity period encoding/decoding (these methods are not meant to be public) """ def setUp(self): self.tests = ((timedelta(minutes=30), 5), (timedelta(hours=16), 151), (timedelta(days=3), 169), (timedelta(weeks=5), 197)) def test_encode(self): for validity, tpVp in self.tests: result = gsmmodem.pdu._encodeRelativeValidityPeriod(validity) self.assertEqual(result, tpVp, 'Failed to encode relative validity period: {0}. Expected: "{1}", got: "{2}"'.format(validity, tpVp, result)) self.assertIsInstance(result, tpVp.__class__, 'Invalid data type returned; expected {0}, got {1}'.format(tpVp.__class__, result.__class__)) def test_decode(self): for validity, tpVp in self.tests: result = gsmmodem.pdu._decodeRelativeValidityPeriod(tpVp) self.assertEqual(result, validity, 'Failed to decode relative validity period: {0}. Expected: "{1}", got: "{2}"'.format(tpVp, validity, result)) def test_decode_invalidTpVp(self): tpVp = 2048 # invalid since > 255 self.assertRaises(ValueError, gsmmodem.pdu._decodeRelativeValidityPeriod, tpVp) def test_encode_validityPeriodTooLong(self): validity = timedelta(weeks=1000) self.assertRaises(ValueError, gsmmodem.pdu._encodeRelativeValidityPeriod, validity) class TestTimestamp(unittest.TestCase): """ Tests for SMS PDU timestamp encoding used for absolute validity period encoding/decoding (these methods are not meant to be public) """ def setUp(self): self.tests = ((datetime(2015, 11, 27, 0, 0, 0, tzinfo=SimpleOffsetTzInfo(0)), b'51117200000000'), (datetime(2015, 11, 27, 0, 0, 0, tzinfo=SimpleOffsetTzInfo(2)), b'51117200000080'), # same as previous but with GMT+2 timezone (datetime(2007, 4, 12, 23, 25, 42, tzinfo=SimpleOffsetTzInfo(8)), b'70402132522423'), (datetime(2007, 4, 12, 23, 25, 42, tzinfo=SimpleOffsetTzInfo(-8)), b'7040213252242B'), # same as previous but with GMT-8 timezone ) def test_encode(self): for timestamp, encodedHex in self.tests: encoded = bytearray(codecs.decode(encodedHex, 'hex_codec')) result = gsmmodem.pdu._encodeTimestamp(timestamp) self.assertEqual(result, encoded, 'Failed to encode timestamp: {0}. Expected: "{1}", got: "{2}"'.format(timestamp, encodedHex, codecs.encode(compat.str(result), 'hex_codec').upper())) def test_decode(self): for timestamp, encoded in self.tests: result = gsmmodem.pdu._decodeTimestamp(encoded) self.assertEqual(result, timestamp, 'Failed to decode timestamp: {0}. Expected: "{1}", got: "{2}"'.format(encoded, timestamp, result)) def test_encode_noTimezone(self): """ Tests encoding without timezone information """ timestamp = datetime(2013, 3, 1, 12, 30, 21) self.assertRaises(ValueError, gsmmodem.pdu._encodeTimestamp, timestamp) class TestSmsPduTzInfo(unittest.TestCase): """ Basic tests for the SmsPduTzInfo class """ def test_pickle(self): """ Ensure SmsPduTzInfo objects can be pickled (mentioneded as requirement of tzinfo implementations in Python docs) """ import pickle obj = gsmmodem.pdu.SmsPduTzInfo('08') self.assertIsInstance(obj, gsmmodem.pdu.SmsPduTzInfo) pickledObj = pickle.dumps(obj) self.assertNotEqual(obj, pickledObj) unpickledObj = pickle.loads(pickledObj) self.assertIsInstance(unpickledObj, gsmmodem.pdu.SmsPduTzInfo) self.assertEqual(obj.utcoffset(0), unpickledObj.utcoffset(0)) def test_dst(self): """ Test SmsPduTzInfo.dst() """ obj = gsmmodem.pdu.SmsPduTzInfo('08') self.assertEqual(obj.dst(0), timedelta(0)) def test_utcoffset(self): """ Test SmsPduTzInfo.utcoffest() """ tests = (('08', 2), ('B2', -8)) for pduOffsetStr, offset in tests: result = gsmmodem.pdu.SmsPduTzInfo(pduOffsetStr) expected = SimpleOffsetTzInfo(offset) self.assertEqual(result.utcoffset(0), expected.utcoffset(0)) class TestUdhConcatenation(unittest.TestCase): """ Tests for UDH concatenation information element """ def setUp(self): self.tests = ((23, 1, 3, b'0003170301'), # 8-bit reference (384, 2, 4, b'080401800402') # 16-bit reference ) def test_encode(self): for ref, number, parts, ieHex in self.tests: concatIe = gsmmodem.pdu.Concatenation() concatIe.reference = ref concatIe.number = number concatIe.parts = parts expected = bytearray(codecs.decode(ieHex, 'hex_codec')) result = concatIe.encode() self.assertEqual(result, expected, 'Failed to encode Concatenation Information Element; expected: "{0}", got: "{1}"'.format(ieHex, codecs.encode(compat.str(result), 'hex_codec').upper())) # Now modify some values and ensure encoded values changes concatIe.reference = ref+1 result = concatIe.encode() self.assertNotEqual(result, expected, 'Modifications to UDH information element object not reflected in encode()') def test_decode(self): for ref, number, parts, ieHex in self.tests: ieData = bytearray(codecs.decode(ieHex, 'hex_codec')) # Test IE constructor with args result = gsmmodem.pdu.InformationElement(ieData[0], ieData[1], ieData[2:]) self.assertIsInstance(result, gsmmodem.pdu.Concatenation, 'Invalid object type returned; expected Concatenation, got {0}'.format(type(result))) self.assertEqual(result.reference, ref, 'Invalid reference; expected {0}, got {1}'.format(ref, result.reference)) self.assertEqual(result.number, number, 'Invalid part number; expected {0}, got {1}'.format(number, result.number)) self.assertEqual(result.parts, parts, 'Invalid total number of parts; expected {0}, got {1}'.format(parts, result.parts)) # Test IE constructor with kwargs result = gsmmodem.pdu.InformationElement(iei=ieData[0], ieLen=ieData[1], ieData=ieData[2:]) self.assertIsInstance(result, gsmmodem.pdu.Concatenation, 'Invalid object type returned; expected Concatenation, got {0}'.format(type(result))) self.assertEqual(result.reference, ref, 'Invalid reference; expected {0}, got {1}'.format(ref, result.reference)) self.assertEqual(result.number, number, 'Invalid part number; expected {0}, got {1}'.format(number, result.number)) self.assertEqual(result.parts, parts, 'Invalid total number of parts; expected {0}, got {1}'.format(parts, result.parts)) class TestUdhPortAddress(unittest.TestCase): """ Tests for UDH application port addressing scheme information element """ def setUp(self): self.tests = ((100, 50, b'04026432'), # 8-bit addresses (1234, 5222, b'050404D21466') # 16-bit addresses ) def test_encode(self): for destination, source, ieHex in self.tests: portIe = gsmmodem.pdu.PortAddress() portIe.source = source portIe.destination = destination expected = bytearray(codecs.decode(ieHex, 'hex_codec')) result = portIe.encode() self.assertEqual(result, expected, 'Failed to encode PortAddress Information Element; expected: "{0}", got: "{1}"'.format(ieHex, codecs.encode(compat.str(result), 'hex_codec').upper())) # Now modify some values and ensure encoded values changes portIe.destination = destination+1 result = portIe.encode() self.assertNotEqual(result, expected, 'Modifications to UDH information element object not reflected in encode()') def test_decode(self): for destination, source, ieHex in self.tests: ieData = bytearray(codecs.decode(ieHex, 'hex_codec')) # Test IE constructor with args result = gsmmodem.pdu.InformationElement(ieData[0], ieData[1], ieData[2:]) self.assertIsInstance(result, gsmmodem.pdu.PortAddress, 'Invalid object type returned; expected Concatenation, got {0}'.format(type(result))) self.assertEqual(result.source, source, 'Invalid origin port number; expected {0}, got {1}'.format(source, result.source)) self.assertEqual(result.destination, destination, 'Invalid destination port number; expected {0}, got {1}'.format(destination, result.destination)) # Test IE constructor with kwargs result = gsmmodem.pdu.InformationElement(iei=ieData[0], ieLen=ieData[1], ieData=ieData[2:]) self.assertIsInstance(result, gsmmodem.pdu.PortAddress, 'Invalid object type returned; expected Concatenation, got {0}'.format(type(result))) self.assertEqual(result.source, source, 'Invalid origin port number; expected {0}, got {1}'.format(source, result.source)) self.assertEqual(result.destination, destination, 'Invalid destination port number; expected {0}, got {1}'.format(destination, result.destination)) class TestSmsPdu(unittest.TestCase): """ Tests encoding/decoding of SMS PDUs """ def test_encodeSmsSubmit(self): """ Tests SMS PDU encoding """ tests = (('+27820001111', 'Hello World!', 0, None, None, False, False, b'0001000B917228001011F100000CC8329BFD065DDF72363904'), ('+27820001111', 'Flash SMS', 0, None, None, False, True, b'0005000B917228001011F10000094676788E064D9B53'), ('+123456789', '世界您好!', 0, timedelta(weeks=52), '+44000000000', False, False, b'07914400000000F01100099121436587F90008F40A4E16754C60A8597DFF01'), ('0126541234', 'Test message: local numbers', 13, timedelta(days=3), '12345', True, False, b'04A12143F5310D0AA110624521430000A91BD4F29C0E6A97E7F3F0B9AC03B1DFE3301BE4AEB7C565F91C'), ('+27820001111', 'Timestamp validity test', 0, datetime(2013, 7, 10, 13, 39, tzinfo=SimpleOffsetTzInfo(2)), None, False, False, b'0019000B917228001011F100003170013193008017D474BB3CA787DB70903DCC4E93D3F43C885E9ED301'), ) for number, text, reference, validity, smsc, rejectDuplicates, sendFlash, pduHex in tests: pdu = bytearray(codecs.decode(pduHex, 'hex_codec')) result = gsmmodem.pdu.encodeSmsSubmitPdu(number, text, reference, validity, smsc, rejectDuplicates, sendFlash) self.assertIsInstance(result, list) self.assertEqual(len(result), 1, 'Only 1 PDU should have been created, but got {0}'.format(len(result))) self.assertIsInstance(result[0], gsmmodem.pdu.Pdu) self.assertEqual(result[0].data, pdu, 'Failed to encode SMS PDU for number: "{0}" and text "{1}". Expected: "{2}", got: "{3}"'.format(number, text, pduHex, codecs.encode(compat.str(result[0].data), 'hex_codec').upper())) def test_decode(self): """ Tests SMS PDU decoding """ tests = ((b'06917228195339040B917228214365F700003130805120618005D4F29C2E03', {'type': 'SMS-DELIVER', 'smsc': '+2782913593', 'number': '+27821234567', 'protocol_id': 0, 'time': datetime(2013, 3, 8, 15, 2, 16, tzinfo=SimpleOffsetTzInfo(2)), 'text': 'Test2'}), (b'07915892000000F0040B915892214365F700007040213252242331493A283D0795C3F33C88FE06C9CB6132885EC6D341EDF27C1E3E97E7207B3A0C0A5241E377BB1D7693E72E', {'type': 'SMS-DELIVER', 'smsc': '+85290000000', 'number': '+85291234567', 'time': datetime(2007, 4, 12, 23, 25, 42, tzinfo=SimpleOffsetTzInfo(8)), 'text': 'It is easy to read text messages via AT commands.'}), (b'06917228195339040B917228214365F70000313062315352800A800D8A5E98D337A910', {'type': 'SMS-DELIVER', 'number': '+27821234567', 'text': '@{tést}!'}), (b'07911326040000F0310D0B911326880736F40000A90FF7FBDD454E87CDE1B0DB357EB701', {'type': 'SMS-SUBMIT', 'smsc': '+31624000000', 'number': '+31628870634', 'validity': timedelta(days=3), 'text': 'www.diafaan.com'}), (b'0006D60B911326880736F4111011719551401110117195714000', {'type': 'SMS-STATUS-REPORT', 'number': '+31628870634', 'reference': 214}), (b'0591721891F1400781721881F800003160526104848059050003C30101916536FB1DCABEEB2074D85E064941B19CAB060319A5C522289C96D3D3ED32286C0FA7D96131BBEC024941B19CAB0603DDD36C36A88C87A7E565D0DB0D82C55EB0DB4B068BCD5C20', {'type': 'SMS-DELIVER', 'number': '2781188', 'smsc': '+2781191', 'text': 'Hello!You have R 19.50 FREE airtime available. R 19.50 will expire on 01/07/2013. ', 'udh': [gsmmodem.pdu.Concatenation(0x00, 0x03, [0xC3, 0x01, 0x01])]}), (b'07914346466554F601000B914316565811F9000806304253F68449', # Valid UCS-2 PDU {'type': 'SMS-SUBMIT', 'number': '+34616585119', 'smsc': '+34646456456', 'text': 'あ叶葉'}), (b'0041010C910661345542F60008A0050003000301306F3044', # UCS-2 PDU; User data length is invalid in this PDU (too long) {'type': 'SMS-SUBMIT', 'number': '+60164355246', 'smsc': None, 'udh': [gsmmodem.pdu.Concatenation(0x00, 0x03, [0x00, 0x03, 0x01])], 'text': 'はい'}), (b'0591721891F101000B917228214365F700040C48656C6C6F20776F726C6421', # 8-bit data coding {'type': 'SMS-SUBMIT', 'number': '+27821234567', 'smsc': '+2781191', 'text': 'Hello world!'}), (b'0019000B917228001011F100003170013193008017D474BB3CA787DB70903DCC4E93D3F43C885E9ED301', # absolute validity period {'text': 'Timestamp validity test', 'validity': datetime(2013, 7, 10, 13, 39, tzinfo=SimpleOffsetTzInfo(2))}), # Semi-invalid status report PDU captured from a ZTE modem (b'0297F1061C0F910B487228297020F5317062419272803170624192138000', {'type': 'SMS-STATUS-REPORT', 'number': '+b08427829207025', # <- broken number (invalid PDU data; the reference number is more than a single byte (or they added something)) 'reference': 28, 'time': datetime(2013, 7, 26, 14, 29, 27, tzinfo=SimpleOffsetTzInfo(2)), 'discharge': datetime(2013, 7, 26, 14, 29, 31, tzinfo=SimpleOffsetTzInfo(2))}), (b'07919762020033F1400DD0CDF2396C7EBB010008415072411084618C0500035602010053004D005300200063006F00640065003A00200034003800350036002C00200063006F006E006600690072006D006100740069006F006E0020006F00660020006100730073006F00630069006100740069006F006E0020006200650074007700650065006E0020006100630063006F0075006E007400200061006E00640020004D00650067', {'type': 'SMS-DELIVER', 'smsc': '+79262000331', 'number': 'Megafon', 'text': 'SMS code: 4856, confirmation of association between account and Meg', 'time': datetime(2014, 5, 27, 14, 1, 48, tzinfo=SimpleOffsetTzInfo(4))}) ) for pdu, expected in tests: result = gsmmodem.pdu.decodeSmsPdu(pdu) self.assertIsInstance(result, dict) for key, value in expected.items(): self.assertIn(key, result) if key == 'udh': self.assertEqual(len(result[key]), len(value), 'Incorrect number of UDH information elements; expected {0}, got {1}'.format(len(result[key]), len(value))) for i in range(len(value)): got = result[key][i] expected = value[i] self.assertIsInstance(got, expected.__class__) self.assertEqual(expected.id, got.id) self.assertEqual(expected.dataLength, got.dataLength) self.assertEqual(expected.data, got.data) if isinstance(expected, gsmmodem.pdu.Concatenation): self.assertEqual(got.reference, expected.reference) self.assertEqual(got.parts, expected.parts) self.assertEqual(got.number, expected.number) elif isinstance(expected, gsmmodem.pdu.PortAddress): self.assertEqual(got.destination, expected.destination) self.assertEqual(got.source, expected.source) else: self.assertEqual(result[key], value, 'Failed to decode PDU value for "{0}". Expected "{1}", got "{2}".'.format(key, value, result[key])) def test_encodeSmsSubmit_concatenated(self): """ Tests concatenated SMS encoding """ tests = (('Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.Ut enim ad minim veniam, quinostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.Duis aute irure dolor in reprehenderit in voluptate velit esse cillum doloe eu fugiat nulla pariatur.Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum', '+15125551234', [b'0045000B915121551532F40000A0050003000301986F79B90D4AC3E7F53688FC66BFE5A0799A0E0AB7CB741668FC76CFCB637A995E9783C2E4343C3D4F8FD3EE33A8CC4ED359A079990C22BF41E5747DDE7E9341F4721BFE9683D2EE719A9C26D7DD74509D0E6287C56F791954A683C86FF65B5E06B5C36777181466A7E3F5B0AB4A0795DDE936284C06B5D3EE741B642FBBD3E1360B14AFA7DD', b'0045000B915121551532F40000A0050003000302DE73BABC4E0695F165F9384D0FD3D36F37A8CE6687DBE337881D16BFE5E939C89D9EA741753A28CC4EC7EB6938A88C0795C3A0F1BBDD7E93DFA0F1DB3D2FC7EB61BA8B584FCF41E13ABD0C4ACBEBF23288FC66BFE5A0B41B242FC3E56574D94D2ECBD37450DA0DB2BFD975383D4C2F83EC65769A0E2ACFE765D038CD66D7DB20F29BFD2E83CA', b'0045000B915121551532F400008C050003000303EA2073FD9C0ED341EE3A9B1D06C1C3F274985E97BB8AF871194E2FD7E5A079DA4D07BDC7E370791CA683C675789A1CA687E920F7DB0D82CBDF6972D94D6781E675371D947683C675363C0C8AD7D3A0B7D99C1EA7C32072795E96D7DD7450FBCD66A7E9A0B03BDD06A5C9A0F29C0E6287C56F79BD0D'] ),) for text, number, hexPdus in tests: result = gsmmodem.pdu.encodeSmsSubmitPdu(number, text, reference=0, requestStatusReport=False, rejectDuplicates=True) self.assertIsInstance(result, list) self.assertEqual(len(result), len(hexPdus), 'Invalid number of PDUs returned; expected {0}, got {1}'.format(len(hexPdus), len(result))) i = 0 for pdu in result: self.assertIsInstance(pdu, gsmmodem.pdu.Pdu) expectedPduHex = hexPdus[i] expectedPdu = bytearray(codecs.decode(expectedPduHex, 'hex_codec')) self.assertEqual(pdu.data, expectedPdu, 'Failed to encode concatentated SMS PDU (PDU {0}/{1}). Expected: "{2}", got: "{3}"'.format(i+1, len(result), expectedPduHex, codecs.encode(compat.str(pdu.data), 'hex_codec').upper())) i += 1 def test_encodeSmsSubmit_invalidValidityType(self): """ Tests SMS PDU encoding when specifying an invalid object type for validity """ self.assertRaises(TypeError, gsmmodem.pdu.encodeSmsSubmitPdu, **{'number': '123', 'text': 'abc', 'validity': 'INVALID'}) def test_decode_invalidPduType(self): """ Tests SMS PDU decoding when an invalid PDU type is specified """ # PDU first octect: 0x43; thus PDU type: 0x03 (invalid) pdu = '0043010C910661345542F60008A0050003000301306F3044' self.assertRaises(gsmmodem.exceptions.EncodingError, gsmmodem.pdu.decodeSmsPdu, pdu) def test_decode_invalidData(self): """ Tests SMS PDU decoding when completely invalid data is specified """ pdu = 'AFSDSDF LJJFKLDJKLFJ# #$KJLKJL SF' self.assertRaises(gsmmodem.exceptions.EncodingError, gsmmodem.pdu.decodeSmsPdu, pdu) pdu = 'AEFDSDFSDFSDFS' self.assertRaises(gsmmodem.exceptions.EncodingError, gsmmodem.pdu.decodeSmsPdu, pdu) def test_encode_Gsm7_divideSMS(self): """ Tests whether text will be devided into a correct number of chunks while using GSM-7 alphabet""" text = "12345-010 12345-020 12345-030 12345-040 12345-050 12345-060" self.assertEqual(len(gsmmodem.pdu.divideTextGsm7(text)), 1) text = "12345-010 12345-020 12345-030 12345-040 12345-050 12345-060 12345-070 12345-080 12345-090 12345-100 12345-010 12345-020 12345-030 12345-040 12345-050 123" self.assertEqual(len(gsmmodem.pdu.divideTextGsm7(text)), 1) text = "12345-010 12345-020 12345-030 12345-040 12345-050 12345-060 12345-070 12345-080 12345-090 12345-100 12345-010 12345-020 12345-030 12345-040 12345-050 1234" self.assertEqual(len(gsmmodem.pdu.divideTextGsm7(text)), 2) text = "12345-010 12345-020 12345-030 12345-040 12345-050 12345-060 12345-070 12345-080 12345-090 12345-100 12345-010 12345-020 12345-030 12345-040 12345-050 12]" self.assertEqual(len(gsmmodem.pdu.divideTextGsm7(text)), 2) text = "12345-010,12345-020,12345-030,12345-040,12345-050,12345-060,12345-070,12345-080,12345-090,12345-100,12345-110,12345-120,12345-130,12345-140,12345-150,[[[[[[[[" self.assertEqual(len(gsmmodem.pdu.divideTextGsm7(text)), 2) def test_encode_Ucs2_divideSMS(self): """ Tests whether text will be devided into a correct number of chunks while using UCS-2 alphabet""" text = "12345-010 12345-020 12345-030 12345-040 12345-050 12345-060" self.assertEqual(len(gsmmodem.pdu.divideTextUcs2(text)), 1) text = "12345-010 12345-020 12345-030 12345-040 12345-050 12345-060 1234567" self.assertEqual(len(gsmmodem.pdu.divideTextUcs2(text)), 1) text = "12345-010 12345-020 12345-030 12345-040 12345-050 12345-060 12345678" self.assertEqual(len(gsmmodem.pdu.divideTextUcs2(text)), 2) text = "12345-010 12345-020 12345-030 12345-040 12345-050 12345-060 123456[" self.assertEqual(len(gsmmodem.pdu.divideTextUcs2(text)), 1) text = "12345-010 12345-020 12345-030 12345-040 12345-050 12345-060 1234567[" self.assertEqual(len(gsmmodem.pdu.divideTextUcs2(text)), 2) text = "12345-010,12345-020,12345-030,12345-040,12345-050,12345-060,123456 12345-010,12345-020,12345-030,12345-040,12345-050,12345-060,1234567" self.assertEqual(len(gsmmodem.pdu.divideTextUcs2(text)), 2) if __name__ == "__main__": unittest.main() ================================================ FILE: test/test_serial_comms.py ================================================ #!/usr/bin/env python """ Test suite for gsmmodem.serial_comms """ from __future__ import print_function import sys, time, unittest, logging from copy import copy from . import compat # For Python 2.6 compatibility import gsmmodem.serial_comms from gsmmodem.exceptions import TimeoutException class MockSerialPackage(object): """ Fake serial package for the GsmModem/SerialComms classes to import during tests """ class Serial(): _REPONSE_TIME = 0.02 """ Mock serial object for use by the GsmModem class during tests """ def __init__(self, *args, **kwargs): # The default value to read/"return" if responseSequence isn't set up, or None for nothing #self.defaultResponse = 'OK\r\n' self.responseSequence = [] self.flushResponseSequence = True self.writeQueue = [] self._alive = True self._readQueue = [] self.writeCallbackFunc = None def read(self, timeout=None): if len(self._readQueue) > 0: return self._readQueue.pop(0) elif len(self.writeQueue) > 0: self._setupReadValue(self.writeQueue.pop(0)) if len(self._readQueue) > 0: return self._readQueue.pop(0) elif self.flushResponseSequence and len(self.responseSequence) > 0: self._setupReadValue(None) if timeout != None: time.sleep(0.001) # time.sleep(min(timeout, self._REPONSE_TIME)) # if timeout > self._REPONSE_TIME and len(self.writeQueue) == 0: # time.sleep(timeout - self._REPONSE_TIME) return '' else: while self._alive: if len(self.writeQueue) > 0: self._setupReadValue(self.writeQueue.pop(0)) if len(self._readQueue) > 0: return self._readQueue.pop(0) # time.sleep(self._REPONSE_TIME) time.sleep(0.05) def _setupReadValue(self, command): if len(self._readQueue) == 0: if len(self.responseSequence) > 0: value = self.responseSequence.pop(0) if type(value) in (float, int): time.sleep(value) if len(self.responseSequence) > 0: self._setupReadValue(command) else: self._readQueue = list(value) def write(self, data): if self.writeCallbackFunc != None: self.writeCallbackFunc(data) self.writeQueue.append(data) def close(self): pass def inWaiting(self): rqLen = len(self._readQueue) for item in self.responseSequence: if type(item) in (int, float): break else: rqLen += len(item) return rqLen class SerialException(Exception): """ Mock Serial Exception """ class TestNotifications(unittest.TestCase): """ Tests reading unsolicited notifications from the serial devices """ def setUp(self): self.mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = self.mockSerial self.tests = (['ABC\r\n'], [' blah blah blah \r\n', '12345\r\n']) def test_callback(self): """ Tests if the notification callback method is correctly called """ for test in self.tests: callbackCalled = [False] def callback(data): callbackCalled[0] = [True] self.assertIsInstance(data, list) self.assertEqual(len(data), len(test)) for i in range(len(test)): self.assertEqual(data[i], test[i][:-2]) serialComms = gsmmodem.serial_comms.SerialComms('-- PORT IGNORED DURING TESTS --', notifyCallbackFunc=callback) serialComms.connect() # Fake a notification serialComms.serial.responseSequence = copy(test) # Wait a bit for the event to be picked up while len(serialComms.serial._readQueue) > 0 or len(serialComms.serial.responseSequence) > 0: time.sleep(0.05) self.assertTrue(callbackCalled[0], 'Notification callback function not called') serialComms.close() def test_noCallback(self): """ Tests notifications when no callback method was specified (nothing should happen) """ for test in self.tests: serialComms = gsmmodem.serial_comms.SerialComms('-- PORT IGNORED DURING TESTS --') serialComms.connect() # Fake a notification serialComms.serial.responseSequence = copy(test) # Wait a bit for the event to be picked up while len(serialComms.serial._readQueue) > 0 or len(serialComms.serial.responseSequence) > 0: time.sleep(0.05) serialComms.close() class TestSerialException(unittest.TestCase): """ Tests SerialException handling """ def setUp(self): self.mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = self.mockSerial self.serialComms = gsmmodem.serial_comms.SerialComms('-- PORT IGNORED DURING TESTS --') self.serialComms.connect() def tearDown(self): self.serialComms.close() def test_readLoopException(self): """ Tests handling a SerialException from inside the read loop thread """ self.assertTrue(self.serialComms.alive) exceptionRaised = [False] callbackCalled = [False] def brokenRead(*args, **kwargs): exceptionRaised[0] = True raise MockSerialPackage.SerialException() self.serialComms.serial.read = brokenRead def errorCallback(ex): callbackCalled[0] = True self.assertIsInstance(ex, MockSerialPackage.SerialException) self.serialComms.fatalErrorCallback = errorCallback # Let the serial comms object attempt to read something self.serialComms.serial.responseSequence = ['12345\r\n'] while not exceptionRaised[0]: time.sleep(0.05) self.assertFalse(self.serialComms.alive) time.sleep(0.05) self.assertTrue(callbackCalled[0], 'Error callback not called on fatal error') class TestWrite(unittest.TestCase): """ Tests writing to the serial device """ def setUp(self): self.mockSerial = MockSerialPackage() gsmmodem.serial_comms.serial = self.mockSerial self.serialComms = gsmmodem.serial_comms.SerialComms('-- PORT IGNORED DURING TESTS --') self.serialComms.connect() def tearDown(self): self.serialComms.close() def test_write(self): """ Tests basic writing operations """ tests = ((['OK\r\n'], ['OK']), (['ERROR\r\n'], ['ERROR']), (['first line\r\n', 'second line\r\n', 'OK\r\n'], ['first line', 'second line', 'OK']), # Some Huawei modems issue this response instead of ERROR for unknown commands; ensure we detect it correctly (['COMMAND NOT SUPPORT\r\n'], ['COMMAND NOT SUPPORT'])) for actual, expected in tests: self.serialComms.serial.responseSequence = actual self.serialComms.serial.flushResponseSequence = True response = self.serialComms.write('test\r') self.assertEqual(response, expected) # Now write without expecting a response response = self.serialComms.write('test2\r', waitForResponse=False) self.assertEqual(response, None) def test_writeTimeout(self): """ Tests that the serial comms write timeout parameter """ # Serial comms will not response (no response sequence specified) self.assertRaises(TimeoutException, self.serialComms.write, 'test\r', waitForResponse=True, timeout=0.1) def test_writeTimeout_data(self): """ Tests passing partial data along with a TimeoutException """ self.serialComms.serial.responseSequence = ['abc\r\n', 0.5, 'def\r\n'] self.serialComms.serial.flushResponseSequence = True try: self.serialComms.write('test\r', waitForResponse=True, timeout=0.1) except TimeoutException as timeout: # The 0.5s pause in the response should cause the write to timeout but still return the first part self.assertEqual(timeout.data, ['abc']) else: self.fail('TimeoutException not thrown') def test_writeTimeout_noData(self): """ Similar to test_writeTimeout(), but checks TimeoutException's data field is None """ try: self.serialComms.write('test\r', waitForResponse=True, timeout=0.1) except TimeoutException as timeout: self.assertEqual(timeout.data, None) else: self.fail('TimeoutException not thrown') if __name__ == "__main__": logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG) unittest.main() ================================================ FILE: test/test_util.py ================================================ #!/usr/bin/env python """ Test suite for gsmmodem.util """ from __future__ import print_function import sys, time, unittest, logging, re from datetime import timedelta from . import compat # For Python 2.6 compatibility from gsmmodem.util import allLinesMatchingPattern, lineMatching, lineStartingWith, lineMatchingPattern, SimpleOffsetTzInfo, removeAtPrefix class TestUtil(unittest.TestCase): """ Tests misc utilities from gsmmodem.util """ def test_lineStartingWith(self): """ Tests function: lineStartingWith """ lines = ['12345', 'abc', 'defghi', 'abcdef', 'efg'] result = lineStartingWith('abc', lines) self.assertEqual(result, 'abc') result = lineStartingWith('d', lines) self.assertEqual(result, 'defghi') result = lineStartingWith('zzz', lines) self.assertEqual(result, None) def test_lineMatching(self): """ Tests function: lineMatching """ lines = ['12345', 'abc', 'defghi', 'abcdef', 'efg'] result = lineMatching('^abc.*$', lines) self.assertEqual(result.string, 'abc') result = lineMatching('^\d+$', lines) self.assertEqual(result.string, '12345') result = lineMatching('^ZZZ\d+$', lines) self.assertEqual(result, None) def test_lineMatchingPattern(self): """ Tests function: lineMatchingPattern """ lines = ['12345', 'abc', 'defghi', 'abcdef', 'efg'] result = lineMatchingPattern(re.compile('^abc.*$'), lines) self.assertEqual(result.string, 'abc') result = lineMatchingPattern(re.compile('^\d+$'), lines) self.assertEqual(result.string, '12345') result = lineMatchingPattern(re.compile('^ZZZ\d+$'), lines) self.assertEqual(result, None) def test_allLinesMatchingPattern(self): """ Tests function: lineStartingWith """ lines = ['12345', 'abc', 'defghi', 'abcdef', 'efg'] result = allLinesMatchingPattern(re.compile('^abc.*$'), lines) self.assertIsInstance(result, list) self.assertEqual(len(result), 2) self.assertEqual(result[0].string, 'abc') self.assertEqual(result[1].string, 'abcdef') result = allLinesMatchingPattern(re.compile('^defghi$'), lines) self.assertIsInstance(result, list) self.assertEqual(len(result), 1) self.assertEqual(result[0].string, 'defghi') result = allLinesMatchingPattern(re.compile('^\d+$'), lines) self.assertIsInstance(result, list) self.assertEqual(len(result), 1) self.assertEqual(result[0].string, '12345') result = allLinesMatchingPattern(re.compile('^ZZZ\d+$'), lines) self.assertIsInstance(result, list) self.assertEqual(result, []) def test_SimpleOffsetTzInfo(self): """ Basic test for the SimpleOffsetTzInfo class """ tests = (2, -4, 0, 3.5) for hours in tests: tz = SimpleOffsetTzInfo(hours) self.assertEqual(tz.offsetInHours, hours) self.assertEqual(tz.utcoffset(None), timedelta(hours=hours)) self.assertEqual(tz.dst(None), timedelta(0)) self.assertIsInstance(tz.__repr__(), str) def test_removeAtPrefix(self): """ Tests function: removeAtPrefix""" tests = (('AT+CLAC', '+CLAC'), ('ATZ', 'Z'), ('+CLAC', '+CLAC'), ('Z', 'Z')) for src, dst in tests: res = removeAtPrefix(src) self.assertEqual(res, dst) if __name__ == "__main__": logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG) unittest.main() ================================================ FILE: tools/at_cmd_init_modem.txt ================================================ # Simple script for GSMTerm to initialize the modem with a few common settings # Load this from within GSMTerm by typing: load /path/to/at_cmd_init_modem.txt ATZ ATE0 AT+CFUN=1 AT+CMEE=1 AT+WIND=0 # Network-related settings AT+COPS=3,0 # Setup incoming calls AT+CLIP=1 AT+CRC=1 AT+CVHU=0 # Setup SMS AT+CMGF=1 AT+CSMP=49,167,0,0 AT+CPMS="ME","ME","ME" AT+CNMI=2,1,0,2 ================================================ FILE: tools/gsmterm.py ================================================ #!/usr/bin/env python """\ Launch script for GSMTerm @author: Francois Aucamp """ from __future__ import print_function import sys from gsmtermlib.terminal import GsmTerm, RawTerm def parseArgs(): """ Argument parser for Python 2.7 and above """ from argparse import ArgumentParser parser = ArgumentParser(description='User-friendly terminal for interacting with a connected GSM modem.') parser.add_argument('port', metavar='PORT', help='port to which the GSM modem is connected; a number or a device name.') parser.add_argument('-b', '--baud', metavar='BAUDRATE', default=115200, help='set baud rate') parser.add_argument('-r', '--raw', action='store_true', help='switch to raw terminal mode') return parser.parse_args() def parseArgsPy26(): """ Argument parser for Python 2.6 """ from gsmtermlib.posoptparse import PosOptionParser, Option parser = PosOptionParser(description='User-friendly terminal for interacting with a connected GSM modem.') parser.add_positional_argument(Option('--port', metavar='PORT', help='port to which the GSM modem is connected; a number or a device name.')) parser.add_option('-b', '--baud', metavar='BAUDRATE', default=115200, help='set baud rate') parser.add_option('-r', '--raw', action='store_true', help='switch to raw terminal mode') options, args = parser.parse_args() if len(args) != 1: parser.error('Incorrect number of arguments - please specify a PORT to connect to, e.g. {0} /dev/ttyUSB0'.format(sys.argv[0])) else: options.port = args[0] return options def main(): args = parseArgsPy26() if sys.version_info[0] == 2 and sys.version_info[1] < 7 else parseArgs() if args.raw: gsmTerm = RawTerm(args.port, args.baud) else: gsmTerm = GsmTerm(args.port, args.baud) gsmTerm.start() gsmTerm.rxThread.join() print('Done.') if __name__ == '__main__': main() ================================================ FILE: tools/gsmtermlib/__init__.py ================================================ ================================================ FILE: tools/gsmtermlib/atcommands.py ================================================ CATEGORIES = ('General', 'Call Control', 'Network Service', 'Security', 'Phonebook', 'SMS', 'Supplementary Services', 'Data', 'Fax', 'Fax Class 2', 'V24-V25', 'Specific') c = CATEGORIES # Format: (COMMAND, (CATEGORY, NAME, VALUES, DEFAULT, DESCRIPTION)) ATCOMMANDS = ( # General ('AT+CGMI', (c[0], 'Manufacturer Identification', None, None, 'Displays the manufacturer identification.')), ('AT+CGMM', (c[0], 'Request Model Identification', None, None, 'Displays the supported frequency bands. With multi-band products the response may be a combination of different bands.')), ('AT+CGMR', (c[0], 'Request Revision Identification', None, None, 'Displays the revised software version.')), ('AT+CGSN', (c[0], 'Product Serial Number', None, None, 'Allows the user application to get the IMEI (International Mobile Equipment Identity, 15-digit number) of the product.')), ('AT+CSCS', (c[0], 'Select TE Character Set', (('', """GSM - GSM default alphabet (default value). PCCP437 - PC character set code page 437. CUSTOM - User defined character set (cf. +WCCS command). HEX - Hexadecimal mode. No character set used; the user can read or write hexadecimal values."""),), None, 'Informs the ME which character set is used by the TE. The ME can convert each character of \ entered or displayed strings. This is used to send, read or write short messages. See also +WPCS for the phonebooks\' character sets.')), ('AT+WPCS', (c[0], 'Phonebook Character Set', (('', """TRANSPARENT - Transparent mode. The strings are displayed and entered as they are stored in SIM or in ME. CUSTOM - User defined character set (cf. +WCCS command). HEX - Hexadecimal mode. No character set used; the user can read or write hexadecimal values."""),), None, 'Informs the ME which character set is used by the TE for the phonebooks. The ME can convert\ each character of entered or displayed strings. This is used to read or write phonebook entries.\ See also +CSCS for the short messages character sets.')), ('AT+CIMI', (c[0], 'Request IMSI', None, None, 'Reads and identifies the IMSI (International Mobile Subscriber Identity) of the SIM card. The PIN may need to be entered before reading the IMSI')), ('AT+CCID', (c[0], 'Card Identification', None, None, 'Orders the product to read the EF-CCID file on the SIM card.')), ('AT+GCAP', (c[0], 'Capabilities List', None, None, 'Displays the complete list of capabilities.')), ('A/', (c[0], 'Repeat Last Command', None, None, 'Repeats the previous command. Only the A/ command itself cannot be repeated.')), ('AT+CPOF', (c[0], 'Power Off', None, None, 'Stops the GSM software stack as well as the hardware layer. The AT+CFUN=0 command is equivalent to +CPOF.')), ('AT+CFUN', (c[0], 'Set Phone Functionality', (('', """0: Set minimum functionality; IMSI detach procedure 1: Set the full functionality mode with a complete software reset"""),), None, "Selects the mobile station's level of functionality. When the application wants to stop the \ product with a power off, or if the application wants to force the product to execute an IMSI \ DETACH procedure, then it must send: AT+CFUN=0 (equivalent to AT+CPOF). This command \ executes an IMSI DETACH and makes a backup copy of some internal parameters in SIM and \ in EEPROM. The SIM card cannot then be accessed. If the mobile equipment is not powered \ off by the application after this command has been sent, a re-start command (AT+CFUN=1) will \ have to issued to restart the whole GSM registration process. If the mobile equipment is turned \ off after this command, then a power on will automatically restart the whole GSM process. The \ AT+CFUN=1 command restarts the entire GSM stack and GSM functionality: a complete \ software reset is performed. All parameters are reset to their previous values if AT&W was not \ used. If you write entries in the phonebook (+CPBW) and then reset the product directly \ (AT+CFUN=1, with no previous AT+CFUN=0 command), some entries may not be written (the \ SIM task does not have enough time to write entries in the SIM card). In addition, the OK \ response will be sent at the last baud rate defined by the +IPR command. With the \ autobauding mode the response can be at a different baud rate, it is therefore preferable to \ save the defined baud rate with AT&W before directly sending the AT+CFUN=1 command.")), ('AT+CPAS', (c[0], 'Phone Activity Status', None, None, """Returns the activity status of the mobile equipment. Response is: +CPAS: where is: 0 ready (allow commands from TA/TE) 1 unavailable (does not allow commands) 2 unknown 3 ringing (ringer is active) 4 call in progress 5 asleep (low functionality)""")), ('AT+CMEE', (c[0], 'Report Mobile Equipment Errors', (('', """0: Disable ME error reports; use only ERROR 1: Enable +CME ERROR: or +CMS ERROR: 2: Enable error result codes with verbose (string) values"""),), None, 'Disables or enables the use of the "+CME ERROR: " or "+CMS ERROR:" result code instead of simply "ERROR".')), ('AT+CKPD', (c[0], 'Keypad Control', (('', 'Keyboard sequence; string of the following characters (0-9, *, #)'),), None, 'Emulates the ME keypad by sending each keystroke as a character in a string.\n\ If emulation fails, a +CME ERROR: is returned. If emulation succeeds, the result \ depends on the GSM sequence activated.')), ('AT+CCLK', (c[0], 'Clock Management', (('', 'String format for date/time is "yy/MM/dd,hh:mm:ss"\nNote: Valid years are 98 (for 1998) to 97 (for 2097). The seconds field is not mandatory.'),), None, 'Sets or gets the current date and time of the ME real-time clock.')), ('AT+CALA', (c[0], 'Alarm Management', (('', 'String format for alarms: "yy/MM/dd,hh:mm:ss" (see +CCLK)\nNote: Seconds are taken into account.'), ('', 'Offset in the alarm list, range 1 to 16')), None, 'Sets the alarm date/time in the ME. The maximum number of alarms is 16.')), # Call Control ('ATD', (c[1], 'Dial command', (('', 'Destination phone number'),), None, "The ATD command sets a voice, data or fax call. As per GSM 02.30, the dial command also \ controls supplementary services.\n\ For a data or a fax call, the application sends the following ASCII string to the product (the bearer must \ be previously selected with the +CBST command):\n\ ATD where is the destination phone number;\n\ For a voice call, the application sends the following ASCII string to the product: (the bearer may be \ selected previously, if not a default bearer is used).\n\ ATD; where is the destination phone number.\n\ Please note that for an international number, the local international prefix does not need to be set \ (usually 00) but does need to be replaced by the '+' character.\n\ \nThere are other varieties of the ATD command available (using phonebook memory); please search online for help on these.")), ('ATH', (c[1], 'Hang-Up command', (('', '0: Ask for disconnection (default value)\n1: Ask for outgoing call disconnection'),), None, 'The ATH (or ATH0) command disconnects the remote user. In the case of multiple calls, all calls are released \ (active, on-hold and waiting calls). The specific ATH1 command has been appended to disconnect the current \ outgoing call, only in dialing or alerting state (ie. ATH1 can be used only after the ATD command, and before \ its terminal response (OK, NO CARRIER, ...). It can be useful in the case of multiple calls.')), ('ATA', (c[1], 'Answer a Call', None, None, 'When the product receives a call, it sets the RingInd signal and sends the ASCII "RING" or "+CRING: \ " string to the application (+CRING if the cellular result code +CRC is enabled). Then it waits for the \ application to accept the call with the ATA command.')), ('AT+CEER', (c[1], 'Extended Error Report', None, None, 'This command gives the cause of call release when the last call set up (originating or answering) failed.')), ('AT+VTD', (c[1], 'DTMF Signals - set tone duration', (('', 'tone duration.\n*100 is the duration in ms. If < 4, tone duration is 300 ms; if n > 255, the value used is modulo 256.\nDefault value: 300 ms, that is = 3.'),), None, 'The modem can send DTMF tones over the GSM network. This command is used \ to define tone duration (the default value is 300ms). To define this duration, the application uses:\n\ AT+VTD= where *100 gives the duration in ms. If n < 4, tone duration is 300 ms.\n\ See also: AT+VTS')), ('AT+VTS', (c[1], 'DTMF Signals - send tone', (('', 'DTMF tone to transmit. Tone is in {0-9, *, #, A, B, C, D}'),), None, 'The modem can send DTMF tones over the GSM network. This command enables \ tones to be transmitted only when there is an active call.\nSee also: AT+VTD\n\n\ Example:\n\ To send tone sequence 13#, the application sends:\n\ AT+VTS=1;+VTS=3;+VTS=#')), ('ATDL', (c[1], 'Redial Last Telephone Number', None, None, 'This command redials the last number used in the ATD command. The last number dialed is displayed followed by ";" for voice calls only.')), ('AT%D', (c[1], 'Automatic Dialing with DTR', (('', 'Enable or disables automatic message transmission or number dialing.\n\ Informs the product that the number is a voice rather than a fax or data number.\n\ 0 Disables automatic DTR number dialing / message transmission.\n\ 1; Enables automatic DTR dialing if DTR switches from OFF to ON; Dials the phone number in the first \ location of the ADN phonebook. Voice call.\n\ 1 Activates automatic DTR dialing if DTR switches from OFF to ON; Dials the phone number in the first \ location of the ADN phonebook. Data or Fax call.\n\ 2 Activates automatic DTR message transmission if DTR switches from OFF to ON.'),), None, 'This command enables and disables:\n\ - Automatic dialing of the phone number stored in the first location of the ADN phonebook,\n\ - Automatic sending of the short message (SMS) stored in the first location of the SIM.\n\ The number is dialed when DTR OFF switches ON. The short message is sent when DTR OFF switches ON.')), ('ATS0', (c[1], 'Automatic Answer', (('', 'is the number of rings before automatic answer (3 characters padded with zeros)\n\ Range of values is 0 to 255'),), None, 'This S0 parameter determines and controls the modem automatic answering mode.')), ('AT+CICB', (c[1], 'Incoming Call Bearer', (('', '0: Data\n1: Fax\n2: Speech'),), None, 'This command sets the type of incoming calls when no incoming bearer is given (see +CSNS).\nNote: Setting the +CICB command affects the current value of +CSNS.')), ('AT+CSNS', (c[1], 'Single Numbering Scheme', (('', '0: Voice\n2: Fax\n4: Data'),), None, 'This command selects the bearer to be used when an MT single numbering scheme call is set up (see +CICB).\nNote: Setting the +CSNS command affects the current value of +CICB.')), ('AT+VGR', (c[1], 'Gain Control - Reception', (('', 'reception gain'),), None, 'This command is used by the application to tune the receive gain of the speaker.')), ('AT+VGT', (c[1], 'Gain Control - Transmission', (('', 'transmission gain'),), None, 'This command is used by the application to tune the transmit gain of the microphone.')), ('AT+CMUT', (c[1], 'Microphone Mute Control', (('', '0: microphone mute off (default)\n1: microphone mute on'),), None, 'This command mutes the microphone input on the device. This command is only allowed during a call.')), ('AT+CVHU', (c[1], 'Voice Hangup Control', (('', '0: "Drop DTR" ignored but OK response given. ATH disconnects.\n\ 1: "Drop DTR" and ATH ignored but OK response given.\n\ 2: "Drop DTR" behavior according to &D setting. ATH disconnects.'),), None, 'This command selects whether ATH or "drop DTR" causes a voice connection to be disconnected or not. Voice connection also includes alternating mode calls that are currently in voice mode.\n\ When =2, this command must be viewed in conjunction with the V.25ter command &D, or &D will be ignored')), # Network Service ('AT+CSQ', (c[2], 'Signal Quality', None, (('', '0: -113 dBm or less\n1: -111 dBm\n2 to 30: -109 to -53 dBm\n31: -51dBm or greater\n99: not known or not detectable'), ('', '0...7: as RXQUAL values in the table GSM 05.08')), 'This command determines the received signal strength indication () and the channel bit error rate () with or without a SIM card inserted')), ('AT+COPS', (c[2], 'Operator Selection', (('', '0: automatic (default value)\n1: manual\n2: deregistration; ME will be unregistered until =0 or 1 is selected.\n3: set only (for read command AT+COPS?)\n4: manual / automatic ( shall be present), if manual selection fails, automatic mode is entered.\n: format of field'), ('', '0: long alphanumeric format \n1: short alphanumeric format \n2: numeric (default value) : status of \n\n\n 0: unknown\n 1: available\n 2: current\n 3: forbidden'), ('', 'operator identifier (MCC/MNC in numeric format only for operator selection)\nThe long alphanumeric format can be up to 16 characters long. The short alphanumeric format can be up to 8 characters long.'), ), None, 'Select the Network Operator.')), ('AT+CREG', (c[2], 'Network Registration', (('', '0: Disable network registration unsolicited result code (default)\n1: Enable network registration code result code +CREG: \n2: Enable network registration and location information unsolicited result code +CREG: ,, if there is a change of network cell.'),), (('', '0: not registered, ME is not currently searching for a new operator.\n\ 1: registered, home network.\n2: not registered, ME currently searching for a new operator to register to.\n\ 3: registration denied.\n4: unknown.\n5: registered, roaming.'), ('', 'string type; two byte location area code in hexadecimal format'), ('', 'string type; two byte cell ID in hexadecimal format')), 'This command is used by the application to ascertain the registration status of the device.')), ('AT+WOPN', (c[2], 'Read Operator Name')), ('AT+WOPN', (c[2], 'Selection of Preferred PLMN List')), ('AT+CPLS', (c[2], 'Selection of Preferred PLMN List')), ('AT+CPOL', (c[2], 'Preferred Operator List')), ('AT+COPN', (c[2], 'Read Operator Name', None, (('', 'the operator in numeric format'), ('', 'the operator in long alphanumeric format')), 'This command returns the list of all operator names (in numeric and alphanumeric format) stored in the module.')), # Security ('AT+CPIN', (c[3], 'Enter PIN', (('', 'the personal identification number'), ('', 'the personal unblocking key needed to change the PIN; syntax: AT+CPIN=,')), None, 'This command enters the ME passwords (CHV1 / CHV2 / PUK1 / PUK2, etc.), that are required before any ME functionality can be used.')), ('AT+CPIN2', (c[3], 'Enter PIN2', (('', 'the personal identification number 2'), ('', 'the personal unblocking key 2 needed to change the PIN 2; syntax: AT+CPIN=,')), None, 'This command validates the PIN2 code (CHV2) or the PUK2 code (UNBLOCK CHV2) and defines a new \ PIN2 code. Of course, the +CPIN command allows PIN2 or PUK2 codes to be validated, but only when the \ last command executed resulted in PIN2 authentication failure. PIN2 length is between 4 and 8 digits; PUK2 \ length is 8 digits only.')), ('AT+CPINC', (c[3], 'PIN Remaining Attempt Number', None, (('', 'attempts left for PIN1 (0 = blocked, 3 max)'), ('', 'attempts left for PIN2 (0 = blocked, 3 max)'), ('', 'attempts left for PUK1 (0 = blocked, 10 max)'), ('', 'attempts left for PUK2 (0 = blocked, 10 max)')), 'This command gets the number of valid attempts for PIN1 (CHV1), PIN2 (CHV2), PUK1 (UNBLOCK CHV1) and PUK2 (UNBLOCK CHV2) identifiers.')), ('AT+CLCK', (c[3], 'Call Barring')), ('AT+CPWD', (c[3], 'Change Password')), # Phonebook ('AT+CPBS', (c[4], 'Select Phonebook Memory Storage', (('"SM"', 'ADN (SIM phonebook)'), ('"FD"', 'FDN (SIM Fix Dialing, restricted phonebook)'), ('"ON"', 'MSISDN (SIM own numbers)'), ('"EN"', 'EN (SIM emergency number)'), ('"LD"', 'LND (combined ME and SIM last dialing phonebook)'), ('"MC"', 'MSD (ME missed calls list)'), ('"ME"', 'ME (ME phonebook)'), ('"MT"', 'MT (combined ME and SIM phonebook)'), ('"RC"', 'LIC (ME received calls list)'), ('"SN"', 'SDN (Services dialing phonebook)')), None, 'This command selects phonebook memory storage.', 'Available Phonebooks:')), ('AT+CPBR', (c[4], 'Read Phonebook Entries', (('', 'Location of phonebook entry or start of range of locations (if is specified) of the phonebook entries'), ('', 'End of range of locations of the phonebook entries')), None, 'This command returns phonebook entries for a range of locations from the current phonebook memory storage selected with +CPBS.')), ('AT+CPBF', (c[4], 'Find Phonebook Entries', (('', 'Searched starting string (depends on the format of the data stored in the phonebooks'),), None, 'This command returns phonebook entries with alphanumeric fields starting with a given string. The \ +CPBF command can be used to display all phonebook entries sorted in alphabetical order. This command is not allowed for "LD", "RC", "MC", "SN" or "EN" phonebooks, which do not contain alphanumeric fields.')), ('AT+CPBW', (c[4], 'Write Phonebook Entry', (('', 'Integer type value depending on the capacity of the phonebook memory'), ('', 'Phone number in ASCII format'), ('', 'TON/NPI (Type of address byte in integer format)'), ('text>', 'Text label/name of entry. String type')), None, 'This command writes a phonebook entry in location number in the current phonebook memory storage.\n\ This command is not allowed for "EN", "LD", "MC", "RC", "MT", and "SN" phonebooks (they cannot be written).')), ('AT+CPBP', (c[4], 'Phonebook Phone Search')), ('AT+CPBN', (c[4], 'Move Action in Phonebook')), ('AT+CNUM', (c[4], 'Subscriber Number', None, (('', 'optional alphanumeric string associated with '), ('', 'string type phone number with format as specified by '), ('', 'type of address byte in integer format')), 'This command returns the subscriber MSISDN(s). If the subscriber has different MSISDNs for different services, each MSISDN is returned in a separate line.')), ('AT+WAIP', (c[4], 'Avoid Phonebook Initialization')), ('AT+WDCP', (c[4], 'Delete Calls Phonebook')), ('AT+CSVM', (c[4], 'Set Voice Mail Number')), # Short Messages (SMS) ('AT+CSMS', (c[5], 'Select Message Service', (('', '0: SMS AT commands are compatible with GSM 07.05 Phase 2 version 4.7.0.\n\ 1: SMS AT commands are compatible with GSM 07.05 Phase 2 + version'),), None, 'The supported services include originated (SMS-MO) and terminated short messages (SMS-MT) as well as Cell Broadcast Message (SMS-CB) services.')), ('AT+CNMA', (c[5], 'New Message Acknowledgment', (('', '0: send RP-ACK without PDU (same as TEXT mode)\n\ 1: send RP-ACK with optional PDU message\n2: send RP-ERROR with optional PDU message'), ('', 'Length of the PDU message')), None, 'This command allows reception of a new message routed directly to the TE to be acknowledged.\n\ In TEXT mode, only positive acknowledgement to the network (RP-ACK) is possible.\n\ In PDU mode, either positive (RP-ACK) or negative (RP-ERROR) acknowledgement to the network is possible.\n\ Acknowledgement with +CNMA is possible only if the +CSMS parameter is set to 1 (+CSMS=1) when a \ +CMT or +CDS indication is shown (see +CNMI command).\n\ If no acknowledgement occurs within the network timeout, an RP-ERROR is sent to the network. The \ and parameters of the +CNMI command are then reset to zero (do not show new message indication).')), ('AT+CPMS', (c[5], 'Preferred Message Storage', (('', 'Memory used to list, read and delete messages. It can be:\n\ "SM": SMS message storage in SIM (default)\n\ "BM": CBM message storage (in volatile memory).\n\ "SR": Status Report message storage (in SIM if the EF-SMR file exists, otherwise in the ME non volatile memory)\n\ Note: "SR" ME non-volatile memory is cleared when another SIM card is inserted. It is kept, even after a reset, while the same SIM card is used.'), ('', 'Memory to be used to write and send messages\n "SM": SMS message storage in SIM (default)')), (('', 'Used memory 1'),('total1', 'Total memory 1'),('', 'Used memory 2'),('total2', 'Total memory 2')), 'This command allows the message storage area to be selected (for reading, writing, etc).')), ('AT+CMGF', (c[5], 'Preferred Message Format', (('', '0: PDU mode\n1: Text mode'),), None, 'The message formats supported are text mode and PDU mode.')), ('AT+CSAS', (c[5], 'Save Settings')), ('AT+CRES', (c[5], 'Restore Settings')), ('AT+CNMI', (c[5], 'New Message Indication', (('', 'Controls the processing of unsolicited result codes. Values:\n\ 0: Buffer unsolicited result codes in the TA. If TA result code buffer is full, indications can be buffered in \ some other place, or the oldest indications may be discarded and replaced with the new received indications\n\ 1: Discard indication and reject new received message unsolicited result codes when TA-TE link is reserved. Otherwise forward them directly to the TE\n\ 2: Buffer unsolicited result codes in the TA when TA-TE link is reserved and flush them to the TE after reservation. Otherwise forward them directly to the TE\n\ 3: Forward unsolicited result codes directly to the TE. TA-TE link specific inband used to embed result codes and data when TA is in on-line data mode'), ('', 'Sets the result code indication routing for SM-DELIVERs. Default is 0. Values:\n\ 0: No SMS-DELIVER indications are routed.\n\ 1: SMS-DELIVERs are routed using unsolicited code: +CMTI: "SM",\n\ 2: SMS-DELIVERs (except class 2 messages) are routed using unsolicited code: +CMT: [,]\ (PDU mode) or +CMT: ,[,] [,, , , ,, , ] (text mode)\n\ 3: Class 3 SMS-DELIVERs are routed directly using code in =2 ; Message of other classes result in indication =1'), ('', 'Set the rules for storing received CBMs (Cell Broadcast Message). Default is 0. Values:\n\ 0: No CBM indications are routed to the TE. The CBMs are stored.\n\ 1: The CBM is stored and an indication of the memory location is routed to the customer application using unsolicited result code: +CBMI: "BM", \n\ 2: New CBMs are routed directly to the TE using unsolicited result code. +CBM: (PDU mode) or +CBM:,,,,(Text mode) \n\ 3: Class 3 CBMs: as =2. Other classes CBMs: as =1.'), ('', 'for SMS-STATUS-REPORTs. Default is 0. Values:\n\ 0: No SMS-STATUS-REPORTs are routed.\n\ 1: SMS-STATUS-REPORTs are routed using unsolicited code: +CDS: (PDU\n\ mode) or +CDS: ,, [] , [], ,
, (Text mode)\n\ 2: SMS-STATUS-REPORTs are stored and routed using the unsolicited result code: +CDSI: "SR",'), ('', 'Default is 0. Values:\n\ 0: TA buffer of unsolicited result codes defined within this command is flushed to the TE when 1...3\ is entered (OK response shall be given before flushing the codes)\n\ 1: TA buffer of unsolicited result codes defined within this command is cleared when 1...3 is entered.')), None, 'This command selects the procedure for message reception from the network.')), ('AT+CMGR', (c[5], 'Read Message', (('', 'Location of message to read'),), None, 'This command allows the application to read stored messages. The messages are read from the memory selected by the +CPMS command.')), ('AT+CMGL', (c[5], 'List Message', (('', 'Status of messages in memory to list. Allowed values are:\n\ Text mode PDU mode\n\ "REC UNREAD" 0\n\ "REC READ" 1\n\ "STO UNSENT" 2\n\ "STO SENT" 3\n\ "ALL" 4'),), None, 'This command allows the application to read stored messages, by indicating the type of the message to read. The messages are read from the memory selected by the +CPMS command.')), ('AT+CMGS', (c[5], 'Send Message', (('', 'Destination Address (text mode) - message destination phone number'),), None, 'The
field is the address of the terminal to which the message is sent. To send the message, \ simply type, character (ASCII 26). The text can contain all existing characters except and \ (ASCII 27). This command can be aborted using the character when entering text. In PDU \ mode, only hexadecimal characters are used (\'0\'...\'9\',\'A\'...\'F\').')), ('AT+CMGW', (c[5], 'Write Message to Memory')), ('AT+CMSS', (c[5], 'Send Message from Storage', (('', 'location of stored message'), ('', 'destination address'), ('', 'type of destination address')), (('', 'message reference'),), 'This command sends a message stored at location value .')), ('AT+CSMP', (c[5], 'Set Text Mode Parameters', (('', 'byte comprising 6 fields, RP, UDHI, SRR, VPFG, RD and MTI'), ('', 'validity period'), ('', 'used to indicate the higher layer protocol being used or indicates interworking with a certain type of \ telematic device. For example, 0x22 is for group 3 telefax, 0x24 is for voice telephone, 0x25 is for ERMES.'), ('', 'used to determine the way the information is encoded.')), None, 'This command selects a value for , , and .')), ('AT+CMGD', (c[5], 'Delete Message', (('', 'Integer type values in the range of location numbers of SIM Message memory when the preferred message storage is "SM or "SR".'), ('', '0: Delete message at location .\n\ 1: Delete All READ messages\n\ 2: Delete All READ and SENT messages\n\ 3: Delete All READ, SENT and UNSENT messages\n\ 4: Delete All messages.')), None, 'This command deletes one or several messages from preferred message storage.')), ('AT+CSCA', (c[5], 'Service Center Address', (('', 'service center address'),), None, 'This command sets (or queries) the service center to which SMS messages must be sent.')), ('AT+CSCB', (c[5], 'Select Cell Broadcast Message Types', (('', '0 - CBM reception activated\n1 - CBM reception deactivated'), ('', 'message identifiers - indicates the type of message idenitfiers for which the ME should listen'), ('', 'Supported Languages')), None, 'This command selects which types of CBMs are to be received by the ME.')), ('AT+WCBM', (c[5], 'Cell Broadcast Message Identifiers')), ('AT+WMSC', (c[5], 'Message Status Modification')), ('AT+WMGO', (c[5], 'Message Overwriting')), ('AT+WUSS', (c[5], 'Unchange SMS Status')), # Supplementary Services ('AT+CCFC', (c[6], 'Call Forwarding')), ('AT+CCWA', (c[6], 'Call Waiting')), ('AT+CLIR', (c[6], 'Calling Line Identification Restriction', (('', 'Sets the line ID restriction for outgoing calls. Values:\n\ 0: Presentation indicator is used according to the subscription of the CLIR service\n\ 1: CLIR invocation\n2: CLIR suppression'),), (('', 'Shows the subscriber CLIR status in the network. Values:\n\ 0: CLIR not provisioned\n1: CLIR provisioned in permanent mode\n2: Unknown (no network...)\n3: CLIR temporary mode presentation restricted\n4: CLIR temporary mode presentation allowed'),), 'This command controls the Calling Line Identification restriction supplementary service.')), ('AT+CLIP', (c[6], 'Calling Line Identification Presentation', (('', 'Parameter sets/shows the result code presentation in the TA. Values:\n0: Disable\n1: Enable'),), (('', 'Parameter shows the subscriber CLIP service status in the network. Values:\n0: CLIP not provisioned\n1: CLIP provisioned\n2: Unknown (no network...)'),), 'This command controls the calling line identification presentation supplementary service. When presentation \ of the CLI (Calling Line Identification) is enabled (and calling subscriber allows), +CLIP response is returned \ after every RING (or +CRING) result code.')), ('AT+COLP', (c[6], 'Connected Line Identification Presentation')), ('AT+CAOC', (c[6], 'Advice Of Charge')), ('AT+CAMM', (c[6], 'Accumulated Call Meter Maximum')), ('AT+CPUC', (c[6], 'Price Per Unit and Currency Table')), ('AT+CHLD', (c[6], 'Call Related Supplementary Services')), ('AT+CHLD', (c[6], 'Call Related Supplementary Services')), ('AT+CLCC', (c[6], 'List Current Calls', None, (('', 'integer type, call identification as described in GSM 02.30'), ('', '0: mobile originated (MO) call\n2: mobile terminated (MT) call'), ('', '0: active\n\ 1: held\n\ 2: dialing (MO call)\n\ 3: alerting (MO call)\n\ 4: incoming (MT call)\n\ 5: waiting (MT call)'), ('', '0: voice\n\ 1: data\n\ 2: fax\n\ 9: unknown'), ('', '0: call is not one of multiparty (conference) call parties\n\ 1: call is one of multiparty (conference) call parties'), ('', 'string type phone number in format specified by '), ('', 'type of address byte in integer format'), ('', 'optional string type alphanumeric representation of corresponding to the entry found in phonebook')), 'This command returns a list of current calls.')), ('AT+CSSN', (c[6], 'Supplementary Service Notifications')), ('AT+CUSD', (c[6], 'Unstructured Supplementary Service Data', (('', '0: Disable the result code presentation\n\ 1: Enable the result code presentation\n\ 2: Cancel session (not applicable to read command response)'), ('', 'network string, converted in the selected character set'), ('', 'the data coding scheme received (TSM TS 03.38)')), (('', '0: no further user action required (network initiated USSD-Notify, or no further information needed after mobile initiated operation)\n\ 1: further user action required (network initiated USSD-Request, or further information needed after mobile initiated operation)\n\ 2: USSD terminated by network\n\ 4: Operation not supported'),), 'The USSD supplementary service is described in GSM 02.90. It is based on sequences of digits which may \ be entered by a mobile user with a handset. A sequence entered is sent to the network which replies with an \ alphanumerical string, for display only, or for display plus request for the next sequence.\n\ This command is used to:\n\ - Enable or disable the CUSD indication sent to the application by the product when an incoming USSD is received\n\ - Send and receive USSD strings')), ('AT+CCUG', (c[6], 'Closed User Group')), # Data ('AT+CBST', (c[7], 'Bearer Type Selection')), ('AT+FCLASS', (c[7], 'Select Mode', (('', '0: Data\n1: Fax class 1\n2: Fax class 2'),), None, 'This command puts the modem into a particular operating mode (data or fax).')), ('AT+CR', (c[7], 'Service Reporting Control')), ('AT+CRC', (c[7], 'Cellular Result Dodes', (('', '0: Disable extended reports\n1: Enable extended reports'),), None, 'This command shows more detailed ring information for an incoming call (voice or data). Instead of the string \ "RING", an extended string is used to indicate which type of call is ringing (e.g. +CRING: VOICE).\n\ These extended indications are:\n\ +CRING: ASYNC for asynchronous transparent\n\ +CRING: REL ASYNC for asynchronous non-transparent\n\ +CRING: VOICE for normal speech.\n\ +CRING: FAX for fax calls')), ('AT+ILRR', (c[7], 'DTE-DCE Local Rate Reporting')), ('AT+CRLP', (c[7], 'Radio Link Protocol Parameters')), ('AT+DOPT', (c[7], 'Radio Link Protocol Parameters')), ('AT+CGDCONT', (c[7], 'Define PDP Context', (('', 'PDP Context Identifier - a numeric parameter (1-32) which specifies a particular \ PDP context definition. The parameter is local to the TE-MT interface and is used in \ other PDP context-related commands.'), ('', 'A string parameter which specifies the type of packet data protocol. (IP, IPV6, PPP, X.25 etc)'), ('', 'Access Point Name. String parameter; logical name that is used to select the GGSN or external packet data network'), ('', 'String parameter that identifies the MT in the address space applicable to the PDP. If null/omitted, a dynamic address may be requested.'), ('', 'PDP data compression. Values:\n\ 0 - off (default)\n\ 1 - on'), ('', 'PDP header compression. Values:\n\ 0 - off (default)\n\ 1 - on')), None, 'This command specifies the PDP (Packet Data Protocol) context parameter values, such as PDP type (IP, IPV6, PPP, X.25 etc), APN, data compression, header compression, etc.')), ('AT+CGATT', (c[7], 'GPRS attach or detach', (('', 'indicates the state of GPRS attachment:\n\ 0 - detached\n\ 1 - attached\n'),), None, 'The execution command is used to attach the MT to, or detach the MT from, the GPRS\ service. After the command has completed, the MT remains in V.25ter command state.\n\ Any active PDP contexts will be automatically deactivated when the attachment state changes to detached.')), ('AT+CGACT', (c[7], 'PDP context activate or deactivate', (('', 'indicates the state of PDP context activation:\n\ 0 - deactivated\n\ 1 - activated\n'), ('', 'a numeric parameter which specifies a particular PDP context.')), None, 'The execution command is used to activate or deactivate the specified PDP context (s).\n\ After the command has completed, the MT remains in V.25ter command state.')), # Fax ('AT+FTM', (c[8], 'Transmit Speed')), ('AT+FRM', (c[8], 'Receive Speed')), ('AT+FTH', (c[8], 'HDLC Transmit Speed')), ('AT+FRH', (c[8], 'HDLC Receive Speed')), ('AT+FTS', (c[8], 'Stop Transmission and Wait')), ('AT+FRS', (c[8], 'Receive Silence')), # Fax Class 2 ('AT+FDT', (c[9], 'Transmit Data')), ('AT+FDR', (c[9], 'Receive Data')), ('AT+FET', (c[9], 'Transmit Page Punctuation')), ('AT+FPTS', (c[9], 'Page Transfer Status Parameters')), ('AT+FK', (c[9], 'Terminate Session')), ('AT+FBOR', (c[9], 'Page Transfer Bit Order')), ('AT+FBUF', (c[9], 'Buffer Size Report')), ('AT+FCQ', (c[9], 'Copy Quality Checking')), ('AT+FCR', (c[9], 'Capability to Receive')), ('AT+FDIS', (c[9], 'Current Session Parameters')), ('AT+FDCC', (c[9], 'DCE Capabilities Parameters')), ('AT+FLID', (c[9], 'Local ID String')), ('AT+FPHCTO', (c[9], 'Page Transfer Timeout Parameter')), # V24-V25 ('AT+IPR', (c[10], 'Fixed DTE Rate', (('', 'Baud rates that can be used by the DCE.\n0 enables autobauding on some modems.'),), None, 'This commands specifies the data rate at which the DCE will accept commands.')), ('AT+ICF', (c[10], 'DTE-DCE Character Framing', (('', '0: Autodetect\n\ 1: 8 Data 2 Stop\n\ parameter is ignored\n\ 2: 8 Data 1 Parity 1 Stop\n\ If no provided, 3 is used by default as value\n\ 3: 8 Data 1 Stop\n\ parameter is ignored\n\ 4: 7 Data 2 Stop\n\ parameter is ignored\n\ 5: 7 Data 1 Parity 1 Stop\n\ If no provided, 3 is used by default as value\n\ 6: 7 Data 1 Stop\n\ parameter is ignored'), ('', '0: DCD always on\n1: DCD matches the state of the remote modem\'s data carrier'),), None, 'This commands controls the Data Carrier Detect (DCD) signal.')), ('AT&D', (c[10], 'Set DTR Signal', (('', '0: The DTR signal is ignored\n1: Modem switches from data to command mode when DTR switches from ON to OFF\n2: Upon DTR switch from ON to OFF, the call is released'),), None, 'This commands controls the Data Terminal Ready (DTR) signal.')), ('AT&S', (c[10], 'Set DSR Signal', (('', '0: DSR always on\n1: DSR off in command mode. DSR on in data mode.'),), None, 'This commands controls the Data Set Ready (DSR) signal.')), ('ATO', (c[10], 'Back to Online Mode', None, None, 'If a connection has been established and the ME is in command mode, this command allows you to return to online data mode.')), ('ATQ', (c[10], 'Result Code Suppression', (('', '0: DCE transmit result codes\n1: Result codes are suppressed and not transmitted'),), None, 'This command determines whether the mobile equipment sends result codes or not.')), ('ATV', (c[10], 'DCE Response Format', (('', '0: DCE transmits limited headers and trailers and numeric result codes\n1: DCE transmits full headers and trailers and verbose response text'),), None, 'This command determines the DCE response format, with or without header characters . Result codes are provided as numeric or verbose.')), ('ATZ', (c[10], 'Default Configuration', None, None, 'This command restores the configuration profile. Any call is released')), ('AT&W', (c[10], 'Save Configuration', None, None, 'This command writes the active configuration to a non-volatile memory (EEPROM).')), ('AT&T', (c[10], 'Auto-Tests')), ('ATE', (c[10], 'Echo', (('', '0: Characters are not echoed\n1: Characters are echoed'),), None, 'This command is used to determine whether the modem echoes characters received by an external application (DTE).')), ('AT&F', (c[10], 'Restore Factory Settings', (('', '0: Restore factory settings'),), None, 'This command is used to restore the factory settings from EEPROM.')), ('AT&V', (c[10], 'Display Configuration', (('', '0 Displays the modem configuration in RAM. Default value if no parameter provided.\n\ 1: Displays the modem configuration in EEPROM.\n\ 2: Displays the modem factory configuration.'),), None, 'This command is used to display the modem configuration.')), ('ATI', (c[10], 'Request Identification Information', (('', '0: Displays manufacturer followed by model identification. Equivalent to +CGMI and +CGMM.\n\ 3: Displays revision identification. Equivalent to +CGMR.\n\ 4: Displays modem configuration in RAM. Equivalent to &V0.\n\ 5: Displays modem configuration in EEPROM. Equivalent to &V1.\n\ 6: Displays modem data features. Lists the supported data rates, data modes, and fax classes.\n\ 7: Displays modem voice features.'),), None, 'This command causes the product to transmit one or more lines of specific information text.')), ('AT+WMUX', (c[10], 'Multiplexing Mode')), # Specific AT Commands ('AT+CCED', (c[11], 'Cell Environment Description')), ('AT+WIND', (c[11], 'General Indications')), ('AT+ADC', (c[11], 'Analog Digital Converter Measurements')), ('AT+CMER', (c[11], 'Mobile Equipment Event Reporting')), ('AT+CIND', (c[11], 'Indicator Control')), ('AT+CMEC', (c[11], 'Mobile Equipment Control Mode')), ('AT+WLPR', (c[11], 'Read Language Preference')), ('AT+WLPW', (c[11], 'Write Language Preference')), ('AT+WIOR', (c[11], 'Read GPIO Value')), ('AT+WIOW', (c[11], 'Write GPIO Value')), ('AT+WIOM', (c[11], 'Input/Output Management')), ('AT+WAC', (c[11], 'Abort Command')), ('AT+WTONE', (c[11], 'Play Tone')), ('AT+WDTMF', (c[11], 'Play DTMF Tone')), ('AT+WDWL', (c[11], 'Downloading')), ('AT+WVR', (c[11], 'Voice Rate')), ('AT+WDR', (c[11], 'Data Rate')), ('AT+WSVG', (c[11], 'Select Voice Gain')), ('AT+WSTR', (c[11], 'Status Request')), ) ================================================ FILE: tools/gsmtermlib/posoptparse.py ================================================ """ PosOptionParser class gotten from Douglas Mayle at StackOverflow: http://stackoverflow.com/a/664614/1980416 Used for positional argument support similar to argparse (for Python 2.6 compatibility) """ from optparse import OptionParser, Option, IndentedHelpFormatter class PosOptionParser(OptionParser): def format_help(self, formatter=None): class Positional(object): def __init__(self, args): self.option_groups = [] self.option_list = args positional = Positional(self.positional) formatter = IndentedHelpFormatter() formatter.store_option_strings(positional) output = ['\n', formatter.format_heading("Positional Arguments")] formatter.indent() pos_help = [formatter.format_option(opt) for opt in self.positional] pos_help = [line.replace('--','') for line in pos_help] output += pos_help return OptionParser.format_help(self, formatter) + ''.join(output) def add_positional_argument(self, option): try: args = self.positional except AttributeError: args = [] args.append(option) self.positional = args def set_out(self, out): self.out = out ================================================ FILE: tools/gsmtermlib/terminal.py ================================================ #!/usr/bin/env python """\ GSMTerm: A user-friendly terminal for interacting with a GSM modem Note: The "Console" object was copied from pySerial's miniterm.py code @author: Francois Aucamp """ from __future__ import print_function import os, sys, threading, time import serial from gsmmodem.serial_comms import SerialComms from .trie import Trie from gsmmodem.exceptions import TimeoutException # first choose a platform dependant way to read single characters from the console global console if os.name == 'nt': import msvcrt class Console(object): CURSOR_UP = '{0}{1}'.format(chr(0xe0), chr(0x48)) CURSOR_DOWN = '{0}{1}'.format(chr(0xe0), chr(0x50)) CURSOR_LEFT = '{0}{1}'.format(chr(0xe0), chr(0x4b)) CURSOR_RIGHT = '{0}{1}'.format(chr(0xe0), chr(0x4d)) #TODO: find out what this in windows: DELETE = '' HOME = '' END = '' def __init__(self): pass def setup(self): pass # Do nothing for 'nt' def cleanup(self): pass # Do nothing for 'nt' def getkey(self): while True: z = msvcrt.getch() if z == '\xe0': # extended (cursor keys, etc) z += msvcrt.getch() return z elif z == '\0': # functions keys, ignore msvcrt.getch() else: if z == '\r': return '\n' return z console = Console() elif os.name == 'posix': import termios, tty class Console(object): CURSOR_UP = '{0}{1}{2}'.format(chr(27), chr(91), chr(65)) CURSOR_DOWN = '{0}{1}{2}'.format(chr(27), chr(91), chr(66)) CURSOR_LEFT = '{0}{1}{2}'.format(chr(27), chr(91), chr(68)) CURSOR_RIGHT = '{0}{1}{2}'.format(chr(27), chr(91), chr(67)) DELETE = '{0}{1}{2}{3}'.format(chr(27), chr(91), chr(51), chr(126)) HOME = '{0}{1}{2}'.format(chr(27), chr(79), chr(72)) END = '{0}{1}{2}'.format(chr(27), chr(79), chr(70)) def __init__(self): self.fd = sys.stdin.fileno() def setup(self): self.old = termios.tcgetattr(self.fd) new = termios.tcgetattr(self.fd) new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG new[6][termios.VMIN] = 1 new[6][termios.VTIME] = 0 termios.tcsetattr(self.fd, termios.TCSANOW, new) # def setup(self): # self.oldSettings = termios.tcgetattr(self.fd) # tty.setraw(self.fd) def getkey(self): c = os.read(self.fd, 4) #print (len(c)) #for a in c: # print('rx:',ord(a)) return c def cleanup(self): termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old) console = Console() def cleanup_console(): console.cleanup() console.setup() sys.exitfunc = cleanup_console # terminal modes have to be restored on exit... else: raise NotImplementedError("Sorry no implementation for your platform (%s) available." % sys.platform) class RawTerm(SerialComms): """ "Raw" terminal - basically just copies console input to serial, and prints out anything read """ EXIT_CHARACTER = '\x1d' # CTRL+] WRITE_TERM = '\r' # Write terminator character def __init__(self, port, baudrate=9600): super(RawTerm, self).__init__(port, baudrate, notifyCallbackFunc=self._handleModemNotification) self.port = port self.baudrate = baudrate self.echo = True def _handleModemNotification(self, lines): for line in lines: print(line) def printStartMessage(self): print('\nRaw terminal connected to {0} at {1}bps.\nPress CTRL+] to exit.\n'.format(self.port, self.baudrate)) def start(self): self.connect() # Start input thread self.alive = True self.inputThread = threading.Thread(target=self._inputLoop) self.inputThread.daemon = True self.inputThread.start() self.printStartMessage() def stop(self): self.alive = False if threading.current_thread() != self.inputThread: self.inputThread.join() self.close() def _inputLoop(self): """ Loop and copy console->serial until EXIT_CHARCTER character is found. """ try: while self.alive: try: c = console.getkey() except KeyboardInterrupt: print('kbint') c = serial.to_bytes([3]) if c == self.EXIT_CHARACTER: self.stop() elif c == '\n': # Convert newline input into \r self.serial.write(self.WRITE_TERM) if self.echo: # Locally just echo the real newline sys.stdout.write(c) sys.stdout.flush() else: #print('writing: ', c) self.serial.write(c) if self.echo: sys.stdout.write(c) sys.stdout.flush() except: self.alive = False raise class GsmTerm(RawTerm): """ User-friendly terminal for interacting with a GSM modem. Some features: tab-completion, help """ PROMPT = 'GSM> ' SMS_PROMPT = '> ' EXIT_CHARACTER_2 = chr(4) # CTRL+D BACKSPACE_CHARACTER = chr(127) CTRL_Z_CHARACTER = chr(26) # Used when entering SMS messages with AT+CMGS ESC_CHARACTER = chr(27) # Used to cancel entering SMS messages with AT+CMGS RESET_SEQ = '\033[0m' COLOR_SEQ = '\033[1;{0}m' BOLD_SEQ = '\033[1m' # ANSI colour escapes COLOR_RED = COLOR_SEQ.format(30+1) COLOR_GREEN = COLOR_SEQ.format(30+2) COLOR_YELLOW = COLOR_SEQ.format(30+3) COLOR_BLUE = COLOR_SEQ.format(30+4) COLOR_MAGENTA = COLOR_SEQ.format(30+5) COLOR_WHITE = COLOR_SEQ.format(30+7) COLOR_CYAN = COLOR_SEQ.format(30+6) def __init__(self, port, baudrate=9600, useColor=True): super(GsmTerm, self).__init__(port, baudrate) self.inputBuffer = [] self.history = [] self.historyPos = 0 self.useColor = useColor self.cursorPos = 0 if self.useColor: self.PROMPT = self._color(self.COLOR_GREEN, self.PROMPT) self.SMS_PROMPT = self._color(self.COLOR_GREEN, self.SMS_PROMPT) self._initAtCommandsTrie() # Flag that indicates whether the user is typing an SMS message's text self._typingSms = False def printStartMessage(self): # self.stdscr.addstr('GSMTerm started. Press CTRL+] to exit.') print('\nGSMTerm connected to {0} at {1}bps.\nPress CTRL+] or CTRL+D to exit.\n'.format(self.port, self.baudrate)) self._refreshInputPrompt() def _color(self, color, msg): """ Converts a message to be printed to the user's terminal in red """ if self.useColor: return '{0}{1}{2}'.format(color, msg, self.RESET_SEQ) else: return msg def _boldFace(self, msg): """ Converts a message to be printed to the user's terminal in bold """ return self._color(self.BOLD_SEQ, msg) def _handleModemNotification(self, lines): # Clear any input prompt self._removeInputPrompt() if self._typingSms: self.PROMPT = self._color(self.COLOR_CYAN, lines[0]) if lines[-1] == 'ERROR': print(self._color(self.COLOR_RED, '\n'.join(lines))) else: print(self._color(self.COLOR_CYAN, '\n'.join(lines))) self._refreshInputPrompt() def _addToHistory(self, command): self.history.append(command) if len(self.history) > 100: self.history = self.history[1:] def _inputLoop(self): """ Loop and copy console->serial until EXIT_CHARCTER character is found. """ # Switch statement for handling "special" characters actionChars = {self.EXIT_CHARACTER: self._exit, self.EXIT_CHARACTER_2: self._exit, console.CURSOR_LEFT: self._cursorLeft, console.CURSOR_RIGHT: self._cursorRight, console.CURSOR_UP: self._cursorUp, console.CURSOR_DOWN: self._cursorDown, '\n': self._doConfirmInput, '\t': self._doCommandCompletion, self.CTRL_Z_CHARACTER: self._handleCtrlZ, self.ESC_CHARACTER: self._handleEsc, self.BACKSPACE_CHARACTER: self._handleBackspace, console.DELETE: self._handleDelete, console.HOME: self._handleHome, console.END: self._handleEnd} try: while self.alive: try: c = console.getkey() except KeyboardInterrupt: c = serial.to_bytes([3]) if c in actionChars: # Handle character directly actionChars[c]() elif len(c) == 1 and self._isPrintable(c): self.inputBuffer.insert(self.cursorPos, c) self.cursorPos += 1 self._refreshInputPrompt() #else: # for a in c: # print('GOT:',a,'(',ord(a),')') except: self.alive = False raise def _handleCtrlZ(self): """ Handler for CTRL+Z keypresses """ if self._typingSms: self.serial.write(''.join(self.inputBuffer)) self.serial.write(self.CTRL_Z_CHARACTER) self._typingSms = False self.inputBuffer = [] self.cursorPos = 0 sys.stdout.write('\n') self._refreshInputPrompt() def _handleEsc(self): """ Handler for CTRL+Z keypresses """ if self._typingSms: self.serial.write(self.ESC_CHARACTER) self._typingSms = False self.inputBuffer = [] self.cursorPos = 0 def _exit(self): """ Shuts down the terminal (and app) """ self._removeInputPrompt() print(self._color(self.COLOR_YELLOW, 'CLOSING TERMINAL...')) self.stop() def _cursorLeft(self): """ Handles "cursor left" events """ if self.cursorPos > 0: self.cursorPos -= 1 sys.stdout.write(console.CURSOR_LEFT) sys.stdout.flush() def _cursorRight(self): """ Handles "cursor right" events """ if self.cursorPos < len(self.inputBuffer): self.cursorPos += 1 sys.stdout.write(console.CURSOR_RIGHT) sys.stdout.flush() def _cursorUp(self): """ Handles "cursor up" events """ if self.historyPos > 0: self.historyPos -= 1 clearLen = len(self.inputBuffer) self.inputBuffer = list(self.history[self.historyPos]) self.cursorPos = len(self.inputBuffer) self._refreshInputPrompt(clearLen) def _cursorDown(self): """ Handles "cursor down" events """ if self.historyPos < len(self.history)-1: clearLen = len(self.inputBuffer) self.historyPos += 1 self.inputBuffer = list(self.history[self.historyPos]) self.cursorPos = len(self.inputBuffer) self._refreshInputPrompt(clearLen) def _handleBackspace(self): """ Handles backspace characters """ if self.cursorPos > 0: #print( 'cp:',self.cursorPos,'was:', self.inputBuffer) self.inputBuffer = self.inputBuffer[0:self.cursorPos-1] + self.inputBuffer[self.cursorPos:] self.cursorPos -= 1 #print ('cp:', self.cursorPos,'is:', self.inputBuffer) self._refreshInputPrompt(len(self.inputBuffer)+1) def _handleDelete(self): """ Handles "delete" characters """ if self.cursorPos < len(self.inputBuffer): self.inputBuffer = self.inputBuffer[0:self.cursorPos] + self.inputBuffer[self.cursorPos+1:] self._refreshInputPrompt(len(self.inputBuffer)+1) def _handleHome(self): """ Handles "home" character """ self.cursorPos = 0 self._refreshInputPrompt(len(self.inputBuffer)) def _handleEnd(self): """ Handles "end" character """ self.cursorPos = len(self.inputBuffer) self._refreshInputPrompt(len(self.inputBuffer)) def _doConfirmInput(self): if self._typingSms: # SMS messages are confirmed with CTRL+Z or canceled with ESC inputStr = ''.join(self.inputBuffer[:self.cursorPos]) self.serial.write(inputStr) self.inputBuffer = self.inputBuffer[self.cursorPos:] self.cursorPos = 0 sys.stdout.write('\n') self._refreshInputPrompt() return # Convert newline input into \r\n if len(self.inputBuffer) > 0: inputStr = ''.join(self.inputBuffer).strip() self.inputBuffer = [] self.cursorPos = 0 inputStrLen = len(inputStr) if len(inputStr) > 0: self._addToHistory(inputStr) self.historyPos = len(self.history) if inputStrLen > 2: if inputStr[0] == '?': # ?COMMAND # Help requested with function self._printCommandHelp(inputStr[1:]) return elif inputStr[-1] == inputStr[-2] == '?': # COMMAND?? # Help requested with function cmd = inputStr[:-3 if inputStr[-3] == '=' else -2] self._printCommandHelp(cmd) return inputStrLower = inputStr.lower() if inputStrLower.startswith('help'): # help COMMAND # Alternative help invocation self._printCommandHelp(inputStr[5:]) return elif inputStrLower.startswith('ls'): if inputStrLower == 'lscat': sys.stdout.write('\n') for category in self.completion.categories: sys.stdout.write('{0}\n'.format(category)) self._refreshInputPrompt(len(self.inputBuffer)) return elif inputStrLower == 'ls': sys.stdout.write('\n') for command in self.completion: sys.stdout.write('{0:<8} - {1}\n'.format(command, self.completion[command][1])) self._refreshInputPrompt(len(self.inputBuffer)) return else: ls = inputStrLower.split(' ', 1) if len(ls) == 2: category = ls[1].lower() if category in [cat.lower() for cat in self.completion.categories]: sys.stdout.write('\n') for command in self.completion: commandHelp = self.completion[command] if category == commandHelp[0].lower(): sys.stdout.write('{0:<8} - {1}\n'.format(command, commandHelp[1])) self._refreshInputPrompt(len(self.inputBuffer)) return elif inputStrLower.startswith('load'): # Load a file containing AT commands to issue load = inputStr.split(' ', 1) if len(load) == 2: filename = load[1].strip() try: f = open(filename, 'r') except IOError: sys.stdout.write('\n{0}\n'.format(self._color(self.COLOR_RED, 'File not found: "{0}"'.format(filename)))) self._refreshInputPrompt(len(self.inputBuffer)) else: atCommands = f.readlines() f.close() sys.stdout.write('\n') for atCommand in atCommands: atCommand = atCommand.strip() if len(atCommand) > 0 and atCommand[0] != '#': self.inputBuffer = list(atCommand.strip()) self._refreshInputPrompt(len(self.inputBuffer)) self._doConfirmInput() time.sleep(0.1) return if len(inputStr) > 0: if inputStrLower.startswith('at+cmgs='): # Prepare for SMS input self._typingSms = True try: sys.stdout.write('\n') sys.stdout.flush() response = self.write(inputStr + self.WRITE_TERM, waitForResponse=True, timeout=3, expectedResponseTermSeq='> ') except TimeoutException: self._typingSms = False else: sys.stdout.write(self._color(self.COLOR_YELLOW, 'Type your SMS message, and press CTRL+Z to send it or press ESC to cancel.\n')) self.SMS_PROMPT = self._color(self.COLOR_GREEN, response[0]) self._refreshInputPrompt() return self.serial.write(inputStr) self.serial.write(self.WRITE_TERM) # Locally just echo the real newline sys.stdout.write('\n') sys.stdout.flush() def _printGeneralHelp(self): sys.stdout.write(self._color(self.COLOR_WHITE, '\n\n== GSMTerm Help ==\n\n')) sys.stdout.write('{0} Press the up & down arrow keys to move backwards or forwards through your command history.\n\n'.format(self._color(self.COLOR_YELLOW, 'Command History:'))) sys.stdout.write('{0} Press the TAB key to provide command completion suggestions. Press the TAB key after a command is fully typed (with or without a "=" character) to quickly see its syntax.\n\n'.format(self._color(self.COLOR_YELLOW, 'Command Completion:'))) sys.stdout.write('{0} Type a command, followed with two quesetion marks to access its documentation, e.g. "??". Alternatively, precede the command with a question mark ("?"), or type "help ".\n\n'.format(self._color(self.COLOR_YELLOW, 'Command Documentation:'))) sys.stdout.write('{0} Type "ls [category]" to list the available AT commands known to GSMTerm for the given category (or all commands if no category is specified).\nType "lscat" to see a list of categories.\n\n'.format(self._color(self.COLOR_YELLOW, 'List Available Commands:'))) sys.stdout.write('{0} Type "load " to load and execute a file containing AT commands, separated by newlines, e.g. "load ./myscript.txt".\n\n'.format(self._color(self.COLOR_YELLOW, 'Load Script:'))) sys.stdout.write('To exit GSMTerm, press CTRL+] or CTRL+D.\n\n') self._refreshInputPrompt(len(self.inputBuffer)) def _printCommandHelp(self, command=None): if command == None or len(command.strip()) == 0: # Print general help self._printGeneralHelp() return try: command = command.strip() commandHelp = self.completion[command.upper()] except KeyError: noHelp = True else: noHelp = commandHelp == None if noHelp: sys.stdout.write('\r No help available for: {0}\n'.format(self._color(self.COLOR_WHITE, command))) else: sys.stdout.write('\n\n{0} ({1})\n\n'.format(self._color(self.COLOR_WHITE, commandHelp[1]), command)) sys.stdout.write('{0} {1}\n'.format(self._color(self.COLOR_YELLOW, 'Category:'), commandHelp[0])) if len(commandHelp) == 2: sys.stdout.write('\nNo detailed help available for this command.\n\n') self._refreshInputPrompt(len(self.inputBuffer)) return sys.stdout.write('{0} {1}\n'.format(self._color(self.COLOR_YELLOW, 'Description:'), commandHelp[4])) valuesIsEnum = len(commandHelp) >= 6 if valuesIsEnum: # "Values" is an enum of allowed values (not multiple variables); use custom label sys.stdout.write('{0} '.format(self._color(self.COLOR_YELLOW, commandHelp[5]))) else: sys.stdout.write('{0} '.format(self._color(self.COLOR_YELLOW, 'Values:'))) commandValues = commandHelp[2] syntax = [self._color(self.COLOR_WHITE, command)] if commandValues != None: if '+' in command or command.upper() in ['ATS0']: syntax.append(self._color(self.COLOR_WHITE, '=')) sys.stdout.write('\n') first = True for value, valueDesc in commandValues: if first: first = False else: syntax.append(',' if not valuesIsEnum else '|') syntax.append(self._color(self.COLOR_MAGENTA, value)) sys.stdout.write(' {0} {1}\n'.format(self._color(self.COLOR_MAGENTA, value), valueDesc.replace('\n', '\n' + ' ' * (len(value) + 2)) if valueDesc != None else '')) else: sys.stdout.write('No parameters.\n') returnValues = commandHelp[3] if returnValues != None: sys.stdout.write('{0} '.format(self._color(self.COLOR_YELLOW, 'Response Values:'))) sys.stdout.write('\n') for value, valueDesc in returnValues: sys.stdout.write(' {0} {1}\n'.format(self._color(self.COLOR_CYAN, value), valueDesc.replace('\n', '\n' + ' ' * (len(value) + 2)) if valueDesc != None else '')) sys.stdout.write('{0}\n {1}\n\n'.format(self._color(self.COLOR_YELLOW, 'Command Syntax:'), ''.join(syntax))) self._refreshInputPrompt(len(self.inputBuffer)) def _doCommandCompletion(self): """ Command-completion method """ prefix = ''.join(self.inputBuffer).strip().upper() matches = self.completion.keys(prefix) matchLen = len(matches) if matchLen == 0 and prefix[-1] == '=': try: command = prefix[:-1] except KeyError: pass else: self.__printCommandSyntax(command) elif matchLen > 0: if matchLen == 1: if matches[0] == prefix: # User has already entered command - show command syntax self.__printCommandSyntax(prefix) else: # Complete only possible command self.inputBuffer = list(matches[0]) self.cursorPos = len(self.inputBuffer) self._refreshInputPrompt(len(self.inputBuffer)) return else: commonPrefix = self.completion.longestCommonPrefix(''.join(self.inputBuffer)) self.inputBuffer = list(commonPrefix) self.cursorPos = len(self.inputBuffer) if matchLen > 20: matches = matches[:20] matches.append('... ({0} more)'.format(matchLen - 20)) sys.stdout.write('\n') for match in matches: sys.stdout.write(' {0} '.format(match)) sys.stdout.write('\n') sys.stdout.flush() self._refreshInputPrompt(len(self.inputBuffer)) def __printCommandSyntax(self, command): """ Command-completion helper method: print command syntax """ commandHelp = self.completion[command] if commandHelp != None and len(commandHelp) > 2: commandValues = commandHelp[2] #commandDefault = commandHelp[3] displayHelp = [self._color(self.COLOR_WHITE, command)] if commandValues != None: valuesIsEnum = len(commandHelp) >= 6 if '+' in command or command.upper() in ['ATS0']: displayHelp.append(self._color(self.COLOR_WHITE, '=')) displayHelp.append(('|' if valuesIsEnum else ',').join([value[0] for value in commandValues])) sys.stdout.write('\r Syntax: {0}\n'.format(self._color(self.COLOR_WHITE, ''.join(displayHelp)))) sys.stdout.flush() self._refreshInputPrompt(len(self.inputBuffer)) def _isPrintable(self, char): return 33 <= ord(char) <= 126 or char.isspace() def _refreshInputPrompt(self, clearLen=0): termPrompt = self.SMS_PROMPT if self._typingSms else self.PROMPT endPoint = clearLen if clearLen > 0 else len(self.inputBuffer) sys.stdout.write('\r{0}{1}{2}{3}'.format(termPrompt, ''.join(self.inputBuffer), (clearLen - len(self.inputBuffer)) * ' ', console.CURSOR_LEFT * (endPoint - self.cursorPos))) sys.stdout.flush() def _removeInputPrompt(self): termPrompt = self.SMS_PROMPT if self._typingSms else self.PROMPT sys.stdout.write('\r{0}\r'.format(' ' * (len(termPrompt) + len(self.inputBuffer)))) def _initAtCommandsTrie(self): self.completion = Trie() from .atcommands import ATCOMMANDS, CATEGORIES for command, help in ATCOMMANDS: if help != None: self.completion[command] = help else: self.completion[command] = None self.completion.categories = CATEGORIES ================================================ FILE: tools/gsmtermlib/trie.py ================================================ """ Pure Python trie implementation for strings """ # Compensate for differences between Python 2 and 3 import sys if sys.version_info[0] >= 3: dictKeysIter = dict.keys dictItemsIter = dict.items dictValuesIter = dict.values else: #pragma: no cover dictKeysIter = dict.iterkeys dictItemsIter = dict.iteritems dictValuesIter = dict.itervalues class Trie(object): def __init__(self, key=None, value=None): self.slots = {} self.key = key self.value = value def __setitem__(self, key, value): if key == None: raise ValueError('Key may not be None') if len(key) == 0: # All of the original key's chars have been nibbled away self.value = value self.key = '' return c = key[0] if c not in self.slots: # Unused slot - no collision if self.key != None and len(self.key) > 0: # This was a "leaf" previously - create a new branch for its current value branchC = self.key[0] branchKey = self.key[1:] if len(self.key) > 1 else '' self.slots[branchC] = Trie(branchKey, self.value) self.key = None self.value = None if branchC != c: self.slots[c] = Trie(key[1:], value) else: self.slots[c][key[1:]] = value else: # Store specified value in a new branch and return self.slots[c] = Trie(key[1:], value) else: trie = self.slots[c] trie[key[1:]] = value def __delitem__(self, key): if key == None: raise ValueError('Key may not be None') if len(key) == 0: if self.key == '': self.key = None self.value = None return else: raise KeyError(key) c = key[0] if c in self.slots: trie = self.slots[c] if key[1:] == trie.key: if len(trie.slots) > 0: trie.key = None trie.value = None else: del self.slots[c] # Remove the node else: del trie[key[1:]] else: raise KeyError(key) def __getitem__(self, key): if key == None: raise ValueError('Key may not be None') if len(key) == 0: if self.key == '': # All of the original key's chars have ben nibbled away return self.value else: raise KeyError(key) c = key[0] if c in self.slots: trie = self.slots[c] return trie[key[1:]] elif key == self.key: return self.value else: raise KeyError(key) def __contains__(self, key): try: self.__getitem__(key) except KeyError: return False return True def __len__(self): global dictValuesIter n = 1 if self.key != None else 0 for trie in dictValuesIter(self.slots): n += len(trie) return n def get(self, key, default=None): try: return self.__getitem__(key) except KeyError: return default def _allKeys(self, prefix): """ Private implementation method. Use keys() instead. """ global dictItemsIter result = [prefix + self.key] if self.key != None else [] for key, trie in dictItemsIter(self.slots): result.extend(trie._allKeys(prefix + key)) return result def keys(self, prefix=None): """ Return all or possible keys in this trie If prefix is None, return all keys. If prefix is a string, return all keys that start with this string """ if prefix == None: return self._allKeys('') else: return self._filteredKeys(prefix, '') def _filteredKeys(self, key, prefix): global dictKeysIter global dictItemsIter if len(key) == 0: result = [prefix + self.key] if self.key != None else [] for c, trie in dictItemsIter(self.slots): result.extend(trie._allKeys(prefix + c)) else: c = key[0] if c in dictKeysIter(self.slots): result = [] trie = self.slots[c] result.extend(trie._filteredKeys(key[1:], prefix+c)) else: result = [prefix + self.key] if self.key != None and self.key.startswith(key) else [] return result def longestCommonPrefix(self, prefix=''): """ Return the longest common prefix shared by all keys that start with prefix (note: the return value will always start with the specified prefix) """ return self._longestCommonPrefix(prefix, '') def _longestCommonPrefix(self, key, prefix): if len(key) == 0: if self.key != None: return prefix + self.key else: slotKeys = list(self.slots.keys()) if len(slotKeys) == 1: c = slotKeys[0] return self.slots[c]._longestCommonPrefix('', prefix + c) else: return prefix elif self.key != None: if self.key.startswith(key): return prefix + self.key else: return '' # nothing starts with the specified prefix else: c = key[0] if c in self.slots: return self.slots[c]._longestCommonPrefix(key[1:], prefix + c) else: return '' # nothing starts with the specified prefix def __iter__(self): for k in list(self.keys()): yield k raise StopIteration ================================================ FILE: tools/identify-modem.py ================================================ #!/usr/bin/env python """\ Simple script to assist with identifying a GSM modem The debug information obtained by this script (when using -d) can be used to aid test cases (since I don't have access to every modem in the world ;-) ) @author: Francois Aucamp """ from __future__ import print_function import sys from gsmmodem.modem import GsmModem from gsmmodem.exceptions import TimeoutException, PinRequiredError, IncorrectPinError def parseArgs(): """ Argument parser for Python 2.7 and above """ from argparse import ArgumentParser parser = ArgumentParser(description='Identify and debug attached GSM modem') parser.add_argument('port', metavar='PORT', help='port to which the GSM modem is connected; a number or a device name.') parser.add_argument('-b', '--baud', metavar='BAUDRATE', default=115200, help='set baud rate') parser.add_argument('-p', '--pin', metavar='PIN', default=None, help='SIM card PIN') parser.add_argument('-d', '--debug', action='store_true', help='dump modem debug information (for python-gsmmodem development)') parser.add_argument('-w', '--wait', type=int, default=0, help='Wait for modem to start, in seconds') return parser.parse_args() def parseArgsPy26(): """ Argument parser for Python 2.6 """ from gsmtermlib.posoptparse import PosOptionParser, Option parser = PosOptionParser(description='Identify and debug attached GSM modem') parser.add_positional_argument(Option('--port', metavar='PORT', help='port to which the GSM modem is connected; a number or a device name.')) parser.add_option('-b', '--baud', metavar='BAUDRATE', default=115200, help='set baud rate') parser.add_option('-p', '--pin', metavar='PIN', default=None, help='SIM card PIN') parser.add_option('-d', '--debug', action='store_true', help='dump modem debug information (for python-gsmmodem development)') parser.add_option('-w', '--wait', type=int, default=0, help='Wait for modem to start, in seconds') options, args = parser.parse_args() if len(args) != 1: parser.error('Incorrect number of arguments - please specify a PORT to connect to, e.g. {0} /dev/ttyUSB0'.format(sys.argv[0])) else: options.port = args[0] return options def main(): args = parseArgsPy26() if sys.version_info[0] == 2 and sys.version_info[1] < 7 else parseArgs() print ('args:',args) modem = GsmModem(args.port, args.baud) print('Connecting to GSM modem on {0}...'.format(args.port)) try: modem.connect(args.pin, waitingForModemToStartInSeconds=args.wait) except PinRequiredError: sys.stderr.write('Error: SIM card PIN required. Please specify a PIN with the -p argument.\n') sys.exit(1) except IncorrectPinError: sys.stderr.write('Error: Incorrect SIM card PIN entered.\n') sys.exit(1) if args.debug: # Print debug info print('\n== MODEM DEBUG INFORMATION ==\n') print('ATI', modem.write('ATI', parseError=False)) print('AT+CGMI:', modem.write('AT+CGMI', parseError=False)) print('AT+CGMM:', modem.write('AT+CGMM', parseError=False)) print('AT+CGMR:', modem.write('AT+CGMR', parseError=False)) print('AT+CFUN=?:', modem.write('AT+CFUN=?', parseError=False)) print('AT+WIND=?:', modem.write('AT+WIND=?', parseError=False)) print('AT+WIND?:', modem.write('AT+WIND?', parseError=False)) print('AT+CPMS=?:', modem.write('AT+CPMS=?', parseError=False)) print('AT+CNMI=?:', modem.write('AT+CNMI=?', parseError=False)) print('AT+CVHU=?:', modem.write('AT+CVHU=?', parseError=False)) print('AT+CSMP?:', modem.write('AT+CSMP?', parseError=False)) print('AT+GCAP:', modem.write('AT+GCAP', parseError=False)) print('AT+CPIN?', modem.write('AT+CPIN?', parseError=False)) print('AT+CLAC:', modem.write('AT+CLAC', parseError=False)) print() else: # Print basic info print('\n== MODEM INFORMATION ==\n') print('Manufacturer:', modem.manufacturer) print('Model:', modem.model) print('Revision:', modem.revision if modem.revision != None else 'N/A') print('\nIMEI:', modem.imei if modem.imei != None else 'N/A') print('IMSI:', modem.imsi if modem.imsi != None else 'N/A') print('\nNetwork:', modem.networkName) print('Signal strength:', modem.signalStrength) print() if __name__ == '__main__': main() ================================================ FILE: tools/sendsms.py ================================================ #!/usr/bin/env python """\ Simple script to send an SMS message @author: Francois Aucamp """ from __future__ import print_function import sys, logging from gsmmodem.modem import GsmModem, SentSms from gsmmodem.exceptions import TimeoutException, PinRequiredError, IncorrectPinError def parseArgs(): """ Argument parser for Python 2.7 and above """ from argparse import ArgumentParser parser = ArgumentParser(description='Simple script for sending SMS messages') parser.add_argument('-i', '--port', metavar='PORT', help='port to which the GSM modem is connected; a number or a device name.') parser.add_argument('-l', '--lock-path', metavar='PATH', help='Use oslo.concurrency to prevent concurrent access to modem') parser.add_argument('-b', '--baud', metavar='BAUDRATE', default=115200, help='set baud rate') parser.add_argument('-p', '--pin', metavar='PIN', default=None, help='SIM card PIN') parser.add_argument('-d', '--deliver', action='store_true', help='wait for SMS delivery report') parser.add_argument('-w', '--wait', type=int, default=0, help='Wait for modem to start, in seconds') parser.add_argument('--CNMI', default='', help='Set the CNMI of the modem, used for message notifications') parser.add_argument('--debug', action='store_true', help='turn on debug (serial port dump)') parser.add_argument('destination', metavar='DESTINATION', help='destination mobile number') parser.add_argument('message', nargs='?', metavar='MESSAGE', help='message to send, defaults to stdin-prompt') return parser.parse_args() def parseArgsPy26(): """ Argument parser for Python 2.6 """ from gsmtermlib.posoptparse import PosOptionParser, Option parser = PosOptionParser(description='Simple script for sending SMS messages') parser.add_option('-i', '--port', metavar='PORT', help='port to which the GSM modem is connected; a number or a device name.') parser.add_option('-b', '--baud', metavar='BAUDRATE', default=115200, help='set baud rate') parser.add_option('-p', '--pin', metavar='PIN', default=None, help='SIM card PIN') parser.add_option('-d', '--deliver', action='store_true', help='wait for SMS delivery report') parser.add_option('-w', '--wait', type=int, default=0, help='Wait for modem to start, in seconds') parser.add_option('--CNMI', default='', help='Set the CNMI of the modem, used for message notifications') parser.add_positional_argument(Option('--destination', metavar='DESTINATION', help='destination mobile number')) options, args = parser.parse_args() if len(args) != 1: parser.error('Incorrect number of arguments - please specify a DESTINATION to send to, e.g. {0} 012789456'.format(sys.argv[0])) else: options.destination = args[0] options.message = None options.lock_path = None return options def main(): args = parseArgsPy26() if sys.version_info[0] == 2 and sys.version_info[1] < 7 else parseArgs() if args.port == None: sys.stderr.write('Error: No port specified. Please specify the port to which the GSM modem is connected using the -i argument.\n') sys.exit(1) if args.lock_path is None: send_sms(args) else: try: from oslo_concurrency import lockutils except ImportError: print('oslo_concurrency package is missing') sys.exit(1) # apply `lockutils.synchronized` decorator and run decorator = lockutils.synchronized('python_gsmmodem_sendsms', external=True, lock_path=args.lock_path) decorator(send_sms)(args) def send_sms(args): modem = GsmModem(args.port, args.baud, AT_CNMI=args.CNMI) if args.debug: # enable dump on serial port logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG) print('Connecting to GSM modem on {0}...'.format(args.port)) try: modem.connect(args.pin, waitingForModemToStartInSeconds=args.wait) except PinRequiredError: sys.stderr.write('Error: SIM card PIN required. Please specify a PIN with the -p argument.\n') sys.exit(1) except IncorrectPinError: sys.stderr.write('Error: Incorrect SIM card PIN entered.\n') sys.exit(1) print('Checking for network coverage...') try: modem.waitForNetworkCoverage(5) except TimeoutException: print('Network signal strength is not sufficient, please adjust modem position/antenna and try again.') modem.close() sys.exit(1) else: if args.message is None: print('\nPlease type your message and press enter to send it:') text = raw_input('> ') else: text = args.message if args.deliver: print ('\nSending SMS and waiting for delivery report...') else: print('\nSending SMS message...') try: sms = modem.sendSms(args.destination, text, waitForDeliveryReport=args.deliver) except TimeoutException: print('Failed to send message: the send operation timed out') modem.close() sys.exit(1) else: modem.close() if sms.report: print('Message sent{0}'.format(' and delivered OK.' if sms.status == SentSms.DELIVERED else ', but delivery failed.')) else: print('Message sent.') if __name__ == '__main__': main()