├── .github
└── workflows
│ └── workflow.yml
├── .gitignore
├── .travis.yml
├── GSMTC35
├── GSMTC35.py
└── __init__.py
├── LICENSE.md
├── README.md
├── TC35_module.jpg
├── codecov.yml
├── doc
└── at_commands_tc35.pdf
├── examples
└── rest_api
│ ├── internal_db.py
│ └── rest_api.py
├── setup.cfg
├── setup.py
└── tests
├── GSMTC35_test.py
└── __init__.py
/.github/workflows/workflow.yml:
--------------------------------------------------------------------------------
1 | name: Publish Python Package
2 |
3 | # To trigger the workflow, you need to tag the library with a version number like this:
4 | #git tag v1.0.0
5 | #git push origin v1.0.0
6 |
7 | on:
8 | push:
9 | tags:
10 | - 'v*.*.*' # Triggers on version tags like v1.0.0, v2.1.3, etc.
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@v3
19 |
20 | - name: Set up Python
21 | uses: actions/setup-python@v4
22 | with:
23 | python-version: '3.x' # Choose your Python version (e.g., '3.9')
24 |
25 | - name: Install dependencies
26 | run: |
27 | python -m pip install --upgrade pip
28 | pip install build
29 |
30 | - name: Build package
31 | run: python -m build
32 |
33 | - name: Publish to PyPI
34 | env:
35 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
36 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
37 | run: |
38 | pip install twine
39 | twine upload dist/*
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Database used in the example project
2 | examples/rest_api/*.db
3 |
4 | # Byte-compiled / optimized / DLL files
5 | __pycache__/
6 | *.py[cod]
7 | *$py.class
8 |
9 | # C extensions
10 | *.so
11 |
12 | # Distribution / packaging
13 | .Python
14 | env/
15 | build/
16 | develop-eggs/
17 | dist/
18 | downloads/
19 | eggs/
20 | .eggs/
21 | lib/
22 | lib64/
23 | parts/
24 | sdist/
25 | var/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *,cover
49 | .hypothesis/
50 |
51 | # Translations
52 | *.mo
53 | *.pot
54 |
55 | # Django stuff:
56 | *.log
57 | local_settings.py
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # IPython Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # dotenv
82 | .env
83 |
84 | # virtualenv
85 | venv/
86 | ENV/
87 |
88 | # Spyder project settings
89 | .spyderproject
90 |
91 | # Rope project settings
92 | .ropeproject
93 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | # Test python 2.7 version
4 | #- "2.7"
5 | # Test most used python 3 version
6 | - "3.5"
7 | - "3.6"
8 | - "3.7"
9 | - "3.8"
10 |
11 | sudo: false
12 | install:
13 | - pip install codecov
14 | - pip install -U twine wheel setuptools
15 |
16 | script:
17 | # (Re)install library
18 | # This implicitly covers build (and test coverage on setup.py)
19 | - pip uninstall GSMTC35 --yes
20 | - coverage run -p setup.py install
21 | # Launch library test (+ test coverage)
22 | - coverage run -p setup.py test
23 | # Prepare results before sending them back to codecov
24 | - coverage combine
25 | # Check that we can get rest api example dependencies
26 | - pip install -e ".[restapi]"
27 | # Check if there is no issue in setup.py file
28 | - rm -rf dist
29 | - python setup.py sdist
30 | - python setup.py bdist_wheel
31 | - twine check dist/*
32 | #- twine upload dist/*
33 |
34 | # Push the results back to codecov
35 | after_success:
36 | - codecov
37 |
--------------------------------------------------------------------------------
/GSMTC35/GSMTC35.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | GSM TC35 library: Call, receive call, send/receive/delete SMS, enter the PIN, ...
6 |
7 | It is also possible to use command line to easily use this class from
8 | shell (launch this python file with '-h' parameter to get more information).
9 |
10 | Non-exhaustive class functionality list:
11 | - Check PIN state
12 | - Enter/Lock/Unlock/Change PIN
13 | - Send/Get/Delete SMS
14 | - Call/Re-call
15 | - Hang up/Pick-up call
16 | - Get/Add/Delete phonebook entries (phone numbers + contact names)
17 | - Sleep (Low power consumption)
18 | - Check if someone is calling
19 | - Check if there is a call in progress
20 | - Get last call duration
21 | - Check if module is alive
22 | - Get IDs (manufacturer, model, revision, IMEI, IMSI)
23 | - Set module to manufacturer state
24 | - Switch off
25 | - Reboot
26 | - Check sleep mode status
27 | - Get the current used operator
28 | - Get the signal strength (in dBm)
29 | - Set and get the date from the module internal clock
30 | - Get list of operators
31 | - Get list of neighbour cells
32 | - Get accumulated call meter and accumulated call meter max (in home units)
33 | - Get temperature status
34 | - Change the baudrate mode of the GSM module
35 | """
36 | __author__ = 'Quentin Comte-Gaz'
37 | __email__ = "quentin@comte-gaz.com"
38 | __license__ = "MIT License"
39 | __copyright__ = "Copyright Quentin Comte-Gaz (2024)"
40 | __python_version__ = "3.+"
41 | __version__ = "2.1.1 (2024/09/13)"
42 | __status__ = "Usable for any project"
43 |
44 | import binascii
45 | import serial, serial.tools.list_ports
46 | import time, sys, getopt
47 | import logging
48 | import datetime
49 | from math import ceil
50 | from random import randint
51 |
52 | class GSMTC35:
53 | """GSM TC35 class
54 |
55 | Calling setup() function is necessary in order to make this class work properly
56 | If you don't know the serial port to use, call this script to show all of them:
57 | '''
58 | import serial, serial.tools.list_ports
59 | print(str(list(serial.tools.list_ports.comports())))
60 | '''
61 | """
62 | ######################### Enums and static variables #########################
63 | __BASE_AT = "AT"
64 | __NORMAL_AT = "AT+"
65 | __RETURN_OK = "OK"
66 | __RETURN_ERROR = "ERROR"
67 | __CTRL_Z = "\x1a"
68 | __DATE_FORMAT = "%y/%m/%d,%H:%M:%S"
69 |
70 | class eRequiredPin:
71 | READY = "READY"
72 | PIN = "SIM PIN"
73 | PUK = "SIM PUK"
74 | PIN2 = "SIM PIN2"
75 | PUK2 = "SIM PUK2"
76 |
77 | class eSMS:
78 | UNREAD_SMS = "REC UNREAD"
79 | READ_SMS = "REC READ"
80 | UNSENT_SMS = "STO UNSENT"
81 | SENT_SMS = "STO SENT"
82 | ALL_SMS = "ALL"
83 |
84 | class __eSmsPdu:
85 | UNREAD_SMS = "0"
86 | READ_SMS = "1"
87 | UNSENT_SMS = "2"
88 | SENT_SMS = "3"
89 | ALL_SMS = "4"
90 |
91 | @staticmethod
92 | def __smsTypeTextToPdu(smsTypeAsText):
93 | if smsTypeAsText == GSMTC35.eSMS.UNREAD_SMS:
94 | return GSMTC35.__eSmsPdu.UNREAD_SMS
95 | elif smsTypeAsText == GSMTC35.eSMS.READ_SMS:
96 | return GSMTC35.__eSmsPdu.READ_SMS
97 | elif smsTypeAsText == GSMTC35.eSMS.UNSENT_SMS:
98 | return GSMTC35.__eSmsPdu.UNSENT_SMS
99 | elif smsTypeAsText == GSMTC35.eSMS.SENT_SMS:
100 | return GSMTC35.__eSmsPdu.SENT_SMS
101 | elif smsTypeAsText == GSMTC35.eSMS.ALL_SMS:
102 | return GSMTC35.__eSmsPdu.ALL_SMS
103 | elif smsTypeAsText == GSMTC35.__eSmsPdu.UNREAD_SMS or \
104 | smsTypeAsText == GSMTC35.__eSmsPdu.READ_SMS or \
105 | smsTypeAsText == GSMTC35.__eSmsPdu.UNSENT_SMS or \
106 | smsTypeAsText == GSMTC35.__eSmsPdu.SENT_SMS or \
107 | smsTypeAsText == GSMTC35.__eSmsPdu.ALL_SMS:
108 | return smsTypeAsText
109 | else:
110 | # If an error occured, get all messages
111 | return GSMTC35.__eSmsPdu.ALL_SMS
112 |
113 | @staticmethod
114 | def __smsTypePduToText(smsTypeAsPdu):
115 | if smsTypeAsPdu == GSMTC35.__eSmsPdu.UNREAD_SMS:
116 | return GSMTC35.eSMS.UNREAD_SMS
117 | elif smsTypeAsPdu == GSMTC35.__eSmsPdu.READ_SMS:
118 | return GSMTC35.eSMS.READ_SMS
119 | elif smsTypeAsPdu == GSMTC35.__eSmsPdu.UNSENT_SMS:
120 | return GSMTC35.eSMS.UNSENT_SMS
121 | elif smsTypeAsPdu == GSMTC35.__eSmsPdu.SENT_SMS:
122 | return GSMTC35.eSMS.SENT_SMS
123 | elif smsTypeAsPdu == GSMTC35.__eSmsPdu.ALL_SMS:
124 | return GSMTC35.eSMS.ALL_SMS
125 | elif smsTypeAsPdu == GSMTC35.eSMS.UNREAD_SMS or \
126 | smsTypeAsPdu == GSMTC35.eSMS.READ_SMS or \
127 | smsTypeAsPdu == GSMTC35.eSMS.UNSENT_SMS or \
128 | smsTypeAsPdu == GSMTC35.eSMS.SENT_SMS or \
129 | smsTypeAsPdu == GSMTC35.eSMS.ALL_SMS:
130 | return smsTypeAsPdu
131 | else:
132 | # If an error occured, get all messages
133 | return GSMTC35.eSMS.ALL_SMS
134 |
135 | class eCall:
136 | NOCALL = -1
137 | ACTIVE = 0
138 | HELD = 1
139 | DIALING = 2
140 | ALERTING = 3
141 | INCOMING = 4
142 | WAITING = 5
143 |
144 | @staticmethod
145 | def eCallToString(data):
146 | if data == GSMTC35.eCall.NOCALL:
147 | return "NOCALL"
148 | elif data == GSMTC35.eCall.ACTIVE:
149 | return "ACTIVE"
150 | elif data == GSMTC35.eCall.HELD:
151 | return "HELD"
152 | elif data == GSMTC35.eCall.DIALING:
153 | return "DIALING"
154 | elif data == GSMTC35.eCall.ALERTING:
155 | return "ALERTING"
156 | elif data == GSMTC35.eCall.INCOMING:
157 | return "INCOMING"
158 | elif data == GSMTC35.eCall.WAITING:
159 | return "WAITING"
160 |
161 | return "UNDEFINED"
162 |
163 | class ePhonebookType:
164 | CURRENT = "" # Phonebook in use
165 | SIM = "SM" # Main phonebook on SIM card
166 | GSM_MODULE = "ME" # Main phonebook on GSM module
167 | LAST_DIALLING = "LD" # Last dialed numbers (stored in SIM card)
168 | MISSED_CALLS = "MC" # Last missed calls (stored in GSM module)
169 | RECEIVED_CALLS = "RC" # Last received calls (stored in GSM module)
170 | MSISDNS = "ON" # Mobile Station ISDN Numbers (stored in GSM module or SIM card)
171 |
172 | class __ePhoneNumberType:
173 | ERROR = -1
174 | LOCAL = 129
175 | INTERNATIONAL = 145
176 |
177 | class eForwardClass:
178 | VOICE = 1
179 | DATA = 2
180 | FAX = 4
181 | SMS = 8
182 | DATA_CIRCUIT_SYNC = 16
183 | DATA_CIRCUIT_ASYNC = 32
184 | DEDICATED_PACKED_ACCESS = 64
185 | DEDICATED_PAD_ACCESS = 128
186 |
187 | @staticmethod
188 | def eForwardClassToString(data):
189 | data = int(data)
190 | if data == GSMTC35.eForwardClass.VOICE:
191 | return "VOICE"
192 | elif data == GSMTC35.eForwardClass.DATA:
193 | return "DATA"
194 | elif data == GSMTC35.eForwardClass.FAX:
195 | return "FAX"
196 | elif data == GSMTC35.eForwardClass.SMS:
197 | return "SMS"
198 | elif data == GSMTC35.eForwardClass.DATA_CIRCUIT_SYNC:
199 | return "DATA_CIRCUIT_SYNC"
200 | elif data == GSMTC35.eForwardClass.DATA_CIRCUIT_ASYNC:
201 | return "DATA_CIRCUIT_ASYNC"
202 | elif data == GSMTC35.eForwardClass.DEDICATED_PACKED_ACCESS:
203 | return "DEDICATED_PACKED_ACCESS"
204 | elif data == GSMTC35.eForwardClass.DEDICATED_PAD_ACCESS:
205 | return "DEDICATED_PAD_ACCESS"
206 |
207 | return "UNDEFINED"
208 |
209 | class eForwardReason:
210 | UNCONDITIONAL = 0
211 | MOBILE_BUSY = 1
212 | NO_REPLY = 2
213 | NOT_REACHABLE = 3
214 | ALL_CALL_FORWARDING = 4
215 | ALL_CONDITIONAL_CALL_FORWARDING = 5
216 |
217 | @staticmethod
218 | def eForwardReasonToString(data):
219 | data = int(data)
220 | if data == GSMTC35.eForwardReason.UNCONDITIONAL:
221 | return "UNCONDITIONAL"
222 | elif data == GSMTC35.eForwardReason.MOBILE_BUSY:
223 | return "MOBILE_BUSY"
224 | elif data == GSMTC35.eForwardReason.NO_REPLY:
225 | return "NO_REPLY"
226 | elif data == GSMTC35.eForwardReason.NOT_REACHABLE:
227 | return "NOT_REACHABLE"
228 | elif data == GSMTC35.eForwardReason.ALL_CALL_FORWARDING:
229 | return "ALL_CALL_FORWARDING"
230 | elif data == GSMTC35.eForwardReason.ALL_CONDITIONAL_CALL_FORWARDING:
231 | return "ALL_CONDITIONAL_CALL_FORWARDING"
232 |
233 | return "UNDEFINED"
234 |
235 | ############################ STANDALONE FUNCTIONS ############################
236 | @staticmethod
237 | def changeBaudrateMode(old_baudrate, new_baudrate, port, pin="", puk="", pin2="", puk2="",
238 | parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE,
239 | bytesize=serial.EIGHTBITS):
240 | """Change baudrate mode (can be done only if GSM module is not currently used)
241 |
242 | Keyword arguments:
243 | old_baudrate -- (int) Baudrate value usable to communicate with the GSM module
244 | new_baudrate -- (int) New baudrate value to communicate with the GSM module
245 | /!\ Use "0" to let the GSM module use "auto-baudrate" mode
246 | port -- (string) Serial port name of the GSM serial connection
247 | pin -- (string, optional) PIN number if locked
248 | puk -- (string, optional) PUK number if locked
249 | pin2 -- (string, optional) PIN2 number if locked
250 | puk2 -- (string, optional) PUK2 number if locked
251 | parity -- (pySerial parity, optional) Serial connection parity (PARITY_NONE, PARITY_EVEN, PARITY_ODD PARITY_MARK, PARITY_SPACE)
252 | stopbits -- (pySerial stop bits, optional) Serial connection stop bits (STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO)
253 | bytesize -- (pySerial byte size, optional) Serial connection byte size (FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS)
254 |
255 | return: (bool) Baudrate changed
256 | """
257 | gsm = GSMTC35()
258 | if not gsm.setup(_port=port, _pin=pin, _puk=puk, _pin2=pin2, _puk2=puk2,
259 | _baudrate=old_baudrate, _parity=parity,
260 | _stopbits=stopbits, _bytesize=bytesize):
261 | logging.error("Impossible to initialize the GSM module")
262 | return False
263 |
264 | if not gsm.__selectBaudrateCommunicationType(new_baudrate):
265 | logging.error("Impossible to modify the baudrate")
266 | return False
267 |
268 | return True
269 |
270 |
271 | ################################### INIT ####################################
272 | def __init__(self):
273 | """Initialize the GSM module class with undefined serial connection"""
274 | self.__initialized = False
275 | self.__serial = serial.Serial()
276 | self.__timeout_sec = 0
277 |
278 |
279 | ################################### SETUP ####################################
280 | def setup(self, _port, _pin="", _puk="", _pin2="", _puk2="",
281 | _baudrate=115200, _parity=serial.PARITY_NONE,
282 | _stopbits=serial.STOPBITS_ONE, _bytesize=serial.EIGHTBITS,
283 | _timeout_sec=2):
284 | """Initialize the class (can be launched multiple time if setup changed or module crashed)
285 |
286 | Keyword arguments:
287 | _port -- (string) Serial port name of the GSM serial connection
288 | _baudrate -- (int, optional) Baudrate of the GSM serial connection
289 | _pin -- (string, optional) PIN number if locked (not needed to do it now but would improve reliability)
290 | _puk -- (string, optional) PUK number if locked (not needed to do it now but would improve reliability)
291 | _pin2 -- (string, optional) PIN2 number if locked (not needed to do it now but would improve reliability)
292 | _puk2 -- (string, optional) PUK2 number if locked (not needed to do it now but would improve reliability)
293 | _parity -- (pySerial parity, optional) Serial connection parity (PARITY_NONE, PARITY_EVEN, PARITY_ODD PARITY_MARK, PARITY_SPACE)
294 | _stopbits -- (pySerial stop bits, optional) Serial connection stop bits (STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO)
295 | _bytesize -- (pySerial byte size, optional) Serial connection byte size (FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS)
296 | _timeout_sec -- (int, optional) Default timeout in sec for GSM module to answer commands
297 |
298 | return: (bool) Module initialized
299 | """
300 | # Close potential previous GSM session
301 | self.__timeout_sec = _timeout_sec
302 | try:
303 | self.close()
304 | except Exception:
305 | pass
306 |
307 | # Create new GSM session
308 | try:
309 | self.__serial = serial.Serial(
310 | port=_port,
311 | baudrate=_baudrate,
312 | parity=_parity,
313 | stopbits=_stopbits,
314 | bytesize=_bytesize,
315 | timeout=_timeout_sec
316 | )
317 | except serial.serialutil.SerialException:
318 | logging.error("Invalid serial port '"+str(_port)+"'")
319 | return False
320 |
321 | # Initialize the GSM module with specific commands
322 | is_init = True
323 | if self.__serial.isOpen():
324 | # Disable echo from GSM device
325 | if not self.__sendCmdAndCheckResult(GSMTC35.__BASE_AT+"E0"):
326 | logging.warning("Can't disable echo mode (ATE0 command)")
327 | # Use verbose answer (GSM module will return str like "OK\r\n" and not like "0")
328 | if not self.__sendCmdAndCheckResult(GSMTC35.__BASE_AT+"V1"):
329 | logging.error("Can't set proper answer type from GSM module (ATV command)")
330 | is_init = False
331 | # Use non-verbose error result ("ERROR" instead of "+CME ERROR: (...)")
332 | if not self.__sendCmdAndCheckResult(GSMTC35.__NORMAL_AT+"CMEE=0"):
333 | logging.warning("Can't set proper error format returned by GSM module (CMEE command)")
334 |
335 | # Enter PIN/PUK/PIN2/PUK2 as long as it is required (and that all goes well)
336 | # If PIN/PUK/PIN2/PUK2 in not specified but is needed, a warning will be displayed
337 | # but the function will continue.
338 | pin_status = ""
339 | while is_init and (pin_status != GSMTC35.eRequiredPin.READY):
340 | req_pin_result, pin_status = self.getPinStatus()
341 | if (not req_pin_result) or (len(pin_status) <=0):
342 | logging.error("Failed to get PIN status")
343 | is_init = False
344 | elif pin_status == GSMTC35.eRequiredPin.READY:
345 | logging.debug("No PIN needed")
346 | break
347 | elif pin_status == GSMTC35.eRequiredPin.PIN:
348 | if len(_pin) > 0:
349 | if not self.enterPin(_pin):
350 | logging.error("Invalid PIN \""+str(_pin)+"\" (YOU HAVE A MAXIMUM OF 3 TRY)")
351 | is_init = False
352 | else:
353 | logging.debug("PIN entered with success")
354 | else:
355 | logging.warning("Some initialization may not work without PIN activated")
356 | break
357 | elif pin_status == GSMTC35.eRequiredPin.PUK:
358 | if len(_puk) > 0:
359 | if not self.enterPin(_puk):
360 | logging.error("Invalid PUK \""+str(_puk)+"\"")
361 | is_init = False
362 | else:
363 | logging.debug("PUK entered with success")
364 | else:
365 | logging.warning("Some initialization may not work without PUK activated")
366 | break
367 | elif pin_status == GSMTC35.eRequiredPin.PIN2:
368 | if len(_pin2) > 0:
369 | if not self.enterPin(_pin2):
370 | logging.error("Invalid PIN2 \""+str(_pin2)+"\" (YOU HAVE A MAXIMUM OF 3 TRY)")
371 | is_init = False
372 | else:
373 | logging.debug("PIN2 entered with success")
374 | else:
375 | logging.warning("Some initialization may not work without PIN2 activated")
376 | break
377 | elif pin_status == GSMTC35.eRequiredPin.PUK2:
378 | if len(_puk2) > 0:
379 | if not self.enterPin(_puk2):
380 | logging.error("Invalid PUK2 \""+str(_puk2)+"\"")
381 | is_init = False
382 | else:
383 | logging.debug("PUK2 entered with success")
384 | else:
385 | logging.warning("Some initialization may not work without PUK2 activated")
386 | break
387 |
388 | #Disable asynchronous triggers (SMS, calls, temperature)
389 | self.__disableAsynchronousTriggers()
390 |
391 | # Set to text mode
392 | if not self.__sendCmdAndCheckResult(GSMTC35.__NORMAL_AT+"CMGF=1"):
393 | logging.error("Impossible to set module to text mode (CMGF command)")
394 | is_init = False
395 | # Select fixed baudrate communication
396 | if not self.__selectBaudrateCommunicationType(_baudrate):
397 | # Some function will not work if this is not activated (alarm, wake-up ACK, ...)
398 | logging.warning("Impossible to have fixed baudrate communication (IPR command)")
399 |
400 | self.__initialized = is_init
401 | if not self.__initialized:
402 | self.__serial.close()
403 |
404 | return self.__initialized
405 |
406 | def isInitialized(self):
407 | """Check if GSM class is initialized"""
408 | return self.__initialized
409 |
410 | def close(self):
411 | """Close GSM session (free the GSM serial port)"""
412 | # Try to put auto-baudrate mode back
413 | self.__selectBaudrateCommunicationType(0)
414 |
415 | # Then close the serial port
416 | self.__serial.close()
417 |
418 |
419 | def reboot(self, waiting_time_sec=10):
420 | """Reboot GSM module (you need to initialize the GSM module again after a reboot)
421 |
422 | Keyword arguments:
423 | additional_timeout -- (int, optional) Additional time (in sec) to reboot
424 |
425 | return: (bool) Reboot successful
426 | """
427 | restarted = self.__sendCmdAndCheckResult(cmd=GSMTC35.__NORMAL_AT+"CFUN=1,1",
428 | result="^SYSSTART",
429 | additional_timeout=waiting_time_sec)
430 |
431 | # Be sure user will not use the class without initializing it again
432 | if restarted:
433 | self.close()
434 |
435 | return restarted
436 |
437 |
438 | ######################### INTERNAL UTILITY FUNCTIONS #########################
439 | @staticmethod
440 | def __deleteQuote(quoted_string):
441 | """Delete first and last " or ' from {quoted_string}
442 |
443 | Keyword arguments:
444 | quoted_string -- (string) String to get rid of quotes
445 |
446 | return: (string) {quoted_string} without quotes
447 | """
448 | str_lengh = len(quoted_string)
449 | if str_lengh > 1:
450 | if (quoted_string[0] == '"') or (quoted_string[0] == "'"):
451 | # Delete first ' or "
452 | quoted_string = quoted_string[1:]
453 | str_lengh = len(quoted_string)
454 | if str_lengh >= 1:
455 | if (quoted_string[str_lengh-1] == '"') or (quoted_string[str_lengh-1] == "'"):
456 | # Delete last ' or "
457 | quoted_string = quoted_string[:str_lengh-1]
458 | return quoted_string
459 |
460 |
461 | def __readLine(self):
462 | """Read one line from the serial port (not blocking)
463 |
464 | Note: Even if the end of line is not found, the data is returned
465 |
466 | return: (string) Line without the end of line (empty if nothing received)
467 | """
468 | eol = '\r\n'
469 | leneol = len(eol)
470 | line = ""
471 | while True:
472 | c = self.__serial.read(1)
473 | if c:
474 | line += c.decode()
475 | if line[-leneol:] == eol:
476 | line = line[:len(line)-leneol]
477 | break
478 | else:
479 | if str(line).startswith(">"):
480 | logging.debug("Reading line while GSM is waiting content")
481 | else:
482 | logging.warning("Received data without eol: \""+str(line)+"\"")
483 | break
484 | logging.debug("[IN] "+str(line))
485 | return line
486 |
487 |
488 | def __deleteAllRxData(self):
489 | """Delete all received data from the serial port
490 |
491 | return: (int) Number of deleted bytes
492 | """
493 | bytesToRead = self.__serial.inWaiting()
494 | if bytesToRead <= 0:
495 | return 0
496 |
497 | data = self.__serial.read(bytesToRead)
498 | logging.debug("[DELETED]"+str(data))
499 |
500 | return bytesToRead
501 |
502 |
503 | def __waitDataContains(self, content, error_result, additional_timeout=0):
504 | """Wait to receive specific data from the serial port
505 |
506 | Keyword arguments:
507 | content -- (string) Data to wait from the serial port
508 | error_result -- (string) Line meaning an error occured (sent by the module), empty means not used
509 | additional_timeout -- (int) Additional time to wait the match (added with base timeout)
510 |
511 | return: (bool) Is data received before timeout (if {error_result} is received, False is returned)
512 | """
513 | start_time = time.time()
514 | while time.time() - start_time < self.__timeout_sec + additional_timeout:
515 | while self.__serial.inWaiting() > 0:
516 | line = self.__readLine()
517 | if content in line:
518 | return True
519 | if len(error_result) > 0 and error_result == line:
520 | logging.error("GSM module returned error \""+str(error_result)+"\"")
521 | return False
522 | # Wait 100ms if no data in the serial buffer
523 | time.sleep(.100)
524 | #logging.error("Impossible to get line containing \""+str(content)+"\" on time")
525 | return False
526 |
527 |
528 | def __getNotEmptyLine(self, content="", error_result=__RETURN_ERROR, additional_timeout=0):
529 | """Wait to receive a line containing at least {content} (or any char if {content} is empty)
530 |
531 | Keyword arguments:
532 | content -- (string) Data to wait from the serial port
533 | error_result -- (string) Line meaning an error occured (sent by the module)
534 | additional_timeout -- (int) Additional time to wait the match (added with base timeout)
535 |
536 | return: (string) Line received (without eol), empty if not found or if an error occured
537 | """
538 | start_time = time.time()
539 | while time.time() - start_time < self.__timeout_sec + additional_timeout:
540 | while self.__serial.inWaiting() > 0:
541 | line = self.__readLine()
542 | if len(error_result) > 0 and (str(error_result) == str(line)):
543 | logging.error("GSM module returned error \""+str(error_result)+"\"")
544 | return ""
545 | elif (content in line) and len(line) > 0:
546 | return line
547 | # Wait 100ms if no data in the serial buffer
548 | time.sleep(.100)
549 | logging.error("Impossible to get line containing \""+str(content)+"\" on time")
550 | return ""
551 |
552 |
553 | def __sendLine(self, before, after=""):
554 | """Send line to the serial port as followed: {before}\r\n{after}
555 |
556 | Keyword arguments:
557 | before -- (string) Data to send before the end of line
558 | after -- (string) Data to send after the end of line
559 |
560 | return: (bool) Send line worked?
561 | """
562 | if self.__serial.write("{}\r\n".format(before).encode()):
563 | logging.debug("[OUT] "+str(before))
564 | if after != "":
565 | time.sleep(0.100)
566 | if self.__serial.write(after.encode()) > 0:
567 | logging.debug("[OUT] "+str(after))
568 | return True
569 | else:
570 | logging.warning("Failed to write \""+str(after)+"\" to GSM (after).")
571 | else:
572 | return True
573 | else:
574 | logging.warning("Failed to write \""+str(after)+"\" to GSM (before).")
575 | return False
576 |
577 | def __sendCmdAndGetNotEmptyLine(self, cmd, after="", additional_timeout=0,
578 | content="", error_result=__RETURN_ERROR):
579 | """Send command to the GSM module and get line containing {content}
580 |
581 | Keyword arguments:
582 | cmd -- (string) Command to send to the module (without eol)
583 | after -- (string, optional) Data to send to the module after the end of line
584 | additional_timeout -- (int, optional) Additional time (in sec) to wait the content to appear
585 | content -- (string, optional) Data to wait from the GSM module (line containing this will be returned
586 | error_result -- (string) Line meaning an error occured (sent by the module)
587 |
588 | return: (string) Line without the end of line containing {content} (empty if nothing received or if an error occured)
589 | """
590 | self.__deleteAllRxData()
591 | if self.__sendLine(cmd, after):
592 | return self.__getNotEmptyLine(content, error_result, additional_timeout)
593 | return ""
594 |
595 |
596 | def __sendCmdAndGetFullResult(self, cmd, after="", additional_timeout=0,
597 | result=__RETURN_OK, error_result=__RETURN_ERROR):
598 | """Send command to the GSM module and get all lines before {result}
599 |
600 | Keyword arguments:
601 | cmd -- (string) Command to send to the module (without eol)
602 | after -- (string, optional) Data to send to the module after the end of line
603 | additional_timeout -- (int, optional) Additional time (in sec) to wait the content to appear
604 | result -- (string, optional) Line to wait from the GSM module (all lines will be returned BEFORE the {result} line)
605 | error_result -- (string) Line meaning an error occured (sent by the module)
606 |
607 | return: ([string,]) All lines without the end of line (empty if nothing received or if an error occured)
608 | """
609 | val_result = []
610 |
611 | self.__deleteAllRxData()
612 | if not self.__sendLine(cmd, after):
613 | return val_result
614 |
615 | start_time = time.time()
616 | while time.time() - start_time < self.__timeout_sec + additional_timeout:
617 | while self.__serial.inWaiting() > 0:
618 | line = self.__readLine()
619 | if (result == line) and len(line) > 0:
620 | return val_result
621 | if len(error_result) > 0 and (error_result == line):
622 | logging.error("Error returned by GSM module for \""+str(cmd)+"\" command")
623 | return []
624 | elif line != "":
625 | val_result.append(line)
626 | # Wait 100ms if no data in the serial buffer
627 | time.sleep(.100)
628 |
629 | logging.error("Impossible to get line equal to \""+str(result)+"\" on time")
630 | return val_result
631 |
632 |
633 | def __sendCmdAndCheckResult(self, cmd, after="", additional_timeout=0,
634 | result=__RETURN_OK, error_result=__RETURN_ERROR):
635 | """Send command to the GSM module and wait specific result
636 |
637 | Keyword arguments:
638 | cmd -- (string) Command to send to the module (without eol)
639 | after -- (string, optional) Data to send to the module after the end of line
640 | additional_timeout -- (int, optional) Additional time (in sec) to wait the result
641 | result -- (string, optional) Data to wait from the GSM module
642 | error_result -- (string) Line meaning an error occured (sent by the module)
643 |
644 | return: (bool) Command successful (result returned from the GSM module)
645 | """
646 | self.__deleteAllRxData()
647 | if not self.__sendLine(cmd, after):
648 | return False
649 |
650 | result = self.__waitDataContains(result, error_result, additional_timeout)
651 |
652 | if not result:
653 | logging.error("Sending \""+str(cmd)+"\" and \""+str(after)+"\" failed")
654 |
655 | return result
656 |
657 |
658 | def __deleteSpecificSMS(self, index):
659 | """Delete SMS with specific index
660 |
661 | Keyword arguments:
662 | index -- (int) Index of the SMS to delete from the GSM module (can be found by reading SMS)
663 |
664 | Note: Even if this function is not done for that: On some device, GSMTC35.eSMS.ALL_SMS,
665 | GSMTC35.eSMS.UNREAD_SMS and GSMTC35.eSMS.READ_SMS may be used instead of
666 | {index} to delete multiple SMS at once (not working for GSMTC35).
667 |
668 | return: (bool) Delete successful
669 | """
670 | return self.__sendCmdAndCheckResult(cmd=GSMTC35.__NORMAL_AT+"CMGD="+str(index))
671 |
672 |
673 | @staticmethod
674 | def __guessPhoneNumberType(phone_number):
675 | """Guess phone number type from phone number
676 |
677 | Keyword arguments:
678 | phone_number -- (string) Phone number
679 |
680 | return: (GSMTC35.__ePhoneNumberType) Phone number type
681 | """
682 | # Is it an international phone number?
683 | if len(phone_number) > 1 and phone_number[0] == "+":
684 | return GSMTC35.__ePhoneNumberType.INTERNATIONAL
685 |
686 | # Is it a valid local phone number?
687 | try:
688 | int(phone_number)
689 | return GSMTC35.__ePhoneNumberType.LOCAL
690 | except ValueError:
691 | pass
692 |
693 | logging.error("Phone number "+str(phone_number)+" is not valid")
694 | return GSMTC35.__ePhoneNumberType.ERROR
695 |
696 |
697 | def __selectPhonebook(self, phonebook_type):
698 | """Select phonebook in order to use it for future operations on phonebooks
699 |
700 | Note: If {phonebook_type} specifies "Current phonebook", no action will be
701 | made and the function will return True
702 |
703 | Keyword arguments:
704 | phonebook_type -- (GSMTC35.ePhonebookType) Phonebook type
705 |
706 | return: (bool) Phonebook selected
707 | """
708 | if phonebook_type == GSMTC35.ePhonebookType.CURRENT:
709 | return True
710 |
711 | return self.__sendCmdAndCheckResult(GSMTC35.__NORMAL_AT+"CPBS=\""
712 | +str(phonebook_type)+"\"")
713 |
714 |
715 | def __getCurrentPhonebookRange(self):
716 | """Get information about current phonebook restrictions (min and max entry
717 | indexes, max phone number length and max contact name length)
718 |
719 | return: (int, int, int, int) First entry index, Last entry index, max phone
720 | number length, max contact name length (for all elements: -1 if data is invalid)
721 | """
722 | # Send the command to get all info
723 | result = self.__sendCmdAndGetNotEmptyLine(cmd=GSMTC35.__NORMAL_AT+"CPBR=?",
724 | content="+CPBR: ")
725 |
726 | index_min = -1
727 | index_max = -1
728 | index_max_phone_length = -1
729 | max_contact_name_length = -1
730 |
731 | if result == "" or len(result) <= 8 or result[:7] != "+CPBR: ":
732 | logging.error("Phonebook information request failed")
733 | return index_min, index_max, index_max_phone_length, max_contact_name_length
734 |
735 | # Get result without "+CPBR: "
736 | result = result[7:]
737 |
738 | # Delete potential "(" and ")" from the result
739 | result = result.replace("(","")
740 | result = result.replace(")","")
741 |
742 | # Split index_min and the other part of the result
743 | split_result = result.split("-")
744 | if len(split_result) < 2:
745 | logging.error("Impossible to split phonebook information")
746 | return index_min, index_max, index_max_phone_length, max_contact_name_length
747 |
748 | try:
749 | index_min = int(split_result[0])
750 | except ValueError:
751 | # Index min is not correct, let's try to get other elements
752 | logging.warning("Impossible to get the phonebook min index")
753 |
754 | # Split last elements
755 | split_result = split_result[1].split(",")
756 |
757 | # Get the index_max
758 | if len(split_result) >= 1:
759 | try:
760 | index_max = int(split_result[0])
761 | except ValueError:
762 | # Index max is not correct, let's try to get other elements
763 | logging.warning("Impossible to get the phonebook max index (value error)")
764 | else:
765 | logging.warning("Impossible to get the phonebook max index (length error)")
766 |
767 | # Get max phone length
768 | if len(split_result) >= 2:
769 | try:
770 | index_max_phone_length = int(split_result[1])
771 | except ValueError:
772 | # Max phone length is not correct, let's try to get other elements
773 | logging.warning("Impossible to get the phonebook max phone length (value error)")
774 | else:
775 | logging.warning("Impossible to get the phonebook max phone length (length error)")
776 |
777 | # Get contact name length
778 | if len(split_result) >= 3:
779 | try:
780 | max_contact_name_length = int(split_result[2])
781 | except ValueError:
782 | # Max phone length is not correct, let's try to get other elements
783 | logging.warning("Impossible to get the phonebook max contact name length (value error)")
784 | else:
785 | logging.warning("Impossible to get the phonebook max contact name length (length error)")
786 |
787 | # Delete last "OK" from buffer
788 | self.__waitDataContains(self.__RETURN_OK, self.__RETURN_ERROR)
789 |
790 | # Return final result
791 | return index_min, index_max, index_max_phone_length, max_contact_name_length
792 |
793 |
794 | def __selectBaudrateCommunicationType(self, baudrate):
795 | """Select baudrate communication type with the module (fixed baudrate of auto-baudrate)
796 |
797 | Keyword arguments:
798 | baudrate -- (int) 0 for auto-baudrate or baudrate value for fixed baudrate
799 |
800 | return: (bool) Baudrate selected
801 | """
802 | return self.__sendCmdAndCheckResult(GSMTC35.__NORMAL_AT+"IPR="+str(baudrate))
803 |
804 |
805 | def __setInternalClockToSpecificDate(self, date):
806 | """Set the GSM module internal clock to specific date
807 |
808 | Keyword arguments:
809 | date -- (datetime.datetime) Date to set in the internal clock
810 |
811 | return: (bool) Date successfully modified
812 | """
813 | return self.__sendCmdAndCheckResult(cmd=GSMTC35.__NORMAL_AT+"CCLK=\""
814 | +date.strftime(GSMTC35.__DATE_FORMAT)+"\"")
815 |
816 |
817 | def __addAlarm(self, date, message=""):
818 | """Add an alarm to show a message at the exact specified time
819 |
820 | Note: The reference date is the one from the internal clock
821 | (see {getDateFromInternalClock()} to get the reference clock)
822 |
823 | Keyword arguments:
824 | date -- (datetime.datetime) Date
825 |
826 | return: (bool) Alarm successfully set
827 | """
828 | message_in_cmd = ""
829 | if len(message) > 0:
830 | message_in_cmd = ",\""+str(message)+"\""
831 |
832 | return self.__sendCmdAndCheckResult(cmd=GSMTC35.__NORMAL_AT+"CALA=\""
833 | +date.strftime(GSMTC35.__DATE_FORMAT)
834 | +"\",0,0"+message_in_cmd)
835 |
836 |
837 | def __addAlarmAsAChrono(self, time_in_sec, message=""):
838 | """Add an alarm to show a message in {time_in_sec} seconds
839 |
840 | Note: The reference date is the one from the internal clock
841 | (see {getDateFromInternalClock()} to get the reference clock)
842 |
843 | Keyword arguments:
844 | time_in_sec -- (int) Time to wait before the alarm will happen
845 |
846 | return: (bool) Alarm successfully set
847 | """
848 | date = self.getDateFromInternalClock()
849 | if date == -1:
850 | return False
851 |
852 | date = date + datetime.timedelta(seconds=time_in_sec)
853 |
854 | return self.__addAlarm(date, message)
855 |
856 |
857 | def __disableAsynchronousTriggers(self):
858 | """Disable asynchronous triggers (SMS, calls, temperature)
859 |
860 | return: (bool) All triggers disabled
861 | """
862 | all_disable = True
863 | # Don't show received call in buffer without query
864 | if not self.__sendCmdAndCheckResult(GSMTC35.__NORMAL_AT+"CLIP=0"):
865 | logging.warning("Can't disable mode showing phone number when calling (CLIP command)")
866 | all_disable = False
867 | # Don't show received SMS in buffer without query
868 | if not self.__sendCmdAndCheckResult(GSMTC35.__NORMAL_AT+"CNMI=0,0"):
869 | logging.warning("Can't disable mode showing received SMS (CNMI command)")
870 | all_disable = False
871 | # Don't show temperature issue without query
872 | if not self.__sendCmdAndCheckResult(GSMTC35.__BASE_AT+"^SCTM=0"):
873 | logging.warning("Can't disable mode showing critical temperature (SCTM command)")
874 | all_disable = False
875 |
876 | return all_disable
877 |
878 | __gsm0338_base_table = u"@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\x1bÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà"
879 | __gsm0338_extra_table = u"````````````````````^```````````````````{}`````\\````````````[~]`|````````````````````````````````````€``````````````````````````"
880 |
881 | @staticmethod
882 | def __gsm0338Encode(plaintext):
883 | res = ""
884 | for c in plaintext:
885 | idx = GSMTC35.__gsm0338_base_table.find(c)
886 | if idx != -1:
887 | res += chr(int(idx))
888 | continue
889 | idx = GSMTC35.__gsm0338_extra_table.find(c)
890 | if idx != -1:
891 | res += chr(27) + chr(int(idx))
892 | return res
893 |
894 | @staticmethod
895 | def __gsm0338Decode(text):
896 | result = []
897 | normal_table = True
898 | for i in text:
899 | if int(i) == 27:
900 | normal_table = False
901 | else:
902 | if normal_table:
903 | result += GSMTC35.__gsm0338_base_table[int(i)]
904 | else:
905 | result += GSMTC35.__gsm0338_extra_table[int(i)]
906 | normal_table = True
907 |
908 | return "".join(result)
909 |
910 | @staticmethod
911 | def __is7BitCompatible(plaintext):
912 | """Check that the data can be encoded in GSM03.38 (extra table included)
913 |
914 | Keyword arguments:
915 | plaintext -- (bytes) Content to check if can be encoded into 7bit
916 |
917 | return: (bool) Data can be encoded into 7Bit
918 | """
919 | try:
920 | # Do not encode data if not 7bit compatible
921 | for c in str(plaintext):
922 | if (c == '`') or ((not (c in GSMTC35.__gsm0338_base_table)) and (not (c in GSMTC35.__gsm0338_extra_table))):
923 | return False
924 | except (UnicodeEncodeError, UnicodeDecodeError):
925 | logging.debug("Unicode detected so data not 7bit compatible")
926 | return False
927 |
928 | return True
929 |
930 | @staticmethod
931 | def __unpack7bit(content, header_length=0, message_length=0):
932 | """Decode byte with Default Alphabet encoding ('7bit')
933 |
934 | Function logic inspired from https://github.com/pmarti/python-messaging/blob/master/messaging/utils.py#L173
935 |
936 | Keyword arguments:
937 | content -- (bytes) Content to decode as hexa
938 |
939 | return: (bytes) Decoded content
940 | """
941 | try:
942 | unichr
943 | except NameError:
944 | unichr = chr
945 |
946 | count = last = 0
947 | result = []
948 | try:
949 | for i in range(0, len(content), 2):
950 | byte = int(content[i:i + 2], 16)
951 | mask = 0x7F >> count
952 | out = ((byte & mask) << count) + last
953 | last = byte >> (7 - count)
954 | result.append(out)
955 |
956 | if len(result) >= 0xa0:
957 | break
958 |
959 | if count == 6:
960 | result.append(last)
961 | last = 0
962 |
963 | count = (count + 1) % 7
964 |
965 | result = ''.join(map(unichr, result))
966 |
967 | # Convert GSM 7bit encodage (GSM03.38) into normal string
968 | return GSMTC35.__gsm0338Decode(result[:message_length].encode())
969 | except ValueError:
970 | return ''
971 |
972 | @staticmethod
973 | def __unpack8bit(encoded_data):
974 | """Decode hexa byte encoded with 8bit encoding
975 |
976 | Keyword arguments:
977 | encoded_data -- (bytes) Content to decode
978 |
979 | return: (bytes) Decoded content
980 | """
981 | encoded_data = [ord(x) for x in encoded_data]
982 | return ''.join([chr(x) for x in encoded_data])
983 |
984 | @staticmethod
985 | def __unpackUCS2(encoded_data):
986 | """Decode hexa byte encoded with extended encoding (UTF-16 / UCS2)
987 |
988 | Keyword arguments:
989 | encoded_data -- (bytes) Content to decode
990 |
991 | return: (bytes) Decoded content
992 | """
993 | return encoded_data.decode('utf-16be')
994 |
995 | @staticmethod
996 | def __packUCS2(content, user_data_id=0):
997 | """Encode bytes into hexadecimal representation of extended encoded User Data with User Data Length (UTF-16 / UCS2)
998 |
999 | Keyword arguments:
1000 | content -- (bytes) Content to encode
1001 | user_data_id -- (int[1:255] or 0 for random, optional, default: random) ID of the potential multipart message
1002 |
1003 | return: ([bytes]) List of Hexadecimal representation of extended encoded User Data with User Data Length (UTF-16 / UCS2)
1004 | """
1005 | # Check if message can be sent in one part or is multipart
1006 | if (len(content) > 70):
1007 | logging.debug("Encoding multipart message in UCS-2 (Utf-16)")
1008 | # Get all parts
1009 | n = 67 # Max number of unicode char in multipart message (excepting header)
1010 | all_msg_to_encode = [content[i:i+n] for i in range(0, len(content), n)]
1011 | logging.debug("Messages to encode:\n - "+'\n - '.join(all_msg_to_encode))
1012 | all_encoded_msg = []
1013 | nb_of_parts = len(all_msg_to_encode)
1014 | # Have same user data ID for all message parts
1015 | if user_data_id == 0:
1016 | user_data_id = randint(0, 255)
1017 | # Encode data as multipart messages
1018 | for current_id in range(nb_of_parts):
1019 | encoded_message = binascii.hexlify(all_msg_to_encode[current_id].encode('utf-16be')).decode()
1020 | if len(encoded_message) % 4 != 0:
1021 | encoded_message = str("00") + str(encoded_message)
1022 |
1023 | encoded_message = GSMTC35.__generateMultipartUDH(user_data_id, current_id+1, nb_of_parts, True) + encoded_message
1024 |
1025 | encoded_message_length = format(int(ceil(len(encoded_message)/2)), 'x')
1026 | if len(encoded_message_length) % 2 != 0:
1027 | encoded_message_length = "0" + encoded_message_length
1028 |
1029 | encoded_message = str(str(encoded_message_length) + str(encoded_message)).upper().replace("'", "")
1030 |
1031 | all_encoded_msg.append(encoded_message)
1032 |
1033 | return all_encoded_msg
1034 | else:
1035 | encoded_message = binascii.hexlify(content.encode('utf-16be')).decode()
1036 | if len(encoded_message) % 4 != 0:
1037 | encoded_message = str("00") + str(encoded_message)
1038 |
1039 | encoded_message_length = format(int(2*((len(content)))), 'x')
1040 | if len(encoded_message_length) % 2 != 0:
1041 | encoded_message_length = "0" + encoded_message_length
1042 |
1043 | return [str(str(encoded_message_length) + str(encoded_message)).upper().replace("'", "")]
1044 |
1045 | @staticmethod
1046 | def __generateMultipartUDH(user_data_id, current_part, nb_of_parts, string_mode=False):
1047 | """Generate User Data Header for multipart message purpose
1048 |
1049 | Keyword arguments:
1050 | user_data_id -- (int[0:255]) ID of the multipart message
1051 | current_part -- (int) Current part of the multipart message
1052 | nb_of_parts -- (int) Number of parts of the full multipart message
1053 | string_mode -- (bool) Returning the UDH as string or as unicode?
1054 |
1055 | return: (string) User Data Header
1056 | """
1057 | if string_mode:
1058 | # UDHL (User Data Header Length, not including UDHL byte)
1059 | result = "05"
1060 | # Information element identifier (not used)
1061 | result += "00"
1062 | # Header Length, not including this byte
1063 | result += "03"
1064 | # User Data ID (reference)
1065 | result += '{:02X}'.format(user_data_id)
1066 | # Number of parts
1067 | result += '{:02X}'.format(nb_of_parts)
1068 | # Current part
1069 | result += '{:02X}'.format(current_part)
1070 | else:
1071 | # UDHL (User Data Header Length, not including UDHL byte)
1072 | result = "\x05"
1073 | # Information element identifier (not used)
1074 | result += "\x00"
1075 | # Header Length, not including this byte
1076 | result += "\x03"
1077 | # User Data ID (reference)
1078 | result += chr(user_data_id)
1079 | # Number of parts
1080 | result += chr(nb_of_parts)
1081 | # Current part
1082 | result += chr(current_part)
1083 |
1084 | return result
1085 |
1086 | @staticmethod
1087 | def __pack7Bit(plaintext, user_data_id=0):
1088 | """Encode bytes into hexadecimal representation of 7bit GSM encoding with length (very basic UTF-8)
1089 |
1090 | Function logic inspired from https://github.com/pmarti/python-messaging/blob/master/messaging/utils.py#L98
1091 |
1092 | Keyword arguments:
1093 | plaintext -- (bytes) Content to encode
1094 | user_data_id -- (int[1:255] or 0 for random, optional, default: random) ID of the potential multipart message
1095 |
1096 | return: (bool, [bytes]) (Successfully encoded, List of Hexadecimal representation of 7bit GSM encoded User Data with User Data Length (very basic UTF-8))
1097 | """
1098 | # Do not encode data if not 7bit compatible
1099 | if not GSMTC35.__is7BitCompatible(plaintext):
1100 | return False, []
1101 |
1102 | encoded_message = ""
1103 |
1104 | # Be sure that message is a string
1105 | if sys.version_info >= (3,):
1106 | txt = plaintext.encode().decode('latin1')
1107 | else:
1108 | txt = plaintext
1109 |
1110 | # Encode string in GSM 03.38 encoding
1111 | txt = GSMTC35.__gsm0338Encode(plaintext)
1112 |
1113 | # Check if message can be sent in one part or is multipart
1114 | if (len(txt) > 140):
1115 | logging.debug("Encoding multipart message in 7bit")
1116 | # Get all parts that needs to be encoded
1117 | n = 138 # Max number of 7 bit chars in multipart message (excepting header)
1118 | all_msg_to_encode = [txt[i:i+n] for i in range(0, len(txt), n)]
1119 | logging.debug("Messages to encode:\n - "+'\n - '.join(all_msg_to_encode))
1120 | all_encoded_msg = []
1121 | nb_of_parts = len(all_msg_to_encode)
1122 | # Have same user data ID for all message parts
1123 | if user_data_id == 0:
1124 | user_data_id = randint(0, 255)
1125 | # Encode data as multipart messages
1126 | for current_id in range(nb_of_parts):
1127 | txt = "\x00\x00\x00\x00\x00\x00" + "\x00"+ all_msg_to_encode[current_id]
1128 | tl = len(txt)
1129 | txt += '\x00'
1130 | msgl = int(len(txt) * 7 / 8)
1131 | op = [-1] * msgl
1132 | c = shift = 0
1133 |
1134 | for n in range(msgl):
1135 | if shift == 6:
1136 | c += 1
1137 |
1138 | shift = n % 7
1139 | lb = ord(txt[c]) >> shift
1140 | hb = (ord(txt[c + 1]) << (7 - shift) & 255)
1141 | op[n] = lb + hb
1142 | c += 1
1143 |
1144 | for i, char in enumerate(GSMTC35.__generateMultipartUDH(user_data_id, current_id+1, nb_of_parts)):
1145 | op[i] = ord(char)
1146 |
1147 | encoded_message = chr(tl) + ''.join(map(chr, op))
1148 |
1149 | all_encoded_msg.append(str(''.join(["%02x" % ord(n) for n in encoded_message])).upper().replace("'", ""))
1150 |
1151 | return True, all_encoded_msg
1152 | else:
1153 | # Encode data as normal message
1154 | logging.debug("Encoding one SMS in 7bit")
1155 | tl = len(txt)
1156 | txt += '\x00'
1157 | msgl = int(len(txt) * 7 / 8)
1158 | op = [-1] * msgl
1159 | c = shift = 0
1160 |
1161 | for n in range(msgl):
1162 | if shift == 6:
1163 | c += 1
1164 |
1165 | shift = n % 7
1166 | lb = ord(txt[c]) >> shift
1167 | hb = (ord(txt[c + 1]) << (7 - shift) & 255)
1168 | op[n] = lb + hb
1169 | c += 1
1170 |
1171 | encoded_message = chr(tl) + ''.join(map(chr, op))
1172 |
1173 | return True, [str(''.join(["%02x" % ord(n) for n in encoded_message])).upper().replace("'", "")]
1174 |
1175 | @staticmethod
1176 | def __decodePduSms(msg, decode_sms):
1177 | """Decode PDU SMS content
1178 |
1179 | Keyword arguments:
1180 | msg -- (string) PDU hexa string to decoded
1181 | decode_sms -- (bool) Is it needed to decode SMS content ?
1182 |
1183 | return: (list) List of decoded content containing potentially 'phone_number', 'date', 'time', 'sms',
1184 | 'sms_encoded', 'service_center_type', 'service_center_phone_number', 'phone_number_type',
1185 | 'charset'
1186 | if message has an header: 'header_iei', 'header_ie_data'
1187 | if message is a multipart message (MMS): 'header_multipart_ref_id',
1188 | 'header_multipart_current_part_nb', 'header_multipart_nb_of_part'
1189 | """
1190 | result = {}
1191 |
1192 | # Be sure message is of hexa type
1193 | try:
1194 | int(str(msg), 16)
1195 | except ValueError:
1196 | logging.error("Can't decode PDU SMS because is not hexadecimal content: \""+str(msg)+"\"")
1197 | return result
1198 |
1199 | # Service center data (type and phone number)
1200 | lengthServiceCenter = int(msg[:2], 16)
1201 | msg = msg[2:]
1202 |
1203 | serviceCenterType = int(msg[:2], 16)
1204 | result["service_center_type"] = serviceCenterType
1205 | msg = msg[2:]
1206 |
1207 | serviceCenterEncodedPhone = msg[:lengthServiceCenter*2-2]
1208 | if (lengthServiceCenter%2 != 0):
1209 | serviceCenterEncodedPhone = serviceCenterEncodedPhone[0:len(serviceCenterEncodedPhone)-2] + serviceCenterEncodedPhone[len(serviceCenterEncodedPhone)-1:]
1210 | msg = msg[lengthServiceCenter*2-2:]
1211 |
1212 | serviceCenterDecodedPhone = ""
1213 | for number in range(0,lengthServiceCenter*2-3):
1214 | if number %2 == 0:
1215 | serviceCenterDecodedPhone = serviceCenterDecodedPhone + serviceCenterEncodedPhone[number]
1216 | else:
1217 | serviceCenterDecodedPhone = serviceCenterDecodedPhone[:len(serviceCenterDecodedPhone) - 1] + str(serviceCenterEncodedPhone[number]) + serviceCenterDecodedPhone[len(serviceCenterDecodedPhone) - 1:]
1218 | result["service_center_phone_number"] = str(serviceCenterDecodedPhone)
1219 |
1220 | # First byte
1221 | firstByte = int(msg[:2], 16)
1222 | if firstByte & 0b1000000:
1223 | contentContainsHeader = True
1224 | else:
1225 | contentContainsHeader = False
1226 | msg = msg[2:]
1227 |
1228 | # Sender Phone data (type and number)
1229 | lengthSenderPhoneNumber = int(msg[:2], 16)
1230 | msg = msg[2:]
1231 |
1232 | senderType = int(msg[:2], 16)
1233 | msg = msg[2:]
1234 | result["phone_number_type"] = senderType
1235 |
1236 | phoneNumberEncoded = msg[:lengthSenderPhoneNumber+1]
1237 | if (lengthSenderPhoneNumber%2 != 0):
1238 | phoneNumberEncoded = phoneNumberEncoded[0:len(phoneNumberEncoded)-2] + phoneNumberEncoded[len(phoneNumberEncoded)-1:]
1239 | msg = msg[lengthSenderPhoneNumber+1:]
1240 |
1241 | phoneNumberDecoded = ""
1242 | if senderType == GSMTC35.__ePhoneNumberType.INTERNATIONAL:
1243 | phoneNumberDecoded = "+" + phoneNumberDecoded
1244 |
1245 | for number in range(0,lengthSenderPhoneNumber):
1246 | if number %2 == 0:
1247 | phoneNumberDecoded = phoneNumberDecoded + phoneNumberEncoded[number]
1248 | else:
1249 | phoneNumberDecoded = phoneNumberDecoded[:len(phoneNumberDecoded) - 1] + str(phoneNumberEncoded[number]) + phoneNumberDecoded[len(phoneNumberDecoded) - 1:]
1250 |
1251 | result["phone_number"] = str(phoneNumberDecoded)
1252 |
1253 | # Protocol ID / TP-PID
1254 | # protocolId = int(msg[:2], 16)
1255 | msg = msg[2:]
1256 |
1257 | # Data coding scheme / TP-DCS
1258 | dataCodingScheme = int(msg[:2], 16)
1259 | msg = msg[2:]
1260 |
1261 | # Timestamp
1262 | timestampEncoded = msg[:14]
1263 | msg = msg[14:]
1264 | dateDecoded = timestampEncoded[1] + timestampEncoded[0] + "/" \
1265 | + timestampEncoded[3] + timestampEncoded[2] + "/" \
1266 | + timestampEncoded[5] + timestampEncoded[4]
1267 | timeDecoded = timestampEncoded[7] + timestampEncoded[6] + ":" \
1268 | + timestampEncoded[9] + timestampEncoded[8] + ":" \
1269 | + timestampEncoded[11] + timestampEncoded[10] + ""
1270 | gmt = timestampEncoded[13] + timestampEncoded[12]
1271 | gmtDecoded = ""
1272 | if (int(gmt[1], 16) >= 8):
1273 | gmtDecoded = "GMT-"
1274 | gmt = gmt[0] + str(int(gmt[1], 16) - 8)
1275 | else:
1276 | gmtDecoded += "GMT+"
1277 | gmtDecoded += str(int(gmt, 10)/4)
1278 | result["date"] = str(dateDecoded)
1279 | result["time"] = str(str(timeDecoded)+" "+str(gmtDecoded))
1280 |
1281 | # Message content
1282 | messageLength = int(msg[:2], 16)
1283 | msg = msg[2:]
1284 |
1285 | # Charset
1286 | if (dataCodingScheme & 0xc0) == 0:
1287 | if dataCodingScheme & 0x20:
1288 | logging.error("Not possible to find correct encoding")
1289 | if decode_sms:
1290 | return result
1291 | else:
1292 | charset = "unknown"
1293 | try:
1294 | charset = {0x00: '7bit', 0x04: '8bit', 0x08: 'utf16-be'}[dataCodingScheme & 0x0c]
1295 | except KeyError:
1296 | logging.error("Not possible to find correct encoding")
1297 | if decode_sms:
1298 | return result
1299 | else:
1300 | charset = "unknown"
1301 | elif (dataCodingScheme & 0xf0) in (0xc0, 0xd0):
1302 | charset = '7bit'
1303 | elif (dataCodingScheme & 0xf0) == 0xe0:
1304 | charset = 'utf16-be'
1305 | elif (dataCodingScheme & 0xf0) == 0xf0:
1306 | charset = {0x00: '7bit', 0x04: '8bit'}[dataCodingScheme & 0x04]
1307 | else:
1308 | logging.error("Not possible to find correct encoding")
1309 | if decode_sms:
1310 | return result
1311 | else:
1312 | charset = "unknown"
1313 |
1314 | result["charset"] = str(charset)
1315 |
1316 | # SMS content header
1317 | headerLength = 0
1318 | if contentContainsHeader:
1319 | headerLength = int(msg[:2], 16)
1320 | if headerLength > 0:
1321 | if charset == '7bit':
1322 | headerLength = int(ceil(headerLength * 7.0 / 8.0))
1323 | if ((headerLength % 2) != 0):
1324 | headerLength = headerLength + 1
1325 | result["header_iei"] = int(msg[2:4], 16)
1326 | headerIeLength = int(msg[4:6], 16)
1327 | result["header_ie_data"] = msg[6:6+headerIeLength*2]
1328 | # Add multipart information if IEI is of type 'Concatenated short message' (0x00 or 0x08)
1329 | if result["header_iei"] == 0 or result["header_iei"] == 8:
1330 | result["header_multipart_ref_id"] = int(result["header_ie_data"][:2], 16)
1331 | result["header_multipart_nb_of_part"] = int(result["header_ie_data"][2:4], 16)
1332 | result["header_multipart_current_part_nb"] = int(result["header_ie_data"][4:6], 16)
1333 |
1334 | # SMS Content
1335 | user_data = ""
1336 | logging.debug("Encoded "+str(charset)+" SMS content: "+str(msg))
1337 | if charset == '7bit': # Default Alphabet aka basic 7 bit coding - 03.38 S6.2.1
1338 | user_data = GSMTC35.__unpack7bit(msg, headerLength, messageLength)
1339 | # Remove header (+ header size byte) from the message
1340 | if contentContainsHeader:
1341 | user_data = user_data[headerLength+1:]
1342 | user_data_encoded = binascii.hexlify(user_data.encode()).decode()
1343 | elif charset == '8bit': # 8 bit coding is "user defined". S6.2.2
1344 | # TODO: Handle header message (please provide me an example full --debug log to help me)
1345 | user_data = GSMTC35.__unpack8bit(binascii.unhexlify(msg))
1346 | user_data_encoded = msg
1347 | elif charset == 'utf16-be': # UTF-16 aka UCS2, S6.2.3
1348 | user_data = GSMTC35.__unpackUCS2(binascii.unhexlify(msg))
1349 | if contentContainsHeader:
1350 | user_data = user_data[int(ceil((headerLength+1)/2)):]
1351 | user_data_encoded = msg[int((headerLength+1)*2):]
1352 |
1353 | else:
1354 | logging.error("Not possible to find correct encoding")
1355 | if decode_sms:
1356 | return result
1357 | else:
1358 | user_data_encoded = msg
1359 |
1360 | result["sms"] = user_data
1361 | logging.debug("Decoded SMS content: "+user_data)
1362 | user_data_encoded = user_data_encoded.upper()
1363 | result["sms_encoded"] = user_data_encoded
1364 | logging.debug("Re-encoded SMS content: "+user_data_encoded)
1365 |
1366 | return result
1367 |
1368 | ######################## INFO AND UTILITY FUNCTIONS ##########################
1369 | def isAlive(self):
1370 | """Check if the GSM module is alive (answers to AT commands)
1371 |
1372 | return: (bool) Is GSM module alive
1373 | """
1374 | return self.__sendCmdAndCheckResult(GSMTC35.__BASE_AT)
1375 |
1376 |
1377 | def getManufacturerId(self):
1378 | """Get the GSM module manufacturer identification
1379 |
1380 | return: (string) Manufacturer identification
1381 | """
1382 | # Send request and get data
1383 | result = self.__sendCmdAndGetNotEmptyLine(cmd=GSMTC35.__NORMAL_AT+"CGMI")
1384 | # Delete the "OK" of the request from the buffer
1385 | if result != "":
1386 | self.__waitDataContains(self.__RETURN_OK, self.__RETURN_ERROR)
1387 | return result
1388 |
1389 |
1390 | def getModelId(self):
1391 | """Get the GSM module model identification
1392 |
1393 | return: (string) Model identification
1394 | """
1395 | # Send request and get data
1396 | result = self.__sendCmdAndGetNotEmptyLine(cmd=GSMTC35.__NORMAL_AT+"CGMM")
1397 | # Delete the "OK" of the request from the buffer
1398 | if result != "":
1399 | self.__waitDataContains(self.__RETURN_OK, self.__RETURN_ERROR)
1400 | return result
1401 |
1402 |
1403 | def getRevisionId(self):
1404 | """Get the GSM module revision identification of software status
1405 |
1406 | return: (string) Revision identification
1407 | """
1408 | # Send request and get data
1409 | result = self.__sendCmdAndGetNotEmptyLine(cmd=GSMTC35.__NORMAL_AT+"CGMR")
1410 | # Delete the "OK" of the request from the buffer
1411 | if result != "":
1412 | self.__waitDataContains(self.__RETURN_OK, self.__RETURN_ERROR)
1413 | return result
1414 |
1415 |
1416 | def getIMEI(self):
1417 | """Get the product serial number ID (IMEI)
1418 |
1419 | return: (string) Product serial number ID (IMEI)
1420 | """
1421 | # Send request and get data
1422 | result = self.__sendCmdAndGetNotEmptyLine(cmd=GSMTC35.__NORMAL_AT+"CGSN")
1423 | # Delete the "OK" of the request from the buffer
1424 | if result != "":
1425 | self.__waitDataContains(self.__RETURN_OK, self.__RETURN_ERROR)
1426 | return result
1427 |
1428 |
1429 | def getIMSI(self):
1430 | """Get the International Mobile Subscriber Identity (IMSI)
1431 |
1432 | return: (string) International Mobile Subscriber Identity (IMSI)
1433 | """
1434 | # Send request and get data
1435 | result = self.__sendCmdAndGetNotEmptyLine(cmd=GSMTC35.__NORMAL_AT+"CIMI")
1436 | # Delete the "OK" of the request from the buffer
1437 | if result != "":
1438 | self.__waitDataContains(self.__RETURN_OK, self.__RETURN_ERROR)
1439 | return result
1440 |
1441 |
1442 | def setModuleToManufacturerState(self):
1443 | """Set the module parameters to manufacturer state
1444 |
1445 | return: (bool) Reset successful
1446 | """
1447 | return self.__sendCmdAndCheckResult(cmd=GSMTC35.__BASE_AT+"&F0")
1448 |
1449 |
1450 | def switchOff(self):
1451 | """Switch off the module (module will not respond after this request)
1452 |
1453 | Connection to serial port is also terminated, an init will be needed
1454 | to use this class again.
1455 |
1456 | return: (bool) Switch off successful
1457 | """
1458 | # Send request and get data
1459 | result = self.__sendCmdAndCheckResult(cmd=GSMTC35.__BASE_AT+"^SMSO",
1460 | result="MS OFF")
1461 |
1462 | if result:
1463 | # Delete the "OK" of the request from the buffer
1464 | self.__waitDataContains(self.__RETURN_OK, self.__RETURN_ERROR)
1465 |
1466 | # Close the connection
1467 | self.close()
1468 |
1469 | return result
1470 |
1471 |
1472 | def getOperatorName(self):
1473 | """Get current used operator name
1474 |
1475 | return: (string) Operator name
1476 | """
1477 | operator = ""
1478 |
1479 | # Set the COPS command correctly
1480 | if not self.__sendCmdAndCheckResult(cmd=GSMTC35.__NORMAL_AT+"COPS=3,0"):
1481 | logging.error("Impossible to set the COPS command")
1482 | return operator
1483 |
1484 | # Send the command to get the operator name
1485 | result = self.__sendCmdAndGetNotEmptyLine(cmd=GSMTC35.__NORMAL_AT+"COPS?",
1486 | content="+COPS: ")
1487 | if result == "" or len(result) <= 8 or result[0:7] != "+COPS: ":
1488 | logging.error("Command to get the operator name failed")
1489 | return operator
1490 |
1491 | # Get result without "+COPS: "
1492 | result = result[7:]
1493 |
1494 | # Split remaining data from the line
1495 | split_list = result.split(",")
1496 | if len(split_list) < 3:
1497 | logging.error("Impossible to split operator information")
1498 | return operator
1499 |
1500 | # Get the operator name without quote (3th element from the list)
1501 | operator = GSMTC35.__deleteQuote(split_list[2])
1502 |
1503 | # Delete last "OK" from buffer
1504 | self.__waitDataContains(self.__RETURN_OK, self.__RETURN_ERROR)
1505 |
1506 | return operator
1507 |
1508 |
1509 | def getSignalStrength(self):
1510 | """Get current signal strength in dBm
1511 | Range: -113 to -51 dBm (other values are incorrect)
1512 |
1513 | return: (int) -1 if not valid, else signal strength in dBm
1514 | """
1515 | sig_strength = -1
1516 |
1517 | # Send the command to get the signal power
1518 | result = self.__sendCmdAndGetNotEmptyLine(cmd=GSMTC35.__NORMAL_AT+"CSQ",
1519 | content="+CSQ: ")
1520 | #Check result:
1521 | if result == "" or len(result) <= 8 or result[:6] != "+CSQ: ":
1522 | logging.error("Command to get signal strength failed")
1523 | return sig_strength
1524 |
1525 | # Get result without "+CSQ: "
1526 | result = result[6:]
1527 |
1528 | # Split remaining data from the line
1529 | # (at least one element is in it due to previous check)
1530 | split_list = result.split(",")
1531 |
1532 | # Get the received signal strength (1st element)
1533 | try:
1534 | sig_strength = int(split_list[0])
1535 | except ValueError:
1536 | logging.error("Impossible to convert \""+str(split_list[0])+"\" into integer")
1537 | return sig_strength
1538 |
1539 | # Delete last "OK" from buffer
1540 | if sig_strength != -1:
1541 | self.__waitDataContains(self.__RETURN_OK, self.__RETURN_ERROR)
1542 |
1543 | # 99 means the GSM couldn't get the information, negative values are invalid
1544 | if sig_strength >= 99 or sig_strength < 0:
1545 | sig_strength = -1
1546 |
1547 | # Convert received data to dBm
1548 | if sig_strength != -1:
1549 | #(0, <=-113dBm), (1, -111dBm), (2, -109dBm), (30, -53dBm), (31, >=-51dBm)
1550 | # --> strength (dBm) = 2* received data from module - 113
1551 | sig_strength = 2*sig_strength - 113
1552 |
1553 | return sig_strength
1554 |
1555 |
1556 | def getOperatorNames(self):
1557 | """Get list of operator names stored in the GSM module
1558 |
1559 | return: ([string,]) List of operator names or empty list if an error occured
1560 | """
1561 | operators = self.__sendCmdAndGetFullResult(cmd=GSMTC35.__NORMAL_AT+"COPN")
1562 | result = []
1563 |
1564 | if len(operators) <= 0:
1565 | logging.error("Command to get operator names failed")
1566 | return result
1567 |
1568 | for operator in operators:
1569 | operator_name = ""
1570 | if len(operator) > 8 and operator[:7] == "+COPN: ":
1571 | operator = operator[7:]
1572 | # Split remaining data from the line
1573 | split_list = operator.split(",")
1574 | if len(split_list) >= 2:
1575 | # Get the operator name without quote (2nd element)
1576 | operator_name = GSMTC35.__deleteQuote(split_list[1])
1577 | else:
1578 | logging.warning("Impossible to parse operator information \""+operator+"\"")
1579 | else:
1580 | logging.warning("Impossible to get operator from \""+operator+"\" line")
1581 | if operator_name != "":
1582 | result.append(operator_name)
1583 |
1584 | return result
1585 |
1586 |
1587 | def getNeighbourCells(self, waiting_time_sec=5):
1588 | """Get neighbour cells
1589 |
1590 | Keyword arguments:
1591 | waiting_time_sec -- (int, optional) Time to wait query to execute
1592 |
1593 | return: ([{'chann':(int), 'rs':(int), 'dbm':(int), 'plmn':(int), 'bcc':(int), 'c1':(int), 'c2':(int)}, ...]) List of neighbour cells
1594 | chann (int): ARFCN (Absolute Frequency Channel Number) of the BCCH carrier
1595 | rs (int): RSSI (Received signal strength) of the BCCH carrier, decimal value from
1596 | 0 to 63. The indicated value is composed of the measured value in dBm
1597 | plus an offset. This is in accordance with a formula specified in 3GPP TS 05.08.
1598 | dbm (int): Receiving level in dBm
1599 | plmn (int): Public land mobile network (PLMN) ID code
1600 | bcc (int): Base station colour code
1601 | c1 (int): Coefficient for base station selection
1602 | c2 (int): Coefficient for base station reselection
1603 | """
1604 | neighbour_cells = []
1605 |
1606 | lines = self.__sendCmdAndGetFullResult(cmd=GSMTC35.__BASE_AT+"^MONP",
1607 | additional_timeout=waiting_time_sec)
1608 |
1609 | for line in lines:
1610 | split_line = line.split()
1611 | if len(split_line) >= 7:
1612 | try:
1613 | neighbour_cell = {}
1614 | neighbour_cell['chann'] = int(split_line[0])
1615 | neighbour_cell['rs'] = int(split_line[1])
1616 | neighbour_cell['dbm'] = int(split_line[2])
1617 | neighbour_cell['plmn'] = int(split_line[3])
1618 | neighbour_cell['bcc'] = int(split_line[4])
1619 | neighbour_cell['c1'] = int(split_line[5])
1620 | neighbour_cell['c2'] = int(split_line[6])
1621 | neighbour_cells.append(neighbour_cell)
1622 | except ValueError:
1623 | if split_line[0] == "chann":
1624 | # We don't need to get first line returned by the GSM module
1625 | pass
1626 | else:
1627 | logging.warning("Invalid parameters in neighbour cell line \""+str(line)+"\"")
1628 | else:
1629 | logging.warning("Invalid number of element to parse for neighbour cell \""+str(line)+"\"")
1630 |
1631 | return neighbour_cells
1632 |
1633 |
1634 | def getAccumulatedCallMeter(self):
1635 | """Get the accumulated call meter in home units
1636 |
1637 | return: (int or long) Accumulated call meter value in home units (-1 if an error occurred)
1638 | """
1639 | int_result = -1
1640 |
1641 | # Send request and get data
1642 | result = self.__sendCmdAndGetNotEmptyLine(cmd=GSMTC35.__NORMAL_AT+"CACM?")
1643 |
1644 | if result == "" or len(result) <= 8 or result[0:7] != "+CACM: ":
1645 | logging.error("Command to get the accumulated call meter failed")
1646 | return int_result
1647 |
1648 | # Get result without "+CACM: " and without '"'
1649 | result = GSMTC35.__deleteQuote(result[7:])
1650 |
1651 | try:
1652 | int_result = int(result, 16)
1653 | except ValueError:
1654 | logging.error("Impossible to convert hexadecimal value \""+str(result)+"\" into integer")
1655 | return int_result
1656 |
1657 | # Delete the "OK" of the request from the buffer
1658 | if result != -1:
1659 | self.__waitDataContains(self.__RETURN_OK, self.__RETURN_ERROR)
1660 |
1661 | return int_result
1662 |
1663 |
1664 | def getAccumulatedCallMeterMaximum(self):
1665 | """Get the accumulated call meter maximum in home units
1666 |
1667 | return: (int or long) Accumulated call meter maximum value in home units (-1 if an error occurred)
1668 | """
1669 | int_result = -1
1670 |
1671 | # Send request and get data
1672 | result = self.__sendCmdAndGetNotEmptyLine(cmd=GSMTC35.__NORMAL_AT+"CAMM?")
1673 |
1674 | if result == "" or len(result) <= 8 or result[0:7] != "+CAMM: ":
1675 | logging.error("Command to get the accumulated call meter failed")
1676 | return int_result
1677 |
1678 | # Get result without "+CAMM: " and without '"'
1679 | result = GSMTC35.__deleteQuote(result[7:])
1680 |
1681 | try:
1682 | int_result = int(result, 16)
1683 | except ValueError:
1684 | logging.error("Impossible to convert hexadecimal value \""+str(result)+"\" into integer")
1685 | return int_result
1686 |
1687 | # Delete the "OK" of the request from the buffer
1688 | if result != -1:
1689 | self.__waitDataContains(self.__RETURN_OK, self.__RETURN_ERROR)
1690 |
1691 | return int_result
1692 |
1693 |
1694 | def isTemperatureCritical(self):
1695 | """Check if the temperature of the module is inside or outside the
1696 | warning limits.
1697 |
1698 | return: (bool) Temperature is critical (warning sent by the module)
1699 | """
1700 | is_critical = False
1701 |
1702 | result = self.__sendCmdAndGetNotEmptyLine(cmd=GSMTC35.__BASE_AT+"^SCTM?")
1703 |
1704 | if result == "" or len(result) <= 8 or result[0:7] != "^SCTM: ":
1705 | logging.error("Command to get the temperature status failed")
1706 | return is_critical
1707 |
1708 | # Get result without "^SCTM:"
1709 | result = result[7:]
1710 |
1711 | # Split data
1712 | split_list = result.split(",")
1713 | if len(split_list) < 2:
1714 | logging.error("Impossible to split temperature status")
1715 | return is_critical
1716 |
1717 | # Get the received temperature status (2nd element)
1718 | try:
1719 | if int(split_list[1]) != 0:
1720 | is_critical = True
1721 | else:
1722 | is_critical = False
1723 | except ValueError:
1724 | logging.error("Impossible to convert \""+str(split_list[1])+"\" into integer")
1725 | return is_critical
1726 |
1727 | # Delete the "OK" of the request from the buffer
1728 | self.__waitDataContains(self.__RETURN_OK, self.__RETURN_ERROR)
1729 |
1730 | return is_critical
1731 |
1732 |
1733 | ############################### TIME FUNCTIONS ###############################
1734 | def setInternalClockToCurrentDate(self):
1735 | """Set the GSM module internal clock to current date
1736 |
1737 | return: (bool) Date successfully modified
1738 | """
1739 | return self.__setInternalClockToSpecificDate(datetime.datetime.now())
1740 |
1741 |
1742 | def getDateFromInternalClock(self):
1743 | """Get the date from the GSM module internal clock
1744 |
1745 | return: (datetime.datetime) Date stored in the GSM module or -1 if an error occured
1746 | """
1747 | # Send the command to get the date
1748 | result = self.__sendCmdAndGetNotEmptyLine(cmd=GSMTC35.__NORMAL_AT+"CCLK?",
1749 | content="+CCLK: ")
1750 | if result == "" or len(result) <= 8 or result[:7] != "+CCLK: ":
1751 | logging.error("Command to get internal clock failed")
1752 | return -1
1753 |
1754 | # Get date result without "+CCLK: " and delete quote
1755 | date = GSMTC35.__deleteQuote(result[7:])
1756 |
1757 | # Delete last "OK" from buffer
1758 | self.__waitDataContains(self.__RETURN_OK, self.__RETURN_ERROR)
1759 |
1760 | # Get the date from string format to date type
1761 | try:
1762 | return datetime.datetime.strptime(date, GSMTC35.__DATE_FORMAT)
1763 | except ValueError:
1764 | logging.error("Invalid date returned by GSM: "+str(date))
1765 |
1766 | return -1
1767 |
1768 |
1769 | ############################ PHONEBOOK FUNCTIONS #############################
1770 | def getPhonebookEntries(self, phonebook_type = ePhonebookType.CURRENT, waiting_time_sec=60):
1771 | """Get a list of phonebook entries (contact name, phone number and index)
1772 |
1773 | Keyword arguments:
1774 | phonebook_type -- (GSMTC35.ePhonebookType, optional) Phonebook type
1775 | waiting_time_sec -- (int, optional) Time to wait phonebook entries to be sent by GSM module
1776 |
1777 | return: ([{index:(int), phone_number:(string), contact_name:(string)}, ...])
1778 | List of dictionary (each dictionary is a phonebook entry containing the
1779 | entry index, the phone number and the contact name)
1780 | """
1781 | phonebook_entries = []
1782 |
1783 | # Select the correct phonebook
1784 | if not self.__selectPhonebook(phonebook_type):
1785 | logging.error("Impossible to select the phonebook")
1786 | return phonebook_entries
1787 |
1788 | # Get information about phonebook range
1789 | index_min, index_max, max_length_phone, max_length_name = self.__getCurrentPhonebookRange()
1790 | if index_min < 0 or index_max < 0 or index_min > index_max:
1791 | logging.error("Phonebook min or max indexes are not valid")
1792 | return phonebook_entries
1793 |
1794 | # Get the phonebook data
1795 | lines = self.__sendCmdAndGetFullResult(cmd=GSMTC35.__NORMAL_AT+"CPBR="+str(index_min)+","+str(index_max),
1796 | additional_timeout=waiting_time_sec)
1797 |
1798 | if len(lines) <= 0:
1799 | logging.warning("Impossible to get phonebook entries (error or no entries)")
1800 | return phonebook_entries
1801 |
1802 | for line in lines:
1803 | if line[:7] == "+CPBR: ":
1804 | # Get result without "+CMGL: "
1805 | line = line[7:]
1806 | # Split remaining data from the line
1807 | split_list = line.split(",")
1808 | if len(split_list) >= 4:
1809 | try:
1810 | entry = {}
1811 | entry["index"] = int(split_list[0])
1812 | entry["phone_number"] = str(split_list[1]).replace('"', '')
1813 | entry["contact_name"] = str(split_list[3]).replace('"', '')
1814 | phonebook_entries.append(entry)
1815 | except ValueError:
1816 | logging.warning("Impossible to add this phonebook entry \""+str(line)+"\"")
1817 | else:
1818 | logging.warning("Impossible to split phonebook entry options \""+str(line)+"\"")
1819 | else:
1820 | logging.warning("Invalid phonebook entry line \""+str(line)+"\"")
1821 |
1822 | return phonebook_entries
1823 |
1824 |
1825 | def addEntryToPhonebook(self, phone_number, contact_name, phonebook_type = ePhonebookType.CURRENT):
1826 | """Add an entry to the phonebook (contact name and phone number)
1827 |
1828 | Keyword arguments:
1829 | phone_number -- (string) Phone number to add in the entry
1830 | contact_name -- (string) Name of contact associated with {phone_number}
1831 | phonebook_type -- (GSMTC35.ePhonebookType, optional) Phonebook type
1832 |
1833 | return: (bool) Entry added
1834 | """
1835 | # Get phone number type (local, international, ...)
1836 | phone_number_type = GSMTC35.__guessPhoneNumberType(phone_number)
1837 | if phone_number_type == GSMTC35.__ePhoneNumberType.ERROR:
1838 | logging.error("Impossible to guess the phone number type from the phone number")
1839 | return False
1840 |
1841 | # Select the correct phonebook
1842 | if not self.__selectPhonebook(phonebook_type):
1843 | logging.error("Impossible to select the phonebook")
1844 | return False
1845 |
1846 | # Check size of contact name and phone number
1847 | index_min, index_max, max_length_phone, max_length_name = self.__getCurrentPhonebookRange()
1848 | if max_length_phone < 0 or max_length_name < 0 or len(phone_number) > max_length_phone or len(contact_name) > max_length_name:
1849 | logging.error("Phonebook max phone number and contact name length are not valid")
1850 | return False
1851 |
1852 | # Add the entry
1853 | return self.__sendCmdAndCheckResult(GSMTC35.__NORMAL_AT+"CPBW=,\""
1854 | +str(phone_number)+"\","+str(phone_number_type)
1855 | +",\""+str(contact_name)+"\"")
1856 |
1857 |
1858 | def deleteEntryFromPhonebook(self, index, phonebook_type = ePhonebookType.CURRENT):
1859 | """Delete a phonebook entry
1860 |
1861 | Keyword arguments:
1862 | index -- (int) Index of the entry to delete
1863 | phonebook_type -- (GSMTC35.ePhonebookType, optional) Phonebook type
1864 |
1865 | return: (bool) Entry deleted
1866 | """
1867 | # Select the correct phonebook
1868 | if not self.__selectPhonebook(phonebook_type):
1869 | logging.error("Impossible to select the phonebook")
1870 | return False
1871 |
1872 | return self.__sendCmdAndCheckResult(GSMTC35.__NORMAL_AT+"CPBW="+str(index))
1873 |
1874 |
1875 | def deleteAllEntriesFromPhonebook(self, phonebook_type = ePhonebookType.CURRENT):
1876 | """Delete all phonebook entries
1877 |
1878 | Keyword arguments:
1879 | phonebook_type -- (GSMTC35.ePhonebookType, optional) Phonebook type
1880 |
1881 | return: (bool) All entries deleted
1882 | """
1883 | # Select the correct phonebook
1884 | if not self.__selectPhonebook(phonebook_type):
1885 | logging.error("Impossible to select the phonebook")
1886 | return False
1887 |
1888 | # Get entries to delete
1889 | entries = self.getPhonebookEntries(GSMTC35.ePhonebookType.CURRENT)
1890 |
1891 | # Delete all phonebook entries
1892 | all_deleted = True
1893 | for entry in entries:
1894 | if not self.deleteEntryFromPhonebook(entry['index'], GSMTC35.ePhonebookType.CURRENT):
1895 | logging.warning("Impossible to delete entry "+str(entry['index'])+" ("+str(entry['contact_name'])+")")
1896 | all_deleted = False
1897 |
1898 | return all_deleted
1899 |
1900 |
1901 | ############################### SMS FUNCTIONS ################################
1902 | def sendSMS(self, phone_number, msg, force_text_mode=False, network_delay_sec=5):
1903 | """Send SMS/MMS to specific phone number
1904 |
1905 | Must be max 140 normal char or max 70 special char if you want to send it as a SMS
1906 | Else it will be sent as MMS
1907 |
1908 | Keyword arguments:
1909 | phone_number -- (string) Phone number (can be local or international)
1910 | msg -- (unicode) Message to send (max 140 normal char or max 70 special char)
1911 | force_text_mode -- (bool, default: PDU mode used) Force to use Text Mode instead of PDU mode (NOT RECOMMENDED)
1912 | network_delay_sec -- (int, default: 5sec) Network delay to add when waiting SMS to be sent
1913 |
1914 | return: (bool) SMS sent
1915 | """
1916 | result = False
1917 | if len(msg) <= 0 or len(phone_number) <= 0:
1918 | logging.error("Message to send or phone number can't be empty")
1919 | return False
1920 |
1921 | using_text_mode = force_text_mode
1922 | if not using_text_mode:
1923 | using_text_mode = not self.__sendCmdAndCheckResult(cmd=GSMTC35.__NORMAL_AT+"CMGF=0")
1924 |
1925 | if not using_text_mode:
1926 | use_7bit, all_encoded_user_data_and_length = GSMTC35.__pack7Bit(msg)
1927 | if use_7bit:
1928 | logging.debug("Message will be sent in 7bit mode (default GSM alphabet)")
1929 | else:
1930 | # Encode message into UCS-2 (UTF16)
1931 | logging.debug("Message will be sent in UCS-2 mode (Utf16)")
1932 | all_encoded_user_data_and_length = GSMTC35.__packUCS2(msg)
1933 |
1934 | if len(all_encoded_user_data_and_length) <= 0:
1935 | logging.error("Failed to encode SMS content")
1936 | return False
1937 |
1938 | logging.debug("all_encoded_user_data_and_length:\n - "+str('\n - '.join(all_encoded_user_data_and_length)))
1939 |
1940 | # Encode phone number
1941 | encoded_phone_number = ""
1942 | phone_number = phone_number.replace("+","")
1943 | previous_char_phone = ""
1944 | current_pos = 0
1945 | for c in phone_number:
1946 | current_pos = current_pos + 1
1947 | if current_pos % 2 == 0:
1948 | encoded_phone_number = str(encoded_phone_number) + str(c) + str(previous_char_phone)
1949 | previous_char_phone = c
1950 | encoded_phone_number = str(encoded_phone_number) + str("F") + str(previous_char_phone)
1951 | logging.debug("encoded_phone_number="+encoded_phone_number)
1952 |
1953 | # Get phone number length
1954 | encoded_phone_number_length = format((len(encoded_phone_number) - 1), 'x')
1955 | if len(encoded_phone_number_length) != 2:
1956 | encoded_phone_number_length = "0" + encoded_phone_number_length
1957 | logging.debug("encoded_phone_number_length="+str(encoded_phone_number_length))
1958 |
1959 | # Create fully encoded message
1960 | # SCA (service center length (1 byte) + service center address information)
1961 | base_encoded_message = "00"
1962 | # PDU Type
1963 | # - Bit 7: Reply Path (not used, should be 0)
1964 | # - Bit 6: UDHI (1 <=> UD contains header in addition to message)
1965 | # - Bit 5: SRR (Status report requested ?, should be 0)
1966 | # - Bit 4: VP field present? (should be 0)
1967 | # - Bit 3: VP field (0 <=> relative, 1 <=> absolute, should be 0)
1968 | # - Bit 2: RD (0 <=> Accept SMS-Submit with same SMSC, 1 <=> Reject, should be 0)
1969 | # - Bit 1&0: Message Type (should be "SMS-Submit" <=> "01")
1970 | if len(all_encoded_user_data_and_length) > 1:
1971 | base_encoded_message += "41"
1972 | else:
1973 | base_encoded_message += "01"
1974 | # MR (Message reference, must be random between 0 and 255)
1975 | base_encoded_message += '{:02X}'.format(randint(0, 255))
1976 | # Destination Address
1977 | base_encoded_message += encoded_phone_number_length
1978 | base_encoded_message += "91" # Type of number (International)
1979 | base_encoded_message += encoded_phone_number
1980 | # Protocol identifier
1981 | base_encoded_message += "00" # Protocol Identifier (PID, Short Message <=> "00")
1982 | # Data Coding Scheme (DCS, "08" <=> UCS2, "00" <=> GSM 7 bit)
1983 | if use_7bit:
1984 | base_encoded_message += "00"
1985 | else:
1986 | base_encoded_message += "08"
1987 |
1988 | # User data length (UDL, 1 byte) + User data (UD)
1989 | result = True
1990 | count = 0
1991 | for encoded_user_data_and_length in all_encoded_user_data_and_length:
1992 | fully_encoded_message = base_encoded_message + encoded_user_data_and_length
1993 | fully_encoded_message = fully_encoded_message.upper()
1994 | logging.debug("fully encoded message="+str(fully_encoded_message))
1995 |
1996 | # Wait a bit every time a part of the message is sent
1997 | if (len(all_encoded_user_data_and_length) > 1) and (count > 0):
1998 | logging.debug("Wait a bit before send next message part")
1999 | time.sleep(network_delay_sec)
2000 | count += 1
2001 |
2002 | # Send the SMS or all multipart messages (MMS)
2003 | # AT+CMGS=SIZE with SIZE = message size - service center length and content (1 byte)
2004 | if not self.__sendCmdAndCheckResult(cmd=GSMTC35.__NORMAL_AT+"CMGS=" \
2005 | +str(int((len(fully_encoded_message)-2)/2)), \
2006 | after=fully_encoded_message+GSMTC35.__CTRL_Z, \
2007 | additional_timeout=network_delay_sec):
2008 | result = False
2009 |
2010 | if not self.__sendCmdAndCheckResult(cmd=GSMTC35.__NORMAL_AT+"CMGF=1"):
2011 | logging.warning("Could not go back to text mode")
2012 | else:
2013 | if using_text_mode and (not force_text_mode):
2014 | logging.warning("Could not go to PDU mode, trying to send message in normal mode, some character may be missing")
2015 |
2016 | msg_length = len(msg)
2017 | # Check if must be sent in multiple SMS or not (separate SMS since Text mode can't handle multipart SMS)
2018 | n = 140
2019 | if msg_length > 70:
2020 | if GSMTC35.__is7BitCompatible(msg):
2021 | if msg_length > 140:
2022 | logging.warning("Message will be sent in multiple <=140 char SMS (not multipart SMS because Text Mode is used)")
2023 | else:
2024 | logging.debug("SMS can be sent in one basic part")
2025 | else:
2026 | logging.warning("Message will be sent in multiple <=70 char SMS (not multipart SMS because Text Mode is used)")
2027 | n = 70
2028 | else:
2029 | logging.debug("SMS can be sent in one unicode or basic part")
2030 |
2031 | # Sending all SMS
2032 | all_sms_to_send = [msg[i:i+n] for i in range(0, len(msg), n)]
2033 | result = True
2034 | for sms_to_send in all_sms_to_send:
2035 | if not self.__sendCmdAndCheckResult(cmd=GSMTC35.__NORMAL_AT+"CMGS=\"" \
2036 | +phone_number+"\"", \
2037 | after=sms_to_send+GSMTC35.__CTRL_Z, \
2038 | additional_timeout=network_delay_sec):
2039 | result = False
2040 |
2041 | return result
2042 |
2043 |
2044 | def getSMS(self, sms_type=eSMS.ALL_SMS, decode_sms=True, force_text_mode=False, waiting_time_sec=10):
2045 | """Get SMS (using PDU mode, fallback with Text mode if failed)
2046 |
2047 | Keyword arguments:
2048 | sms_type -- (string) Type of SMS to get (possible values: GSMTC35.eSMS.ALL_SMS,
2049 | GSMTC35.eSMS.UNREAD_SMS or GSMTC35.eSMS.READ_SMS)
2050 | decode_sms -- (bool, optional, default: True) Decode SMS content or keep it in encoded format (+ charset)
2051 | force_text_mode -- (bool, optional, default: False) Force to use 'text mode' instead of 'pdu mode' to get sms (may lead to inconsistent sms content)
2052 | waiting_time_sec -- (int, optional) Time to wait SMS to be displayed by GSM module
2053 |
2054 | return: ([{"index":, "status":, "phone_number":, "date":, "time":, "sms", "sms_encoded":},]) List of requested SMS (list of dictionaries)
2055 | Explanation of dictionaries content:
2056 | - index (int) Index of the SMS from the GSM module point of view
2057 | - status (GSMTC35.eSMS) SMS type
2058 | - phone_number (string) Phone number which send the SMS
2059 | - date (string) Date SMS was received
2060 | - time (string) Time SMS was received
2061 | - sms (string) Content of the SMS (decoded, if PDU mode is not used or did not work then content may vary depending on device)
2062 | - sms_encoded (string) Content of the SMS (encoded in hexadecimal readable format. Data not given if PDU mode did not worked or is not used)
2063 | If PDU mode worked, additional 'bonus' fields will be available:
2064 | - phone_number_type (int) Phone number type using GSM 04.08 specification (145 <=> international, 129 <=> national)
2065 | - service_center_type (int) Service center phone number type using GSM 04.08 specification (145 <=> international, 129 <=> national)
2066 | - service_center_phone_number (string) Service center phone number
2067 | - charset (string) Charset used by the sender to encode the SMS
2068 | If PDU mode worked and that the SMS has an header:
2069 | - header_iei (int) Header IEI of the SMS
2070 | - header_ie_data (string) Header IE data of the SMS (encoded in hexadecimal readable format)
2071 | If PDU mode worked and that the SMS has an header and is multipart (MMS):
2072 | - header_multipart_ref_id (int) ID of the MMS
2073 | - header_multipart_current_part_nb (int) Current part of the MMS
2074 | - header_multipart_nb_of_part (int) Total number of part of the MMS
2075 | """
2076 | all_sms = []
2077 |
2078 | using_text_mode = force_text_mode
2079 | if not force_text_mode:
2080 | # Trying to go in PDU mode (if fails, use text mode)
2081 | using_text_mode = not self.__sendCmdAndCheckResult(cmd=GSMTC35.__NORMAL_AT+"CMGF=0")
2082 |
2083 | if not using_text_mode:
2084 | # Getting SMS using PDU mode
2085 | all_lines_retrieved = False
2086 | lines = self.__sendCmdAndGetFullResult(cmd=GSMTC35.__NORMAL_AT+"CMGL="+GSMTC35.__smsTypeTextToPdu(str(sms_type)), error_result="",
2087 | additional_timeout=waiting_time_sec)
2088 | sms = {}
2089 | for line in lines:
2090 | if line[:7] == "+CMGL: ":
2091 | # A new SMS is found
2092 | line = line[7:]
2093 | split_list = line.split(",")
2094 | if len(split_list) >= 4:
2095 | try:
2096 | sms["index"] = int(split_list[0])
2097 | sms["status"] = GSMTC35.__smsTypePduToText(split_list[1])
2098 | except ValueError:
2099 | logging.error("One of the SMS is not valid, command options: \""+str(line)+"\"")
2100 | sms = {}
2101 | elif "index" in sms:
2102 | # Content of the previously detected SMS should be there
2103 | # Do not throw if SMS is not decoded successfully (for reliability)
2104 | is_decoded = False
2105 | try:
2106 | decoded_data = GSMTC35.__decodePduSms(line, decode_sms)
2107 | is_decoded = True
2108 | except ValueError as e:
2109 | logging.error("One of the SMS is not valid, sms hexa content: \""+str(line)+"\": "+str(e))
2110 | decoded_data = {}
2111 | except IndexError as e:
2112 | logging.error("One of the SMS is not valid, sms hexa content: \""+str(line)+"\": "+str(e))
2113 | decoded_data = {}
2114 |
2115 | if is_decoded and ("sms" in decoded_data) and ("phone_number" in decoded_data) \
2116 | and ("date" in decoded_data) and ("time" in decoded_data) and ("charset" in decoded_data):
2117 | # SMS is valid (merge sms data and add sms to all sms)
2118 | sms.update(decoded_data)
2119 | all_sms.append(sms)
2120 |
2121 | # Let's check if there is other sms !
2122 | sms = {}
2123 | else:
2124 | # Inconsistent data, continue
2125 | logging.warning("One of the SMS is not valid, command options (2): \""+str(line)+"\"")
2126 | sms = {}
2127 | # Go back to text mode
2128 | if not self.__sendCmdAndCheckResult(cmd=GSMTC35.__NORMAL_AT+"CMGF=1"):
2129 | logging.warning("Could not go back to text mode")
2130 | else:
2131 | # Getting SMS using text mode
2132 | if using_text_mode and (not force_text_mode):
2133 | logging.warning("Could not go to PDU mode, trying to get sms with normal mode, some character may not be displayed")
2134 | all_lines_retrieved = False
2135 | lines = self.__sendCmdAndGetFullResult(cmd=GSMTC35.__NORMAL_AT+"CMGL=\""+str(sms_type)+"\"", error_result="",
2136 | additional_timeout=waiting_time_sec)
2137 | while not all_lines_retrieved:
2138 | # Make sure the "OK" sent by the module is not part of an SMS
2139 | if len(lines) > 0:
2140 | additional_line = self.__getNotEmptyLine("", "", 0)
2141 | if len(additional_line) > 0:
2142 | lines.append(self.__RETURN_OK) # Lost SMS part
2143 | lines.append(additional_line)
2144 | else:
2145 | all_lines_retrieved = True
2146 | else:
2147 | all_lines_retrieved = True
2148 | # Parse SMS from lines
2149 | sms = {}
2150 | for line in lines:
2151 | if line[:7] == "+CMGL: ":
2152 | if bool(sms):
2153 | all_sms.append(sms)
2154 | sms = {}
2155 | # Get result without "+CMGL: "
2156 | line = line[7:]
2157 | # Split remaining data from the line
2158 | split_list = line.split(",")
2159 | if len(split_list) >= 6:
2160 | try:
2161 | sms["index"] = int(split_list[0])
2162 | sms["status"] = GSMTC35.__deleteQuote(split_list[1])
2163 | sms["phone_number"] = GSMTC35.__deleteQuote(split_list[2])
2164 | sms["date"] = GSMTC35.__deleteQuote(split_list[4])
2165 | sms["time"] = GSMTC35.__deleteQuote(split_list[5])
2166 | sms["sms"] = ""
2167 | sms["charset"] = "TC35TextModeInconsistentCharset"
2168 | except ValueError:
2169 | logging.error("One of the SMS is not valid, command options: \""+str(line)+"\"")
2170 | sms = {}
2171 | elif bool(sms):
2172 | if ("sms" in sms) and (sms["sms"] != ""):
2173 | sms["sms"] = sms["sms"] + "\n" + line
2174 | else:
2175 | sms["sms"] = line
2176 | else:
2177 | logging.error("\""+line+"\" not usable")
2178 |
2179 | # Last SMS must also be stored
2180 | if ("index" in sms) and ("sms" in sms) and not (sms in all_sms):
2181 | # An empty line may appear in last SMS due to GSM module communication
2182 | if (len(sms["sms"]) >= 1) and (sms["sms"][len(sms["sms"])-1:len(sms["sms"])] == "\n"):
2183 | sms["sms"] = sms["sms"][:len(sms["sms"])-1]
2184 | all_sms.append(sms)
2185 |
2186 | return all_sms
2187 |
2188 |
2189 | def deleteSMS(self, sms_type = eSMS.ALL_SMS):
2190 | """Delete multiple or one SMS
2191 |
2192 | Keyword arguments:
2193 | sms_type -- (string or int, optional) Type or index of SMS to delete (possible values:
2194 | index of the SMS to delete (integer), GSMTC35.eSMS.ALL_SMS,
2195 | GSMTC35.eSMS.UNREAD_SMS or GSMTC35.eSMS.READ_SMS)
2196 |
2197 | return: (bool) All SMS of {sms_type} type are deleted
2198 | """
2199 | # Case sms_type is an index:
2200 | try:
2201 | return self.__deleteSpecificSMS(int(sms_type))
2202 | except ValueError:
2203 | pass
2204 |
2205 | # Case SMS index must be found to delete them
2206 | all_delete_ok = True
2207 |
2208 | all_sms_to_delete = self.getSMS(sms_type)
2209 | for sms in all_sms_to_delete:
2210 | sms_delete_ok = self.__deleteSpecificSMS(sms["index"])
2211 | all_delete_ok = all_delete_ok and sms_delete_ok
2212 |
2213 | return all_delete_ok
2214 |
2215 |
2216 | ############################### CALL FUNCTIONS ###############################
2217 | def hangUpCall(self):
2218 | """Stop current call (hang up)
2219 |
2220 | return: (bool) Hang up is a success
2221 | """
2222 | result = self.__sendCmdAndCheckResult(cmd=GSMTC35.__NORMAL_AT+"CHUP")
2223 | if not result:
2224 | # Try to hang up with an other method if the previous one didn't work
2225 | logging.warning("First method to hang up call failed...\r\nTrying an other...")
2226 | result = self.__sendCmdAndCheckResult(cmd=GSMTC35.__BASE_AT+"H")
2227 |
2228 | return result
2229 |
2230 |
2231 | def isSomeoneCalling(self, wait_time_sec=0):
2232 | """Check if there is an incoming call (blocking for {wait_time_sec} seconds)
2233 |
2234 | Keyword arguments:
2235 | wait_time_sec -- (int, optional) Additional time to wait someone to call
2236 |
2237 | return: (bool) There is an incoming call waiting to be picked up
2238 | """
2239 | result = self.__sendCmdAndGetNotEmptyLine(cmd=GSMTC35.__NORMAL_AT+"CPAS",
2240 | additional_timeout=wait_time_sec,
2241 | content="+CPAS:")
2242 | return ("3" in result)
2243 |
2244 |
2245 | def isCallInProgress(self):
2246 | """Check if there is a call in progress
2247 |
2248 | return: (bool) There is a call in progress
2249 | """
2250 | result = self.__sendCmdAndGetNotEmptyLine(cmd=GSMTC35.__NORMAL_AT+"CPAS",
2251 | content="+CPAS:")
2252 | return ("4" in result)
2253 |
2254 |
2255 | def pickUpCall(self):
2256 | """Answer incoming call (pick up)
2257 |
2258 | return: (bool) An incoming call has been picked up
2259 | """
2260 | return self.__sendCmdAndCheckResult(cmd=GSMTC35.__BASE_AT+"A;")
2261 |
2262 |
2263 | def call(self, phone_number, hide_phone_number=False, waiting_time_sec=20):
2264 | """Call {phone_number} and wait {waiting_time_sec} it's picked up (previous call will be terminated)
2265 |
2266 | WARNING: This function does not end the call:
2267 | - If picked up: Call will finished once the other phone stops the call
2268 | - If not picked up: Will leave a voice messaging of undefined time (depending on the other phone)
2269 |
2270 | Keyword arguments:
2271 | phone_number -- (string) Phone number to call
2272 | hide_phone_number -- (bool, optional) Enable/Disable phone number presentation to called phone
2273 | waiting_time_sec -- (int, optional) Blocking time in sec to wait a call to be picked up
2274 |
2275 | return: (bool) Call in progress
2276 | """
2277 | # Hang up is necessary in order to not have false positive
2278 | #(sending an ATD command while a call is already in progress would return an "OK" from the GSM module)
2279 | self.hangUpCall()
2280 |
2281 | if not hide_phone_number:
2282 | return self.__sendCmdAndCheckResult(cmd=GSMTC35.__BASE_AT+"D"
2283 | +phone_number+";",
2284 | additional_timeout=waiting_time_sec)
2285 | else:
2286 | return self.__sendCmdAndCheckResult(cmd=GSMTC35.__BASE_AT+"D#31#"
2287 | +phone_number+";",
2288 | additional_timeout=waiting_time_sec)
2289 |
2290 |
2291 | def reCall(self, waiting_time_sec=20):
2292 | """Call last called {phone_number} and wait {waiting_time_sec} it's picked up (previous call will be terminated)
2293 |
2294 | WARNING: This function does not end the call:
2295 | - If picked up: Call will finished once the other phone stops the call
2296 | - If not picked up: Will leave a voice messaging of undefined time (depending on the other phone)
2297 |
2298 | Keyword arguments:
2299 | waiting_time_sec -- (int, optional) Blocking time in sec to wait a call to be picked up
2300 |
2301 | return: (bool) Call in progress or in voice messaging
2302 | """
2303 | # Hang up is necessary in order to not have false positive
2304 | self.hangUpCall()
2305 | return self.__sendCmdAndCheckResult(cmd=GSMTC35.__BASE_AT+"DL;",
2306 | additional_timeout=waiting_time_sec)
2307 |
2308 |
2309 | def getLastCallDuration(self):
2310 | """Get duration of last call
2311 |
2312 | return: (int or long) Last call duration in seconds (-1 if error)
2313 | """
2314 | call_duration = -1
2315 |
2316 | # Send the command to get the last call duration
2317 | result = self.__sendCmdAndGetNotEmptyLine(cmd=GSMTC35.__BASE_AT+"^SLCD",
2318 | content="^SLCD: ")
2319 |
2320 | # Get the call duration from the received line
2321 | if result == "" or len(result) <= 7 or result[:7] != "^SLCD: ":
2322 | logging.error("Command to get last call duration failed")
2323 | return call_duration
2324 |
2325 | # Get the call duration
2326 | call_duration = result[7:]
2327 |
2328 | # Convert to seconds
2329 | try:
2330 | h, m, s = call_duration.split(':')
2331 | call_duration = int(h) * 3600 + int(m) * 60 + int(s)
2332 | except ValueError:
2333 | call_duration = -1
2334 |
2335 | # Delete last "OK" from buffer
2336 | self.__waitDataContains(self.__RETURN_OK, self.__RETURN_ERROR)
2337 |
2338 | return call_duration
2339 |
2340 |
2341 | def getCurrentCallState(self):
2342 | """Check the current call state and get potential phone number
2343 |
2344 | return: (GSMTC35.eCall, string) Return the call state (NOCALL = -1, ACTIVE = 0,
2345 | HELD = 1, DIALING = 2, ALERTING = 3, INCOMING = 4,
2346 | WAITING = 5) followed by the potential phone
2347 | number (empty if not found)
2348 | """
2349 | data = self.__sendCmdAndGetNotEmptyLine(cmd=GSMTC35.__NORMAL_AT+"CLCC",
2350 | content="+CLCC:", error_result=self.__RETURN_OK)
2351 | call_state = GSMTC35.eCall.NOCALL
2352 | phone = ""
2353 |
2354 | if len(data) <= 8 or data[:7] != "+CLCC: ":
2355 | # No call
2356 | return call_state, phone
2357 |
2358 | data = data[7:]
2359 | split_list = data.split(",")
2360 | if len(split_list) < 3:
2361 | logging.error("Impossible to split current call data")
2362 | return call_state, phone
2363 |
2364 | # Get call state (3th element from the list)
2365 | try:
2366 | call_state = int(split_list[2])
2367 | except ValueError:
2368 | logging.warning("Impossible to get call state")
2369 |
2370 | # Get the phone number if it exists
2371 | if len(split_list) >= 6:
2372 | phone = GSMTC35.__deleteQuote(split_list[5])
2373 | else:
2374 | logging.warning("Impossible to get phone number")
2375 |
2376 | # Delete last "OK" from buffer
2377 | self.__waitDataContains(self.__RETURN_OK, self.__RETURN_ERROR)
2378 |
2379 | return call_state, phone
2380 |
2381 | ############################# FORWARD FUNCTIONS ##############################
2382 | def setForwardStatus(self, forward_reason, forward_class, enable, phone_number=None):
2383 | """Enable/disable call/sms/data/fax forwarding
2384 |
2385 | Keyword arguments:
2386 | forward_reason -- (eForwardReason) Reason to forward (unconditional, phone busy, ...)
2387 | forward_class -- (eForwardClass) Type of information to forward (SMS, call, fax, data, ...)
2388 | enable -- (bool) Enable (True) or disable (False) forwarding ?
2389 | phone_number -- (str, optional) Phone number to use if enabling forwarding (mandatory) or phone number to disable (optional)
2390 |
2391 | return: (bool) Request success?
2392 | """
2393 | # Guess phone number type which is needed for enabling forwarding
2394 | phone_number_type = ""
2395 | if phone_number:
2396 | phone_number_type = str(GSMTC35.__guessPhoneNumberType(phone_number))
2397 | else:
2398 | phone_number = ""
2399 |
2400 | # Forwarding mode
2401 | if enable:
2402 | # Register and activate call forwarding
2403 | mode = "3"
2404 | else:
2405 | # Erase and deactivate call forwarding
2406 | mode = "4"
2407 |
2408 | return self.__sendCmdAndCheckResult(cmd=GSMTC35.__NORMAL_AT+"CCFC="\
2409 | +str(forward_reason)+","+str(mode)+","+str(phone_number)+","\
2410 | +str(phone_number_type)+","+str(forward_class),\
2411 | additional_timeout=15)
2412 |
2413 | def getForwardStatus(self):
2414 | """Get forward status (is call/data/fax/sms forwarded to an other phone number?)
2415 |
2416 | return: ([{'enabled':bool, 'class':str, 'phone_number':str, 'is_international':bool}]) List of forwarded status
2417 | """
2418 | forwards = self.__sendCmdAndGetFullResult(cmd=GSMTC35.__NORMAL_AT+"CCFC=0,2", additional_timeout=15)
2419 | result = []
2420 |
2421 | if len(forwards) <= 0:
2422 | logging.error("Command to get forward status failed")
2423 | return result
2424 |
2425 | for forward in forwards:
2426 | enabled_status = ""
2427 | _class = ""
2428 | if len(forward) > 8 and forward[:7] == "+CCFC: ":
2429 | forward = forward[7:]
2430 | # Split remaining data from the line
2431 | split_list = forward.split(",")
2432 | if len(split_list) >= 2:
2433 | # Get all data
2434 | enabled_status = bool(split_list[0] == "1")
2435 | _class = GSMTC35.eForwardClassToString(int(split_list[1]))
2436 | forward_res = {"enabled": enabled_status, "class": _class}
2437 | if len(split_list) >= 3:
2438 | forward_res["phone_number"] = str(split_list[2])
2439 | if len(split_list) >= 4:
2440 | forward_res["is_international"] = bool(int(split_list[3]) == GSMTC35.__ePhoneNumberType.INTERNATIONAL)
2441 | result.append(forward_res)
2442 | else:
2443 | logging.warning("Impossible to parse forward information \""+forward+"\"")
2444 | else:
2445 | logging.warning("Impossible to get forward from \""+forward+"\" line")
2446 |
2447 | return result
2448 |
2449 |
2450 | ################################ PIN FUNCTIONS ###############################
2451 | def getPinStatus(self):
2452 | """Check if the SIM card PIN is ready (PUK may also be needed)
2453 |
2454 | return: (bool, GSMTC35.eRequiredPin) (Did request worked?, Required PIN ("READY" if none needed)
2455 | """
2456 | res = self.__sendCmdAndGetFullResult(cmd=GSMTC35.__NORMAL_AT+"CPIN?")
2457 |
2458 | if len(res) <= 0:
2459 | return False, ""
2460 |
2461 | base_cpin="+CPIN: "
2462 | res = ','.join(res)
2463 | if str(base_cpin+GSMTC35.eRequiredPin.READY) in res:
2464 | required_pin = GSMTC35.eRequiredPin.READY
2465 | elif str(base_cpin+GSMTC35.eRequiredPin.PIN2) in res:
2466 | required_pin = GSMTC35.eRequiredPin.PIN2
2467 | elif str(base_cpin+GSMTC35.eRequiredPin.PUK2) in res:
2468 | required_pin = GSMTC35.eRequiredPin.PUK2
2469 | elif str(base_cpin+GSMTC35.eRequiredPin.PIN) in res:
2470 | required_pin = GSMTC35.eRequiredPin.PIN
2471 | elif str(base_cpin+GSMTC35.eRequiredPin.PUK) in res:
2472 | required_pin = GSMTC35.eRequiredPin.PUK
2473 | else:
2474 | logging.warning("Failed to understand if PIN(2)/PUK(2) are needed.")
2475 | return False, ""
2476 |
2477 | logging.debug("Found PIN status: "+str(required_pin))
2478 | return True, required_pin
2479 |
2480 |
2481 | def enterPin(self, pin):
2482 | """Enter the SIM card PIN to be able to call/receive call and send/receive SMS
2483 |
2484 | WARNING: This function may lock your SIM card if you try to enter more than
2485 | 3 wrong PIN numbers.
2486 |
2487 | Keyword arguments:
2488 | pin -- (string) SIM card PIN
2489 |
2490 | return: (bool) PIN is correct
2491 | """
2492 | return self.__sendCmdAndCheckResult(cmd=GSMTC35.__NORMAL_AT+"CPIN="+str(pin),
2493 | additional_timeout=10)
2494 |
2495 |
2496 | def lockSimPin(self, current_pin):
2497 | """Lock the use of the SIM card with PIN (the PIN will be asked after a reboot)
2498 |
2499 | Keyword arguments:
2500 | current_pin -- (int or string) Current PIN number linked to the SIM card
2501 |
2502 | return: (bool) SIM PIN lock enabled
2503 | """
2504 | return self.__sendCmdAndCheckResult(cmd=GSMTC35.__NORMAL_AT+"CLCK=\"SC\",1,"
2505 | +str(current_pin))
2506 |
2507 |
2508 | def unlockSimPin(self, current_pin):
2509 | """Unlock the use of the SIM card with PIN (the PIN will be asked after a reboot)
2510 |
2511 | Keyword arguments:
2512 | current_pin -- (int or string) Current PIN number linked to the SIM card
2513 |
2514 | return: (bool) SIM PIN unlock enabled
2515 | """
2516 | return self.__sendCmdAndCheckResult(cmd=GSMTC35.__NORMAL_AT+"CLCK=\"SC\",0,"
2517 | +str(current_pin))
2518 |
2519 |
2520 | def changePin(self, old_pin, new_pin):
2521 | """Edit PIN number stored in the SIM card
2522 |
2523 | Note: A call to this function will lock SIM Pin
2524 | You need to call {unlockSimPin()} to unlock it.
2525 |
2526 | Keyword arguments:
2527 | old_pin -- (int or string) Current PIN
2528 | new_pin -- (int or string) PIN to use for future PIN login
2529 |
2530 | return: (bool) SIM PIN edited
2531 | """
2532 | if not self.lockSimPin(old_pin):
2533 | logging.error("Impossible to lock SIM card with PIN before changing PIN")
2534 | return False
2535 |
2536 | return self.__sendCmdAndCheckResult(cmd=GSMTC35.__NORMAL_AT+"CPWD=\"SC\",\""
2537 | +str(old_pin)+"\",\""+str(new_pin)+"\"")
2538 |
2539 |
2540 | ################################# SLEEP MODE #################################
2541 | def isInSleepMode(self):
2542 | """Check if the GSM module is in sleep mode (if yes, nothing can be done
2543 | until it wakes up).
2544 |
2545 | return: (bool) GSM module is in sleep mode
2546 | """
2547 | # Send the command to get sleep mode state
2548 | result = self.__sendCmdAndGetNotEmptyLine(cmd=GSMTC35.__NORMAL_AT+"CFUN?",
2549 | content="+CFUN: ")
2550 | if result == "":
2551 | # Module is in sleep mode
2552 | return True
2553 |
2554 | # At this point, we are sure the module is not sleeping since at least
2555 | # one char was received from the GSM module.
2556 | # (Checking the returned value is here only to send warning
2557 | # if something is not logical in the result or to handle impossible case)
2558 |
2559 | if len(result) < 8 or result[:7] != "+CFUN: ":
2560 | logging.warning("Impossible to get valid result from sleep query")
2561 | # Since some char were in the buffer, there is no sleep mode
2562 | return False
2563 |
2564 | # Get result without "+CFUN: "
2565 | result = result[7:]
2566 |
2567 | try:
2568 | if int(result) == 0:
2569 | # The module answers that it is in sleep mode
2570 | # (this case is impossible... but who knows)
2571 | return True
2572 | except ValueError:
2573 | logging.warning("Impossible to convert \""+str(result)+"\" into integer")
2574 |
2575 | # Delete last "OK" from buffer
2576 | self.__waitDataContains(self.__RETURN_OK, self.__RETURN_ERROR)
2577 |
2578 | return False
2579 |
2580 | def waitEndOfSleepMode(self, max_additional_waiting_time_in_sec=-1):
2581 | """Blocking until module wakes-up or timeout
2582 |
2583 | Keyword arguments:
2584 | max_additional_waiting_time_in_sec -- (int, default: -1) Max time to wait module to wake up (-1 for indefinitly)
2585 |
2586 | return: (bool, bool, bool, bool, bool) Sleep is now finished, Waked-up by timer,
2587 | Waked-up by call, Waked-up by SMS, Waked-up by temperature
2588 | (Waked up flags are not relevant if non blocking)
2589 | """
2590 | gsm_waked_up_by_alarm = False
2591 | gsm_waked_up_by_call = False
2592 | gsm_waked_up_by_sms = False
2593 | gsm_waked_up_by_temperature = False
2594 |
2595 | # Check if GSM really sleeping before waiting indefinitly
2596 | if self.isAlive():
2597 | return True, gsm_waked_up_by_alarm, gsm_waked_up_by_call, gsm_waked_up_by_sms, gsm_waked_up_by_temperature
2598 |
2599 | # Wait until any element arrive from buffer to stop the sleep mode (or timeout)
2600 | time_to_wait = 3600
2601 | if max_additional_waiting_time_in_sec > 0:
2602 | time_to_wait = max_additional_waiting_time_in_sec
2603 | data = self.__getNotEmptyLine(additional_timeout=time_to_wait)
2604 |
2605 | if len(data) > 0:
2606 | # At least one character was received (it means sleep mode is not active anymore)
2607 | if len(data) >= 5:
2608 | wakeup_type = data[:5]
2609 | if wakeup_type == "+CMTI":
2610 | gsm_waked_up_by_sms = True
2611 | elif wakeup_type == "+CLIP" or wakeup_type == "RING":
2612 | gsm_waked_up_by_call = True
2613 | elif wakeup_type == "^SCTM":
2614 | gsm_waked_up_by_temperature = True
2615 | elif wakeup_type == "+CALA":
2616 | gsm_waked_up_by_alarm = True
2617 |
2618 | # Set to asynchronous element to default state
2619 | self.__disableAsynchronousTriggers()
2620 |
2621 | return True, gsm_waked_up_by_alarm, gsm_waked_up_by_call, gsm_waked_up_by_sms, gsm_waked_up_by_temperature
2622 | else:
2623 | if max_additional_waiting_time_in_sec <= 0:
2624 | # Retry indefinitly until it wakes up
2625 | return self.waitEndOfSleepMode(-1)
2626 | else:
2627 | logging.warning("Module still sleeping after timeout")
2628 |
2629 | return False, gsm_waked_up_by_alarm, gsm_waked_up_by_call, gsm_waked_up_by_sms, gsm_waked_up_by_temperature
2630 |
2631 | def sleep(self, wake_up_with_timer_in_sec=-1, wake_up_with_call=False,
2632 | wake_up_with_sms=False, wake_up_with_temperature_warning=False,
2633 | blocking=True, max_additional_waiting_time_in_sec=-1):
2634 | """Putting module in sleep mode until a specific action occurs (enter low power mode)
2635 | Blocking or non blocking that it also wakes up
2636 |
2637 | Keyword arguments:
2638 | wake_up_with_timer_in_sec -- (int) Time before waking-up the module (in sec), -1 to not use timer
2639 | wake_up_with_call -- (bool) Wake-up the module if a call is received
2640 | wake_up_with_sms -- (bool) Wake-up the module if a SMS is received
2641 | wake_up_with_temperature_warning -- (bool) Wake-up the module too high or too low
2642 | blocking -- (bool, default: True) Wait the module wakes up
2643 | max_additional_waiting_time_in_sec -- (int, default: -1) Max time to wait module to wake up (-1 for indefinitly),
2644 | not taken into account if non blocking
2645 |
2646 | return: (bool, bool, bool, bool, bool) Sleep was entered and is now finished, Waked-up by timer,
2647 | Waked-up by call, Waked-up by SMS, Waked-up by temperature
2648 | (Waked up flags are not relevant if non blocking)
2649 | """
2650 | min_alarm_sec = 10
2651 | gsm_waked_up_by_alarm = False
2652 | gsm_waked_up_by_call = False
2653 | gsm_waked_up_by_sms = False
2654 | gsm_waked_up_by_temperature = False
2655 |
2656 | # Do not allow infinite sleep (better stop the device with {switchOff()})
2657 | if (not wake_up_with_call) and (not wake_up_with_sms) \
2658 | and (not wake_up_with_temperature_warning) \
2659 | and (wake_up_with_timer_in_sec < min_alarm_sec):
2660 | logging.error("Sleep can't be used without any possibility to wake up")
2661 | logging.error("Be sure at least one trigger is used (and timer >= 10sec)")
2662 | return False, gsm_waked_up_by_alarm, gsm_waked_up_by_call, gsm_waked_up_by_sms, gsm_waked_up_by_temperature
2663 |
2664 | # Enable all requested wake up
2665 | if wake_up_with_call:
2666 | if not self.__sendCmdAndCheckResult(cmd=GSMTC35.__NORMAL_AT+"CLIP=1"):
2667 | logging.error("Impossible to enable the wake up with call")
2668 | self.__disableAsynchronousTriggers()
2669 | return False, gsm_waked_up_by_alarm, gsm_waked_up_by_call, gsm_waked_up_by_sms, gsm_waked_up_by_temperature
2670 |
2671 | if wake_up_with_sms:
2672 | if not self.__sendCmdAndCheckResult(cmd=GSMTC35.__NORMAL_AT+"CNMI=1,1"):
2673 | logging.error("Impossible to enable the wake up with SMS")
2674 | self.__disableAsynchronousTriggers()
2675 | return False, gsm_waked_up_by_alarm, gsm_waked_up_by_call, gsm_waked_up_by_sms, gsm_waked_up_by_temperature
2676 |
2677 | if wake_up_with_temperature_warning:
2678 | if not self.__sendCmdAndCheckResult(cmd=GSMTC35.__BASE_AT+"^SCTM=1"):
2679 | logging.error("Impossible to enable the wake up with temperature report")
2680 | self.__disableAsynchronousTriggers()
2681 | return False, gsm_waked_up_by_alarm, gsm_waked_up_by_call, gsm_waked_up_by_sms, gsm_waked_up_by_temperature
2682 |
2683 | if wake_up_with_timer_in_sec >= min_alarm_sec:
2684 | if not self.__addAlarmAsAChrono(wake_up_with_timer_in_sec + 1, "SLEEP"): # Add one sec (due to query time)
2685 | logging.error("Impossible to enable the wake up with alarm")
2686 | self.__disableAsynchronousTriggers()
2687 | return False, gsm_waked_up_by_alarm, gsm_waked_up_by_call, gsm_waked_up_by_sms, gsm_waked_up_by_temperature
2688 |
2689 | # Sleep
2690 | if not self.__sendCmdAndCheckResult(cmd=GSMTC35.__NORMAL_AT+"CFUN=0"):
2691 | logging.error("Impossible to enable sleep mode")
2692 | self.__disableAsynchronousTriggers()
2693 | return False, gsm_waked_up_by_alarm, gsm_waked_up_by_call, gsm_waked_up_by_sms, gsm_waked_up_by_temperature
2694 |
2695 | if blocking:
2696 | return self.waitEndOfSleepMode(max_additional_waiting_time_in_sec)
2697 |
2698 | return True, gsm_waked_up_by_alarm, gsm_waked_up_by_call, gsm_waked_up_by_sms, gsm_waked_up_by_temperature
2699 |
2700 | ################################# HELP FUNCTION ################################
2701 | def __help(func="", filename=__file__):
2702 | """Show help on how to use command line GSM module functions
2703 |
2704 | Keyword arguments:
2705 | func -- (string, optional) Command line function requiring help, none will show all function
2706 | filename -- (string, optional) File name of the python script implementing the commands
2707 | """
2708 | func = func.lower()
2709 | filename = "python " + str(filename)
2710 |
2711 | # Help
2712 | if func in ("h", "help"):
2713 | print("Give information to use all or specific GSM class commands\r\n"
2714 | +"\r\n"
2715 | +"Usage:\r\n"
2716 | +filename+" -h [command (default: none)]\r\n"
2717 | +filename+" --help [command (default: none)]\r\n"
2718 | +"\r\n"
2719 | +"Example:\r\n"
2720 | +filename+" -h \r\n"
2721 | +filename+" -h baudrate \r\n"
2722 | +filename+" --help \r\n"
2723 | +filename+" --help baudrate")
2724 | return
2725 | elif func == "":
2726 | print("HELP (-h, --help): Give information to use all or specific GSM class commands")
2727 |
2728 | # Baudrate
2729 | if func in ("b", "baudrate"):
2730 | print("Specifiy serial baudrate for GSM module <-> master communication\r\n"
2731 | +"Default value (if not called): 115200\r\n"
2732 | +"\r\n"
2733 | +"Usage:\r\n"
2734 | +filename+" -b [baudrate]\r\n"
2735 | +filename+" --baudrate [baudrate]\r\n"
2736 | +"\r\n"
2737 | +"Example:\r\n"
2738 | +filename+" -b 9600\r\n"
2739 | +filename+" --baudrate 115200 \r\n")
2740 | return
2741 | elif func == "":
2742 | print("BAUDRATE (-b, --baudrate): Specify serial baudrate for GSM module <-> master communication (Optional)")
2743 |
2744 | # Serial Port
2745 | if func in ("u", "serialport"):
2746 | print("Specify serial port for GSM module <-> master communication\r\n"
2747 | +"Default value (if not called): COM1\r\n"
2748 | +"\r\n"
2749 | +"Usage:\r\n"
2750 | +filename+" -u [port]\r\n"
2751 | +filename+" --serialPort [port]\r\n"
2752 | +"\r\n"
2753 | +"Example:\r\n"
2754 | +filename+" -u COM4\r\n"
2755 | +filename+" --serialPort /dev/ttyS3 \r\n")
2756 | return
2757 | elif func == "":
2758 | print("SERIAL PORT (-u, --serialPort): Specify serial port for GSM module <-> master communication (Optional)")
2759 |
2760 | # PIN
2761 | if func in ("p", "pin"):
2762 | print("Specify SIM card PIN\r\n"
2763 | +"Default value (if not called): No PIN for SIM card\r\n"
2764 | +"\r\n"
2765 | +"Usage:\r\n"
2766 | +filename+" -p [pin number]\r\n"
2767 | +filename+" --pin [pin number]\r\n"
2768 | +"\r\n"
2769 | +"Example:\r\n"
2770 | +filename+" -p 1234\r\n"
2771 | +filename+" --pin 0000 \r\n")
2772 | return
2773 | elif func == "":
2774 | print("PIN (-p, --pin): Specify SIM card PIN (Optional)")
2775 |
2776 | # PUK
2777 | if func in ("y", "puk"):
2778 | print("Specify SIM card PUK\r\n"
2779 | +"Default value (if not called): No PUK for SIM card\r\n"
2780 | +"\r\n"
2781 | +"Usage:\r\n"
2782 | +filename+" -y [puk number]\r\n"
2783 | +filename+" --puk [puk number]\r\n"
2784 | +"\r\n"
2785 | +"Example:\r\n"
2786 | +filename+" -y 12345678\r\n"
2787 | +filename+" --puk 12345678 \r\n")
2788 | return
2789 | elif func == "":
2790 | print("PUK (-y, --puk): Specify SIM card PUK (Optional)")
2791 |
2792 | # PIN2
2793 | if func in ("x", "pin2"):
2794 | print("Specify SIM card PIN2\r\n"
2795 | +"Default value (if not called): No PIN2 for SIM card\r\n"
2796 | +"\r\n"
2797 | +"Usage:\r\n"
2798 | +filename+" -x [pin2 number]\r\n"
2799 | +filename+" --pin2 [pin2 number]\r\n"
2800 | +"\r\n"
2801 | +"Example:\r\n"
2802 | +filename+" -x 1234\r\n"
2803 | +filename+" --pin2 0000 \r\n")
2804 | return
2805 | elif func == "":
2806 | print("PIN2 (-x, --pin2): Specify SIM card PIN2 (Optional)")
2807 |
2808 | # PUK2
2809 | if func in ("v", "puk2"):
2810 | print("Specify SIM card PUK2\r\n"
2811 | +"Default value (if not called): No PUK2 for SIM card\r\n"
2812 | +"\r\n"
2813 | +"Usage:\r\n"
2814 | +filename+" -v [puk2 number]\r\n"
2815 | +filename+" --puk2 [puk2 number]\r\n"
2816 | +"\r\n"
2817 | +"Example:\r\n"
2818 | +filename+" -v 1234\r\n"
2819 | +filename+" --puk2 0000 \r\n")
2820 | return
2821 | elif func == "":
2822 | print("PUK2 (-v, --puk2): Specify SIM card PUK2 (Optional)")
2823 |
2824 | # Is alive
2825 | if func in ("a", "isalive"):
2826 | print("Check if the GSM module is alive (answers ping)\r\n"
2827 | +"\r\n"
2828 | +"Usage:\r\n"
2829 | +filename+" -a\r\n"
2830 | +filename+" --isAlive\r\n")
2831 | return
2832 | elif func == "":
2833 | print("IS ALIVE (-a, --isAlive): Check if the GSM module answers ping")
2834 |
2835 | # Call
2836 | if func in ("c", "call"):
2837 | print("Call a phone number\r\n"
2838 | +"\r\n"
2839 | +"Usage:\r\n"
2840 | +filename+" -c [phone number] [Hide phone number? True/False (default: False)] [pick-up wait in sec (default: 20sec)]\r\n"
2841 | +filename+" --call [phone number] [Hide phone number? True/False (default: False)] [pick-up wait in sec (default: 20sec)]\r\n"
2842 | +"\r\n"
2843 | +"Example:\r\n"
2844 | +filename+" -c +33601234567\r\n"
2845 | +filename+" --call 0601234567 True\r\n"
2846 | +filename+" --call 0601234567 False 30\r\n"
2847 | +"\r\n"
2848 | +"Note:\r\n"
2849 | +" - The call may still be active after this call\r\n"
2850 | +" - Local or international phone numbers may not work depending on your GSM module\r\n")
2851 | return
2852 | elif func == "":
2853 | print("CALL (-c, --call): Call a phone number")
2854 |
2855 | # Stop call
2856 | if func in ("t", "hangupcall"):
2857 | print("Stop current phone call\r\n"
2858 | +"\r\n"
2859 | +"Usage:\r\n"
2860 | +filename+" -t\r\n"
2861 | +filename+" --hangUpCall\r\n")
2862 | return
2863 | elif func == "":
2864 | print("STOP CALL (-t, --hangUpCall): Stop current phone call")
2865 |
2866 | # Is someone calling
2867 | if func in ("i", "issomeonecalling"):
2868 | print("Check if someone is trying to call the GSM module\r\n"
2869 | +"\r\n"
2870 | +"Usage:\r\n"
2871 | +filename+" -i\r\n"
2872 | +filename+" --isSomeoneCalling\r\n")
2873 | return
2874 | elif func == "":
2875 | print("IS SOMEONE CALLING (-i, --isSomeoneCalling): Check if someone is trying to call the GSM module")
2876 |
2877 | # Pick-up call
2878 | if func in ("n", "pickupcall"):
2879 | print("Pick up call (if someone is calling the GSM module)\r\n"
2880 | +"\r\n"
2881 | +"Usage:\r\n"
2882 | +filename+" -n\r\n"
2883 | +filename+" --pickUpCall\r\n")
2884 | return
2885 | elif func == "":
2886 | print("PICK UP CALL (-n, --pickUpCall): Pick up (answer) call")
2887 |
2888 | # Send normal/special SMS/MMS
2889 | if func in ("s", "sendsms"):
2890 | print("Send SMS or MMS\r\n"
2891 | +"\r\n"
2892 | +"Usage:\r\n"
2893 | +filename+" -s [phone number] [message]\r\n"
2894 | +filename+" --sendSMS [phone number] [message]\r\n"
2895 | +"\r\n"
2896 | +"Example:\r\n"
2897 | +filename+" -s +33601234567 \"Hello!\r\nNew line!\"\r\n"
2898 | +filename+" --sendSMS 0601234567 \"Hello!\r\nNew line!\"\r\n")
2899 | return
2900 | elif func == "":
2901 | print("SEND SMS OR MMS (-s, --sendSMS): Send SMS or MMS")
2902 |
2903 | # Send encoded SMS/MMS
2904 | if func in ("m", "sendencodedsms"):
2905 | print("Send encoded SMS or MMS\r\n"
2906 | +"\r\n"
2907 | +"Usage:\r\n"
2908 | +filename+" -m [phone number] [message in hexa]\r\n"
2909 | +filename+" --sendEncodedSMS [phone number] [message in hexa]\r\n"
2910 | +"\r\n"
2911 | +"Example:\r\n"
2912 | +filename+" -m +33601234567 48656C6C6F21\r\n"
2913 | +filename+" --sendEncodedSMS 0601234567 48656C6C6F21\r\n")
2914 | return
2915 | elif func == "":
2916 | print("SEND ENCODED SMS OR MMS (-m, --sendEncodedSMS): Send encoded SMS or MMS")
2917 |
2918 | # Send text mode SMS (dependant of GSM)
2919 | if func in ("e", "sendtextmodesms"):
2920 | print("Send SMS using Text Mode TC35 encoding (NOT RECOMMENDED)\r\n"
2921 | +"\r\n"
2922 | +"Usage:\r\n"
2923 | +filename+" -e [phone number] [message]\r\n"
2924 | +filename+" --sendTextModeSMS [phone number] [message]\r\n"
2925 | +"\r\n"
2926 | +"Example:\r\n"
2927 | +filename+" -e +33601234567 \"Hello!\r\nNew line!\"\r\n"
2928 | +filename+" --sendTextModeSMS 0601234567 \"Hello!\r\nNew line!\"\r\n")
2929 | return
2930 | elif func == "":
2931 | print("SEND SMS WITH TEXT MODE (-e, --sendTextModeSMS): Send SMS using Text Mode TC35 encoding (NOT RECOMMENDED)")
2932 |
2933 | # Get SMS
2934 | if func in ("g", "getsms"):
2935 | print("Get SMS\r\n"
2936 | +"\r\n"
2937 | +"Usage:\r\n"
2938 | +filename+" -g [sms type]\r\n"
2939 | +filename+" --getSMS [sms type]\r\n"
2940 | +"SMS Type: \""+str(GSMTC35.eSMS.ALL_SMS)+"\", \""
2941 | +str(GSMTC35.eSMS.UNREAD_SMS)+"\" and \""
2942 | +str(GSMTC35.eSMS.READ_SMS)+"\"\r\n"
2943 | +"\r\n"
2944 | +"Example:\r\n"
2945 | +filename+" -g \""+str(GSMTC35.eSMS.UNREAD_SMS)+"\"\r\n"
2946 | +filename+" --getSMS \""+str(GSMTC35.eSMS.ALL_SMS)+"\"\r\n")
2947 | return
2948 | elif func == "":
2949 | print("GET SMS (-g, --getSMS): Get SMS")
2950 |
2951 | # Get encoded SMS
2952 | if func in ("f", "getencodedsms"):
2953 | print("Get SMS\r\n"
2954 | +"\r\n"
2955 | +"Usage:\r\n"
2956 | +filename+" -f [sms type]\r\n"
2957 | +filename+" --getEncodedSMS [sms type]\r\n"
2958 | +"SMS Type: \""+str(GSMTC35.eSMS.ALL_SMS)+"\", \""
2959 | +str(GSMTC35.eSMS.UNREAD_SMS)+"\" and \""
2960 | +str(GSMTC35.eSMS.READ_SMS)+"\"\r\n"
2961 | +"\r\n"
2962 | +"Example:\r\n"
2963 | +filename+" -f \""+str(GSMTC35.eSMS.UNREAD_SMS)+"\"\r\n"
2964 | +filename+" --getEncodedSMS \""+str(GSMTC35.eSMS.ALL_SMS)+"\"\r\n")
2965 | return
2966 | elif func == "":
2967 | print("GET ENCODED SMS (-f, --getEncodedSMS): Get SMS in Hexadecimal without decoding")
2968 |
2969 | # Get Text mode SMS
2970 | if func in ("j", "gettextmodesms"):
2971 | print("Get SMS\r\n"
2972 | +"\r\n"
2973 | +"Usage:\r\n"
2974 | +filename+" -j [sms type]\r\n"
2975 | +filename+" --getTextModeSMS [sms type]\r\n"
2976 | +"SMS Type: \""+str(GSMTC35.eSMS.ALL_SMS)+"\", \""
2977 | +str(GSMTC35.eSMS.UNREAD_SMS)+"\" and \""
2978 | +str(GSMTC35.eSMS.READ_SMS)+"\"\r\n"
2979 | +"\r\n"
2980 | +"Example:\r\n"
2981 | +filename+" -j \""+str(GSMTC35.eSMS.UNREAD_SMS)+"\"\r\n"
2982 | +filename+" --getTextModeSMS \""+str(GSMTC35.eSMS.ALL_SMS)+"\"\r\n")
2983 | return
2984 | elif func == "":
2985 | print("GET TEXT MODE SMS (-j, --getTextModeSMS): Get SMS using Text Mode TC35 decoding (NOT RECOMMENDED)")
2986 |
2987 | # Delete SMS
2988 | if func in ("d", "deletesms"):
2989 | print("Delete SMS\r\n"
2990 | +"\r\n"
2991 | +"Usage:\r\n"
2992 | +filename+" -d [sms type]\r\n"
2993 | +filename+" --deleteSMS [sms type]\r\n"
2994 | +"SMS Type: Index of the SMS (integer), \""+str(GSMTC35.eSMS.ALL_SMS)
2995 | +"\", \""+str(GSMTC35.eSMS.UNREAD_SMS)+"\" and \""
2996 | +str(GSMTC35.eSMS.READ_SMS)+"\"\r\n"
2997 | +"\r\n"
2998 | +"Example:\r\n"
2999 | +filename+" -d \""+str(GSMTC35.eSMS.UNREAD_SMS)+"\"\r\n"
3000 | +filename+" --deleteSMS \""+str(GSMTC35.eSMS.ALL_SMS)+"\"\r\n")
3001 | return
3002 | elif func == "":
3003 | print("DELETE SMS (-d, --deleteSMS): Delete SMS")
3004 |
3005 | # Get information
3006 | if func in ("o", "information"):
3007 | print("Get information from module and network (IMEI, clock, operator, ...)\r\n"
3008 | +"\r\n"
3009 | +"Usage:\r\n"
3010 | +filename+" -o\r\n"
3011 | +filename+" --information")
3012 | return
3013 | elif func == "":
3014 | print("GET INFORMATION (-o, --information): Get information from module and network")
3015 |
3016 | # Use case examples:
3017 | if func == "":
3018 | example_port = "COMx"
3019 | for p in list(serial.tools.list_ports.comports()):
3020 | if p.device:
3021 | example_port = str(p.device)
3022 | break
3023 | print("\r\n"
3024 | +"Some examples (if serial port is '"+example_port+"' and sim card pin is '1234'):\r\n"
3025 | +" - Call someone: "+filename+" --serialPort "+example_port+" --pin 1234 --call +33601234567\r\n"
3026 | +" - Hang up call: "+filename+" --serialPort "+example_port+" --pin 1234 --hangUpCall\r\n"
3027 | +" - Pick up call: "+filename+" --serialPort "+example_port+" --pin 1234 --pickUpCall\r\n"
3028 | +" - Send SMS/MMS: "+filename+" --serialPort "+example_port+" --pin 1234 --sendSMS +33601234567 \"Hello you!\r\nNew line :)\"\r\n"
3029 | +" - Send encoded SMS/MMS: "+filename+" --serialPort "+example_port+" --pin 1234 --sendEncodedSMS +33601234567 48656C6C6F21\r\n"
3030 | +" - Get all SMS (decoded): "+filename+" --serialPort "+example_port+" --pin 1234 --getSMS \""+str(GSMTC35.eSMS.ALL_SMS)+"\"\r\n"
3031 | +" - Get all SMS (encoded): "+filename+" --serialPort "+example_port+" --pin 1234 --getEncodedSMS \""+str(GSMTC35.eSMS.ALL_SMS)+"\"\r\n"
3032 | +" - Delete all SMS: "+filename+" --serialPort "+example_port+" --pin 1234 --deleteSMS \""+str(GSMTC35.eSMS.ALL_SMS)+"\"\r\n"
3033 | +" - Get information: "+filename+" --serialPort "+example_port+" --pin 1234 --information"+"\"\r\n"
3034 | +" - You can have a lot more information on how commands are performed using '--debug' command"+"\"\r\n"
3035 | +" - You can hide debug, warning and error information using '--nodebug' command")
3036 |
3037 | print("\r\nList of available serial ports:")
3038 | ports = list(serial.tools.list_ports.comports())
3039 | for p in ports:
3040 | print(p)
3041 |
3042 |
3043 | ################################# MAIN FUNCTION ###############################
3044 | def main(parsed_args = sys.argv[1:]):
3045 | """Shell GSM utility function"""
3046 |
3047 | baudrate = 115200
3048 | serial_port = ""
3049 | pin = ""
3050 | puk = ""
3051 | pin2 = ""
3052 | puk2 = ""
3053 |
3054 | # Get options
3055 | try:
3056 | opts, args = getopt.getopt(parsed_args, "hlactsdemniogfjzb:u:p:y:x:v:",
3057 | ["baudrate=", "serialPort=", "pin=", "puk=", "pin2=", "puk2=", "debug", "nodebug", "help",
3058 | "isAlive", "call", "hangUpCall", "isSomeoneCalling",
3059 | "pickUpCall", "sendSMS", "sendEncodedSMS", "sendTextModeSMS", "deleteSMS", "getSMS",
3060 | "information", "getEncodedSMS", "getTextModeSMS"])
3061 | except getopt.GetoptError as err:
3062 | print("[ERROR] "+str(err))
3063 | __help()
3064 | sys.exit(1)
3065 |
3066 | # Show help or add debug information (if requested)
3067 | for o, a in opts:
3068 | if o in ("-h", "--help"):
3069 | if len(args) >= 1:
3070 | __help(args[0])
3071 | else:
3072 | __help()
3073 | sys.exit(0)
3074 | elif o in ("-l", "--debug"):
3075 | print("Debugging...")
3076 | logger = logging.getLogger()
3077 | logger.setLevel(logging.DEBUG)
3078 | elif o in ("-z", "--nodebug"):
3079 | logger = logging.getLogger()
3080 | logger.setLevel(logging.CRITICAL)
3081 |
3082 | # Get parameters
3083 | for o, a in opts:
3084 | if o in ("-b", "--baudrate"):
3085 | print("Baudrate: "+str(a))
3086 | baudrate = a
3087 | continue
3088 | if o in ("-u", "--serialPort"):
3089 | print("Serial port: "+a)
3090 | serial_port = a
3091 | continue
3092 | if o in ("-p", "--pin"):
3093 | print("PIN: "+a)
3094 | pin = a
3095 | continue
3096 | if o in ("-y", "--puk"):
3097 | print("PUK: "+a)
3098 | puk = a
3099 | continue
3100 | if o in ("-x", "--pin2"):
3101 | print("PIN2: "+a)
3102 | pin2 = a
3103 | continue
3104 | if o in ("-v", "--puk2"):
3105 | print("PUK2: "+a)
3106 | puk2 = a
3107 | continue
3108 |
3109 | if serial_port == "":
3110 | for p in list(serial.tools.list_ports.comports()):
3111 | if p.device:
3112 | serial_port = str(p.device)
3113 | logging.warning("Using first found serial port ("+serial_port+"), specify serial port if this one is not working...")
3114 | break
3115 | if serial_port == "":
3116 | print("No specified serial port (and none found)...\r\n")
3117 | __help()
3118 | sys.exit(1)
3119 |
3120 | # Initialize GSM
3121 | gsm = GSMTC35()
3122 | is_init = gsm.setup(_port=serial_port, _baudrate=baudrate, _pin=pin, _puk=puk, _pin2=pin2, _puk2=puk2)
3123 | print("GSM init with serial port {} and baudrate {}: {}".format(serial_port, baudrate, is_init))
3124 | if (not is_init):
3125 | print("[ERROR] You must configure the serial port (and the baudrate), use '-h' to get more information.")
3126 | print("[HELP] List of available serial ports:")
3127 | ports = list(serial.tools.list_ports.comports())
3128 | for p in ports:
3129 | print("[HELP] "+str(p))
3130 | sys.exit(2)
3131 |
3132 | # Be sure PIN(2)/PUK(2) are not needed
3133 | req_pin_status, required_pin = gsm.getPinStatus()
3134 | if not(req_pin_status) or (required_pin != GSMTC35.eRequiredPin.READY):
3135 | if len(required_pin) > 0:
3136 | print("[ERROR] "+str(required_pin)+" is needed")
3137 | else:
3138 | print("[ERROR] Failed to check PIN status")
3139 | sys.exit(2)
3140 | else:
3141 | print("PIN and PUK not needed")
3142 |
3143 | # Launch requested command
3144 | for o, a in opts:
3145 | if o in ("-a", "--isAlive"):
3146 | is_alive = gsm.isAlive()
3147 | print("Is alive: {}".format(is_alive))
3148 | if is_alive:
3149 | sys.exit(0)
3150 | sys.exit(2)
3151 |
3152 | elif o in ("-c", "--call"):
3153 | if len(args) > 0:
3154 | if args[0] != "":
3155 | hidden = False
3156 | if len(args) > 1:
3157 | if (args[1].lower() == "true") or args[1] == "1":
3158 | hidden = True
3159 | if hidden:
3160 | print("Calling "+args[0]+" in invisible mode...")
3161 | else:
3162 | print("Calling "+args[0]+" in normal mode...")
3163 |
3164 | if len(args) > 2:
3165 | result = gsm.call(args[0], hidden, int(args[2]))
3166 | else:
3167 | result = gsm.call(args[0], hidden)
3168 | print("Call picked up: "+str(result))
3169 | if result:
3170 | sys.exit(0)
3171 | sys.exit(2)
3172 | else:
3173 | print("[ERROR] You must specify a valid phone number")
3174 | sys.exit(2)
3175 | else:
3176 | print("[ERROR] You must specify a phone number to call")
3177 | sys.exit(2)
3178 |
3179 | elif o in ("-t", "--hangUpCall"):
3180 | print("Hanging up call...")
3181 | result = gsm.hangUpCall()
3182 | print("Hang up call: "+str(result))
3183 | if result:
3184 | sys.exit(0)
3185 | sys.exit(2)
3186 |
3187 | elif o in ("-s", "--sendSMS"):
3188 | if len(args) < 2:
3189 | print("[ERROR] You need to specify the phone number and the message")
3190 | sys.exit(1)
3191 | msg = args[1]
3192 | # Python2.7-3 compatibility:
3193 | try:
3194 | msg = args[1].encode().decode('utf-8')
3195 | except (AttributeError, UnicodeEncodeError, UnicodeDecodeError):
3196 | pass
3197 | result = gsm.sendSMS(str(args[0]), msg)
3198 | print("SMS sent: "+str(result))
3199 | if result:
3200 | sys.exit(0)
3201 | else:
3202 | sys.exit(2)
3203 |
3204 | elif o in ("-m", "--sendEncodedSMS"):
3205 | if len(args) < 2:
3206 | print("[ERROR] You need to specify the phone number and the message")
3207 | sys.exit(1)
3208 | try:
3209 | decoded_content = bytearray.fromhex(args[1]).decode('utf-8')
3210 | except (AttributeError, UnicodeEncodeError, UnicodeDecodeError):
3211 | print("[ERROR] Failed to decode (in UTF-8) your hexadecimal encoded message")
3212 | sys.exit(1)
3213 | result = gsm.sendSMS(str(args[0]), decoded_content)
3214 | print("SMS encoded sent: "+str(result))
3215 | if result:
3216 | sys.exit(0)
3217 | else:
3218 | sys.exit(2)
3219 |
3220 | elif o in ("-e", "--sendTextModeSMS"):
3221 | if len(args) < 2:
3222 | print("[ERROR] You need to specify the phone number and the message")
3223 | sys.exit(1)
3224 | msg = args[1]
3225 | # Python2.7-3 compatibility:
3226 | try:
3227 | msg = args[1].encode().decode('utf-8')
3228 | except AttributeError:
3229 | pass
3230 | result = gsm.sendSMS(str(args[0]), msg, True)
3231 | print("SMS sent using Text Mode: "+str(result))
3232 | if result:
3233 | sys.exit(0)
3234 | else:
3235 | sys.exit(2)
3236 |
3237 | elif o in ("-d", "--deleteSMS"):
3238 | if len(args) < 1:
3239 | print("[ERROR] You need to specify the type of SMS to delete")
3240 | print("[ERROR] Possible values: index of the SMS, \""+str(GSMTC35.eSMS.ALL_SMS)+"\", \""
3241 | +str(GSMTC35.eSMS.UNREAD_SMS)+"\" and \""+str(GSMTC35.eSMS.READ_SMS)+"\"")
3242 | sys.exit(1)
3243 | result = gsm.deleteSMS(str(args[0]))
3244 | print("SMS deleted: "+str(result))
3245 | if result:
3246 | sys.exit(0)
3247 | else:
3248 | sys.exit(2)
3249 |
3250 | elif o in ("-g", "--getSMS"):
3251 | if len(args) < 1:
3252 | print("[ERROR] You need to specify the type of SMS to get")
3253 | print("[ERROR] Possible values: \""+str(GSMTC35.eSMS.ALL_SMS)+"\", \""
3254 | +str(GSMTC35.eSMS.UNREAD_SMS)+"\" and \""+str(GSMTC35.eSMS.READ_SMS)+"\"")
3255 | sys.exit(1)
3256 | received_sms = gsm.getSMS(str(args[0]))
3257 | print("List of SMS:")
3258 | for sms in received_sms:
3259 | multipart = ""
3260 | if "header_multipart_ref_id" in sms and "header_multipart_nb_of_part" in sms and "header_multipart_current_part_nb" in sms:
3261 | multipart = ", multipart '" + str(sms["header_multipart_ref_id"]) + "' (" + str(sms["header_multipart_current_part_nb"]) + "/" + str(sms["header_multipart_nb_of_part"]) + ")"
3262 | try:
3263 | print(str(sms["phone_number"])+" (id " +str(sms["index"])+", "
3264 | +str(sms["status"])+", "+str(sms["date"])+" "+str(sms["time"])+str(multipart)
3265 | +"): "+str(sms["sms"]))
3266 | except UnicodeEncodeError:
3267 | logging.warning("Can't display SMS content as unicode, displaying it as utf-8")
3268 | print(str(sms["phone_number"])+" (id " +str(sms["index"])+", "
3269 | +str(sms["status"])+", "+str(sms["date"])+" "+str(sms["time"])+str(multipart)
3270 | +"): "+str(sms["sms"].encode("utf-8")))
3271 | sys.exit(0)
3272 |
3273 | elif o in ("-f", "--getEncodedSMS"):
3274 | if len(args) < 1:
3275 | print("[ERROR] You need to specify the type of SMS to get")
3276 | print("[ERROR] Possible values: \""+str(GSMTC35.eSMS.ALL_SMS)+"\", \""
3277 | +str(GSMTC35.eSMS.UNREAD_SMS)+"\" and \""+str(GSMTC35.eSMS.READ_SMS)+"\"")
3278 | sys.exit(1)
3279 | received_sms = gsm.getSMS(str(args[0]), False)
3280 | print("List of encoded SMS:")
3281 | for sms in received_sms:
3282 | if "charset" in sms:
3283 | charset = sms["charset"]
3284 | else:
3285 | charset = "unknown"
3286 | res = str(sms["phone_number"])+" (id " +str(sms["index"])+", " \
3287 | +str(sms["status"])+", "+str(charset)+", "+str(sms["date"])+" "+str(sms["time"])
3288 | if "header_iei" in sms and "header_ie_data" in sms:
3289 | res = res + ", header '" + str(sms["header_iei"]) + "' with data: '" + str(sms["header_ie_data"])+"'"
3290 | if "header_multipart_ref_id" in sms and "header_multipart_nb_of_part" in sms and "header_multipart_current_part_nb" in sms:
3291 | res = res + ", multipart '" + str(sms["header_multipart_ref_id"]) + "' (" + str(sms["header_multipart_current_part_nb"]) + "/" + str(sms["header_multipart_nb_of_part"]) + ")"
3292 | print(res+"): "+str(sms["sms_encoded"]))
3293 | sys.exit(0)
3294 |
3295 | elif o in ("-j", "--getTextModeSMS"):
3296 | if len(args) < 1:
3297 | print("[ERROR] You need to specify the type of SMS to get")
3298 | print("[ERROR] Possible values: \""+str(GSMTC35.eSMS.ALL_SMS)+"\", \""
3299 | +str(GSMTC35.eSMS.UNREAD_SMS)+"\" and \""+str(GSMTC35.eSMS.READ_SMS)+"\"")
3300 | sys.exit(1)
3301 | received_sms = gsm.getSMS(str(args[0]), False, True)
3302 | print("List of text mode SMS:")
3303 | for sms in received_sms:
3304 | print(str(sms["phone_number"])+" (id " +str(sms["index"])+", "
3305 | +str(sms["status"])+", "+str(sms["date"])+" "+str(sms["time"])
3306 | +"): "+str(sms["sms"]))
3307 | sys.exit(0)
3308 |
3309 | elif o in ("-n", "--pickUpCall"):
3310 | print("Picking up call...")
3311 | result = gsm.pickUpCall()
3312 | print("Pick up call: "+str(result))
3313 | if result:
3314 | sys.exit(0)
3315 | else:
3316 | sys.exit(2)
3317 |
3318 | elif o in ("-i", "--isSomeoneCalling"):
3319 | result = gsm.isSomeoneCalling()
3320 | print("Is someone calling: "+str(result))
3321 | sys.exit(0)
3322 |
3323 | elif o in ("-o", "--information"):
3324 | if not gsm.isAlive():
3325 | print("GSM module is not alive, can't get information")
3326 | sys.exit(2)
3327 | print("Is module alive: True")
3328 | print("GSM module Manufacturer ID: "+str(gsm.getManufacturerId()))
3329 | print("GSM module Model ID: "+str(gsm.getModelId()))
3330 | print("GSM module Revision ID: "+str(gsm.getRevisionId()))
3331 | print("Product serial number ID (IMEI): "+str(gsm.getIMEI()))
3332 | print("International Mobile Subscriber Identity (IMSI): "+str(gsm.getIMSI()))
3333 | print("Current operator: "+str(gsm.getOperatorName()))
3334 | sig_strength = gsm.getSignalStrength()
3335 | if sig_strength != -1:
3336 | print("Signal strength: "+str(sig_strength)+"dBm")
3337 | else:
3338 | print("Signal strength: Wrong value")
3339 | print("Date from internal clock: "+str(gsm.getDateFromInternalClock()))
3340 | print("Last call duration: "+str(gsm.getLastCallDuration())+"sec")
3341 |
3342 | list_operators = gsm.getOperatorNames()
3343 | operators = ""
3344 | for operator in list_operators:
3345 | if operators != "":
3346 | operators = operators + ", "
3347 | operators = operators + operator
3348 | print("List of stored operators: "+operators)
3349 |
3350 | call_state, phone_number = gsm.getCurrentCallState()
3351 | str_call_state = GSMTC35.eCallToString(call_state)
3352 |
3353 | if phone_number != "":
3354 | print("Call status: "+str(str_call_state)+" (phone number: "+str(phone_number)+")")
3355 | else:
3356 | print("Call status: "+str(str_call_state))
3357 | print("Neighbour cells: "+str(gsm.getNeighbourCells()))
3358 | print("Accumulated call meter: "+str(gsm.getAccumulatedCallMeter())+" home units")
3359 | print("Accumulated call meter max: "+str(gsm.getAccumulatedCallMeterMaximum())+" home units")
3360 | print("Is GSM module temperature critical: "+str(gsm.isTemperatureCritical()))
3361 | print("Is GSM module in sleep mode: "+str(gsm.isInSleepMode()))
3362 |
3363 | sys.exit(0)
3364 | print("[ERROR] You must call one action, use '-h' to get more information.")
3365 | sys.exit(1)
3366 |
3367 | if __name__ == '__main__':
3368 | main()
3369 |
--------------------------------------------------------------------------------
/GSMTC35/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuentinCG/GSM-TC35-Python-Library/d5bd78de29dfa4d0eeb2cdcf8b2ca0284a8bbe0c/GSMTC35/__init__.py
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Quentin Comte-Gaz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GSM TC35 Python library
2 | [](https://badge.fury.io/py/GSMTC35) [](https://codecov.io/gh/QuentinCG/GSM-TC35-Python-Library) [](https://github.com/QuentinCG/GSM-TC35-Python-Library/blob/master/LICENSE.md) [](https://paypal.me/QuentinCG) [](https://pepy.tech/project/GSMTC35) [](https://pepy.tech/project/GSMTC35)
3 |
4 | ## What is it
5 |
6 | This python library is designed to be integrated in python or shell projects using TC35 module.
7 | It is multi-platform and compatible with python 3+.
8 |
9 | Most functionalities should work with other GSM module using AT commands.
10 |
11 |
12 |
13 | ## Functionalities
14 |
15 | Non-exhaustive list of GSMTC35 class functionalities:
16 | - Check/Enter PIN/PUK
17 | - Lock/Unlock/Change PIN
18 | - Send/Receive/Delete SMS/MMS
19 | - Call/Re-call (possible to hide phone number)
20 | - Hang-up/Pick-up call
21 | - Enable/disable/check Call/SMS/Fax forwarding
22 | - Get/Add/Delete phonebook entries (phone numbers + contact names)
23 | - Sleep with wake up possibilities (Low power consumption)
24 | - Check if someone is calling
25 | - Check if there is a call in progress
26 | - Check call status (call/ringing/...) and get the associated phone number
27 | - Get last call duration
28 | - Check if module is alive
29 | - Switch off
30 | - Reboot
31 | - Check sleep mode status
32 | - Get IDs (manufacturer, model, revision, IMEI, IMSI)
33 | - Set module to manufacturer state
34 | - Get the current used operator
35 | - Get the signal strength (in dBm)
36 | - Set and get the date from the module internal clock
37 | - Get list of operators
38 | - Get list of neighbour cells
39 | - Get accumulated call meter and accumulated call meter max (in home units)
40 | - Get temperature status
41 | - Change the baudrate mode
42 |
43 | Non-exhaustive list of shell commands:
44 | - Send/Receive/Delete SMS/MMS
45 | - Call
46 | - Hang-up/Pick-up call
47 | - Show information (PIN status, operator, signal strength, last call duration, manufacturer/model/revision ID, IMEI, IMSI, date from internal clock, call status and associated phone number, operator list, neighbour cells, accumulated call meter (max), temperature status, sleep mode status)
48 |
49 | ## How to install (python script and shell)
50 |
51 | - Install package calling `pip install GSMTC35` (or `python setup.py install` from the root of this repository)
52 | - Connect your GSM module to a serial port
53 | - Get the port name (you can find it out by calling `python GSMTC35/GSMTC35.py --help` from the root of this repository)
54 | - Load your shell or python script
55 |
56 | Note: If you want to install test dependency and execute the library test, the command is `python setup.py test`
57 |
58 | ## How to use in shell
59 |
60 | ```shell
61 | # Get help
62 | python GSMTC35.py --help
63 |
64 | # Send SMS or MMS (in UTF-8, using PDU mode)
65 | python GSMTC35.py --serialPort COM4 --pin 1234 --sendSMS +33601234567 "Hello from shell! 你好,你是?"
66 |
67 | # Send SMS/MMS (encoded in UTF-8 hexadecimal, using PDU mode)
68 | python GSMTC35.py --serialPort COM4 --pin 1234 --sendEncodedSMS +33601234567 48656C6C6F2066726F6D207368656C6C2120E4BDA0E5A5BDEFBC8CE4BDA0E698AFEFBC9F
69 |
70 | # Send (multiple) SMS (in UTF-8, using 'Text Mode', NOT RECOMMENDED)
71 | python GSMTC35.py --serialPort COM4 --pin 1234 --sendTextModeSMS +33601234567 "Hello from shell!"
72 |
73 | # Get SMS/MMS (decoded, in plain text)
74 | python GSMTC35.py --serialPort COM4 --pin 1234 --getSMS "ALL"
75 |
76 | # Get SMS/MMS (encoded, in hexadecimal, charset specified in response)
77 | python GSMTC35.py --serialPort COM4 --pin 1234 --getEncodedSMS "ALL"
78 |
79 | # Get SMS (decoded by TC35 using 'Text Mode', NOT RECOMMENDED)
80 | python GSMTC35.py --serialPort COM4 --pin 1234 --getTextModeSMS "ALL"
81 |
82 | # Delete SMS
83 | python GSMTC35.py --serialPort COM4 --pin 1234 --deleteSMS "ALL"
84 |
85 | # Call
86 | python GSMTC35.py --serialPort COM4 --pin 1234 --call +33601234567
87 |
88 | # Call in hidden mode
89 | python GSMTC35.py --serialPort COM4 --pin 1234 --call +33601234567 True
90 |
91 | # Hang up call
92 | python GSMTC35.py --serialPort COM4 --pin 1234 --hangUpCall
93 |
94 | # Pick up call
95 | python GSMTC35.py --serialPort COM4 --pin 1234 --pickUpCall
96 |
97 | # Show GSM module and network information
98 | python GSMTC35.py --serialPort COM4 --pin 1234 --information
99 |
100 | # Use "--debug" to show more information during command
101 | # Use "--nodebug" to not show any warning information during command
102 | ```
103 |
104 | ## How to use in python script
105 |
106 | Example of python script using this library:
107 |
108 | ```python
109 | import sys
110 | from GSMTC35.GSMTC35 import GSMTC35
111 |
112 | gsm = GSMTC35()
113 | pin = "1234"
114 | puk = "12345678"
115 | pin2 = "4321"
116 | puk2 = "87654321"
117 |
118 | # Mandatory step (PIN/PUK/PIN2/PUK2 will be entered if required, not needed to specify them)
119 | if not gsm.setup(_port="COM3", _pin=pin, _puk=puk, _pin2=pin2, _puk2=puk2):
120 | print("Setup error")
121 | sys.exit(2)
122 |
123 | if not gsm.isAlive():
124 | print("The GSM module is not responding...")
125 | sys.exit(2)
126 |
127 | # Send SMS or MMS (if > 140 normal char or > 70 unicode char)
128 | print("SMS sent: "+str(gsm.sendSMS("+33601234567", u'Hello from python script!!! 你好,你是?')))
129 |
130 | # Send (multiple) SMS (encoded by TC35 using 'Text Mode', NOT RECOMMENDED)
131 | print("SMS Text Mode sent: "+str(gsm.sendSMS("+33601234567", 'Hello from python script!!!', True)))
132 |
133 | # Show all received SMS/MMS (decoded)
134 | rx_sms = gsm.getSMS(GSMTC35.eSMS.ALL_SMS)
135 | print("List of SMS (decoded):")
136 | for sms in rx_sms:
137 | print(str(sms["phone_number"])+" (id " +str(sms["index"])+", "
138 | +str(sms["status"])+", "+str(sms["date"])+" "+str(sms["time"])
139 | +"): "+str(sms["sms"]))
140 |
141 | # Show all received SMS/MMS (encoded)
142 | rx_encoded_sms = gsm.getSMS(GSMTC35.eSMS.ALL_SMS, False)
143 | print("List of SMS (encoded):")
144 | for sms in rx_encoded_sms:
145 | print(str(sms["phone_number"])+" (id " +str(sms["index"])+", "
146 | +str(sms["status"])+", "+str(sms["charset"])+", "
147 | +str(sms["date"])+" "+str(sms["time"])+"): "+str(sms["sms"]))
148 |
149 | # Show all received SMS (using text mode, NOT RECOMMENDED)
150 | rx_text_mode_sms = gsm.getSMS(GSMTC35.eSMS.ALL_SMS, False, True)
151 | print("List of SMS (using text mode, NOT RECOMMENDED):")
152 | for sms in rx_text_mode_sms:
153 | print(str(sms["phone_number"])+" (id " +str(sms["index"])+", "
154 | +str(sms["status"])+", "+str(sms["date"])+" "+str(sms["time"])
155 | +"): "+str(sms["sms"]))
156 |
157 | # Delete all received SMS
158 | print("Delete all SMS: "+str(gsm.deleteSMS(GSMTC35.eSMS.ALL_SMS)))
159 |
160 | # Call
161 | print("Called: "+str(gsm.call(phone_number="0601234567", hide_phone_number=False)))
162 |
163 | # Re-call same number
164 | print("Re-called: "+str(gsm.reCall()))
165 |
166 | # Last call duration
167 | print("Last call duration: "+str(gsm.getLastCallDuration())+"sec")
168 |
169 | # Pick up call
170 | print("Picked up: "+str(gsm.pickUpCall()))
171 |
172 | # Hang up call
173 | print("Hanged up: "+str(gsm.hangUpCall()))
174 |
175 | # Check Call/SMS/Fax/Data forwarding
176 | print("Call/SMS/Fax/Data forwarding status: "+str(gsm.getForwardStatus()))
177 |
178 | # Enable/disable Call/SMS/Fax/Data forwarding
179 | print("Enable call forwarding: "+str(gsm.setForwardStatus(GSMTC35.eForwardReason.UNCONDITIONAL, GSMTC35.eForwardClass.VOICE, True, "+33601020304")))
180 | print("Disable call forwarding: "+str(gsm.setForwardStatus(GSMTC35.eForwardReason.UNCONDITIONAL, GSMTC35.eForwardClass.VOICE, False)))
181 |
182 | # Add entry in GSM module phonebook
183 | print("Added contact to GSM module phonebook: "
184 | +str(gsm.addEntryToPhonebook("0600000000", "Dummy contact",
185 | GSMTC35.ePhonebookType.GSM_MODULE)))
186 |
187 | # Get entry list in GSM module phonebook:
188 | entries = gsm.getPhonebookEntries(GSMTC35.ePhonebookType.GSM_MODULE)
189 | print("List of stored contacts:")
190 | for entry in entries:
191 | print(str(entry['index'])+": "+str(entry['contact_name'])+" -> "+str(entry['phone_number']))
192 |
193 | # Delete all GSM phonebook entries:
194 | print("Deleted all contact from GSM module phonebook: "
195 | +str(gsm.deleteAllEntriesFromPhonebook(GSMTC35.ePhonebookType.GSM_MODULE)))
196 |
197 | # Check if someone is calling
198 | print("Incoming call: "+str(gsm.isSomeoneCalling()))
199 |
200 | # Check if there is a call in progress
201 | print("Call in progress: "+str(gsm.isCallInProgress()))
202 |
203 | # Check if someone is calling, if a call is in progress, dialing and the associated phone number
204 | call_state, phone_number = gsm.getCurrentCallState()
205 | print("Call status: "+str(call_state)+" (associated phone number: "+str(phone_number)+")")
206 | print("(-1=No call, 0=Call active, 1=Held, 2=Dialing, 3=Alerting, 4=Incoming, 5=Waiting)")
207 |
208 | # Edit SIM Pin
209 | print("SIM Locked: "+str(gsm.lockSimPin(pin)))
210 | print("SIM Unlocked: "+str(gsm.unlockSimPin(pin)))
211 | new_pin = pin # (Just for test)
212 | print("SIM Pin changed: "+str(gsm.changePin(pin, new_pin)))
213 |
214 | # Set module clock to current date
215 | print("Clock set: "+str(gsm.setInternalClockToCurrentDate()))
216 |
217 | # Show additional information
218 | print("GSM module Manufacturer ID: "+str(gsm.getManufacturerId()))
219 | print("GSM module Model ID: "+str(gsm.getModelId()))
220 | print("GSM module Revision ID: "+str(gsm.getRevisionId()))
221 | print("Product serial number ID (IMEI): "+str(gsm.getIMEI()))
222 | print("International Mobile Subscriber Identity (IMSI): "+str(gsm.getIMSI()))
223 | print("Current operator: "+str(gsm.getOperatorName()))
224 | sig_strength = gsm.getSignalStrength()
225 | if sig_strength != -1:
226 | print("Signal strength: "+str(sig_strength)+"dBm")
227 | else:
228 | print("Signal strength: Wrong value")
229 | print("Date from internal clock: "+str(gsm.getDateFromInternalClock()))
230 | print("List of operators: "+str(gsm.getOperatorNames()))
231 | print("Neighbour cells: "+str(gsm.getNeighbourCells()))
232 | print("Accumulated call meter: "+str(gsm.getAccumulatedCallMeter())+" home units")
233 | print("Accumulated call meter max: "+str(gsm.getAccumulatedCallMeterMaximum())+" home units")
234 | print("Is temperature critical: "+str(gsm.isTemperatureCritical()))
235 | print("Is in sleep mode: "+str(gsm.isInSleepMode()))
236 |
237 | # Make the GSM module sleep for 20sec (may be wake up by received call or SMS)
238 | sleep_ok, timer_wake, call_wake, sms_wake, temp_wake = \
239 | gsm.sleep(wake_up_with_timer_in_sec=20, wake_up_with_call=True,
240 | wake_up_with_sms=True)
241 | print("GSM was in sleep mode ("+str(sleep_ok)+"), wake-up by: Timer ("
242 | +str(timer_wake)+") or a call ("+str(call_wake)+") or a SMS ("+str(sms_wake)+")")
243 |
244 | # Reboot (an init is needed to use gsm functions after such a call)
245 | print("Reboot: "+str(gsm.reboot()))
246 |
247 | # Switch off device (gsm will not respond after such a call)
248 | print("Switched off: "+str(gsm.switchOff()))
249 |
250 | # At the end, close connection with GSM module
251 | gsm.close()
252 | ```
253 |
254 | ## Examples
255 |
256 | List of examples:
257 | - Expose GSM module to REST-API
258 |
259 | ## License
260 |
261 | This project is under MIT license. This means you can use it as you want (just don't delete the library header).
262 |
263 | ## Contribute
264 |
265 | If you want to add more examples or improve the library, just create a pull request with proper commit message and right wrapping.
266 |
--------------------------------------------------------------------------------
/TC35_module.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuentinCG/GSM-TC35-Python-Library/d5bd78de29dfa4d0eeb2cdcf8b2ca0284a8bbe0c/TC35_module.jpg
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | codecov:
2 | require_ci_to_pass: yes
3 |
4 | coverage:
5 | ignore:
6 | - "example/**"
7 | - "doc/**"
8 |
--------------------------------------------------------------------------------
/doc/at_commands_tc35.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuentinCG/GSM-TC35-Python-Library/d5bd78de29dfa4d0eeb2cdcf8b2ca0284a8bbe0c/doc/at_commands_tc35.pdf
--------------------------------------------------------------------------------
/examples/rest_api/internal_db.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Helper to create/use SQLite database (insert/delete/get received/sent SMS)
6 | """
7 |
8 | __author__ = 'Quentin Comte-Gaz'
9 | __email__ = "quentin@comte-gaz.com"
10 | __license__ = "MIT License"
11 | __copyright__ = "Copyright Quentin Comte-Gaz (2024)"
12 | __python_version__ = "3.+"
13 | __version__ = "1.0 (2024/09/13)"
14 | __status__ = "Ready for production"
15 |
16 | import os
17 | import sqlite3
18 | import logging
19 |
20 | class InternalDB():
21 | def __init__(self, db_filename):
22 | """Initialize the internal database class"""
23 | self.db_filename = db_filename
24 | self.createDatabaseIfNeeded()
25 |
26 | def createDatabaseIfNeeded(self):
27 | """Create the database needed for the class to work (created at init but can be called again if failed)
28 |
29 | return: (bool) Database created
30 | """
31 | # Create database only if not already exist
32 | if not os.path.exists(self.db_filename):
33 | # Create the database
34 | try:
35 | with sqlite3.connect(self.db_filename) as conn:
36 | logging.debug("Creating database at "+str(self.db_filename))
37 | schema = """CREATE TABLE sms (
38 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
39 | timestamp INTEGER NOT NULL,
40 | received BOOLEAN NOT NULL,
41 | phone_number VARCHAR(30) NOT NULL,
42 | content TEXT NOT NULL
43 | );
44 | """
45 | conn.execute(schema)
46 | except sqlite3.OperationalError as e:
47 | logging.error("Failed to create database: "+str(e))
48 | return False
49 |
50 | self.initialized = True
51 | return True
52 |
53 |
54 | def insertSMS(self, timestamp, received, phone_number, content):
55 | """Insert SMS in the database
56 |
57 | Keyword arguments:
58 | timestamp -- (int) Timestamp of the SMS (when it was sent or received)
59 | received -- (bool) Was it a received SMS (True) or a sent SMS (False) ?
60 | phone_number -- (string) Phone number of the interlocutor
61 | content -- (string) SMS content
62 |
63 | return: (bool) SMS inserted in the database
64 | """
65 | if not self.initialized:
66 | logging.error("Class not initialized")
67 | return False
68 |
69 | if not content:
70 | logging.warning("Empty SMS will not be stored in the database")
71 | return False
72 |
73 | try:
74 | with sqlite3.connect(self.db_filename) as conn:
75 | conn.execute("""
76 | INSERT INTO sms
77 | (timestamp, received, phone_number, content)
78 | VALUES (?, ?, ?, ?)""",
79 | (int(timestamp), bool(received), str(phone_number), str(content)))
80 | except ValueError as e:
81 | logging.error("Failed to prepare request: "+str(e))
82 | return False
83 | except sqlite3.OperationalError as e:
84 | logging.error("Failed to execute request: "+str(e))
85 | return False
86 |
87 | return True
88 |
89 | def deleteSMS(self, sms_id=None, phone_number=None, before_timestamp=None):
90 | """Delete SMS from the database
91 |
92 | WARNING: If no parameters are specified, all SMS will be deleted from the database
93 |
94 | Keyword arguments:
95 | sms_id -- (int, optional) ID of the SMS to delete
96 | phone_number -- (string, optional) Only phone number to delete
97 | before_timestamp -- (int, optional) Maximum timestamp
98 |
99 | return: (bool, int) Success, Number of deleted SMS
100 | """
101 | if not self.initialized:
102 | logging.error("Class not initialized")
103 | return False, 0
104 |
105 | try:
106 | with sqlite3.connect(self.db_filename) as conn:
107 | # Base request
108 | request = "DELETE FROM sms"
109 | params = []
110 | # Potential conditions
111 | if (sms_id is not None) or (phone_number is not None) or (before_timestamp is not None):
112 | request += " WHERE "
113 | if (sms_id is not None):
114 | request += " id = ?"
115 | params.append(int(sms_id))
116 | if (phone_number is not None):
117 | if len(params) > 0:
118 | request += " AND"
119 | request += " phone_number = ?"
120 | params.append(str(phone_number))
121 | if (before_timestamp is not None):
122 | if len(params) > 0:
123 | request += " AND"
124 | request += " timestamp <= ?"
125 | params.append(int(before_timestamp))
126 |
127 | # Do the request
128 | return True, conn.execute(request, params).rowcount
129 |
130 | except ValueError as e:
131 | logging.error("Failed to prepare request: "+str(e))
132 | return False, []
133 | except sqlite3.OperationalError as e:
134 | logging.error("Failed to execute request: "+str(e))
135 | return False, []
136 |
137 | logging.error("Unknown error")
138 | return False, []
139 |
140 | def getSMS(self, phone_number=None, after_timestamp=None, limit=None):
141 | """Get SMS from the database
142 |
143 | Keyword arguments:
144 | phone_number -- (string, optional) Only phone number to get
145 | after_timestamp -- (int, optional) Minimum timestamp
146 | limit -- (int, optional) Max number of SMS to get (note: Not optimized since "ORDER BY" is not usable)
147 |
148 | return: (bool, [{},]) Success, all SMS (with 'id', 'timestamp', 'received', 'phone_number', 'content')
149 | """
150 | if not self.initialized:
151 | logging.error("Class not initialized")
152 | return False, []
153 |
154 | try:
155 | with sqlite3.connect(self.db_filename) as conn:
156 | # Base request
157 | request = "SELECT id, timestamp, received, phone_number, content FROM sms"
158 | params = []
159 | # Potential conditions
160 | if (phone_number is not None) or (after_timestamp is not None):
161 | request += " WHERE"
162 | if (phone_number is not None):
163 | request += " phone_number = ?"
164 | params.append(str(phone_number))
165 | if (after_timestamp is not None):
166 | if (phone_number is not None):
167 | request += " AND"
168 | request += " timestamp >= ?"
169 | params.append(int(after_timestamp))
170 | # Potential limit
171 | if limit is not None:
172 | request += " LIMIT ?"
173 | params.append(int(limit))
174 | # Order
175 | # Note: Order is not handled by default with sqlite3 package...
176 | #request += " ORDER BY timestamp"
177 |
178 | # Do the SQLite request
179 | cursor = conn.cursor()
180 | cursor.execute(request, params)
181 |
182 | # Fetch all SMS
183 | res = []
184 | for row in cursor.fetchall():
185 | sms_id, timestamp, received, phone_number, content = row
186 | sms_data = {}
187 | sms_data["id"] = int(sms_id)
188 | sms_data["timestamp"] = int(timestamp)
189 | sms_data["received"] = bool(received)
190 | sms_data["phone_number"] = str(phone_number)
191 | sms_data["content"] = str(content)
192 | res.append(sms_data)
193 | return True, res
194 | except ValueError as e:
195 | logging.error("Failed to prepare request: "+str(e))
196 | return False, []
197 | except sqlite3.OperationalError as e:
198 | logging.error("Failed to execute request: "+str(e))
199 | return False, []
200 |
201 | logging.error("Unknown error")
202 | return False, []
203 |
204 | # ---- Launch example of use if script executed directly ----
205 | if __name__ == '__main__':
206 | logger = logging.getLogger()
207 | logger.setLevel(logging.DEBUG)
208 |
209 | logging.debug("This is an example of use of the internal DB class:")
210 |
211 | logging.debug("---Creating the database (if doesn't already exist)---")
212 | internal_db = InternalDB("test.db")
213 |
214 | logging.debug("---Inserting dummy SMS in the database---")
215 | res = internal_db.insertSMS(timestamp=5, received=True, phone_number="+33601020304", content="48657920796F752021")\
216 | and internal_db.insertSMS(timestamp=6, received=True, phone_number="+33601020304", content="48657920796F752021")
217 | if not res:
218 | logging.warning("Failed to insert SMS")
219 |
220 | logging.debug("---Reading all SMS from the database---")
221 | res, data = internal_db.getSMS()#phone_number="+33601020304", after_timestamp=5, limit=1)
222 | if not res:
223 | logging.warning("Failed to read all SMS from the database, we will not try to delete SMS then...")
224 | else:
225 | logging.debug("All SMS:\n"+str(data))
226 |
227 | logging.debug("---Deleting first found SMS from the database---")
228 | res, number_of_deleted_sms = internal_db.deleteSMS(sms_id=int(data[0]["id"]))#phone_number="+33601020304", before_timestamp=5)
229 | if not res:
230 | logging.warning("Failed to delete SMS")
231 | else:
232 | logging.debug("Deleted "+str(number_of_deleted_sms)+" SMS")
233 |
--------------------------------------------------------------------------------
/examples/rest_api/rest_api.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | REST API to use the GSM module (in progress):
6 | - Are GSM module and PIN ready to work? (GET http://127.0.0.1:8080/api/ping)
7 | - Check call status (GET http://127.0.0.1:8080/api/call)
8 | - Call (POST http://127.0.0.1:8080/api/call with header data 'phone_number' and optional 'hide_phone_number')
9 | - Hang up call (DELETE http://127.0.0.1:8080/api/call)
10 | - Pick up call (PUT http://127.0.0.1:8080/api/call)
11 | - Get SMS/MMS (GET http://127.0.0.1:8080/api/sms with optional header data 'phone_number', 'after_timestamp' and 'limit')
12 | - Send SMS/MMS (POST http://127.0.0.1:8080/api/sms with header data 'phone_number', 'content' and optional 'is_content_in_hexa_format')
13 | - Delete SMS/MMS (DELETE http://127.0.0.1:8080/api/sms with optional header data 'id', 'phone_number', 'before_timestamp')
14 | - Get module date (GET http://127.0.0.1:8080/api/date)
15 | - Set module date to current date (POST http://127.0.0.1:8080/api/date)
16 | - Get module or SIM information (GET http://127.0.0.1:8080/api/info with header data 'request')
17 |
18 | Requirement:
19 | - Install (pip install) 'flask', 'flask_restful' and 'flask-httpauth', ['pyopenssl']
20 | (or `pip install -e ".[restapi]"` from root folder)
21 |
22 | TODO:
23 | - Get config as file parameters (using 'getopt') instead of hardcoded in file
24 | - Use better authentification (basic-auth is not optimized, token based auth would be more secured): https://blog.miguelgrinberg.com/post/restful-authentication-with-flask
25 | - Have possibility to chose between authentification type (no auth, basic auth, token-based auth)
26 | """
27 | __author__ = 'Quentin Comte-Gaz'
28 | __email__ = "quentin@comte-gaz.com"
29 | __license__ = "MIT License"
30 | __copyright__ = "Copyright Quentin Comte-Gaz (2024)"
31 | __python_version__ = "3.+"
32 | __version__ = "0.2 (2024/09/13)"
33 | __status__ = "Can be used for test but not for production (not fully secured)"
34 |
35 |
36 | from flask import Flask, request
37 | from flask_restful import Resource, Api
38 | from flask_httpauth import HTTPBasicAuth
39 |
40 | from datetime import datetime
41 | import time
42 | import logging
43 | import binascii
44 | import serial
45 |
46 | # Import our internal database helper
47 | from internal_db import InternalDB
48 |
49 | # Relative path to import GSMTC35 (not needed if GSMTC35 installed from pip)
50 | import sys
51 | sys.path.append("../..")
52 |
53 | from GSMTC35 import GSMTC35
54 |
55 |
56 | # ---- Config ----
57 | pin = "1234"
58 | puk = "12345678"
59 | port = "COM8"
60 | api_database_filename = "sms.db"
61 | http_port = 8080
62 | http_prefix = "/api"
63 | BASIC_AUTH_DATA = {
64 | "basic_user": "test"
65 | }
66 | use_debug = True
67 |
68 | # SSL
69 | # - No certificate: None
70 | # - Self signed certificate: 'adhoc'
71 | # - Your own certificate: ('cert.pem', 'key.pem')
72 | # WARNING: Use a certificate for production !
73 | api_ssl_context = None
74 |
75 |
76 | # ---- App base ----
77 | if use_debug:
78 | logger = logging.getLogger()
79 | logger.setLevel(logging.DEBUG)
80 |
81 | app = Flask(__name__)
82 | api = Api(app, prefix=http_prefix)
83 |
84 | api_database = InternalDB(api_database_filename)
85 |
86 | # ---- Authentification (basic-auth) ----
87 | auth = HTTPBasicAuth()
88 |
89 | @auth.verify_password
90 | def verify(username, password):
91 | """Verify basic authentification credentials (confront to BASIC_AUTH_DATA)
92 |
93 | Keyword arguments:
94 | username -- (str) Username
95 | username -- (str) Password
96 |
97 | return: (bool) Access granted?
98 | """
99 | if not (username and password):
100 | return False
101 |
102 | return BASIC_AUTH_DATA.get(username) == password
103 |
104 | # ---- Base functions ----
105 | def getGSM():
106 | """Base function to get initialized GSM class
107 |
108 | return (bool, GSMTC35, string): success, GSM class, error explanation
109 | """
110 | gsm = GSMTC35.GSMTC35()
111 |
112 | try:
113 | if not gsm.isInitialized():
114 | if not gsm.setup(_port="COM8", _pin=pin, _puk=puk):
115 | return False, gsm, str("Failed to initialize GSM/SIM")
116 |
117 | except serial.serialutil.SerialException:
118 | return False, gsm, str("Failed to connect to GSM module")
119 |
120 | return True, gsm, str("")
121 |
122 | def checkBoolean(value):
123 | """Return a bool from a string (or bool)"""
124 | if isinstance(value, bool):
125 | return value
126 | return str(value).lower() == "true" or str(value) == "1"
127 |
128 | # ---- API class ----
129 | class Ping(Resource):
130 | """Are GSM module and PIN ready to work?"""
131 | @auth.login_required
132 | def get(self):
133 | """Are GSM module and PIN ready to work? (GET)
134 |
135 | return (json):
136 | - (bool) 'result': Request worked?
137 | - (str) 'status': Are GSM module and PIN ready to work?
138 | - (str, optional) 'error': Error explanation if request failed
139 | """
140 | valid_gsm, gsm, error = getGSM()
141 | if valid_gsm:
142 | return {"result": True, "status": gsm.isAlive()}
143 | else:
144 | return {"result": False, "error": error}
145 |
146 | class Date(Resource):
147 | """Get module internal date/Set module internal date to current date"""
148 | @auth.login_required
149 | def get(self):
150 | """Get module date as '%m/%d/%Y %H:%M:%S format' (GET)
151 |
152 | return (json):
153 | - (bool) 'result': Request worked?
154 | - (str) 'date': Module date
155 | - (str, optional) 'error': Error explanation if request failed
156 | """
157 | valid_gsm, gsm, error = getGSM()
158 | if valid_gsm:
159 | gsm_date = gsm.getDateFromInternalClock()
160 | if gsm_date != -1:
161 | return {"result": True, "date": gsm_date.strftime("%m/%d/%Y %H:%M:%S")}
162 | else:
163 | return {"result": False, "error": "Module failed to send date in time."}
164 | else:
165 | return {"result": False, "error": error}
166 | @auth.login_required
167 | def post(self):
168 | """Set module date to current computer date (POST)
169 |
170 | return (json):
171 | - (bool) 'result': Request sent?
172 | - (bool) 'status': Module date updated?
173 | - (str, optional) 'error': Error explanation if request failed
174 | """
175 | valid_gsm, gsm, error = getGSM()
176 | if valid_gsm:
177 | return {"result": True, "status": gsm.setInternalClockToCurrentDate()}
178 | else:
179 | return {"result": False, "error": error}
180 |
181 | class Call(Resource):
182 | """Call/Get call status/Pick up call/Hang up call"""
183 | @auth.login_required
184 | def get(self):
185 | """Get current call state (GET)
186 |
187 | return (json):
188 | - (bool) 'result': Request worked?
189 | - (str) 'status': Current call state
190 | - (str, optional) 'error': Error explanation if request failed
191 | """
192 | valid_gsm, gsm, error = getGSM()
193 | if valid_gsm:
194 | phone_status, phone = gsm.getCurrentCallState()
195 | res = {"result": True, "status": GSMTC35.GSMTC35.eCallToString(phone_status)}
196 | if len(phone) > 0:
197 | res["phone"] = phone
198 | return res
199 | else:
200 | return {"result": False, "error": error}
201 | @auth.login_required
202 | def post(self):
203 | """Call specific phone number, possible to hide your phone (POST)
204 |
205 | Header should contain:
206 | - (str) 'phone_number': Phone number to call
207 | - (bool, optional, default: false) 'hide_phone_number': Hide phone number
208 |
209 | return (json):
210 | - (bool) 'result': Request worked?
211 | - (bool) 'status': Call in progress?
212 | - (str, optional) 'error': Error explanation if request failed
213 | """
214 | _phone_number = request.headers.get('phone_number', default = None, type = str)
215 | if _phone_number is None:
216 | return {"result": False, "error": "Please specify a phone number (phone_number)"}
217 | _hide_phone_number = request.headers.get('hide_phone_number', default = "false", type = str)
218 | _hide_phone_number = checkBoolean(_hide_phone_number)
219 | valid_gsm, gsm, error = getGSM()
220 | if valid_gsm:
221 | return {"result": True, "status": gsm.call(phone_number=_phone_number, hide_phone_number=_hide_phone_number)}
222 | else:
223 | return {"result": False, "error": error}
224 | @auth.login_required
225 | def put(self):
226 | """Pick-up call (PUT)
227 |
228 | return (json):
229 | - (bool) 'result': Request worked?
230 | - (bool) 'status': Pick-up worked?
231 | - (str, optional) 'error': Error explanation if request failed
232 | """
233 | valid_gsm, gsm, error = getGSM()
234 | if valid_gsm:
235 | return {"result": True, "status": gsm.pickUpCall()}
236 | else:
237 | return {"result": False, "error": error}
238 | @auth.login_required
239 | def delete(self):
240 | """Hang-up call (DELETE)
241 |
242 | return (json):
243 | - (bool) 'result': Request worked?
244 | - (bool) 'status': Hang-up worked?
245 | - (str, optional) 'error': Error explanation if request failed
246 | """
247 | valid_gsm, gsm, error = getGSM()
248 | if valid_gsm:
249 | return {"result": True, "status": gsm.hangUpCall()}
250 | else:
251 | return {"result": False, "error": error}
252 |
253 | class Sms(Resource):
254 | """Send SMS/Get SMS/Delete SMS"""
255 | @auth.login_required
256 | def get(self):
257 | """Get SMS (GET)
258 |
259 | Header should contain:
260 | - (str, optional, default: All phone number) 'phone_number': Specific phone number to get SMS from
261 | - (int, optional, default: All timestamp) 'after_timestamp': Minimum timestamp (UTC) to get SMS from
262 | - (int, optional, default: No limit) 'limit': Maximum number of SMS to get
263 |
264 | return (json):
265 | - (bool) 'result': Request worked?
266 | - (list of sms) 'sms': List of all found SMS
267 | - (str, optional) 'error': Error explanation if request failed
268 | """
269 | _phone_number = request.headers.get('phone_number', default = None, type = str)
270 | _after_timestamp = request.headers.get('after_timestamp', default = None, type = int)
271 | _limit = request.headers.get('limit', default = None, type = int)
272 | valid_gsm, gsm, error = getGSM()
273 | if valid_gsm:
274 | # Get all SMS from GSM module
275 | all_gsm_sms = gsm.getSMS()
276 | if all_gsm_sms:
277 | # Insert all GSM module SMS into the database
278 | all_mms = []
279 | for gsm_sms in all_gsm_sms:
280 | _timestamp = int(time.mktime(datetime.strptime(str(str(gsm_sms['date']) + " " + str(gsm_sms['time'].split(' ')[0])), "%y/%m/%d %H:%M:%S").timetuple()))
281 | if ('header_multipart_ref_id' in gsm_sms) and ('header_multipart_current_part_nb' in gsm_sms) and ('header_multipart_nb_of_part' in gsm_sms):
282 | all_mms.append(gsm_sms)
283 | else:
284 | if not api_database.insertSMS(timestamp=_timestamp, received=True, phone_number=gsm_sms['phone_number'], content=gsm_sms['sms_encoded']):
285 | logging.warning("Failed to insert SMS into database")
286 |
287 | # Try to merge multipart SMS into MMS before storing them into the database
288 | while len(all_mms) > 0:
289 | ref_id = all_mms[0]['header_multipart_ref_id']
290 | nb_of_part = all_mms[0]['header_multipart_nb_of_part']
291 | _timestamp = int(time.mktime(datetime.strptime(str(str(all_mms[0]['date']) + " " + str(all_mms[0]['time'].split(' ')[0])), "%y/%m/%d %H:%M:%S").timetuple()))
292 | _phone_number = all_mms[0]['phone_number']
293 | parts = {}
294 | parts[int(all_mms[0]['header_multipart_current_part_nb'])] = all_mms[0]['sms_encoded']
295 | all_mms.remove(all_mms[0])
296 | all_sms_to_remove = []
297 |
298 | for sms in all_mms:
299 | if sms['header_multipart_ref_id'] == ref_id:
300 | parts[int(sms['header_multipart_current_part_nb'])] = sms['sms_encoded']
301 | all_sms_to_remove.append(sms)
302 |
303 | for sms_to_remove in all_sms_to_remove:
304 | all_mms.remove(sms_to_remove)
305 |
306 | full_msg = ""
307 | for current_part in range(nb_of_part):
308 | try:
309 | full_msg += parts[current_part+1]
310 | except KeyError:
311 | logging.warning("Missing part of the MMS... Missing part may be received later and will be stored as an other SMS!")
312 |
313 | if not api_database.insertSMS(timestamp=_timestamp, received=True, phone_number=_phone_number, content=full_msg):
314 | logging.warning("Failed to insert SMS into database")
315 |
316 | # Delete all SMS from the module (because they are stored in the database)
317 | gsm.deleteSMS()
318 | # Return all SMS following the right pattern
319 | res, all_db_sms = api_database.getSMS(phone_number=_phone_number, after_timestamp=_after_timestamp, limit=_limit)
320 | if res:
321 | return {"result": True, "sms": all_db_sms}
322 | else:
323 | return {"result": False, "error": "Failed to get SMS from database"}
324 | else:
325 | return {"result": False, "error": error}
326 | @auth.login_required
327 | def post(self):
328 | """Send SMS (POST)
329 |
330 | Header should contain:
331 | - (str) 'phone_number': Phone number to send the SMS
332 | - (str) 'content': Content of the SMS (in utf-8 or hexa depending on other parameters)
333 | - (bool, optional, default: False) 'is_content_in_hexa_format': Is content in hexadecimal format?
334 |
335 | return (json):
336 | - (bool) 'result': Request worked?
337 | - (bool) 'status': SMS sent?
338 | - (str, optional) 'error': Error explanation if request failed
339 | """
340 | _phone_number = request.headers.get('phone_number', default = None, type = str)
341 | if _phone_number is None:
342 | return {"result": False, "error": "Please specify a phone number (phone_number)"}
343 | _content = request.headers.get('content', default = None, type = str)
344 | if _content is None:
345 | return {"result": False, "error": "Please specify a SMS content (content)"}
346 | _is_in_hexa_format = request.headers.get('is_content_in_hexa_format', default = "false", type = str)
347 | _is_in_hexa_format = checkBoolean(_is_in_hexa_format)
348 | valid_gsm, gsm, error = getGSM()
349 | if valid_gsm:
350 | if _is_in_hexa_format:
351 | try:
352 | _content = bytearray.fromhex(_content).decode('utf-8')
353 | except (AttributeError, UnicodeEncodeError, UnicodeDecodeError):
354 | return {"result": False, "error": "Failed to decode content"}
355 | status_send_sms = gsm.sendSMS(_phone_number, _content)
356 | if status_send_sms:
357 | if not api_database.insertSMS(timestamp=int(time.time()), received=False,
358 | phone_number=str(_phone_number),
359 | content=str(binascii.hexlify(_content.encode()).decode())):
360 | logging.warning("Failed to insert sent SMS into the database")
361 | return {"result": True, "status": status_send_sms}
362 | else:
363 | return {"result": False, "error": error}
364 | @auth.login_required
365 | def delete(self):
366 | """Delete SMS (DELETE)
367 |
368 | Header should contain:
369 | - (int, optional, default: All ID) 'id': ID to delete
370 | - (str, optional, default: All phone numbers) 'phone_number': Phone number to delete
371 | - (int, optional, default: All timestamp) 'before_timestamp': Timestamp (UTC) before it should be deleted
372 |
373 | return (json):
374 | - (bool) 'result': Request worked?
375 | - (int) 'count': Number of deleted SMS
376 | - (str, optional) 'error': Error explanation if request failed
377 | """
378 | _id = request.headers.get('id', default = None, type = int)
379 | _phone_number = request.headers.get('phone_number', default = None, type = str)
380 | _before_timestamp = request.headers.get('before_timestamp', default = None, type = int)
381 | result, count_deleted = api_database.deleteSMS(sms_id=_id, phone_number=_phone_number, before_timestamp=_before_timestamp)
382 | if result:
383 | return {"result": True, "count": int(count_deleted)}
384 | else:
385 | return {"result": False, "error": "Failed to delete all SMS from database"}
386 |
387 | class Info(Resource):
388 | """Get information on module or SIM"""
389 | @auth.login_required
390 | def get(self):
391 | """Get Information (GET)
392 |
393 | Header should contain:
394 | - (str) 'request': Request specific data:
395 | - 'last_call_duration': Get last call duration (in sec)
396 | - 'manufacturer': Get manufacturer ID
397 | - 'model': Get model ID
398 | - 'revision': Get revision ID
399 | - 'IMEI': Get IMEI
400 | - 'IMSI': Get IMSI
401 | - 'sleep_mode_status': Check if module in sleep mode (True=sleeping, False=Not sleeping)
402 | - 'current_used_operator': Get currently used operator
403 | - 'signal_strength': Get the signal strength (in dBm)
404 | - 'operators_list': Get list of operators
405 | - 'neighbour_cells_list': Get list of neighbour cells
406 | - 'accumulated_call_meter': Get accumulated call meter (in home units)
407 | - 'max_accumulated_call_meter': Get max accumulated call meter (in home units)
408 | - 'temperature_status': Get module temperature status (True=critical, False=OK)
409 |
410 | return (json):
411 | - (bool) 'result': Request worked?
412 | - (int, str, list) 'result': Result of the request (type depends on the request)
413 | - (str, optional) 'error': Error explanation if request failed
414 | """
415 | _request = request.headers.get('request', default = None, type = str)
416 | if _request is None:
417 | return {"result": False, "error": "'request' not specified"}
418 |
419 | valid_gsm, gsm, error = getGSM()
420 | if valid_gsm:
421 | # Execute the correct request
422 | _request = _request.lower()
423 | if _request == 'last_call_duration':
424 | call_duration = gsm.getLastCallDuration()
425 | if call_duration != -1:
426 | response = call_duration
427 | else:
428 | return {"result": False, "error": "Failed to get last call duration"}
429 | elif _request == 'manufacturer':
430 | response = str(gsm.getManufacturerId())
431 | elif _request == 'model':
432 | response = str(gsm.getModelId())
433 | elif _request == 'revision':
434 | response = str(gsm.getRevisionId())
435 | elif _request == 'imei':
436 | response = str(gsm.getIMEI())
437 | elif _request == 'imsi':
438 | response = str(gsm.getIMSI())
439 | elif _request == 'sleep_mode_status':
440 | response = gsm.isInSleepMode()
441 | elif _request == 'current_used_operator':
442 | response = str(gsm.getOperatorName())
443 | elif _request == 'signal_strength':
444 | sig_strength = gsm.getSignalStrength()
445 | if sig_strength != -1:
446 | response = sig_strength
447 | else:
448 | return {"result": False, "error": "Failed to get signal strength"}
449 | elif _request == 'operators_list':
450 | response = gsm.getOperatorNames()
451 | elif _request == 'neighbour_cells_list':
452 | response = gsm.getNeighbourCells()
453 | elif _request == 'accumulated_call_meter':
454 | acc = gsm.getAccumulatedCallMeter()
455 | if acc != -1:
456 | response = acc
457 | else:
458 | return {"result": False, "error": "Failed to get accumulated call meter"}
459 | elif _request == 'max_accumulated_call_meter':
460 | acc = gsm.getAccumulatedCallMeterMaximum()
461 | if acc != -1:
462 | response = acc
463 | else:
464 | return {"result": False, "error": "Failed to get max accumulated call meter"}
465 | elif _request == 'temperature_status':
466 | response = gsm.isTemperatureCritical()
467 | else:
468 | return {"result": False, "error": "Invalid request parameter"}
469 |
470 | return {"result": True, "response": response}
471 | else:
472 | return {"result": False, "error": error}
473 |
474 | api.add_resource(Call, '/call')
475 | api.add_resource(Ping, '/ping')
476 | api.add_resource(Sms, '/sms')
477 | api.add_resource(Date, '/date')
478 | api.add_resource(Info, '/info')
479 |
480 |
481 | # ---- Launch application ----
482 | if __name__ == '__main__':
483 | app.run(port=http_port, ssl_context=api_ssl_context, debug=use_debug)
484 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file = README.md
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 | import io
3 |
4 | with io.open('README.md', 'r', encoding='utf-8') as readme_file:
5 | readme = readme_file.read()
6 |
7 | setup(
8 | name='GSMTC35',
9 | version='2.1.1',
10 | description='GSM TC35/MC35 controller (Send/Receive SMS/MMS/Call and a lot more!)',
11 | long_description_content_type='text/markdown',
12 | long_description=readme,
13 | url='https://github.com/QuentinCG/GSM-TC35-Python-Library',
14 | author='Quentin Comte-Gaz',
15 | author_email='quentin@comte-gaz.com',
16 | license='MIT',
17 | classifiers=[
18 | 'Development Status :: 5 - Production/Stable',
19 | 'Topic :: Communications :: Telephony',
20 | 'Topic :: Terminals :: Serial',
21 | 'Topic :: Software Development :: Libraries',
22 | 'Topic :: Software Development :: Libraries :: Python Modules',
23 | 'Intended Audience :: Developers',
24 | 'License :: OSI Approved :: MIT License',
25 | 'Operating System :: Microsoft :: Windows',
26 | 'Operating System :: POSIX :: Linux',
27 | 'Operating System :: MacOS',
28 | 'Natural Language :: English',
29 | 'Programming Language :: Python :: 3',
30 | 'Programming Language :: Python :: 3.2',
31 | 'Programming Language :: Python :: 3.3',
32 | 'Programming Language :: Python :: 3.4',
33 | 'Programming Language :: Python :: 3.5',
34 | 'Programming Language :: Python :: 3.6',
35 | 'Programming Language :: Python :: 3.7',
36 | 'Programming Language :: Python :: 3.8',
37 | ],
38 | keywords='gsm pdu tc35 mc35 at sms mms call phone pin puk phonebook imei imsi ucs2 7bit forward unlock lock',
39 | packages=["GSMTC35"],
40 | platforms='any',
41 | install_requires=["pyserial"],
42 | tests_require=["mock"],
43 | test_suite="tests",
44 | extras_require={
45 | 'restapi': ["flask", "flask_restful", "flask-httpauth", "pyopenssl"]
46 | }
47 | )
48 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuentinCG/GSM-TC35-Python-Library/d5bd78de29dfa4d0eeb2cdcf8b2ca0284a8bbe0c/tests/__init__.py
--------------------------------------------------------------------------------