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