├── VERSION ├── txgsm ├── __init__.py ├── tests │ ├── __init__.py │ ├── test_utils.py │ ├── base.py │ ├── test_service.py │ └── test_protocol.py ├── utils.py ├── protocol.py └── service.py ├── requirements.txt ├── .coveragerc ├── AUTHORS ├── twisted └── plugins │ └── txgsm_plugin.py ├── requirements-dev.txt ├── setup.cfg ├── MANIFEST.in ├── .gitignore ├── .travis.yml ├── setup.py ├── LICENSE ├── doc ├── index.rst ├── Makefile ├── make.bat └── conf.py └── README.rst /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.0 2 | -------------------------------------------------------------------------------- /txgsm/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /txgsm/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Twisted 2 | pyserial 3 | mock 4 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | ve/* 4 | setup.py 5 | doc/* 6 | txgsm/tests/* 7 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Simon de Haan, http://github.com/smn 2 | Thijs Triemstra, http://github.com/thijstriemstra 3 | -------------------------------------------------------------------------------- /twisted/plugins/txgsm_plugin.py: -------------------------------------------------------------------------------- 1 | from txgsm.service import TxGSMServiceMaker 2 | serviceMaker = TxGSMServiceMaker() 3 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | flake8==2.5.0 2 | coverage==4.0.2 3 | pytest==2.8.2 4 | pytest-xdist==1.13.1 5 | pytest-cov==2.2.0 6 | Sphinx==1.3.1 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --doctest-modules --verbose --ignore=ve/ txgsm/ 3 | 4 | [flake8] 5 | ignore = F403 6 | exclude = ve,doc 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst requirements.pip requirements.txt requirements-dev.txt VERSION LICENSE AUTHORS .coveragerc 2 | include twisted/plugins/txgsm_plugin.py 3 | recursive-include txgsm *.py 4 | graft twisted 5 | 6 | recursive-include doc * 7 | prune doc/_build 8 | 9 | recursive-exclude * __pycache__ 10 | recursive-exclude * *.py[co] 11 | recursive-exclude * *~ 12 | recursive-exclude * .coverage 13 | recursive-exclude * .DS_Store 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Documentation 33 | /doc/_build/ 34 | 35 | # Mr Developer 36 | .mr.developer.cfg 37 | .project 38 | .pydevproject 39 | /ve/ 40 | *.cache 41 | _trial_temp 42 | *.log 43 | *.pid 44 | wheelhouse/ 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - 2.6 5 | - 2.7 6 | - 3.5 7 | cache: pip 8 | matrix: 9 | allow_failures: 10 | - python: 3.5 11 | install: 12 | - pip install -U pip coveralls wheel 13 | - pip install https://github.com/pmarti/python-messaging/archive/master.zip 14 | - pip install -r requirements-dev.txt 15 | - pip install -e . 16 | - flake8 17 | script: 18 | - py.test --cov=txgsm --cov-report=term 19 | after_success: 20 | - coveralls 21 | deploy: 22 | provider: pypi 23 | distributions: sdist bdist_wheel 24 | user: smn 25 | password: 26 | secure: "lALu68z75ANy0tD2SNVh3+IEz/RAbQyKiYtjXkxPKIg/j6wb8BL65CG3dhf9yzfTRR01BvqD4aqCfq6saeaqAyv6r5yEcmLFjMRE3uf9SlRXSnFXxJ+LRedrNJVZsFn3QBrbAZh6S+SMrOcoynrZQ6qHQMFDSxR0jn4b2SWGQyQ=" 27 | on: 28 | tags: true 29 | all_branches: true 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | def listify(filename): 5 | return list(filter(None, open(filename, 'r').read().split('\n'))) 6 | 7 | install_requires = listify("requirements.txt") 8 | 9 | with open('VERSION', 'r') as fp: 10 | version = fp.read().strip() 11 | 12 | setup( 13 | name="txgsm", 14 | version=version, 15 | url='http://github.com/smn/txgsm', 16 | license='BSD', 17 | description="Utilities for talking to a GSM modem over USB via AT " 18 | "commands.", 19 | long_description=open('README.rst', 'r').read(), 20 | author='Simon de Haan', 21 | author_email='simon@praekeltfoundation.org', 22 | packages=[ 23 | "txgsm", 24 | "twisted.plugins", 25 | ], 26 | package_data={ 27 | 'twisted.plugins': ['twisted/plugins/txgsm_plugin.py'], 28 | }, 29 | include_package_data=True, 30 | install_requires=install_requires, 31 | classifiers=[ 32 | 'Development Status :: 3 - Alpha', 33 | 'Intended Audience :: Developers', 34 | 'License :: OSI Approved :: BSD License', 35 | 'Operating System :: POSIX', 36 | 'Topic :: Software Development :: Libraries :: Python Modules', 37 | 'Topic :: System :: Networking', 38 | 'Framework :: Twisted', 39 | 'Programming Language :: Python', 40 | 'Programming Language :: Python :: 2', 41 | 'Programming Language :: Python :: 2.6', 42 | 'Programming Language :: Python :: 2.7', 43 | ], 44 | ) 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Simon de Haan and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the Praekelt Foundation nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 22 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /txgsm/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from twisted.test import proto_helpers 2 | 3 | from txgsm.tests.base import TxGSMBaseTestCase 4 | from txgsm.utils import USSDConsole 5 | 6 | from mock import Mock 7 | 8 | 9 | class USSDConsoleTestCase(TxGSMBaseTestCase): 10 | 11 | def setUp(self): 12 | super(USSDConsoleTestCase, self).setUp() 13 | self.exit = Mock() 14 | self.console = USSDConsole(self.modem, on_exit=self.exit) 15 | self.console_transport = proto_helpers.StringTransport() 16 | self.console.makeConnection(self.console_transport) 17 | 18 | def test_dial_single_screen_session(self): 19 | d = self.console.dial('*100#') 20 | self.assertExchange( 21 | input=['AT+CUSD=1,"*100#",15'], 22 | output=[ 23 | 'OK', 24 | '+CUSD: 2,"foo",25' 25 | ]) 26 | self.assertEqual(self.console_transport.value(), 'foo\n') 27 | return d 28 | 29 | def test_dial_multiple_screen_session(self): 30 | d = self.console.dial('*100#') 31 | self.assertExchange( 32 | input=['AT+CUSD=1,"*100#",15'], 33 | output=[ 34 | 'OK', 35 | '+CUSD: 1,"what is your name?",25' 36 | ]) 37 | self.assertEqual(self.console_transport.value(), 38 | 'what is your name?\n%s> ' % (self.console.prefix,)) 39 | self.console_transport.clear() 40 | self.console.lineReceived('foo') 41 | self.assertExchange( 42 | input=['AT+CUSD=1,"foo",15'], 43 | output=[ 44 | 'OK', 45 | '+CUSD: 2,"thanks!",25' 46 | ]) 47 | self.assertEqual(self.console_transport.value(), 'thanks!\n') 48 | self.assertTrue(self.exit.called) 49 | return d 50 | -------------------------------------------------------------------------------- /txgsm/utils.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: txgsm.tests.test_utils -*- 2 | 3 | from twisted.protocols.basic import LineReceiver 4 | 5 | from os import linesep 6 | 7 | 8 | def quote(s): 9 | return s.replace('"', '\"') 10 | 11 | 12 | class Console(LineReceiver): 13 | delimiter = linesep 14 | 15 | def __init__(self, on_input, prefix=''): 16 | self.prefix = prefix 17 | self.on_input = on_input 18 | 19 | def prompt(self): 20 | self.transport.write("%s> " % (self.prefix,)) 21 | 22 | def lineReceived(self, line): 23 | return self.on_input(line) 24 | 25 | 26 | class USSDConsole(Console): 27 | 28 | NO_FURTHER_ACTION = 0 29 | FURTHER_ACTION = 1 30 | NETWORK_TERMINATED = 2 31 | OTHER_CLIENT_RESPONDED = 3 32 | OPERATION_NOT_SUPPORTED = 4 33 | NETWORK_TIMEOUT = 5 34 | 35 | def __init__(self, modem, on_exit): 36 | Console.__init__(self, self.on_input, prefix='USSD ') 37 | self.modem = modem 38 | self.on_exit = on_exit 39 | 40 | def dial(self, number): 41 | d = self.modem.dial_ussd_code(number) 42 | d.addCallback(self.handle_response) 43 | return d 44 | 45 | def parse_ussd_response(self, resp): 46 | for item in resp: 47 | if not item.startswith('+CUSD'): 48 | continue 49 | 50 | ussd_resp = item.lstrip('+CUSD: ') 51 | operation = ussd_resp[0] 52 | content = ussd_resp[3:-4] 53 | return int(operation), content 54 | 55 | def on_input(self, line): 56 | d = self.modem.send_command('AT+CUSD=1,"%s",15' % (quote(line),), 57 | expect="+CUSD") 58 | d.addCallback(self.handle_response) 59 | return d 60 | 61 | def handle_response(self, result): 62 | operation, content = self.parse_ussd_response(result['response']) 63 | self.sendLine(content) 64 | if operation == self.FURTHER_ACTION: 65 | return self.prompt() 66 | else: 67 | return self.on_exit(operation) 68 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | txgsm 2 | ===== 3 | 4 | txgsm provides utilities for talking to a GSM modem via AT commands. 5 | 6 | Installation 7 | ------------ 8 | 9 | Install txgsm from PyPi directly:: 10 | 11 | pip install txgsm 12 | 13 | Or checkout the source from Github:: 14 | 15 | git clone https://github.com/smn/txgsm.git 16 | 17 | And install the dependencies:: 18 | 19 | pip install -r requirements.txt 20 | pip install -r requirements-dev.txt 21 | pip install -e . 22 | 23 | Usage 24 | ----- 25 | 26 | Probe the modem ``/dev/tty.usbmodem1421`` to see if it is something modem-ish:: 27 | 28 | twistd -n txgsm --device=/dev/tty.usbmodem1421 probe-modem 29 | 30 | To check other options:: 31 | 32 | twistd -n txgsm --help 33 | 34 | Supported modems 35 | ---------------- 36 | 37 | The following modem backends are included: 38 | 39 | ============================================ ========================= 40 | Modem Backend 41 | ============================================ ========================= 42 | `3G dongle for Vodacom`_ ``txgsm.backend.foo`` 43 | `Truteq's GSM CommServ`_ ``txgsm.backend.bar`` 44 | `Raspberry PI SIM800 GSM/GPRS Add-on V2.0`_ ``txgsm.backend.spam`` 45 | ============================================ ========================= 46 | 47 | Pull requests for new backends are very welcome! 48 | 49 | Tests 50 | ----- 51 | 52 | You can run the tests using ``trial``:: 53 | 54 | trial txgsm 55 | 56 | Or using ``py.test`` (that also generates a test coverage report):: 57 | 58 | py.test --cov=txgsm --cov-report=term 59 | 60 | 61 | .. toctree:: 62 | :maxdepth: 2 63 | 64 | 65 | Indices and tables 66 | ================== 67 | 68 | * :ref:`genindex` 69 | * :ref:`modindex` 70 | * :ref:`search` 71 | 72 | 73 | .. _3G dongle for Vodacom: http://www.imei.info/phonedatabase/11862-zte-vodafone-k3772-z/ 74 | .. _Truteq's GSM CommServ: http://www.truteqdevices.com/products/wireless-server-solutions/commserver-v5/ 75 | .. _Raspberry PI SIM800 GSM/GPRS Add-on V2.0: http://wiki.iteadstudio.com/RPI_SIM800_GSM/GPRS_ADD-ON_V2.0 76 | -------------------------------------------------------------------------------- /txgsm/tests/base.py: -------------------------------------------------------------------------------- 1 | from twisted.trial.unittest import TestCase 2 | from twisted.internet.defer import inlineCallbacks, Deferred 3 | from twisted.internet import reactor 4 | from twisted.test import proto_helpers 5 | from twisted.python import log 6 | 7 | from txgsm.protocol import TxGSMProtocol 8 | 9 | 10 | class TxGSMBaseTestCase(TestCase): 11 | 12 | def setUp(self): 13 | self.modem = TxGSMProtocol() 14 | self.modem.verbose = True 15 | self.modem_transport = proto_helpers.StringTransport() 16 | self.modem.makeConnection(self.modem_transport) 17 | 18 | def reply(self, data, delimiter=None, modem=None): 19 | modem = modem or self.modem 20 | dl = delimiter or modem.delimiter 21 | modem.dataReceived(data + dl) 22 | 23 | def wait_for_next_commands(self, clear=True, modem=None, transport=None): 24 | 25 | modem = modem or self.modem 26 | transport = transport or self.modem_transport 27 | 28 | d = Deferred() 29 | 30 | def check_for_input(): 31 | if not transport.value(): 32 | reactor.callLater(0, check_for_input) 33 | return 34 | 35 | commands = transport.value().split(modem.delimiter) 36 | 37 | if clear: 38 | transport.clear() 39 | 40 | d.callback(filter(None, commands)) 41 | 42 | check_for_input() 43 | return d 44 | 45 | @inlineCallbacks 46 | def assertCommands(self, commands, clear=True, modem=None, transport=None): 47 | received_commands = yield self.wait_for_next_commands( 48 | clear=clear, modem=modem, transport=transport) 49 | self.assertEqual(commands, received_commands) 50 | 51 | @inlineCallbacks 52 | def assertExchange(self, input, output, clear=True, modem=None, 53 | transport=None): 54 | yield self.assertCommands(input, clear=clear, modem=modem, 55 | transport=transport) 56 | for reply in output: 57 | self.reply(reply, modem=modem) 58 | 59 | 60 | # Shamelessly copyied from @Hodgestar's contribution to 61 | # https://github.com/praekelt/vumi/ 62 | class LogCatcher(object): 63 | 64 | def __init__(self): 65 | self.logs = [] 66 | 67 | def __enter__(self): 68 | log.theLogPublisher.addObserver(self.logs.append) 69 | return self 70 | 71 | def __exit__(self, *exc_info): 72 | log.theLogPublisher.removeObserver(self.logs.append) 73 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | txgsm 2 | ===== 3 | 4 | Utilities for talking to a GSM modem over USB via AT commands. 5 | Will NOT work with all modems, YMMV. 6 | 7 | |travis|_ |coveralls|_ |pypi|_ |license|_ 8 | 9 | :: 10 | 11 | $ virtualenv ve 12 | (ve)$ pip install . 13 | (ve)$ twistd txgsm --help 14 | 15 | Sending an SMS 16 | -------------- 17 | 18 | Supports multipart & unicode. 19 | 20 | :: 21 | 22 | 23 | (ve)$ twistd -n txgsm --device=/dev/tty.usbmodem1421 \ 24 | > send-sms --to-addr=2776XXXXXXX --message='hello world' 25 | 2013-06-15 11:21:01+0200 [-] Log opened. 26 | 2013-06-15 11:21:01+0200 [-] twistd 13.0.0 (/Users/sdehaan/Documents/Repositories/txgsm/ve/bin/python 2.7.2) starting up. 27 | 2013-06-15 11:21:01+0200 [-] reactor class: twisted.internet.selectreactor.SelectReactor. 28 | 2013-06-15 11:21:01+0200 [-] Connection made 29 | 2013-06-15 11:21:01+0200 [-] Sending: 'AT+CMGF=0' 30 | 2013-06-15 11:21:01+0200 [-] Received: ['OK'] 31 | 2013-06-15 11:21:01+0200 [-] Sending: 'ATE0' 32 | 2013-06-15 11:21:01+0200 [-] Received: ['OK'] 33 | 2013-06-15 11:21:01+0200 [-] Sending: 'AT+CMEE=1' 34 | 2013-06-15 11:21:01+0200 [-] Received: ['OK'] 35 | 2013-06-15 11:21:01+0200 [-] Sending: 'AT+WIND=0' 36 | 2013-06-15 11:21:01+0200 [-] Received: ['OK'] 37 | 2013-06-15 11:21:01+0200 [-] Sending: 'AT+CSMS=1' 38 | 2013-06-15 11:21:01+0200 [-] Received: ['+CSMS: 1,1,1', 'OK'] 39 | 2013-06-15 11:21:01+0200 [-] Sending: 'AT+CMGS=23' 40 | 2013-06-15 11:21:01+0200 [-] Received: ['> '] 41 | 2013-06-15 11:21:01+0200 [-] Sending: '0001000B817267443908F600000BE8329BFD06DDDF723619\x1a' 42 | 2013-06-15 11:21:04+0200 [-] Received: ['+CMGS: 198', 'OK'] 43 | 2013-06-15 11:21:04+0200 [-] Main loop terminated. 44 | 2013-06-15 11:21:04+0200 [-] Server Shut Down. 45 | $ 46 | 47 | Interacting with a USSD Service 48 | ------------------------------- 49 | 50 | Provide the USSD code you want to dial with ``--code=...``. 51 | Adding ``-v`` or ``--verbose`` to see the AT commands. 52 | 53 | :: 54 | 55 | (ve)$ twistd -n txgsm --device=/dev/tty.usbmodem1421 ussd-session --code='*120*8864#' 56 | 2013-06-15 19:37:52+0200 [-] Log opened. 57 | 2013-06-15 19:37:52+0200 [-] twistd 13.0.0 (/Users/sdehaan/Documents/Repositories/txgsm/ve/bin/python 2.7.2) starting up. 58 | 2013-06-15 19:37:52+0200 [-] reactor class: twisted.internet.selectreactor.SelectReactor. 59 | 2013-06-15 19:37:52+0200 [-] Connecting to modem. 60 | 2013-06-15 19:37:53+0200 [-] Connected, starting console for: *120*8864# 61 | 2013-06-15 19:37:53+0200 [-] Dialing: *120*8864# 62 | What would you like to search Wikipedia for? 63 | USSD > HIV 64 | 1. HIV 65 | 2. HIV/AIDS 66 | 3. HIV/AIDS in China 67 | 4. Diagnosis of HIV/AIDS 68 | 5. History of HIV/AIDS 69 | 6. Circumcision and HIV 70 | 7. AIDS dementia complex 71 | 8. HIV/AIDS in Ukraine 72 | USSD > 2 73 | 1. HIV/AIDS 74 | 2. Signs and symptoms 75 | 3. Transmission 76 | 4. Virology 77 | 5. Pathophysiology 78 | 6. Diagnosis 79 | 7. Prevention 80 | 8. Management 81 | 9. Prognosis 82 | 10. Epidemiology 83 | USSD > 1 84 | Human immunodeficiency virus infection / acquired immunodeficiency syndrome (HIV/AIDS) is a disease of the human immune system ... 85 | (Full content sent by SMS.) 86 | 2013-06-15 19:38:24+0200 [-] Main loop terminated. 87 | 2013-06-15 19:38:24+0200 [-] Server Shut Down. 88 | 89 | 90 | .. |travis| image:: https://travis-ci.org/smn/txgsm.png?branch=develop 91 | .. _travis: https://travis-ci.org/smn/txgsm 92 | 93 | .. |coveralls| image:: https://coveralls.io/repos/smn/txgsm/badge.png?branch=develop 94 | .. _coveralls: https://coveralls.io/r/smn/txgsm 95 | 96 | .. |pypi| image:: https://img.shields.io/pypi/v/txgsm.svg 97 | .. _pypi: https://pypi.python.org/pypi/txgsm 98 | 99 | .. |license| image:: https://img.shields.io/badge/license-BSD-blue.svg 100 | .. _license: https://raw.githubusercontent.com/smn/txgsm/develop/LICENSE 101 | 102 | -------------------------------------------------------------------------------- /txgsm/protocol.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: txgsm.tests.test_protocol -*- 2 | # -*- coding: utf-8 -*- 3 | from twisted.internet import reactor 4 | from twisted.protocols.basic import LineReceiver 5 | from twisted.internet.defer import Deferred 6 | from twisted.python import log 7 | 8 | from .utils import quote 9 | 10 | from messaging.sms import SmsSubmit, SmsDeliver 11 | 12 | 13 | class TxGSMProtocol(LineReceiver): 14 | 15 | CTRL_Z = '\x1a' 16 | delimiter = '\r\n' 17 | verbose = False 18 | 19 | def __init__(self): 20 | # AT switches being purely line oriented and sometimes not 21 | # especially when sending multipart SMSs which has a '^> $' prompt 22 | # without a '\r\n' that we need to wait for. 23 | # As a result using lineReceived() does not always work. 24 | self.setRawMode() 25 | self.deferreds = [] 26 | self.buffer = b'' 27 | 28 | def log(self, msg): 29 | if self.verbose: 30 | log.msg(msg) 31 | 32 | def connectionMade(self): 33 | self.log('Connection made') 34 | 35 | def send_command(self, command, expect='OK', timeout=None): 36 | self.log('Sending: %r' % (command,)) 37 | resp = Deferred() 38 | resp.addCallback(self.debug) 39 | if timeout: 40 | reactor.callLater(timeout, resp.cancel) 41 | self.deferreds.append((command, expect, resp)) 42 | self.sendLine(command) 43 | return resp 44 | 45 | def debug(self, resp): 46 | self.log('Received: %r' % (resp,)) 47 | return resp 48 | 49 | def next(self, command, expect='OK'): 50 | def handler(result): 51 | d = self.send_command(command, expect) 52 | d.addCallback(lambda r: result + [r]) 53 | return d 54 | return handler 55 | 56 | def configure_modem(self): 57 | # Sensible defaults shamelessly copied from pygsm. 58 | d = Deferred() 59 | d.addCallback(self.next('ATE0')) # Disable echo 60 | d.addCallback(self.next('AT+CMGF=0')) # PDU mode 61 | d.addCallback(self.next('AT+CMEE=1')) # More useful errors 62 | d.addCallback(self.next('AT+CSMS=1')) # set SMS mode to phase 2+ 63 | d.callback([]) 64 | return d 65 | 66 | def send_sms(self, msisdn, text): 67 | sms = SmsSubmit(msisdn, text) 68 | # NOTE: The use of the Deferred here is a bit wonky 69 | # I'm using it like this because it makes adding callbacks 70 | # in a for-loop easier since we're potentially sending 71 | # SMSs bigger than 160 chars. 72 | d = Deferred() 73 | for pdu in sms.to_pdu(): 74 | d.addCallback(self.next( 75 | 'AT+CMGS=%d' % (pdu.length,), 76 | expect='> ')) 77 | d.addCallback(self.next('%s%s' % (pdu.pdu, self.CTRL_Z))) 78 | 79 | d.callback([]) 80 | return d 81 | 82 | def dial_ussd_code(self, code): 83 | return self.send_command('AT+CUSD=1,"%s",15' % (quote(code),), 84 | expect='+CUSD') 85 | 86 | def list_received_messages(self, status=4): 87 | d = self.send_command('AT+CMGL=%i' % (status,)) 88 | 89 | def parse_cmgl_response(result): 90 | response = result['response'] 91 | # Lines alternative between the +CMGL response and the 92 | # actual PDU containing the SMS 93 | found = False 94 | messages = [] 95 | for line in response: 96 | if line.startswith('+CMGL:'): 97 | found = True 98 | elif found: 99 | messages.append(SmsDeliver(line)) 100 | found = False 101 | 102 | return messages 103 | 104 | d.addCallback(parse_cmgl_response) 105 | return d 106 | 107 | def probe(self): 108 | """ 109 | See if we're talking to something GSM-like and if so, 110 | try and get some useful information out of it. 111 | """ 112 | d = Deferred() 113 | d.addCallback(self.next('ATE0')) 114 | d.addCallback(self.next('AT+CIMI')) 115 | d.addCallback(self.next('AT+CGMM')) 116 | reactor.callLater(0, d.callback, []) 117 | return d 118 | 119 | def rawDataReceived(self, data): 120 | self.buffer += data 121 | 122 | if not self.deferreds: 123 | log.err('Unsollicited response: %r' % (data,)) 124 | return 125 | 126 | _, expect, _ = self.deferreds[0] 127 | 128 | if expect in self.buffer: 129 | command, expect, deferred = self.deferreds.pop(0) 130 | return_buffer, self.buffer = self.buffer, b'' 131 | result = { 132 | 'command': [command], 133 | 'expect': expect, 134 | 'response': filter(None, return_buffer.split(self.delimiter)) 135 | } 136 | deferred.callback(result) 137 | -------------------------------------------------------------------------------- /txgsm/tests/test_service.py: -------------------------------------------------------------------------------- 1 | from twisted.internet.defer import inlineCallbacks 2 | 3 | from txgsm.tests.base import TxGSMBaseTestCase, LogCatcher 4 | from txgsm.service import TxGSMServiceMaker, TxGSMService, Options 5 | from txgsm.utils import USSDConsole 6 | 7 | from mock import Mock 8 | 9 | 10 | class TxGSMServiceTestCase(TxGSMBaseTestCase): 11 | 12 | def setUp(self): 13 | super(TxGSMServiceTestCase, self).setUp() 14 | TxGSMService.serial_port_class = Mock() 15 | self.patch(TxGSMService, 'getProtocol', self.patch_get_protocol) 16 | self.patch(TxGSMServiceMaker, 'shutdown', self.patch_shutdown) 17 | 18 | def patch_get_protocol(self): 19 | # Protocol created by TxGSMBaseTestCase 20 | return self.modem 21 | 22 | def patch_shutdown(self, result=None): 23 | # noop, pass along result instead of shutting down 24 | return result 25 | 26 | @inlineCallbacks 27 | def assert_configure_modem(self): 28 | yield self.assertExchange(['ATE0'], ['OK']) 29 | yield self.assertExchange(['AT+CMGF=0'], ['OK']) 30 | yield self.assertExchange(['AT+CMEE=1'], ['OK']) 31 | yield self.assertExchange(['AT+CSMS=1'], ['OK']) 32 | 33 | def make_service(self, command, cmdOptions=[], connOptions=[]): 34 | args = ['--device', '/dev/foo'] 35 | args.extend(connOptions) 36 | args.extend([command] + cmdOptions) 37 | service_options = Options() 38 | service_options.parseOptions(args) 39 | service_maker = TxGSMServiceMaker() 40 | service = service_maker.makeService(service_options) 41 | service.startService() 42 | return service 43 | 44 | @inlineCallbacks 45 | def test_send_sms_command(self): 46 | service = self.make_service('send-sms', [ 47 | '--to-addr', '+27761234567', 48 | '--message', 'hello world', 49 | ]) 50 | yield self.assert_configure_modem() 51 | yield self.assertCommands(['AT+CMGS=23']) 52 | self.reply('> ', delimiter='') 53 | [pdu_payload] = yield self.wait_for_next_commands() 54 | self.reply('OK') 55 | response = yield service.onProtocol 56 | self.assertEqual(response, [ 57 | { 58 | 'command': ['AT+CMGS=23'], 59 | 'expect': '> ', 60 | 'response': ['> '] 61 | }, 62 | { 63 | 'command': ['0001000B917267214365F700000B' 64 | 'E8329BFD06DDDF723619\x1a'], 65 | 'expect': 'OK', 66 | 'response': ['OK'] 67 | } 68 | ]) 69 | 70 | @inlineCallbacks 71 | def test_list_sms(self): 72 | service = self.make_service('list-sms', [ 73 | '--status', 4 74 | ]) 75 | yield self.assert_configure_modem() 76 | with LogCatcher() as catcher: 77 | yield self.assertExchange( 78 | input=['AT+CMGL=4'], 79 | output=[ 80 | '+CMGL: 1,0,,39', 81 | ('07911326040011F5240B911326880736F40000111081017362' + 82 | '401654747A0E4ACF41F4329E0E6A97E7F3F0B90C8A01'), 83 | '+CMGL: 2,0,,39', 84 | ('07911326040011F5240B911326880736F40000111081017323' + 85 | '401654747A0E4ACF41F4329E0E6A97E7F3F0B90C9201'), 86 | 'OK' 87 | ]) 88 | yield service.onProtocol 89 | [sms1_log, sms2_log] = catcher.logs 90 | self.assertEqual('This is text message 1', 91 | sms1_log['message'][0]['text']) 92 | self.assertEqual('This is text message 2', 93 | sms2_log['message'][0]['text']) 94 | 95 | @inlineCallbacks 96 | def test_ussd_session(self): 97 | responses = [] 98 | self.patch(USSDConsole, 'handle_response', responses.append) 99 | 100 | service = self.make_service('ussd-session', [ 101 | '--code', '*100#' 102 | ]) 103 | yield self.assert_configure_modem() 104 | yield self.assertExchange( 105 | input=['AT+CUSD=1,"*100#",15'], 106 | output=[ 107 | 'OK', 108 | ('+CUSD: 2,"Your balance is R48.70. Out of Airtime? ' 109 | 'Dial *111# for Airtime Advance. T&Cs apply.",255') 110 | ]) 111 | standard_io = yield service.onProtocol 112 | standard_io.loseConnection() 113 | [ussd_resp] = responses 114 | self.assertEqual(ussd_resp['response'], [ 115 | 'OK', 116 | ('+CUSD: 2,"Your balance is R48.70. Out of Airtime? ' 117 | 'Dial *111# for Airtime Advance. T&Cs apply.",255') 118 | ]) 119 | 120 | def test_service_options(self): 121 | baudrate = 115200 122 | timeout = 1000 123 | service = self.make_service('probe-modem', connOptions=[ 124 | '--baudrate', baudrate, '--timeout', timeout]) 125 | 126 | self.assertEqual(service.conn_options['baudrate'], baudrate) 127 | self.assertEqual(service.conn_options['timeout'], timeout) 128 | 129 | @inlineCallbacks 130 | def test_probe_modem(self): 131 | service = self.make_service('probe-modem', []) 132 | with LogCatcher() as catcher: 133 | yield self.assertExchange(['ATE0'], ['OK']) 134 | yield self.assertExchange(['AT+CIMI'], ['01234123412341234', 'OK']) 135 | yield self.assertExchange(['AT+CGMM'], ['Foo Bar Corp', 'OK']) 136 | yield service.onProtocol 137 | 138 | [entry1, entry2] = catcher.logs 139 | self.assertEqual(entry1['message'][0], 'Manufacturer: Foo Bar Corp') 140 | self.assertEqual(entry2['message'][0], 'IMSI: 01234123412341234') 141 | -------------------------------------------------------------------------------- /txgsm/service.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: txgsm.tests.test_service -*- 2 | import sys 3 | from zope.interface import implementer 4 | 5 | from twisted.python import usage 6 | from twisted.plugin import IPlugin 7 | from twisted.application.service import Service 8 | from twisted.application.service import IServiceMaker 9 | from twisted.internet.defer import inlineCallbacks, Deferred, returnValue 10 | from twisted.internet import reactor, stdio 11 | from twisted.internet.serialport import SerialPort 12 | from twisted.python import log 13 | 14 | from txgsm.protocol import TxGSMProtocol 15 | from txgsm.utils import USSDConsole 16 | 17 | 18 | class SendSMS(usage.Options): 19 | 20 | optParameters = [ 21 | ["to-addr", None, None, "The address to send an SMS to."], 22 | ["message", None, None, "The message to send"], 23 | ] 24 | 25 | 26 | class ListSMS(usage.Options): 27 | optParameters = [ 28 | ['status', 's', 4, 'What messages to read (0: unread, 1: read, ' 29 | '2: unsent, 3: sent, 4: all)'], 30 | ] 31 | 32 | 33 | class USSDSession(usage.Options): 34 | 35 | optParameters = [ 36 | ['code', None, None, "The USSD code to dial"], 37 | ] 38 | 39 | 40 | class ProbeModem(usage.Options): 41 | pass 42 | 43 | 44 | class Options(usage.Options): 45 | 46 | subCommands = [ 47 | ['send-sms', None, SendSMS, "Send an SMS"], 48 | ['list-sms', None, ListSMS, "List SMSs on Modem"], 49 | ['ussd-session', None, USSDSession, 'Start a USSD session'], 50 | ['probe-modem', None, ProbeModem, 51 | 'Probe the device to see if it is something modem-ish'], 52 | ] 53 | 54 | optFlags = [ 55 | ["verbose", "v", "Log AT commands"], 56 | ] 57 | 58 | optParameters = [ 59 | ["device", "d", None, "The device to connect to."], 60 | ["baudrate", "b", 9600, "Baud rate such as 9600 or 115200."], 61 | ["timeout", "t", None, "Set a read timeout value."], 62 | ] 63 | 64 | 65 | class TxGSMService(Service): 66 | 67 | protocol = TxGSMProtocol 68 | serial_port_class = SerialPort 69 | 70 | def __init__(self, device, **conn_options): 71 | self.device = device 72 | self.conn_options = conn_options 73 | self.onProtocol = Deferred() 74 | self.onProtocol.addErrback(log.err) 75 | 76 | def getProtocol(self): 77 | return self.protocol() 78 | 79 | def startService(self): 80 | p = self.getProtocol() 81 | self.port = self.serial_port_class(p, self.device, reactor, 82 | **self.conn_options) 83 | self.onProtocol.callback(p) 84 | 85 | def stopService(self): 86 | self.port.loseConnection() 87 | 88 | 89 | @implementer(IServiceMaker, IPlugin) 90 | class TxGSMServiceMaker(object): 91 | tapname = 'txgsm' 92 | description = ("Utilities for talking to a GSM modem over USB via AT " 93 | "commands.") 94 | options = Options 95 | 96 | def makeService(self, options): 97 | device = options['device'] 98 | conn_options = { 99 | 'baudrate': int(options['baudrate']), 100 | 'timeout': None 101 | } 102 | if options['timeout'] is not None: 103 | conn_options['timeout'] = int(options['timeout']) 104 | 105 | service = TxGSMService(device, **conn_options) 106 | service.onProtocol.addCallback(self.set_verbosity, options) 107 | 108 | dispatch = { 109 | 'send-sms': self.send_sms, 110 | 'list-sms': self.list_sms, 111 | 'ussd-session': self.ussd_session, 112 | 'probe-modem': self.probe_modem, 113 | } 114 | 115 | callback = dispatch.get(options.subCommand) 116 | if callback: 117 | service.onProtocol.addCallback(callback, options) 118 | return service 119 | else: 120 | sys.exit(str(options)) 121 | 122 | def set_verbosity(self, modem, options): 123 | modem.verbose = options['verbose'] 124 | return modem 125 | 126 | @inlineCallbacks 127 | def send_sms(self, modem, options): 128 | cmd_options = options.subOptions 129 | yield modem.configure_modem() 130 | result = yield modem.send_sms(cmd_options['to-addr'], 131 | cmd_options['message']) 132 | returnValue(self.shutdown(result)) 133 | 134 | @inlineCallbacks 135 | def list_sms(self, modem, options): 136 | cmd_options = options.subOptions 137 | yield modem.configure_modem() 138 | messages = yield modem.list_received_messages( 139 | int(cmd_options['status'])) 140 | for message in messages: 141 | log.msg(message.data) 142 | returnValue(self.shutdown()) 143 | 144 | @inlineCallbacks 145 | def ussd_session(self, modem, options): 146 | log.msg('Connecting to modem.') 147 | cmd_options = options.subOptions 148 | yield modem.configure_modem() 149 | log.msg('Connected, starting console for: %s' % (cmd_options['code'],)) 150 | console = USSDConsole(modem, on_exit=self.shutdown) 151 | sio = stdio.StandardIO(console) 152 | log.msg('Dialing: %s' % (cmd_options['code'],)) 153 | yield console.dial(cmd_options['code']) 154 | returnValue(sio) 155 | 156 | @inlineCallbacks 157 | def probe_modem(self, modem, options): 158 | result = yield modem.probe() 159 | [_, imsi_result, manufacturer_result] = result 160 | imsi, ok = imsi_result['response'] 161 | manufacturer, ok = manufacturer_result['response'] 162 | log.msg('Manufacturer: %s' % (manufacturer,)) 163 | log.msg('IMSI: %s' % (imsi,)) 164 | self.shutdown() 165 | 166 | def shutdown(self, resp=None): 167 | reactor.callLater(0, reactor.stop) 168 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(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/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/txgsm.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/txgsm.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/txgsm" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/txgsm" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /txgsm/tests/test_protocol.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from twisted.internet.defer import inlineCallbacks 4 | from twisted.trial.unittest import TestCase 5 | 6 | from txgsm.tests.base import TxGSMBaseTestCase, LogCatcher 7 | from txgsm.service import TxGSMService 8 | from txgsm.protocol import TxGSMProtocol 9 | 10 | from mock import Mock 11 | 12 | 13 | class TxGSMProtocolTestCase(TxGSMBaseTestCase): 14 | 15 | timeout = 1 16 | 17 | @inlineCallbacks 18 | def test_configure_modem(self): 19 | d = self.modem.configure_modem() 20 | yield self.assertExchange(['ATE0'], ['OK']) 21 | yield self.assertExchange(['AT+CMGF=0'], ['OK']) 22 | yield self.assertExchange(['AT+CMEE=1'], ['OK']) 23 | yield self.assertExchange(['AT+CSMS=1'], ['OK']) 24 | response = yield d 25 | self.assertEqual(response, [ 26 | {'command': ['ATE0'], 'expect': 'OK', 'response': ['OK']}, 27 | {'command': ['AT+CMGF=0'], 'expect': 'OK', 'response': ['OK']}, 28 | {'command': ['AT+CMEE=1'], 'expect': 'OK', 'response': ['OK']}, 29 | {'command': ['AT+CSMS=1'], 'expect': 'OK', 'response': ['OK']} 30 | ]) 31 | 32 | @inlineCallbacks 33 | def test_send_sms(self): 34 | d = self.modem.send_sms('+27761234567', 'hello world') 35 | yield self.assertCommands(['AT+CMGS=23']) 36 | self.reply('> ', delimiter='') 37 | [pdu_payload] = yield self.wait_for_next_commands() 38 | self.reply('OK') 39 | response = yield d 40 | self.assertEqual(response, [ 41 | { 42 | 'command': ['AT+CMGS=23'], 43 | 'expect': '> ', 44 | 'response': ['> '] 45 | }, 46 | { 47 | 'command': ['0001000B917267214365F700000B' 48 | 'E8329BFD06DDDF723619\x1a'], 49 | 'expect': 'OK', 50 | 'response': ['OK'] 51 | } 52 | ]) 53 | 54 | @inlineCallbacks 55 | def test_send_multipart_sms(self): 56 | d = self.modem.send_sms('+27761234567', '1' * 180) 57 | yield self.assertCommands(['AT+CMGS=153']) 58 | self.reply('> ', delimiter='') 59 | [pdu_payload] = yield self.wait_for_next_commands() 60 | self.reply('OK') 61 | yield self.assertCommands(['AT+CMGS=43']) 62 | self.reply('> ', delimiter='') 63 | [pdu_payload] = yield self.wait_for_next_commands() 64 | self.reply('OK') 65 | response = yield d 66 | self.assertEqual(response, [ 67 | { 68 | 'command': ['AT+CMGS=153'], 69 | 'expect': '> ', 70 | 'response': ['> '] 71 | }, { 72 | 'command': ['0041000B917267214365F70000A005000301020162B158' 73 | '2C168BC562B1582C168BC562B1582C168BC562B1582C16' 74 | '8BC562B1582C168BC562B1582C168BC562B1582C168BC5' 75 | '62B1582C168BC562B1582C168BC562B1582C168BC562B1' 76 | '582C168BC562B1582C168BC562B1582C168BC562B1582C' 77 | '168BC562B1582C168BC562B1582C168BC562B1582C168B' 78 | 'C562B1582C168BC562B1582C168BC562\x1a'], 79 | 'expect': 'OK', 80 | 'response': ['OK'] 81 | }, { 82 | 'command': ['AT+CMGS=43'], 83 | 'expect': '> ', 84 | 'response': ['> '] 85 | }, { 86 | 'command': ['0041000B917267214365F700002205000301020262B158' 87 | '2C168BC562B1582C168BC562B1582C168BC562B118\x1a'], 88 | 'expect': 'OK', 89 | 'response': ['OK'] 90 | }] 91 | ) 92 | 93 | @inlineCallbacks 94 | def test_receive_sms(self): 95 | d = self.modem.list_received_messages(status=4) 96 | # Using PDU samples from 97 | # http://www.diafaan.com/sms-tutorials/gsm-modem-tutorial/at-cmgl-pdu-mode/ 98 | yield self.assertExchange( 99 | input=['AT+CMGL=4'], 100 | output=[ 101 | '+CMGL: 1,0,,39', 102 | ('07911326040011F5240B911326880736F400001110810173624016547' + 103 | '47A0E4ACF41F4329E0E6A97E7F3F0B90C8A01'), 104 | '+CMGL: 2,0,,39', 105 | ('07911326040011F5240B911326880736F400001110810173234016547' + 106 | '47A0E4ACF41F4329E0E6A97E7F3F0B90C9201'), 107 | 'OK' 108 | ]) 109 | result = yield d 110 | [sms1, sms2] = result 111 | self.assertEqual(sms1.data, { 112 | 'csca': '+31624000115', 113 | 'sr': None, 114 | 'type': None, 115 | 'date': datetime(2011, 1, 18, 9, 37, 26), 116 | 'text': u'This is text message 1', 117 | 'fmt': 0, 118 | 'pid': 0, 119 | 'dcs': 0, 120 | 'number': '+31628870634', 121 | }) 122 | self.assertEqual(sms2.data, { 123 | 'csca': '+31624000115', 124 | 'sr': None, 125 | 'type': None, 126 | 'date': datetime(2011, 1, 18, 9, 37, 32), 127 | 'text': u'This is text message 2', 128 | 'fmt': 0, 129 | 'pid': 0, 130 | 'dcs': 0, 131 | 'number': '+31628870634', 132 | }) 133 | 134 | @inlineCallbacks 135 | def test_ussd_session(self): 136 | d = self.modem.dial_ussd_code('*100#') 137 | yield self.assertExchange( 138 | input=['AT+CUSD=1,"*100#",15'], 139 | output=[ 140 | 'OK', 141 | ('+CUSD: 2,"Your balance is R48.70. Out of Airtime? ' 142 | 'Dial *111# for Airtime Advance. T&Cs apply.",255') 143 | ]) 144 | result = yield d 145 | response = result['response'] 146 | self.assertEqual(response[0], 'OK') 147 | self.assertTrue(response[1].startswith('+CUSD: 2')) 148 | 149 | def test_dealing_with_unexpected_events(self): 150 | with LogCatcher() as catcher: 151 | self.reply('+FOO') 152 | [err_log] = catcher.logs 153 | self.assertTrue('Unsollicited response' in err_log['message'][0]) 154 | self.assertTrue('+FOO' in err_log['message'][0]) 155 | 156 | @inlineCallbacks 157 | def test_probe(self): 158 | d = self.modem.probe() 159 | yield self.assertExchange(['ATE0'], ['OK']) 160 | yield self.assertExchange(['AT+CIMI'], ['01234123412341234', 'OK']) 161 | yield self.assertExchange(['AT+CGMM'], ['Foo Bar Corp', 'OK']) 162 | response = yield d 163 | self.assertEqual(response, [ 164 | { 165 | 'command': ['ATE0'], 166 | 'expect': 'OK', 167 | 'response': ['OK'] 168 | }, { 169 | 'command': ['AT+CIMI'], 170 | 'expect': 'OK', 171 | 'response': ['01234123412341234', 'OK'] 172 | }, { 173 | 'command': ['AT+CGMM'], 174 | 'expect': 'OK', 175 | 'response': ['Foo Bar Corp', 'OK'] 176 | } 177 | ]) 178 | 179 | 180 | class TxGSMServiceTestCase(TestCase): 181 | 182 | def setUp(self): 183 | self.mock_serial = Mock() 184 | self.service = TxGSMService('/dev/foo', bar='baz') 185 | self.service.serial_port_class = self.mock_serial 186 | 187 | @inlineCallbacks 188 | def test_start_service(self): 189 | d = self.service.onProtocol 190 | self.service.startService() 191 | protocol = yield d 192 | self.assertTrue(isinstance(protocol, TxGSMProtocol)) 193 | self.assertTrue(self.mock_serial.called) 194 | [init_call] = self.mock_serial.call_args_list 195 | args, kwargs = init_call 196 | proto, device, reactor = args 197 | self.assertEqual(device, '/dev/foo') 198 | self.assertEqual(kwargs, {'bar': 'baz'}) 199 | 200 | def test_stop_service(self): 201 | self.service.startService() 202 | self.service.port.loseConnection = Mock() 203 | self.service.stopService() 204 | self.assertTrue(self.service.port.loseConnection.called) 205 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\txgsm.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\txgsm.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # txgsm documentation build configuration file. 4 | # 5 | # This file is execfile()d with the current directory set to its 6 | # containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys 15 | import os 16 | import shlex 17 | from datetime import datetime 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | sys.path.insert(0, os.path.abspath('..')) 23 | 24 | # get version 25 | with open(os.path.join('..', 'VERSION'), 'r') as fp: 26 | txgsm_version = fp.read().strip() 27 | 28 | # -- General configuration ------------------------------------------------ 29 | 30 | # If your documentation needs a minimal Sphinx version, state it here. 31 | #needs_sphinx = '1.0' 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | extensions = [ 37 | 'sphinx.ext.autodoc', 38 | 'sphinx.ext.intersphinx', 39 | ] 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ['_templates'] 43 | 44 | # The suffix(es) of source filenames. 45 | # You can specify multiple suffix as a list of string: 46 | # source_suffix = ['.rst', '.md'] 47 | source_suffix = '.rst' 48 | 49 | # The encoding of source files. 50 | #source_encoding = 'utf-8-sig' 51 | 52 | # The master toctree document. 53 | master_doc = 'index' 54 | 55 | # General information about the project. 56 | project = u'txgsm' 57 | author = u'Simon de Haan and individual contributors' 58 | copyright = u'2013-{0}, {1}'.format(datetime.now().year, author) 59 | 60 | # The version info for the project you're documenting, acts as replacement for 61 | # |version| and |release|, also used in various other places throughout the 62 | # built documents. 63 | # 64 | # The short X.Y version. 65 | version = txgsm_version 66 | # The full version, including alpha/beta/rc tags. 67 | release = txgsm_version 68 | 69 | # The language for content autogenerated by Sphinx. Refer to documentation 70 | # for a list of supported languages. 71 | # 72 | # This is also used if you do content translation via gettext catalogs. 73 | # Usually you set "language" from the command line for these cases. 74 | language = None 75 | 76 | # There are two options for replacing |today|: either, you set today to some 77 | # non-false value, then it is used: 78 | #today = '' 79 | # Else, today_fmt is used as the format for a strftime call. 80 | #today_fmt = '%B %d, %Y' 81 | 82 | # List of patterns, relative to source directory, that match files and 83 | # directories to ignore when looking for source files. 84 | exclude_patterns = ['_build'] 85 | 86 | # The reST default role (used for this markup: `text`) to use for all 87 | # documents. 88 | #default_role = None 89 | 90 | # If true, '()' will be appended to :func: etc. cross-reference text. 91 | #add_function_parentheses = True 92 | 93 | # If true, the current module name will be prepended to all description 94 | # unit titles (such as .. function::). 95 | #add_module_names = True 96 | 97 | # If true, sectionauthor and moduleauthor directives will be shown in the 98 | # output. They are ignored by default. 99 | #show_authors = False 100 | 101 | # The name of the Pygments (syntax highlighting) style to use. 102 | pygments_style = 'sphinx' 103 | 104 | # A list of ignored prefixes for module index sorting. 105 | #modindex_common_prefix = [] 106 | 107 | # If true, keep warnings as "system message" paragraphs in the built documents. 108 | #keep_warnings = False 109 | 110 | # If true, `todo` and `todoList` produce output, else they produce nothing. 111 | todo_include_todos = False 112 | 113 | 114 | # -- Options for HTML output ---------------------------------------------- 115 | 116 | # The theme to use for HTML and HTML Help pages. See the documentation for 117 | # a list of builtin themes. 118 | html_theme = 'alabaster' 119 | 120 | # Theme options are theme-specific and customize the look and feel of a theme 121 | # further. For a list of options available for each theme, see the 122 | # documentation. 123 | #html_theme_options = {} 124 | 125 | # Add any paths that contain custom themes here, relative to this directory. 126 | #html_theme_path = [] 127 | 128 | # The name for this set of Sphinx documents. If None, it defaults to 129 | # " v documentation". 130 | #html_title = None 131 | 132 | # A shorter title for the navigation bar. Default is the same as html_title. 133 | #html_short_title = None 134 | 135 | # The name of an image file (relative to this directory) to place at the top 136 | # of the sidebar. 137 | #html_logo = None 138 | 139 | # The name of an image file (within the static path) to use as favicon of the 140 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 141 | # pixels large. 142 | #html_favicon = None 143 | 144 | # Add any paths that contain custom static files (such as style sheets) here, 145 | # relative to this directory. They are copied after the builtin static files, 146 | # so a file named "default.css" will overwrite the builtin "default.css". 147 | html_static_path = ['_static'] 148 | 149 | # Add any extra paths that contain custom files (such as robots.txt or 150 | # .htaccess) here, relative to this directory. These files are copied 151 | # directly to the root of the documentation. 152 | #html_extra_path = [] 153 | 154 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 155 | # using the given strftime format. 156 | #html_last_updated_fmt = '%b %d, %Y' 157 | 158 | # If true, SmartyPants will be used to convert quotes and dashes to 159 | # typographically correct entities. 160 | #html_use_smartypants = True 161 | 162 | # Custom sidebar templates, maps document names to template names. 163 | #html_sidebars = {} 164 | 165 | # Additional templates that should be rendered to pages, maps page names to 166 | # template names. 167 | #html_additional_pages = {} 168 | 169 | # If false, no module index is generated. 170 | #html_domain_indices = True 171 | 172 | # If false, no index is generated. 173 | #html_use_index = True 174 | 175 | # If true, the index is split into individual pages for each letter. 176 | #html_split_index = False 177 | 178 | # If true, links to the reST sources are added to the pages. 179 | #html_show_sourcelink = True 180 | 181 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 182 | #html_show_sphinx = True 183 | 184 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 185 | #html_show_copyright = True 186 | 187 | # If true, an OpenSearch description file will be output, and all pages will 188 | # contain a tag referring to it. The value of this option must be the 189 | # base URL from which the finished HTML is served. 190 | #html_use_opensearch = '' 191 | 192 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 193 | #html_file_suffix = None 194 | 195 | # Language to be used for generating the HTML full-text search index. 196 | # Sphinx supports the following languages: 197 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 198 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 199 | #html_search_language = 'en' 200 | 201 | # A dictionary with options for the search language support, empty by default. 202 | # Now only 'ja' uses this config value 203 | #html_search_options = {'type': 'default'} 204 | 205 | # The name of a javascript file (relative to the configuration directory) that 206 | # implements a search results scorer. If empty, the default will be used. 207 | #html_search_scorer = 'scorer.js' 208 | 209 | # Output file base name for HTML help builder. 210 | htmlhelp_basename = 'txgsmdoc' 211 | 212 | # -- Options for LaTeX output --------------------------------------------- 213 | 214 | latex_elements = { 215 | # The paper size ('letterpaper' or 'a4paper'). 216 | #'papersize': 'letterpaper', 217 | 218 | # The font size ('10pt', '11pt' or '12pt'). 219 | #'pointsize': '10pt', 220 | 221 | # Additional stuff for the LaTeX preamble. 222 | #'preamble': '', 223 | 224 | # Latex figure (float) alignment 225 | #'figure_align': 'htbp', 226 | } 227 | 228 | # Grouping the document tree into LaTeX files. List of tuples 229 | # (source start file, target name, title, 230 | # author, documentclass [howto, manual, or own class]). 231 | latex_documents = [ 232 | (master_doc, 'txgsm.tex', u'txgsm Documentation', 233 | u'Simon de Haan and individual contributors', 'manual'), 234 | ] 235 | 236 | # The name of an image file (relative to this directory) to place at the top of 237 | # the title page. 238 | #latex_logo = None 239 | 240 | # For "manual" documents, if this is true, then toplevel headings are parts, 241 | # not chapters. 242 | #latex_use_parts = False 243 | 244 | # If true, show page references after internal links. 245 | #latex_show_pagerefs = False 246 | 247 | # If true, show URL addresses after external links. 248 | #latex_show_urls = False 249 | 250 | # Documents to append as an appendix to all manuals. 251 | #latex_appendices = [] 252 | 253 | # If false, no module index is generated. 254 | #latex_domain_indices = True 255 | 256 | 257 | # -- Options for manual page output --------------------------------------- 258 | 259 | # One entry per manual page. List of tuples 260 | # (source start file, name, description, authors, manual section). 261 | man_pages = [ 262 | (master_doc, 'txgsm', u'txgsm Documentation', 263 | [author], 1) 264 | ] 265 | 266 | # If true, show URL addresses after external links. 267 | #man_show_urls = False 268 | 269 | 270 | # -- Options for Texinfo output ------------------------------------------- 271 | 272 | # Grouping the document tree into Texinfo files. List of tuples 273 | # (source start file, target name, title, author, 274 | # dir menu entry, description, category) 275 | texinfo_documents = [ 276 | (master_doc, 'txgsm', u'txgsm Documentation', 277 | author, 'txgsm', 'One line description of project.', 278 | 'Miscellaneous'), 279 | ] 280 | 281 | # Documents to append as an appendix to all manuals. 282 | #texinfo_appendices = [] 283 | 284 | # If false, no module index is generated. 285 | #texinfo_domain_indices = True 286 | 287 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 288 | #texinfo_show_urls = 'footnote' 289 | 290 | # If true, do not generate a @detailmenu in the "Top" node's menu. 291 | #texinfo_no_detailmenu = False 292 | 293 | # Configuration for intersphinx. 294 | intersphinx_mapping = { 295 | 'python': ('https://docs.python.org/2/', None), 296 | 'twisted': ('https://twisted.readthedocs.org/en/latest/', None), 297 | 'sphinx': ('https://sphinx.readthedocs.org/en/latest/', None), 298 | } 299 | --------------------------------------------------------------------------------