├── .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 | [![PyPI version](https://badge.fury.io/py/GSMTC35.svg)](https://badge.fury.io/py/GSMTC35) [![codecov](https://codecov.io/gh/QuentinCG/GSM-TC35-Python-Library/branch/master/graph/badge.svg)](https://codecov.io/gh/QuentinCG/GSM-TC35-Python-Library) [![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://github.com/QuentinCG/GSM-TC35-Python-Library/blob/master/LICENSE.md) [![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://paypal.me/QuentinCG) [![Downloads](https://static.pepy.tech/badge/GSMTC35)](https://pepy.tech/project/GSMTC35) [![Downloads](https://static.pepy.tech/badge/GSMTC35/month)](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 --------------------------------------------------------------------------------