Full Code of babca/python-gsmmodem for AI

master ea7db39ade00 cached
54 files
502.4 KB
130.3k tokens
561 symbols
1 requests
Download .txt
Showing preview only (523K chars total). Download the full file or copy to clipboard to get everything.
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 <francois.aucamp@gmail.com>

Thanks to the following people for patches/suggestions:
davidphiliplee <https://github.com/davidphiliplee>
chakphanu <https://github.com/chakphanu>
Jonathan Endersby <https://github.com/jonathanendersby>
the01 <https://github.com/the01>
Frederico Rosmaninho
David Beitey <https://github.com/davidjb>
BOOMER74 <https://github.com/BOOMER74>
Cyril-Roques <https://github.com/Cyril-Roques>
PeteLawler <https://github.com/PeteLawler>
alex-eri <https://github.com/alex-eri>
tomchy <https://github.com/tomchy>
bennyslbs <https://github.com/bennyslbs>
epol <https://github.com/epol>
rags22489664 <https://github.com/rags22489664>
fataevalex <https://github.com/fataevalex>
paolo-losi <https://github.com/paolo-losi>
yuriykashin <https://github.com/yuriykashin>
foXes68 <https://github.com/foXes68>
babca <https://github.com/babca>



================================================
FILE: COPYING
================================================
		   GNU LESSER GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
 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 <francois.aucamp@gmail.com> - 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 <francois.aucamp@gmail.com> - 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 <francois.aucamp@gmail.com> - 0.7
- Support added for tracking SMS status reports
- PIN unlock support
- SMS API cleaned up
- Bugfixes

* Tue Apr 03 2013 Francois Aucamp <francois.aucamp@gmail.com> - 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 <francois.aucamp@gmail.com> - 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 <francois.aucamp@gmail.com> - 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 <francois.aucamp@gmail.com> - 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 <francois.aucamp@gmail.com> - 0.2
- Renamed "gsmterm" module to "gsmtermlib" to avoid conflict between startup
script and module

* Wed Feb 13 2013 Francois Aucamp <francois.aucamp@gmail.com> - 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 <http://www.pip-installer.org>`_ 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
<https://pypi.python.org/pypi/python-gsmmodem-new>`_, 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
<https://pypi.python.org/pypi/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 <http://sphinx-doc.org>`_-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 <http://www.virtualenv.org/>`_, 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 <PID>``.


================================================
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 <target>' where <target> 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
# "<project> v<release> 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 <link> 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 ^<target^>` where ^<target^> 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 <francois.aucamp@gmail.com>
@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: <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 : <er>
    
    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 (<mem1> parameter used for +CPMS)
        self._smsMemWrite = None # Preferred message storage memory for writes (<mem2> 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 <ds>
                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: <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:
                #' <RX timeout>'
        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 L
Download .txt
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
Download .txt
SYMBOL INDEX (561 symbols across 28 files)

FILE: examples/dial_callback_demo.py
  function callStatusCallback (line 27) | def callStatusCallback(call):
  function main (line 53) | def main():

FILE: examples/dial_polling_demo.py
  function main (line 25) | def main():

FILE: examples/incoming_call_demo.py
  function handleIncomingCall (line 22) | def handleIncomingCall(call):
  function main (line 46) | def main():

FILE: examples/own_number_demo.py
  function main (line 17) | def main():

FILE: examples/send_sms_demo.py
  function main (line 23) | def main():

FILE: examples/sms_handler_demo.py
  function handleSms (line 20) | def handleSms(sms):
  function main (line 26) | def main():

FILE: examples/ussd_demo.py
  function main (line 23) | def main():

FILE: gsmmodem/compat.py
  function wrapWait (line 11) | def wrapWait(func):

FILE: gsmmodem/exceptions.py
  class GsmModemException (line 3) | class GsmModemException(Exception):
  class TimeoutException (line 7) | class TimeoutException(GsmModemException):
    method __init__ (line 10) | def __init__(self, data=None):
  class InvalidStateException (line 16) | class InvalidStateException(GsmModemException):
  class InterruptedException (line 20) | class InterruptedException(InvalidStateException):
    method __init__ (line 24) | def __init__(self, message, cause=None):
  class CommandError (line 30) | class CommandError(GsmModemException):
    method __init__ (line 38) | def __init__(self, command=None, type=None, code=None):
  class CmeError (line 50) | class CmeError(CommandError):
    method __new__ (line 56) | def __new__(cls, *args, **kwargs):
    method __init__ (line 68) | def __init__(self, command, code):
  class SecurityException (line 72) | class SecurityException(CmeError):
    method __init__ (line 75) | def __init__(self, command, code):
  class PinRequiredError (line 79) | class PinRequiredError(SecurityException):
    method __init__ (line 84) | def __init__(self, command, code=11):
  class IncorrectPinError (line 88) | class IncorrectPinError(SecurityException):
    method __init__ (line 93) | def __init__(self, command, code=16):
  class PukRequiredError (line 97) | class PukRequiredError(SecurityException):
    method __init__ (line 102) | def __init__(self, command, code=12):
  class CmsError (line 106) | class CmsError(CommandError):
    method __new__ (line 112) | def __new__(cls, *args, **kwargs):
    method __init__ (line 120) | def __init__(self, command, code):
  class SmscNumberUnknownError (line 124) | class SmscNumberUnknownError(CmsError):
    method __init__ (line 129) | def __init__(self, command, code=330):
  class EncodingError (line 133) | class EncodingError(GsmModemException):

FILE: gsmmodem/gprs.py
  class PdpContext (line 22) | class PdpContext(object):
    method __init__ (line 24) | def __init__(self, cid, pdpType, apn, pdpAddress=None, dataCompression...
  class GprsModem (line 48) | class GprsModem(GsmModem):
    method pdpContexts (line 52) | def pdpContexts(self):
    method defaultPdpContext (line 70) | def defaultPdpContext(self):
    method defaultPdpContext (line 75) | def defaultPdpContext(self, pdpContext):
    method definePdpContext (line 79) | def definePdpContext(self, pdpContext):
    method initDataConnection (line 87) | def initDataConnection(self, pdpCid=1):

FILE: gsmmodem/modem.py
  class Sms (line 31) | class Sms(object):
    method __init__ (line 48) | def __init__(self, number, text, smsc=None):
  class ReceivedSms (line 54) | class ReceivedSms(Sms):
    method __init__ (line 57) | def __init__(self, gsmModem, status, number, time, text, smsc=None, ud...
    method reply (line 65) | def reply(self, message):
    method sendSms (line 69) | def sendSms(self, dnumber, message):
    method getModem (line 73) | def getModem(self):
  class SentSms (line 77) | class SentSms(Sms):
    method __init__ (line 84) | def __init__(self, number, text, reference, smsc=None):
    method status (line 90) | def status(self):
  class StatusReport (line 102) | class StatusReport(Sms):
    method __init__ (line 113) | def __init__(self, gsmModem, status, reference, number, timeSent, time...
  class GsmModem (line 123) | class GsmModem(SerialComms):
    method __init__ (line 150) | def __init__(self, port, baudrate=115200, incomingCallCallbackFunc=Non...
    method connect (line 189) | def connect(self, pin=None, waitingForModemToStartInSeconds=0):
    method _unlockSim (line 416) | def _unlockSim(self, pin):
    method write (line 437) | def write(self, data, waitForResponse=True, timeout=10, parseError=Tru...
    method signalStrength (line 500) | def signalStrength(self):
    method manufacturer (line 516) | def manufacturer(self):
    method model (line 521) | def model(self):
    method revision (line 526) | def revision(self):
    method imei (line 534) | def imei(self):
    method imsi (line 539) | def imsi(self):
    method networkName (line 544) | def networkName(self):
    method supportedCommands (line 551) | def supportedCommands(self):
    method smsTextMode (line 596) | def smsTextMode(self):
    method smsTextMode (line 600) | def smsTextMode(self, textMode):
    method smsSupportedEncoding (line 609) | def smsSupportedEncoding(self):
    method smsEncoding (line 653) | def smsEncoding(self):
    method smsEncoding (line 677) | def smsEncoding(self, encoding):
    method _setSmsMemory (line 717) | def _setSmsMemory(self, readDelete=None, write=None):
    method _compileSmsRegexes (line 729) | def _compileSmsRegexes(self):
    method gsmBusy (line 739) | def gsmBusy(self):
    method gsmBusy (line 750) | def gsmBusy(self, gsmBusy):
    method smsc (line 758) | def smsc(self):
    method smsc (line 771) | def smsc(self, smscNumber):
    method ownNumber (line 779) | def ownNumber(self):
    method ownNumber (line 824) | def ownNumber(self, phone_number):
    method waitForNetworkCoverage (line 831) | def waitForNetworkCoverage(self, timeout=None):
    method sendSms (line 882) | def sendSms(self, destination, text, waitForDeliveryReport=False, deli...
    method sendUssd (line 955) | def sendUssd(self, ussdString, responseTimeout=15):
    method checkForwarding (line 989) | def checkForwarding(self, querytype, responseTimeout=15):
    method setForwarding (line 1004) | def setForwarding(self, fwdType, fwdEnable, fwdNumber, responseTimeout...
    method dial (line 1021) | def dial(self, number, timeout=5, callStatusUpdateCallbackFunc=None):
    method processStoredSms (line 1064) | def processStoredSms(self, unreadOnly=False):
    method listStoredSms (line 1087) | def listStoredSms(self, status=Sms.STATUS_ALL, memory=None, delete=Fal...
    method _handleModemNotification (line 1174) | def _handleModemNotification(self, lines):
    method __threadedHandleModemNotification (line 1184) | def __threadedHandleModemNotification(self, lines):
    method _handleIncomingDTMF (line 1234) | def _handleIncomingDTMF(self,line):
    method GetIncomingDTMF (line 1243) | def GetIncomingDTMF(self):
    method _handleIncomingCall (line 1249) | def _handleIncomingCall(self, lines):
    method _handleCallInitiated (line 1294) | def _handleCallInitiated(self, regexMatch, callId=None, callType=1):
    method _handleCallAnswered (line 1308) | def _handleCallAnswered(self, regexMatch, callId=None):
    method _handleCallEnded (line 1325) | def _handleCallEnded(self, regexMatch, callId=None, filterUnanswered=F...
    method _handleCallRejected (line 1342) | def _handleCallRejected(self, regexMatch, callId=None):
    method _handleSmsReceived (line 1350) | def _handleSmsReceived(self, notificationLine):
    method _handleSmsStatusReport (line 1366) | def _handleSmsStatusReport(self, notificationLine):
    method _handleSmsStatusReportTe (line 1388) | def _handleSmsStatusReportTe(self, length, notificationLine):
    method readStoredSms (line 1413) | def readStoredSms(self, index, memory=None):
    method deleteStoredSms (line 1467) | def deleteStoredSms(self, index, memory=None):
    method deleteMultipleStoredSms (line 1482) | def deleteMultipleStoredSms(self, delFlag=4, memory=None):
    method _handleUssd (line 1508) | def _handleUssd(self, lines):
    method _parseCusdResponse (line 1516) | def _parseCusdResponse(self, lines):
    method _placeHolderCallback (line 1549) | def _placeHolderCallback(self, *args):
    method _pollCallStatus (line 1553) | def _pollCallStatus(self, expectedState, callId=None, timeout=None):
  class Call (line 1601) | class Call(object):
    method __init__ (line 1607) | def __init__(self, gsmModem, callId, callType, number, callStatusUpdat...
    method answered (line 1627) | def answered(self):
    method answered (line 1630) | def answered(self, answered):
    method sendDtmfTone (line 1635) | def sendDtmfTone(self, tones):
    method hangup (line 1664) | def hangup(self):
  class IncomingCall (line 1677) | class IncomingCall(Call):
    method __init__ (line 1682) | def __init__(self, gsmModem, number, ton, callerName, callId, callType):
    method answer (line 1700) | def answer(self):
    method hangup (line 1710) | def hangup(self):
  class Ussd (line 1715) | class Ussd(object):
    method __init__ (line 1722) | def __init__(self, gsmModem, sessionActive, message):
    method reply (line 1728) | def reply(self, message):
    method cancel (line 1740) | def cancel(self):

FILE: gsmmodem/pdu.py
  class SmsPduTzInfo (line 52) | class SmsPduTzInfo(tzinfo):
    method __init__ (line 55) | def __init__(self, pduOffsetStr=None):
    method _setPduOffsetStr (line 68) | def _setPduOffsetStr(self, pduOffsetStr):
    method utcoffset (line 88) | def utcoffset(self, dt):
    method dst (line 91) | def dst(self, dt):
  class InformationElement (line 96) | class InformationElement(object):
    method __new__ (line 109) | def __new__(cls, *args, **kwargs): #iei, ieLen, ieData):
    method __init__ (line 121) | def __init__(self, iei, ieLen=0, ieData=None):
    method decode (line 127) | def decode(cls, byteIter):
    method encode (line 141) | def encode(self):
    method __len__ (line 149) | def __len__(self):
  class Concatenation (line 154) | class Concatenation(InformationElement):
    method __init__ (line 174) | def __init__(self, iei=0x00, ieLen=0, ieData=None):
    method encode (line 184) | def encode(self):
  class PortAddress (line 195) | class PortAddress(InformationElement):
    method __init__ (line 207) | def __init__(self, iei=0x04, ieLen=0, ieData=None):
    method encode (line 216) | def encode(self):
  class Pdu (line 235) | class Pdu(object):
    method __init__ (line 238) | def __init__(self, data, tpduLength):
    method __str__ (line 248) | def __str__(self):
  function encodeSmsSubmitPdu (line 256) | def encodeSmsSubmitPdu(number, text, reference=0, validity=None, smsc=No...
  function decodeSmsPdu (line 387) | def decodeSmsPdu(pdu):
  function _decodeUserData (line 450) | def _decodeUserData(byteIter, userDataLen, dataCoding, udhPresent):
  function _decodeRelativeValidityPeriod (line 486) | def _decodeRelativeValidityPeriod(tpVp):
  function _encodeRelativeValidityPeriod (line 501) | def _encodeRelativeValidityPeriod(validityPeriod):
  function _decodeTimestamp (line 524) | def _decodeTimestamp(byteIter):
  function _encodeTimestamp (line 530) | def _encodeTimestamp(timestamp):
  function _decodeDataCoding (line 558) | def _decodeDataCoding(octet):
  function nibble2octet (line 566) | def nibble2octet(addressLen):
  function _decodeAddressField (line 569) | def _decodeAddressField(byteIter, smscField=False, log=False):
  function _encodeAddressField (line 603) | def _encodeAddressField(address, smscField=False):
  function encodeSemiOctets (line 651) | def encodeSemiOctets(number):
  function decodeSemiOctets (line 662) | def decodeSemiOctets(encodedNumber, numberOfOctets=None):
  function encodeTextMode (line 690) | def encodeTextMode(plaintext):
  function encodeGsm7 (line 719) | def encodeGsm7(plaintext, discardInvalid=False):
  function decodeGsm7 (line 750) | def decodeGsm7(encodedText):
  function divideTextGsm7 (line 776) | def divideTextGsm7(plainText):
  function packSeptets (line 819) | def packSeptets(octets, padBits=0):
  function unpackSeptets (line 856) | def unpackSeptets(septets, numberOfSeptets=None, prevOctet=None, shift=7):
  function decodeUcs2 (line 905) | def decodeUcs2(byteIter, numBytes):
  function encodeUcs2 (line 918) | def encodeUcs2(text):
  function divideTextUcs2 (line 935) | def divideTextUcs2(plainText):

FILE: gsmmodem/serial_comms.py
  class SerialComms (line 13) | class SerialComms(object):
    method __init__ (line 25) | def __init__(self, port, baudrate=115200, notifyCallbackFunc=None, fat...
    method connect (line 48) | def connect(self):
    method close (line 58) | def close(self):
    method _handleLineRead (line 64) | def _handleLineRead(self, line, checkForResponseTerm=True):
    method _placeholderCallback (line 84) | def _placeholderCallback(self, *args, **kwargs):
    method _readLoop (line 87) | def _readLoop(self):
    method write (line 124) | def write(self, data, waitForResponse=True, timeout=5, expectedRespons...

FILE: gsmmodem/util.py
  class SimpleOffsetTzInfo (line 9) | class SimpleOffsetTzInfo(tzinfo):
    method __init__ (line 12) | def __init__(self, offsetInHours=None):
    method utcoffset (line 21) | def utcoffset(self, dt):
    method dst (line 24) | def dst(self, dt):
    method __repr__ (line 27) | def __repr__(self):
  function parseTextModeTimeStr (line 30) | def parseTextModeTimeStr(timeStr):
  function lineStartingWith (line 47) | def lineStartingWith(string, lines):
  function lineMatching (line 57) | def lineMatching(regexStr, lines):
  function lineMatchingPattern (line 77) | def lineMatchingPattern(pattern, lines):
  function allLinesMatchingPattern (line 96) | def allLinesMatchingPattern(pattern, lines):
  function removeAtPrefix (line 113) | def removeAtPrefix(string):

FILE: setup.py
  class RunUnitTests (line 14) | class RunUnitTests(Command):
    method initialize_options (line 20) | def initialize_options(self):
    method finalize_options (line 23) | def finalize_options(self):
    method run (line 26) | def run(self):
  class RunUnitTestsCoverage (line 31) | class RunUnitTestsCoverage(Command):
    method initialize_options (line 37) | def initialize_options(self):
    method finalize_options (line 40) | def finalize_options(self):
    method run (line 43) | def run(self):

FILE: test/compat.py
  function assertGreater (line 9) | def assertGreater(self, a, b, msg=None):
  function assertGreaterEqual (line 13) | def assertGreaterEqual(self, a, b, msg=None):
  function assertIsInstance (line 17) | def assertIsInstance(self, a, b, msg=None):
  function assertListEqual (line 21) | def assertListEqual(self, a, b, msg=None):
  function assertIn (line 30) | def assertIn(self, a, b, msg=None):
  function assertNotIn (line 34) | def assertNotIn(self, a, b, msg=None):
  function assertIs (line 38) | def assertIs(self, a, b, msg=None):

FILE: test/fakemodems.py
  class FakeModem (line 6) | class FakeModem(object):
    method __init__ (line 10) | def __init__(self):
    method getResponse (line 23) | def getResponse(self, cmd):
    method pinLock (line 52) | def pinLock(self):
    method pinLock (line 55) | def pinLock(self, pinLock):
    method getAtdResponse (line 63) | def getAtdResponse(self, number):
    method getPreCallInitWaitSequence (line 67) | def getPreCallInitWaitSequence(self):
    method getCallInitNotification (line 71) | def getCallInitNotification(self, callId, callType):
    method getRemoteAnsweredNotification (line 75) | def getRemoteAnsweredNotification(self, callId, callType):
    method getRemoteHangupNotification (line 79) | def getRemoteHangupNotification(self, callId, callType):
    method getRemoteRejectCallNotification (line 82) | def getRemoteRejectCallNotification(self, callId, callType):
    method getIncomingCallNotification (line 87) | def getIncomingCallNotification(self, callerNumber, callType='VOICE', ...
  class GenericTestModem (line 91) | class GenericTestModem(FakeModem):
    method __init__ (line 94) | def __init__(self):
    method getResponse (line 112) | def getResponse(self, cmd):
    method getAtdResponse (line 129) | def getAtdResponse(self, number):
    method getPreCallInitWaitSequence (line 134) | def getPreCallInitWaitSequence(self):
    method getCallInitNotification (line 137) | def getCallInitNotification(self, callId, callType):
    method getRemoteAnsweredNotification (line 140) | def getRemoteAnsweredNotification(self, callId, callType):
    method getRemoteHangupNotification (line 144) | def getRemoteHangupNotification(self, callId, callType):
    method getIncomingCallNotification (line 149) | def getIncomingCallNotification(self, callerNumber, callType='VOICE', ...
  class WavecomMultiband900E1800 (line 153) | class WavecomMultiband900E1800(FakeModem):
    method __init__ (line 160) | def __init__(self):
    method getResponse (line 179) | def getResponse(self, cmd):
    method pinLock (line 189) | def pinLock(self):
    method pinLock (line 192) | def pinLock(self, pinLock):
    method getAtdResponse (line 199) | def getAtdResponse(self, number):
    method getPreCallInitWaitSequence (line 202) | def getPreCallInitWaitSequence(self):
    method getCallInitNotification (line 205) | def getCallInitNotification(self, callId, callType):
    method getRemoteAnsweredNotification (line 210) | def getRemoteAnsweredNotification(self, callId, callType):
    method getRemoteHangupNotification (line 213) | def getRemoteHangupNotification(self, callId, callType):
    method getIncomingCallNotification (line 216) | def getIncomingCallNotification(self, callerNumber, callType='VOICE', ...
    method __str__ (line 219) | def __str__(self):
  class HuaweiK3715 (line 223) | class HuaweiK3715(FakeModem):
    method __init__ (line 226) | def __init__(self):
    method getAtdResponse (line 256) | def getAtdResponse(self, number):
    method getPreCallInitWaitSequence (line 259) | def getPreCallInitWaitSequence(self):
    method getCallInitNotification (line 262) | def getCallInitNotification(self, callId, callType):
    method getRemoteAnsweredNotification (line 265) | def getRemoteAnsweredNotification(self, callId, callType):
    method getRemoteHangupNotification (line 268) | def getRemoteHangupNotification(self, callId, callType):
    method getIncomingCallNotification (line 271) | def getIncomingCallNotification(self, callerNumber, callType='VOICE', ...
    method __str__ (line 274) | def __str__(self):
  class HuaweiE1752 (line 278) | class HuaweiE1752(FakeModem):
    method __init__ (line 283) | def __init__(self):
    method getResponse (line 349) | def getResponse(self, cmd):
    method getAtdResponse (line 362) | def getAtdResponse(self, number):
    method getPreCallInitWaitSequence (line 365) | def getPreCallInitWaitSequence(self):
    method getCallInitNotification (line 368) | def getCallInitNotification(self, callId, callType):
    method getRemoteAnsweredNotification (line 371) | def getRemoteAnsweredNotification(self, callId, callType):
    method getRemoteHangupNotification (line 374) | def getRemoteHangupNotification(self, callId, callType):
    method getIncomingCallNotification (line 377) | def getIncomingCallNotification(self, callerNumber, callType='VOICE', ...
    method __str__ (line 380) | def __str__(self):
  class QualcommM6280 (line 384) | class QualcommM6280(FakeModem):
    method __init__ (line 387) | def __init__(self):
    method getResponse (line 409) | def getResponse(self, cmd):
    method getAtdResponse (line 429) | def getAtdResponse(self, number):
    method getPreCallInitWaitSequence (line 434) | def getPreCallInitWaitSequence(self):
    method getCallInitNotification (line 437) | def getCallInitNotification(self, callId, callType):
    method getRemoteAnsweredNotification (line 440) | def getRemoteAnsweredNotification(self, callId, callType):
    method getRemoteHangupNotification (line 443) | def getRemoteHangupNotification(self, callId, callType):
    method getIncomingCallNotification (line 448) | def getIncomingCallNotification(self, callerNumber, callType='VOICE', ...
    method __str__ (line 451) | def __str__(self):
  class ZteK3565Z (line 455) | class ZteK3565Z(FakeModem):
    method __init__ (line 458) | def __init__(self):
    method getResponse (line 503) | def getResponse(self, cmd):
    method getAtdResponse (line 523) | def getAtdResponse(self, number):
    method getPreCallInitWaitSequence (line 528) | def getPreCallInitWaitSequence(self):
    method getCallInitNotification (line 531) | def getCallInitNotification(self, callId, callType):
    method getRemoteAnsweredNotification (line 534) | def getRemoteAnsweredNotification(self, callId, callType):
    method getRemoteHangupNotification (line 537) | def getRemoteHangupNotification(self, callId, callType):
    method getRemoteRejectCallNotification (line 542) | def getRemoteRejectCallNotification(self, callId, callType):
    method getIncomingCallNotification (line 547) | def getIncomingCallNotification(self, callerNumber, callType='VOICE', ...
    method __str__ (line 550) | def __str__(self):
  class NokiaN79 (line 554) | class NokiaN79(GenericTestModem):
    method __init__ (line 562) | def __init__(self):
    method __str__ (line 590) | def __str__(self):
  function createModems (line 597) | def createModems():

FILE: test/test_gsmterm.py
  class TestTrie (line 15) | class TestTrie(unittest.TestCase):
    method setUp (line 18) | def setUp(self):
    method test_storeSingle (line 27) | def test_storeSingle(self):
    method test_deleteSingle (line 42) | def test_deleteSingle(self):
    method test_storeRetrieveMultiple (line 62) | def test_storeRetrieveMultiple(self):
    method test_storeDeleteMultiple (line 72) | def test_storeDeleteMultiple(self):
    method test_len (line 84) | def test_len(self):
    method test_contains (line 91) | def test_contains(self):
    method test_getMethod (line 97) | def test_getMethod(self):
    method test_keys (line 107) | def test_keys(self):
    method test_overWrite (line 119) | def test_overWrite(self):
    method test_filteredKeys (line 136) | def test_filteredKeys(self):
    method test_longestCommonPrefix (line 156) | def test_longestCommonPrefix(self):
    method test_iter (line 178) | def test_iter(self):
  class TestAtCommands (line 195) | class TestAtCommands(unittest.TestCase):
    method test_loadAtCommands (line 198) | def test_loadAtCommands(self):

FILE: test/test_modem.py
  class MockSerialPackage (line 36) | class MockSerialPackage(object):
    class Serial (line 39) | class Serial():
      method __init__ (line 44) | def __init__(self, *args, **kwargs):
      method read (line 61) | def read(self, timeout=None):
      method _setupReadValue (line 82) | def _setupReadValue(self, command):
      method write (line 103) | def write(self, data):
      method close (line 110) | def close(self):
      method inWaiting (line 113) | def inWaiting(self):
    class SerialException (line 123) | class SerialException(Exception):
  class TestGsmModemGeneralApi (line 127) | class TestGsmModemGeneralApi(unittest.TestCase):
    method setUp (line 130) | def setUp(self):
    method tearDown (line 137) | def tearDown(self):
    method test_manufacturer (line 140) | def test_manufacturer(self):
    method test_model (line 149) | def test_model(self):
    method test_revision (line 158) | def test_revision(self):
    method test_imei (line 170) | def test_imei(self):
    method test_imsi (line 179) | def test_imsi(self):
    method test_networkName (line 188) | def test_networkName(self):
    method test_supportedCommands (line 199) | def test_supportedCommands(self):
    method test_smsc (line 227) | def test_smsc(self):
    method test_signalStrength (line 264) | def test_signalStrength(self):
    method test_waitForNetorkCoverageNoCreg (line 284) | def test_waitForNetorkCoverageNoCreg(self):
    method test_waitForNetorkCoverage (line 301) | def test_waitForNetorkCoverage(self):
    method test_errorTypes (line 336) | def test_errorTypes(self):
    method test_smsEncoding (line 366) | def test_smsEncoding(self):
    method test_smsSupportedEncoding (line 383) | def test_smsSupportedEncoding(self):
  class TestUssd (line 400) | class TestUssd(unittest.TestCase):
    method setUp (line 403) | def setUp(self):
    method tearDown (line 417) | def tearDown(self):
    method test_sendUssd (line 420) | def test_sendUssd(self):
    method test_sendUssd_differentModems (line 441) | def test_sendUssd_differentModems(self):
    method test_sendUssdReply (line 459) | def test_sendUssdReply(self):
    method test_sendUssdResponseBeforeOk (line 476) | def test_sendUssdResponseBeforeOk(self):
    method test_sendUssdExtraRelease (line 498) | def test_sendUssdExtraRelease(self):
    method test_sendUssdError (line 514) | def test_sendUssdError(self):
    method test_sendUssdExtraLinesInResponse (line 523) | def test_sendUssdExtraLinesInResponse(self):
    method test_sendUssd_responseTimeout (line 534) | def test_sendUssd_responseTimeout(self):
  class TestEdgeCases (line 540) | class TestEdgeCases(unittest.TestCase):
    method test_smscPreloaded (line 543) | def test_smscPreloaded(self):
    method test_cfun0 (line 562) | def test_cfun0(self):
    method test_cfunNotSupported (line 584) | def test_cfunNotSupported(self):
    method test_commandNotSupported (line 607) | def test_commandNotSupported(self):
    method test_wavecomConnectSpecifics (line 620) | def test_wavecomConnectSpecifics(self):
    method test_zteConnectSpecifics (line 638) | def test_zteConnectSpecifics(self):
    method test_huaweiConnectSpecifics (line 655) | def test_huaweiConnectSpecifics(self):
    method test_smscSpecifiedBeforeConnect (line 672) | def test_smscSpecifiedBeforeConnect(self):
    method test_cpmsNotSupported (line 697) | def test_cpmsNotSupported(self):
    method test_cnmiNotSupported (line 718) | def test_cnmiNotSupported(self):
    method test_clipNotSupported (line 741) | def test_clipNotSupported(self):
    method test_crcNotSupported (line 768) | def test_crcNotSupported(self):
  class TestGsmModemDial (line 796) | class TestGsmModemDial(unittest.TestCase):
    method tearDown (line 798) | def tearDown(self):
    method init_modem (line 803) | def init_modem(self, modem):
    method test_dial (line 811) | def test_dial(self):
    method test_dialError (line 990) | def test_dialError(self):
    method test_dial_callInitEventTimeout (line 1000) | def test_dial_callInitEventTimeout(self):
    method test_dial_atdTimeout (line 1006) | def test_dial_atdTimeout(self):
  class TestGsmModemPinConnect (line 1015) | class TestGsmModemPinConnect(unittest.TestCase):
    method tearDown (line 1018) | def tearDown(self):
    method init_modem (line 1022) | def init_modem(self, modem):
    method test_connectPinLockedNoPin (line 1029) | def test_connectPinLockedNoPin(self):
    method test_connectPinLockedWithPin (line 1038) | def test_connectPinLockedWithPin(self):
    method test_connectPin_incorrect (line 1056) | def test_connectPin_incorrect(self):
    method test_connectPin_pukRequired (line 1071) | def test_connectPin_pukRequired(self):
    method test_connectPin_timeoutEvents (line 1086) | def test_connectPin_timeoutEvents(self):
  class TestIncomingCall (line 1110) | class TestIncomingCall(unittest.TestCase):
    method tearDown (line 1112) | def tearDown(self):
    method init_modem (line 1117) | def init_modem(self, modem, incomingCallCallbackFunc):
    method test_incomingCallAnswer (line 1125) | def test_incomingCallAnswer(self):
    method test_incomingCallCrcNotSupported (line 1177) | def test_incomingCallCrcNotSupported(self):
    method test_incomingCallCrcChangedExternally (line 1199) | def test_incomingCallCrcChangedExternally(self):
  class TestCall (line 1238) | class TestCall(unittest.TestCase):
    method init_modem (line 1241) | def init_modem(self, modem):
    method testDtmf (line 1249) | def testDtmf(self):
    method testDtmfInterrupted (line 1280) | def testDtmfInterrupted(self):
    method testCallAnsweredCallback (line 1296) | def testCallAnsweredCallback(self):
  class TestSms (line 1311) | class TestSms(unittest.TestCase):
    method setUp (line 1314) | def setUp(self):
    method initModem (line 1336) | def initModem(self, smsReceivedCallbackFunc):
    method test_sendSmsLeaveTextModeOnInvalidCharacter (line 1343) | def test_sendSmsLeaveTextModeOnInvalidCharacter(self):
    method test_sendSmsTextMode (line 1447) | def test_sendSmsTextMode(self):
    method test_sendSmsPduMode (line 1472) | def test_sendSmsPduMode(self):
    method test_sendSmsResponseMixedWithUnsolictedMessages (line 1516) | def test_sendSmsResponseMixedWithUnsolictedMessages(self):
    method test_receiveSmsTextMode (line 1563) | def test_receiveSmsTextMode(self):
    method test_receiveSmsPduMode (line 1618) | def test_receiveSmsPduMode(self):
    method test_sendSms_refCount (line 1670) | def test_sendSms_refCount(self):
    method test_sendSms_waitForDeliveryReport (line 1702) | def test_sendSms_waitForDeliveryReport(self):
    method test_sendSms_reply (line 1734) | def test_sendSms_reply(self):
    method test_sendSms_noCgmsResponse (line 1753) | def test_sendSms_noCgmsResponse(self):
  class TestStoredSms (line 1760) | class TestStoredSms(unittest.TestCase):
    method initModem (line 1763) | def initModem(self, textMode, smsReceivedCallbackFunc):
    method setUp (line 1773) | def setUp(self):
    method tearDown (line 1776) | def tearDown(self):
    method initFakeModemResponses (line 1780) | def initFakeModemResponses(self, textMode):
    method test_listStoredSms_pdu (line 1811) | def test_listStoredSms_pdu(self):
    method test_listStoredSms_text (line 1873) | def test_listStoredSms_text(self):
    method test_processStoredSms (line 1933) | def test_processStoredSms(self):
    method test_deleteStoredSms (line 1975) | def test_deleteStoredSms(self):
    method test_deleteMultipleStoredSms (line 1996) | def test_deleteMultipleStoredSms(self):
    method test_readStoredSms_pdu (line 2028) | def test_readStoredSms_pdu(self):
  class TestSmsStatusReports (line 2064) | class TestSmsStatusReports(unittest.TestCase):
    method initModem (line 2067) | def initModem(self, smsStatusReportCallback):
    method test_receiveStatusReportTextMode (line 2074) | def test_receiveStatusReportTextMode(self):
    method test_receiveSmsPduMode_problemCases (line 2127) | def test_receiveSmsPduMode_problemCases(self):
    method test_receiveStatusReportPduMode (line 2154) | def test_receiveStatusReportPduMode(self):
    method test_receiveSmsPduMode_invalidPDUsRecordedFromModems (line 2215) | def test_receiveSmsPduMode_invalidPDUsRecordedFromModems(self):

FILE: test/test_pdu.py
  class TestSemiOctets (line 16) | class TestSemiOctets(unittest.TestCase):
    method setUp (line 19) | def setUp(self):
    method test_encode (line 24) | def test_encode(self):
    method test_decode (line 30) | def test_decode(self):
    method test_decodeIter (line 38) | def test_decodeIter(self):
  class TestGsm7 (line 46) | class TestGsm7(unittest.TestCase):
    method setUp (line 49) | def setUp(self):
    method test_encode (line 62) | def test_encode(self):
    method test_decode (line 68) | def test_decode(self):
    method test_packSeptets (line 76) | def test_packSeptets(self):
    method test_unpackSeptets_no_limits (line 86) | def test_unpackSeptets_no_limits(self):
    method test_unpackSeptets_with_limits (line 94) | def test_unpackSeptets_with_limits(self):
    method test_encodeInvalid (line 102) | def test_encodeInvalid(self):
    method test_encodeInvalidDiscard (line 108) | def test_encodeInvalidDiscard(self):
  class TestUcs2 (line 116) | class TestUcs2(unittest.TestCase):
    method setUp (line 119) | def setUp(self):
    method test_encode (line 123) | def test_encode(self):
    method test_decode (line 129) | def test_decode(self):
  class TestSmsPduAddressFields (line 136) | class TestSmsPduAddressFields(unittest.TestCase):
    method setUp (line 139) | def setUp(self):
    method test_decodeAddressField (line 151) | def test_decodeAddressField(self):
    method test_encodeAddressField (line 158) | def test_encodeAddressField(self):
  class TestSmsPduSmscFields (line 164) | class TestSmsPduSmscFields(unittest.TestCase):
    method setUp (line 170) | def setUp(self):
    method test_decodeSmscField (line 176) | def test_decodeSmscField(self):
    method test_encodeSmscField (line 183) | def test_encodeSmscField(self):
  class TestRelativeValidityPeriod (line 190) | class TestRelativeValidityPeriod(unittest.TestCase):
    method setUp (line 193) | def setUp(self):
    method test_encode (line 199) | def test_encode(self):
    method test_decode (line 205) | def test_decode(self):
    method test_decode_invalidTpVp (line 210) | def test_decode_invalidTpVp(self):
    method test_encode_validityPeriodTooLong (line 214) | def test_encode_validityPeriodTooLong(self):
  class TestTimestamp (line 219) | class TestTimestamp(unittest.TestCase):
    method setUp (line 222) | def setUp(self):
    method test_encode (line 229) | def test_encode(self):
    method test_decode (line 235) | def test_decode(self):
    method test_encode_noTimezone (line 240) | def test_encode_noTimezone(self):
  class TestSmsPduTzInfo (line 246) | class TestSmsPduTzInfo(unittest.TestCase):
    method test_pickle (line 249) | def test_pickle(self):
    method test_dst (line 260) | def test_dst(self):
    method test_utcoffset (line 265) | def test_utcoffset(self):
  class TestUdhConcatenation (line 274) | class TestUdhConcatenation(unittest.TestCase):
    method setUp (line 277) | def setUp(self):
    method test_encode (line 282) | def test_encode(self):
    method test_decode (line 296) | def test_decode(self):
  class TestUdhPortAddress (line 313) | class TestUdhPortAddress(unittest.TestCase):
    method setUp (line 316) | def setUp(self):
    method test_encode (line 321) | def test_encode(self):
    method test_decode (line 334) | def test_decode(self):
  class TestSmsPdu (line 348) | class TestSmsPdu(unittest.TestCase):
    method test_encodeSmsSubmit (line 351) | def test_encodeSmsSubmit(self):
    method test_decode (line 367) | def test_decode(self):
    method test_encodeSmsSubmit_concatenated (line 459) | def test_encodeSmsSubmit_concatenated(self):
    method test_encodeSmsSubmit_invalidValidityType (line 479) | def test_encodeSmsSubmit_invalidValidityType(self):
    method test_decode_invalidPduType (line 483) | def test_decode_invalidPduType(self):
    method test_decode_invalidData (line 489) | def test_decode_invalidData(self):
    method test_encode_Gsm7_divideSMS (line 496) | def test_encode_Gsm7_divideSMS(self):
    method test_encode_Ucs2_divideSMS (line 509) | def test_encode_Ucs2_divideSMS(self):

FILE: test/test_serial_comms.py
  class MockSerialPackage (line 15) | class MockSerialPackage(object):
    class Serial (line 18) | class Serial():
      method __init__ (line 23) | def __init__(self, *args, **kwargs):
      method read (line 33) | def read(self, timeout=None):
      method _setupReadValue (line 58) | def _setupReadValue(self, command):
      method write (line 69) | def write(self, data):
      method close (line 74) | def close(self):
      method inWaiting (line 77) | def inWaiting(self):
    class SerialException (line 87) | class SerialException(Exception):
  class TestNotifications (line 90) | class TestNotifications(unittest.TestCase):
    method setUp (line 93) | def setUp(self):
    method test_callback (line 99) | def test_callback(self):
    method test_noCallback (line 120) | def test_noCallback(self):
  class TestSerialException (line 132) | class TestSerialException(unittest.TestCase):
    method setUp (line 135) | def setUp(self):
    method tearDown (line 141) | def tearDown(self):
    method test_readLoopException (line 144) | def test_readLoopException(self):
  class TestWrite (line 169) | class TestWrite(unittest.TestCase):
    method setUp (line 172) | def setUp(self):
    method tearDown (line 178) | def tearDown(self):
    method test_write (line 181) | def test_write(self):
    method test_writeTimeout (line 197) | def test_writeTimeout(self):
    method test_writeTimeout_data (line 202) | def test_writeTimeout_data(self):
    method test_writeTimeout_noData (line 214) | def test_writeTimeout_noData(self):

FILE: test/test_util.py
  class TestUtil (line 14) | class TestUtil(unittest.TestCase):
    method test_lineStartingWith (line 17) | def test_lineStartingWith(self):
    method test_lineMatching (line 27) | def test_lineMatching(self):
    method test_lineMatchingPattern (line 37) | def test_lineMatchingPattern(self):
    method test_allLinesMatchingPattern (line 47) | def test_allLinesMatchingPattern(self):
    method test_SimpleOffsetTzInfo (line 67) | def test_SimpleOffsetTzInfo(self):
    method test_removeAtPrefix (line 77) | def test_removeAtPrefix(self):

FILE: tools/gsmterm.py
  function parseArgs (line 14) | def parseArgs():
  function parseArgsPy26 (line 23) | def parseArgsPy26():
  function main (line 37) | def main():

FILE: tools/gsmtermlib/posoptparse.py
  class PosOptionParser (line 9) | class PosOptionParser(OptionParser):
    method format_help (line 10) | def format_help(self, formatter=None):
    method add_positional_argument (line 26) | def add_positional_argument(self, option):
    method set_out (line 34) | def set_out(self, out):

FILE: tools/gsmtermlib/terminal.py
  class Console (line 24) | class Console(object):
    method __init__ (line 35) | def __init__(self):
    method setup (line 38) | def setup(self):
    method cleanup (line 41) | def cleanup(self):
    method getkey (line 44) | def getkey(self):
    method __init__ (line 71) | def __init__(self):
    method setup (line 74) | def setup(self):
    method getkey (line 86) | def getkey(self):
    method cleanup (line 93) | def cleanup(self):
  class Console (line 61) | class Console(object):
    method __init__ (line 35) | def __init__(self):
    method setup (line 38) | def setup(self):
    method cleanup (line 41) | def cleanup(self):
    method getkey (line 44) | def getkey(self):
    method __init__ (line 71) | def __init__(self):
    method setup (line 74) | def setup(self):
    method getkey (line 86) | def getkey(self):
    method cleanup (line 93) | def cleanup(self):
  function cleanup_console (line 98) | def cleanup_console():
  class RawTerm (line 108) | class RawTerm(SerialComms):
    method __init__ (line 114) | def __init__(self, port, baudrate=9600):
    method _handleModemNotification (line 120) | def _handleModemNotification(self, lines):
    method printStartMessage (line 124) | def printStartMessage(self):
    method start (line 127) | def start(self):
    method stop (line 136) | def stop(self):
    method _inputLoop (line 142) | def _inputLoop(self):
  class GsmTerm (line 170) | class GsmTerm(RawTerm):
    method __init__ (line 197) | def __init__(self, port, baudrate=9600, useColor=True):
    method printStartMessage (line 211) | def printStartMessage(self):
    method _color (line 216) | def _color(self, color, msg):
    method _boldFace (line 223) | def _boldFace(self, msg):
    method _handleModemNotification (line 227) | def _handleModemNotification(self, lines):
    method _addToHistory (line 238) | def _addToHistory(self, command):
    method _inputLoop (line 243) | def _inputLoop(self):
    method _handleCtrlZ (line 286) | def _handleCtrlZ(self):
    method _handleEsc (line 297) | def _handleEsc(self):
    method _exit (line 305) | def _exit(self):
    method _cursorLeft (line 311) | def _cursorLeft(self):
    method _cursorRight (line 318) | def _cursorRight(self):
    method _cursorUp (line 325) | def _cursorUp(self):
    method _cursorDown (line 334) | def _cursorDown(self):
    method _handleBackspace (line 343) | def _handleBackspace(self):
    method _handleDelete (line 352) | def _handleDelete(self):
    method _handleHome (line 358) | def _handleHome(self):
    method _handleEnd (line 363) | def _handleEnd(self):
    method _doConfirmInput (line 368) | def _doConfirmInput(self):
    method _printGeneralHelp (line 470) | def _printGeneralHelp(self):
    method _printCommandHelp (line 480) | def _printCommandHelp(self, command=None):
    method _doCommandCompletion (line 533) | def _doCommandCompletion(self):
    method __printCommandSyntax (line 570) | def __printCommandSyntax(self, command):
    method _isPrintable (line 586) | def _isPrintable(self, char):
    method _refreshInputPrompt (line 589) | def _refreshInputPrompt(self, clearLen=0):
    method _removeInputPrompt (line 595) | def _removeInputPrompt(self):
    method _initAtCommandsTrie (line 599) | def _initAtCommandsTrie(self):

FILE: tools/gsmtermlib/trie.py
  class Trie (line 15) | class Trie(object):
    method __init__ (line 17) | def __init__(self, key=None, value=None):
    method __setitem__ (line 22) | def __setitem__(self, key, value):
    method __delitem__ (line 55) | def __delitem__(self, key):
    method __getitem__ (line 79) | def __getitem__(self, key):
    method __contains__ (line 97) | def __contains__(self, key):
    method __len__ (line 104) | def __len__(self):
    method get (line 111) | def get(self, key, default=None):
    method _allKeys (line 117) | def _allKeys(self, prefix):
    method keys (line 125) | def keys(self, prefix=None):
    method _filteredKeys (line 136) | def _filteredKeys(self, key, prefix):
    method longestCommonPrefix (line 153) | def longestCommonPrefix(self, prefix=''):
    method _longestCommonPrefix (line 159) | def _longestCommonPrefix(self, key, prefix):
    method __iter__ (line 182) | def __iter__(self):

FILE: tools/identify-modem.py
  function parseArgs (line 17) | def parseArgs():
  function parseArgsPy26 (line 28) | def parseArgsPy26():
  function main (line 44) | def main():

FILE: tools/sendsms.py
  function parseArgs (line 15) | def parseArgs():
  function parseArgsPy26 (line 31) | def parseArgsPy26():
  function main (line 51) | def main():
  function send_sms (line 70) | def send_sms(args):
Condensed preview — 54 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (535K chars).
[
  {
    "path": ".coveragerc",
    "chars": 541,
    "preview": "# .coveragerc to control coverage.py\n[run]\nbranch = True\nsource =\n    gsmmodem/\n    tools/gsmtermlib\nomit =\n    # Omit P"
  },
  {
    "path": ".flake8",
    "chars": 194,
    "preview": "[flake8]\nmax-line-length = 88\nextend-ignore =\n    E203  # \"Whitespace before ':'\" - not PEP-8 compliant\n    E501  # \"Lin"
  },
  {
    "path": ".github/workflows/publish.yaml",
    "chars": 2108,
    "preview": "# This workflow will upload a Python Package to PyPI when a release is published\n# For more information see:\n#   - https"
  },
  {
    "path": ".gitignore",
    "chars": 377,
    "preview": "*.py[cod]\n\n# Package-building stuff\n*.egg\n*.egg-info\ndist\nbuild\ndocs/_build\n\n# Eclipse project info\n.project\n.pydevproje"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 1840,
    "preview": "repos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v4.1.0\n    hooks:\n      - id: check-case-confli"
  },
  {
    "path": ".prettierrc.yaml",
    "chars": 15,
    "preview": "printWidth: 88\n"
  },
  {
    "path": ".travis.yml",
    "chars": 370,
    "preview": "language: python\npython:\n  - \"3.6\"\n  - \"3.5\"\n  - \"3.4\"\n  - \"3.3\"\n  - \"2.7\"\ninstall:\n  # Install unittest2 on Python 2.6\n"
  },
  {
    "path": "AUTHORS",
    "chars": 869,
    "preview": "Francois Aucamp <francois.aucamp@gmail.com>\n\nThanks to the following people for patches/suggestions:\ndavidphiliplee <htt"
  },
  {
    "path": "COPYING",
    "chars": 7639,
    "preview": "\t\t   GNU LESSER GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software"
  },
  {
    "path": "ChangeLog",
    "chars": 3208,
    "preview": "* Wed Mar 15 2017 babca - 0.12\n– stable release\n- unit tests fixed after rapid merging – credits to: tomchy\n- python3.6 "
  },
  {
    "path": "MANIFEST.in",
    "chars": 97,
    "preview": "include AUTHORS\ninclude ChangeLog\ninclude COPYING\ninclude requirements.txt\ninclude examples/*.py\n"
  },
  {
    "path": "README.rst",
    "chars": 4895,
    "preview": "python-gsmmodem-new\n===================\n*GSM modem module for Python*\n\npython-gsmmodem is a module that allows easy cont"
  },
  {
    "path": "docs/Makefile",
    "chars": 6798,
    "preview": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD "
  },
  {
    "path": "docs/api.rst",
    "chars": 292,
    "preview": "API\n===\n\n\nGSM Modem\n---------\n\n.. automodule:: gsmmodem.modem\n   :members:\n\n\nSerial Communications\n---------------------"
  },
  {
    "path": "docs/conf.py",
    "chars": 8062,
    "preview": "# -*- coding: utf-8 -*-\n#\n# python-gsmmodem documentation build configuration file, created by\n# sphinx-quickstart on Su"
  },
  {
    "path": "docs/examples.rst",
    "chars": 550,
    "preview": "Examples\n========\n\n\nDial Callback\n-------------\n\n.. literalinclude:: ../examples/dial_callback_demo.py\n   :language: pyt"
  },
  {
    "path": "docs/index.rst",
    "chars": 556,
    "preview": ".. python-gsmmodem documentation master file, created by\n   sphinx-quickstart on Sun Aug 11 20:50:25 2013.\n   You can ad"
  },
  {
    "path": "docs/make.bat",
    "chars": 6719,
    "preview": "@ECHO OFF\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\n"
  },
  {
    "path": "examples/dial_callback_demo.py",
    "chars": 2583,
    "preview": "#!/usr/bin/env python\n\n\"\"\"\\\nDemo: dial a number (using callbacks to track call status)\n\nSimple demo app that makes a voi"
  },
  {
    "path": "examples/dial_polling_demo.py",
    "chars": 2624,
    "preview": "#!/usr/bin/env python\n\n\"\"\"\\\nDemo: dial a number (simple example using polling to check call status)\n\nSimple demo app tha"
  },
  {
    "path": "examples/incoming_call_demo.py",
    "chars": 1970,
    "preview": "#!/usr/bin/env python\n\n\"\"\"\\\nDemo: handle incoming calls\n\nSimple demo app that listens for incoming calls, displays the c"
  },
  {
    "path": "examples/own_number_demo.py",
    "chars": 885,
    "preview": "#!/usr/bin/env python\n\n\"\"\"\\\nDemo: read own phone number\n\"\"\"\n\nfrom __future__ import print_function\n\nimport logging\n\nPORT"
  },
  {
    "path": "examples/send_sms_demo.py",
    "chars": 963,
    "preview": "#!/usr/bin/env python\n\n\"\"\"\nDemo: Send Simple SMS Demo\n\nSimple demo to send sms via gsmmodem package\n\"\"\"\nfrom __future__ "
  },
  {
    "path": "examples/sms_handler_demo.py",
    "chars": 1296,
    "preview": "#!/usr/bin/env python\n\n\"\"\"\\\nDemo: handle incoming SMS messages by replying to them\n\nSimple demo app that listens for inc"
  },
  {
    "path": "examples/ussd_demo.py",
    "chars": 1194,
    "preview": "#!/usr/bin/env python\n\n\"\"\"\\\nDemo: Simple USSD example\n\nSimple demo app that initiates a USSD session, reads the string r"
  },
  {
    "path": "gsmmodem/__init__.py",
    "chars": 740,
    "preview": "\"\"\" Package that allows easy control of an attached GSM modem \n\nThe main class for controlling a modem is GsmModem, whic"
  },
  {
    "path": "gsmmodem/compat.py",
    "chars": 1008,
    "preview": "\"\"\" Contains monkey-patched equivalents for a few commonly-used Python 2.7-and-higher functions.\nUsed to provide backwar"
  },
  {
    "path": "gsmmodem/exceptions.py",
    "chars": 4484,
    "preview": "\"\"\" Module defines exceptions used by gsmmodem \"\"\"\n\nclass GsmModemException(Exception):\n    \"\"\" Base exception raised fo"
  },
  {
    "path": "gsmmodem/gprs.py",
    "chars": 4563,
    "preview": "# -*- coding: utf8 -*-\n\n\"\"\" GPRS/Data-specific classes \n\nBRANCH: mms\n\nPLEASE NOTE: *Everything* in this file (PdpContext"
  },
  {
    "path": "gsmmodem/modem.py",
    "chars": 81052,
    "preview": "#!/usr/bin/env python\n\n\"\"\" High-level API classes for an attached GSM modem \"\"\"\n\nimport sys, re, logging, weakref, time,"
  },
  {
    "path": "gsmmodem/pdu.py",
    "chars": 35724,
    "preview": "# -*- coding: utf8 -*-\n\n\"\"\" SMS PDU encoding methods \"\"\"\n\nfrom __future__ import unicode_literals\n\nimport sys, codecs\nfr"
  },
  {
    "path": "gsmmodem/serial_comms.py",
    "chars": 6388,
    "preview": "#!/usr/bin/env python\n\n\"\"\" Low-level serial communications handling \"\"\"\n\nimport sys, threading, logging\n\nimport re\nimpor"
  },
  {
    "path": "gsmmodem/util.py",
    "chars": 4145,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\n\"\"\" Some common utility classes used by tests \"\"\"\n\nfrom datetime import d"
  },
  {
    "path": "pyproject.toml",
    "chars": 250,
    "preview": "[build-system]\nrequires = [\n    \"setuptools>=45\",\n    \"setuptools_scm[toml]>=6.2\",\n    \"wheel\",\n]\nbuild-backend = \"setup"
  },
  {
    "path": "requirements.txt",
    "chars": 2,
    "preview": ".\n"
  },
  {
    "path": "setup.cfg",
    "chars": 1232,
    "preview": "[metadata]\nname = python-gsmmodem-new\ndescription = Control an attached GSM modem: send/receive SMS messages, handle cal"
  },
  {
    "path": "setup.py",
    "chars": 1212,
    "preview": "#!/usr/bin/env python\n\n\"\"\" python-gsmmodem installation script \"\"\"\n\nimport sys\nfrom distutils.core import Command\nfrom s"
  },
  {
    "path": "test/__init__.py",
    "chars": 34,
    "preview": "\"\"\" Tests for python-gsmmodem \"\"\"\n"
  },
  {
    "path": "test/compat.py",
    "chars": 2265,
    "preview": "\"\"\" Contains equivalents for a few commonly-used Python 2.7-and-higher test functions.\nUsed to provide backwards-compati"
  },
  {
    "path": "test/fakemodems.py",
    "chars": 31256,
    "preview": "\"\"\" Module containing fake modem descriptors, for testing \"\"\"\n\nimport abc\nfrom copy import copy\n\nclass FakeModem(object)"
  },
  {
    "path": "test/test_gsmterm.py",
    "chars": 10089,
    "preview": "#!/usr/bin/env python\n\n\"\"\" Test suite for GsmTerm \"\"\"\n\nimport sys, unittest\n\nfrom . import compat # For Python 2.6 compa"
  },
  {
    "path": "test/test_modem.py",
    "chars": 136200,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf8 -*-\n\n\"\"\" Test suite for gsmmodem.modem \"\"\"\n\nfrom __future__ import print_functi"
  },
  {
    "path": "test/test_pdu.py",
    "chars": 36122,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\n\"\"\" Test suite for SMS PDU encoding/decoding algorithms \"\"\"\n\nfrom __futur"
  },
  {
    "path": "test/test_serial_comms.py",
    "chars": 9665,
    "preview": "#!/usr/bin/env python\n\n\"\"\" Test suite for gsmmodem.serial_comms \"\"\"\n\nfrom __future__ import print_function\n\nimport sys, "
  },
  {
    "path": "test/test_util.py",
    "chars": 3625,
    "preview": "#!/usr/bin/env python\n\n\"\"\" Test suite for gsmmodem.util \"\"\"\n\nfrom __future__ import print_function\n\nimport sys, time, un"
  },
  {
    "path": "tools/at_cmd_init_modem.txt",
    "chars": 371,
    "preview": "# Simple script for GSMTerm to initialize the modem with a few common settings\n# Load this from within GSMTerm by typing"
  },
  {
    "path": "tools/gsmterm.py",
    "chars": 1966,
    "preview": "#!/usr/bin/env python\n\n\"\"\"\\\nLaunch script for GSMTerm\n\n@author: Francois Aucamp <francois.aucamp@gmail.com>\n\"\"\"\nfrom __f"
  },
  {
    "path": "tools/gsmtermlib/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tools/gsmtermlib/atcommands.py",
    "chars": 42172,
    "preview": "\nCATEGORIES = ('General', 'Call Control', 'Network Service', 'Security', 'Phonebook', 'SMS', 'Supplementary Services', '"
  },
  {
    "path": "tools/gsmtermlib/posoptparse.py",
    "chars": 1253,
    "preview": "\"\"\" PosOptionParser class gotten from Douglas Mayle at StackOverflow:\nhttp://stackoverflow.com/a/664614/1980416\n\nUsed fo"
  },
  {
    "path": "tools/gsmtermlib/terminal.py",
    "chars": 26010,
    "preview": "#!/usr/bin/env python\n\n\"\"\"\\\nGSMTerm: A user-friendly terminal for interacting with a GSM modem\n\nNote: The \"Console\" obje"
  },
  {
    "path": "tools/gsmtermlib/trie.py",
    "chars": 5987,
    "preview": "\"\"\" Pure Python trie implementation for strings \"\"\"\n\n# Compensate for differences between Python 2 and 3\nimport sys\nif s"
  },
  {
    "path": "tools/identify-modem.py",
    "chars": 4506,
    "preview": "#!/usr/bin/env python\n\n\n\"\"\"\\\nSimple script to assist with identifying a GSM modem\nThe debug information obtained by this"
  },
  {
    "path": "tools/sendsms.py",
    "chars": 5486,
    "preview": "#!/usr/bin/env python\n\n\n\"\"\"\\\nSimple script to send an SMS message\n\n@author: Francois Aucamp <francois.aucamp@gmail.com>\n"
  }
]

About this extraction

This page contains the full source code of the babca/python-gsmmodem GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 54 files (502.4 KB), approximately 130.3k tokens, and a symbol index with 561 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!