From df6f462cb0f2c17c87aab59bfaa037af2ea848e6 Mon Sep 17 00:00:00 2001 From: Anthony Correa Date: Thu, 22 Oct 2015 00:00:00 -0600 Subject: [PATCH] commit 10/22/2015 --- cell_connection.py | 83 +++++ config.ini | 21 ++ datahandling.py | 165 ++++++++- instruments.py | 209 +++++++---- lib/sim900/__init__.py | 0 lib/sim900/__stand_alone__.py | 0 lib/sim900/amsharedmini.py | 60 ++++ lib/sim900/gsm.py | 652 +++++++++++++++++++++++++++++++++ lib/sim900/imei.py | 45 +++ lib/sim900/inetgsm.py | 621 ++++++++++++++++++++++++++++++++ lib/sim900/simshared.py | 62 ++++ lib/sim900/smshandler.py | 653 ++++++++++++++++++++++++++++++++++ lib/sim900/ussdhandler.py | 126 +++++++ main.py | 135 ++++--- system.py | 46 ++- test.py | 19 +- test_retrieve_data.py | 12 - 17 files changed, 2731 insertions(+), 178 deletions(-) create mode 100644 cell_connection.py create mode 100644 config.ini create mode 100644 lib/sim900/__init__.py create mode 100644 lib/sim900/__stand_alone__.py create mode 100644 lib/sim900/amsharedmini.py create mode 100644 lib/sim900/gsm.py create mode 100644 lib/sim900/imei.py create mode 100644 lib/sim900/inetgsm.py create mode 100644 lib/sim900/simshared.py create mode 100644 lib/sim900/smshandler.py create mode 100644 lib/sim900/ussdhandler.py delete mode 100644 test_retrieve_data.py diff --git a/cell_connection.py b/cell_connection.py new file mode 100644 index 0000000..865b083 --- /dev/null +++ b/cell_connection.py @@ -0,0 +1,83 @@ +__author__ = 'asc' + +from lib.sim900.gsm import SimGsm, SimGsmPinRequestState +from lib.sim900.imei import SimImeiRetriever +import serial +import logging +import sys + +def initializeUartPort( + portName = "/dev/ttyAMA0", + baudrate = 9600, + bytesize = serial.EIGHTBITS, + parity = serial.PARITY_NONE, + stopbits = serial.STOPBITS_ONE, + timeout = 0 + ): + + port = serial.Serial() + + #tuning port object + port.port = portName + port.baudrate = baudrate + port.bytesize = bytesize + port.parity = parity + port.stopbits = stopbits + port.timeout = timeout + + return port + +def initializeLogs(loggerLevel, consoleLoggerLevel): + #initializing logging formatter + formatter = logging.Formatter('[%(asctime)s] %(levelname)s: %(message)s') + + #initializing logger + logger = logging.getLogger(__name__) + logger.setLevel(loggerLevel) + + #initializing console handler for logging + consoleLogger = logging.StreamHandler(sys.stdout) + consoleLogger.setLevel(consoleLoggerLevel) + consoleLogger.setFormatter(formatter) + + #adding console appender + logger.addHandler(consoleLogger) + + return (formatter, logger, consoleLogger,) + +def baseOperations(port, logger): + #class for general functions + gsm = SimGsm(port, logger) + + #opening COM port + logger.info("opening port") + if not gsm.openPort(): + logger.error("error opening port: {0}".format(gsm.errorText)) + return None + + #initializing session with SIM900 + logger.info("initializing SIM900 session") + if not gsm.begin(5): + logger.error("error initializing session: {0}".format(gsm.errorText)) + return None + + logger.debug("checking PIN state") + if gsm.pinState != SimGsmPinRequestState.NOPINNEEDED: + logger.debug("PIN needed, entering") + if gsm.pinState == SimGsmPinRequestState.SIM_PIN: + if not gsm.enterPin("1111"): + logger.error("error entering PIN") + else: + logger.debug("PIN OK") + + #retrieving IMEI + sim = SimImeiRetriever(port, logger) + logger.info("retrieving IMEI") + imei = sim.getIMEI() + if imei is None: + logger.error("error retrieving IMEI: {0}".format(sim.errorText)) + return None + + logger.info("IMEI = {0}".format(imei)) + + return (gsm, imei) \ No newline at end of file diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..d10c58c --- /dev/null +++ b/config.ini @@ -0,0 +1,21 @@ +[refresh rates] +refresh camera local = 2 +refresh camera transmit = 5 + +refresh barometer local = 10 +refresh barometer transmit = 5 + +refresh gps local = 2 +refresh gps transmit = 5 + +[camera settings] +low quality resolution = (640, 360) +low quality compression pct = 20 + +[report settings] +report url = "http://10.0.1.4:5010/report" +report image url = "http://10.0.1.4:5010/photo" + +[modem settings] +com port name = "/dev/ttyAMA0" +baud rate = "9600" \ No newline at end of file diff --git a/datahandling.py b/datahandling.py index a8e64a8..64e3cef 100644 --- a/datahandling.py +++ b/datahandling.py @@ -1,13 +1,21 @@ __author__ = 'asc' import os import datetime -from urllib import request +from urllib import request, parse import requests import json +from cell_connection import initializeUartPort, baseOperations, initializeLogs +import logging + +# logging.basicConfig(level=logging.INFO) + + +logger = logging.getLogger(__name__) +LOGGER_LEVEL=logging.DEBUG +CONSOLE_LOGGER_LEVEL=logging.DEBUG SCRIPTDIR = os.path.dirname(os.path.abspath(__file__)) -REPORTTOURL = "http://10.0.1.4:5010/report" -REPORTIMAGETOURL = "http://10.0.1.4:5010/photo" + class Datalogger_Debug (): def __init__(self, path=SCRIPTDIR): @@ -18,6 +26,8 @@ class Datalogger_Debug (): print ("%s - Log Message: %s"% (str(datetime.datetime.now()), message)) elif type == "image": print ("%s - Log Image: %s"% (str(datetime.datetime.now()), message)) + elif type == "data": + print ("%s - Log Message: %s"% (str(datetime.datetime.now()), message)) else: raise Exception @@ -41,16 +51,26 @@ class Datareporter_Debug (): #todo error handling raise Exception + class Datareporter_Debug2 (): - def __init__(self, path=SCRIPTDIR): - #TODO communication + #Debug 2 sends to server + def __init__(self, path=SCRIPTDIR, + report_url = "http://10.0.1.4:5010/report", + report_image_url = "http://10.0.1.4:5010/photo", + com_port_name = "/dev/ttyAMA0", + baud_rate = "9600"): + self.report_url = report_url + self.report_image_url = report_image_url + self.com_port_name = com_port_name, + self.baud_rate = baud_rate pass + @property def status (self): #TODO status check try: - check = json.loads(request.urlopen(REPORTTOURL).read().decode()).get('status') + check = json.loads(request.urlopen(self.report_url).read().decode()).get('status') pass if not check: return (0, "Data reporter functioning properly") @@ -68,18 +88,143 @@ class Datareporter_Debug2 (): print ("%s - Sent Message: %s"% (str(datetime.datetime.now()), message)) elif type == "image": #todo send image - response = requests.post(REPORTIMAGETOURL, files={'file': message}) + response = requests.post(self.report_url, files={'file': message}) print ("%s - Sent Image: %s"% (str(datetime.datetime.now()), message)) elif type == "data": #add date to message message['sent']=str(datetime.datetime.now()) - req = request.Request(REPORTTOURL) + req = request.Request(self.report_image_url) req.add_header('Content-Type', 'application/json') response = request.urlopen(req,json.dumps(message).encode()) the_page = response.read() - return 0 except Exception as e: #todo error handling - return (0, "Reporter error: %s" % e) + pass + +class Datareporter_Debug3 (): + #Debug 2 sends from cell to server + from lib.sim900.inetgsm import SimInetGSM + + def __init__(self, path=SCRIPTDIR, + report_url = "http://10.0.1.4:5010/report", + report_image_url = "http://10.0.1.4:5010/photo", + com_port_name = "/dev/ttyAMA0", + baud_rate = "9600"): + #TODO communication + self.report_url = report_url + self.report_image_url = report_image_url + self.com_port_name = com_port_name, + self.baud_rate = baud_rate + + pass + + @property + def status (self): + #TODO status check + try: + check = json.loads(request.urlopen(self.report_url).read().decode()).get('status') + pass + if not check: + return (0, "Data reporter functioning properly") + else: + return (1, check) + except Exception as e: + return (1, "Data reporter error: %s" % e) + + def send(self, message, type="text"): + if type == "text": + #TODO send text + print ("%s - Sent Message: %s"% (str(datetime.datetime.now()), message)) + elif type == "image": + #todo send image + response = requests.post(self.report_image_url, files={'file': message}) + print ("%s - Sent Image: %s"% (str(datetime.datetime.now()), message)) + + #adding & initializing port object + port = initializeUartPort(portName=self.com_port_name, baudrate=self.baud_rate) + + #initializing logger + (formatter, logger, consoleLogger,) = initializeLogs(LOGGER_LEVEL, CONSOLE_LOGGER_LEVEL) + + #making base operations + d = baseOperations(port, logger) + if d is None: + return False + + (gsm, imei) = d + + inet = self.SimInetGSM(port, logger) + + logger.info("attaching GPRS") + if not inet.attachGPRS("internet", "", "", 1): + logger.error("error attaching GPRS") + return False + + logger.info("ip = {0}".format(inet.ip)) + + #making HTTP GET request + logger.info("making HTTP POST request") + + + + if not inet.httpPOST_DATA( + "home.ascorrea.com", + 5010, + "/photo", + "file={0}".format(message) + ): + logger.error("error making HTTP GET post: {0}".format(inet.errorText)) + return False + + logger.info("httpResult = {0}".format(inet.httpResult)) + if inet.httpResponse is not None: + response = str(inet.httpResponse).replace("\n\r", "\n") + logger.info("response: \"{0}\"".format(response)) + else: + logger.info("empty response") + + elif type == "data": + #adding & initializing port object + port = initializeUartPort(portName=self.com_port_name, baudrate=self.baud_rate) + + #initializing logger + (formatter, logger, consoleLogger,) = initializeLogs(LOGGER_LEVEL, CONSOLE_LOGGER_LEVEL) + + #making base operations + d = baseOperations(port, logger) + if d is None: + return False + + (gsm, imei) = d + + inet = self.SimInetGSM(port, logger) + + logger.info("attaching GPRS") + if not inet.attachGPRS("internet", "", "", 1): + logger.error("error attaching GPRS") + return False + + logger.info("ip = {0}".format(inet.ip)) + + #making HTTP GET request + logger.info("making HTTP POST request") + + + + if not inet.httpPOST( + "home.ascorrea.com", + 5010, + "/report-encoded", + parse.urlencode(message) + ): + logger.error("error making HTTP GET post: {0}".format(inet.errorText)) + return False + + logger.info("httpResult = {0}".format(inet.httpResult)) + if inet.httpResponse is not None: + response = str(inet.httpResponse).replace("\n\r", "\n") + logger.info("response: \"{0}\"".format(response)) + else: + logger.info("empty response") diff --git a/instruments.py b/instruments.py index f818b6f..544db7f 100644 --- a/instruments.py +++ b/instruments.py @@ -1,100 +1,157 @@ __author__ = 'asc' debug = True from random import random +import Adafruit_BMP.BMP085 as BMP085 +import picamera +import configparser +import time +import logging +logger = logging.getLogger(__name__) class Barometer_Debug (): - def __init__(self): - pass + def __init__(self): + pass - @property - def status (self): - return (0, "Barometer functioning properly") + @property + def status (self): + return (0, "Barometer functioning properly") - @property - def temperature (self): - if self.status[0]: - return 'error' + @property + def temperature (self): + if self.status[0]: + return 'error' - if debug: - temp = random()*100 - else: - raise Exception ('Not Debug') - return temp + if debug: + temp = random()*100 + else: + raise Exception ('Not Debug') + return temp - @property - def pressure (self): - if (self.status[0]): - return 'error' + @property + def pressure (self): + if (self.status[0]): + return 'error' - if debug: - press = random() - else: - raise Exception ('Not Debug') - return press + if debug: + press = random() + else: + raise Exception ('Not Debug') + return press - @property - def altitude (self): - if self.status[0]: - return 'error' - if debug: - alt = random()*100000 - else: - raise Exception ('Not Debug') + @property + def altitude (self): + if self.status[0]: + return 'error' + if debug: + alt = random()*100000 + else: + raise Exception ('Not Debug') - return alt + return alt class Camera_Debug(): - def __init__(self): - pass + def __init__(self): + pass - @property - def status (self): - return (0, "Camera functioning properly") + @property + def status (self): + return (0, "Camera functioning properly") - def capture (self): - #todo capture image - return ({"file":open("image.jpg", 'rb')}) + def capture (self): + #todo capture image + return ({"file":open("image2.jpg", 'rb')}) +#camera debug 2 uses actual camera +class Camera_Debug2(): + def __init__(self, low_quality_resolution, + low_quality_compression_pct, + high_quality_resolution=None, + high_qualitycompression_pct=85): + logger.debug("Initializing camera") + time.sleep(1) + self.low_quality_resolution = low_quality_resolution + self.low_quality_compression_pct = low_quality_compression_pct + self._cam = picamera.PiCamera() + logger.debug("Camera intialized") + pass + + @property + def status (self): + return (0, "Camera functioning properly") + + def capture (self, no_low_quality=False, no_high_quality=False): + #todo image adjustments + img_hi = None + img_lo = None + if not no_high_quality: + logger.debug('Taking high quality photo') + self._cam.capture("temp_img_hi.jpg") + img_hi = open("temp_img_hi.jpg", 'rb') + logger.debug('High quality photo taken, file: {}'.format(img_hi)) + if not no_low_quality: + logger.debug('Taking low quality photo (Resolution: {}, JPEG Quality: {}%'.format(self.low_quality_resolution, self.low_quality_compression_pct)) + self._cam.capture("temp_img_lo.jpg", + resize=self.low_quality_resolution, + quality=self.low_quality_compression_pct) + img_lo = open("temp_img_lo.jpg", 'rb') + logger.debug('Low quality photo taken, file: {}'.format(img_lo)) + + return ({"hi":img_hi, + "lo":img_lo}) #barometerdebug2 uses actual barometer -# class Barometer_Debug2 (): -# import Adafruit_BMP.BMP085 as BMP085 -# def __init__(self): -# self.bmp = BMP085.BMP085() -# pass -# -# @property -# def status (self): -# return (0, "Barometer functioning properly") -# -# @property -# def temperature (self): -# if self.status[0]: -# return 'error' -# -# temp = self.bmp.read_temperature() -# return temp -# -# @property -# def pressure (self): -# if (self.status[0]): -# return 'error' -# -# # press = 100000*random() -# press = self.bmp.read_pressure() -# -# return press -# -# @property -# def altitude (self): -# if self.status[0]: -# return 'error' -# #TODO Set the altitude of your current location in meter -# alt = self.bmp.read_altitude() -# # alt = 100000*random() -# return alt +class Barometer_Debug2 (): + def __init__(self): + logger.debug("Intializing barometer") + self.bmp = BMP085.BMP085() + logger.debug("Barometer intilized") + pass + + @property + def status (self): + return (0, "Barometer functioning properly") + + @property + def temperature (self): + if self.status[0]: + return 'error' + + temp = self.bmp.read_temperature() + return temp + + @property + def pressure (self): + if (self.status[0]): + return 'error' + +# press = 100000*random() + press = self.bmp.read_pressure() + + return press + + @property + def altitude (self): + if self.status[0]: + return 'error' + #TODO Set the altitude of your current location in meter + alt = self.bmp.read_altitude() + return alt + + def read(self): + logger.debug("Reading from barometer") + #refresh each instrument + alt = self.altitude + press = self.pressure + temp = self.temperature + result = {"altitude":alt, + "temperature":temp, + "pressure":press, + } + logger.debug("Barometer reads {}".format(result)) + + return result diff --git a/lib/sim900/__init__.py b/lib/sim900/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/sim900/__stand_alone__.py b/lib/sim900/__stand_alone__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/sim900/amsharedmini.py b/lib/sim900/amsharedmini.py new file mode 100644 index 0000000..037510a --- /dev/null +++ b/lib/sim900/amsharedmini.py @@ -0,0 +1,60 @@ +__author__ = 'Bohdan' + +import time + +class AminisLastErrorHolder: + def __init__(self): + self.errorText = "" + self.__hasError = False + + def clearError(self): + self.errorText = "" + self.__hasError = False + + def setError(self, errorText): + self.errorText = errorText + self.__hasError = True + + @property + def hasError(self): + return self.__hasError + +def timeDelta(timeBegin): + end = time.time() + secs = end - timeBegin + msecs = (end - timeBegin) * 1000.0 + + return secs*1000 + msecs + +def splitAndFilter(value, separator): + items = str(value).split(separator) + ret = [] + + for item in items: + item = str(item).strip() + if len(item) == 0: + continue + + ret += [item] + + return ret + +def isFloat(value): + try: + float(value) + return True + except ValueError: + return False + +def strToFloat(value): + value = str(value).strip() + + if len(value) == 0: + return None + + value = value.replace(",", ".") + + try: + return float(value) + except ValueError: + return None \ No newline at end of file diff --git a/lib/sim900/gsm.py b/lib/sim900/gsm.py new file mode 100644 index 0000000..4fc136f --- /dev/null +++ b/lib/sim900/gsm.py @@ -0,0 +1,652 @@ +#The MIT License (MIT) +# +#Copyright (c) 2014-2015 Bohdan Danishevsky ( dbn@aminis.com.ua ) +# +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: +# +#The above copyright notice and this permission notice shall be included in all +#copies or substantial portions of the Software. +# +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +#SOFTWARE. + +""" +This file is part of sim-module package. Implements basic functions of SIM900 modules. + +sim-module package allows to communicate with SIM 900 modules: send SMS, make HTTP requests and use other +functions of SIM 900 modules. + +Copyright (C) 2014-2015 Bohdan Danishevsky ( dbn@aminis.com.ua ) All Rights Reserved. +""" + +import time +import serial +import logging +from lib.sim900.simshared import * + +class GsmSpecialCharacters: + ctrlz = 26 #//Ascii character for ctr+z. End of a SMS. + cr = 0x0d #//Ascii character for carriage return. + lf = 0x0a #//Ascii character for line feed. + +class SimGsmState: + UNKNOWN = 0 + ERROR = 1 + IDLE = 2 + READY = 3 + ATTACHED = 4 + TCPSERVERWAIT = 5 + TCPCONNECTEDSERVER = 6 + TCPCONNECTEDCLIENT = 7 + + +class SimGsmPinRequestState: + UNKNOWN = -1 + NOPINNEEDED = 0 + + SIM_PIN = 1 + SIM_PUK = 2 + + PH_SIM_PIN = 3 + PH_SIM_PUK = 4 + + SIM_PIN2 = 5 + SIM_PUK2 = 6 + +class SimGsmSerialPortHandler(AminisLastErrorHolderWithLogging): + def __init__(self, serial, logger = None): + AminisLastErrorHolderWithLogging.__init__(self, logger) + self.input = bytearray() + self.__serial = serial + + #stores last executed command result + self.lastResult = None + + def openPort(self): + try: + self.__serial.open() + self.flush() + except Exception as e: + self.setError("exception till port openning: {0}".format(e)) + return False + except: + self.setError("error opening port") + return False + + return True + + def __sendRawBytes(self, data, maxWaitTime = 1000): + """ + Sends raw bytes to the SIM module + + :param data: data which must be send + :param maxWaitTime: max wait time for sending sequence + :return: True if data was send, otherwise returns False + """ + bytesToSend = len(data) + sentBytes = 0 + start = time.time() + + self.logger.debug("{0}, sending: {1}".format(inspect.stack()[0][3], data)) + + while sentBytes < bytesToSend: + if timeDelta(start) >= maxWaitTime: + self.setWarn("__sendRawBytes(): timed out") + return False + + sentBytes += self.__serial.write(data[sentBytes : ]) + if sentBytes == 0: + time.sleep(0.001) + continue + + return True + + def print(self, commandString, encoding = "ascii"): + """ + Sends string data to the SIM module + + :param commandString: data what must be sent + :param encoding: before sending string it will be converted to the bytearray with this encoding + :return: True if everything is OK, otherwise returns false + """ + data = bytearray(commandString, encoding) + return self.__sendRawBytes(data) + + def simpleWrite(self, commandLine, encoding = "ascii"): + """ + Just alias for print() method + + :param commandLine: data which must be sent + :param encoding: before sending string it will be converted to the bytearray with this encoding + :return: True if data sent, otherwise returns False + """ + return self.print(commandLine, encoding) + + def printLn(self, commandString, encoding = "ascii"): + """ + Sends string data and CR/LF in the end to the SIM module + + :param commandString: data which must be sent + :param encoding: before sending string it will be converted to the bytearray with this encoding + :return: True if data sent, otherwise returns False + """ + data = bytearray(commandString, encoding) + bytearray([GsmSpecialCharacters.cr, GsmSpecialCharacters.lf]) + return self.__sendRawBytes(data) + + def simpleWriteLn(self, commandLine, encoding = "ascii"): + """ + Just alias for printLn() method + + :param commandLine: data which must be sent + :param encoding: before sending string it will be converted to the bytearray with this encoding + :return: True if data sent, otherwise returns False + """ + + return self.printLn(commandLine, encoding) + + def flushInput(self): + """ + Flushes input buffer + + :return: nothing + """ + try: + self.__serial.flushInput() + except Exception as e: + self.setError("error flushing: {0}".format(e)) + except: + self.setError("error flushing") + + def flushOutput(self): + """ + Flushes output buffer + + :return: nothing + """ + try: + self.__serial.flushOutput() + except Exception as e: + self.setError("error flushing: {0}".format(e)) + except: + self.setError("error flushing") + + def readFixedSzieByteArray(self, bytesCount, maxWaitTime): + start = time.time() + buffer = bytearray() + try: + while True: + #checking for timeout + if timeDelta(start) >= maxWaitTime: + return None + + receivedBytesQty = 0 + while True: + bytesToRead = 10 if ((bytesCount - len(buffer)) >= 10) else 1 + b = self.__serial.read(bytesToRead) + + if (b is None) or (len(b) == 0): + break + + buffer += bytearray(b) + receivedBytesQty += len(b) + + if len(buffer) == bytesCount: + return buffer + + #if we have nothing in input - let's go sleep for some time + if receivedBytesQty == 0: + time.sleep(0.003) + + #comming there by timeout + return None + + except Exception as e: + self.setError(e) + return None + except: + self.setError("reading error...") + return None + + + def readNullTerminatedLn(self, maxWaitTime = 5000, codepage = "ascii"): + start = time.time() + + start = time.time() + buffer = bytearray() + try: + while True: + #checking for timeout + if timeDelta(start) >= maxWaitTime: + return None + + receivedBytesQty = 0 + while True: + b = self.__serial.read(1) + + if (b is None) or (len(b) == 0): + break + + #checking that we have NULL symbol in + idx = b.find(0x00) + if idx != -1: + buffer.extend(b[:idx]) + return buffer.decode(codepage) + + buffer += bytearray(b) + receivedBytesQty += len(b) + + #if we have nothing in input - let's go sleep for some time + if receivedBytesQty == 0: + time.sleep(0.003) + + #comming there by timeout + return None + + except Exception as e: + self.setError(e) + return None + except: + self.setError("reading error...") + return None + + def readLn(self, maxWaitTime = 5000, codepage = "ascii"): + """ + Returns text string from SIM module. Can return even empty strings. + + :param maxWaitTime: max wait interval for operation + :param codepage: code page of result string + :return: received string + """ + start = time.time() + buffer = bytearray() + try: + while True: + #checking for timeout + if timeDelta(start) >= maxWaitTime: + return None + + receivedBytesQty = 0 + while True: + b = self.__serial.read(1) + + if (b is None) or (len(b) == 0): + break + + buffer += bytearray(b) + receivedBytesQty += len(b) + + if codepage is not None: + #checking for line end symbols + line = buffer.decode(codepage) + if '\n' in line: + return line.strip() + elif ord('\n') in buffer: + return buffer + + #if we have nothing in input - let's go sleep for some time + if receivedBytesQty == 0: + time.sleep(0) + + #comming there by timeout + return None + + except Exception as e: + self.setError(e) + return None + except: + self.setError("reading error...") + return None + + def readDataLine(self, maxWaitTime = 500, codepage = "ascii"): + """ + Returns non empty data string. So, if it will receive empty string function will continue non empty string + retrieving + + :param maxWaitTime: max wait time for receiving + :param codepage: code page of result string, if it's a None - will return a bytearray + :return: received string + """ + ret = None + start = time.time() + + while True: + #checking for timeout + if timeDelta(start) >= maxWaitTime: + break + + #reading string + #TODO: need to fix timeout (substract already spent time interval) + line = self.readLn(maxWaitTime, codepage) + + #removing garbage symbols + if line is not None: + line = str(line).strip() + + #if we have non empty string let's return it + if len(line) > 0: + return line + else: + #if we have empty line - let's continue reading + continue + else: + #returning None if None received + if line is None: + return None + + continue + + #we will come here by timeout + return None + + def flush(self): + """ + Flushes input and output buffers + + :return: nothing + """ + try: + self.__serial.flush() + except Exception as e: + self.setError("error flushing: {0}".format(e)) + except: + self.setError("error flushing") + + def closePort(self): + """ + Closes COM port + + :return: nothing + """ + try: + self.__serial.close() + except Exception as e: + self.setError("error closing port: {0}".format(e)) + except: + self.setError("error closing port") + + @staticmethod + def isCrLf(symbol): + """ + Returns True when parameter is CR/LF symbol, otherwise returns False + + :param symbol: symbol for analysis + :return: True when CR/LF symbol, otherwise returns False + """ + return (symbol == GsmSpecialCharacters.cr) or (symbol == GsmSpecialCharacters.lf) + + @staticmethod + def getLastNonEmptyString(strings): + """ + Parses strings array and returns last non empty string from array + + :param strings: strings array for analysis + :return: last non empty string, otherwise None + """ + + #if there is no data - returning None + if strings is None: + return None + + qty = len(strings) + if qty == 0: + return None + + #looking for last non empty string + for i in range(qty): + s = str(strings[-(i+1)]).strip() + if len(s) > 0: + return s + + return None + + @staticmethod + def removeEndResult(strings, targetString): + """ + Searches and removes last string which contains result + :param strings: + :param targetString: + :return: + """ + ret = "" + + #searching for target string + while len(strings) > 0: + s = str(strings[-1]).strip() + + strings.pop(len(strings)-1) + if s == targetString: + break + + #compiling result + qty = len(strings) + for i in range(qty): + ret += strings[i] + + return ret + + @staticmethod + def parseStrings(buffer, encoding = "ascii"): + """ + Parses string (from given encoding), looks for cr/lf and retutrns strings array + :param buffer: input string + :param encoding: encoding + :return: strings array + """ + + #decoding + bigString = buffer.decode(encoding) + + #searching for cr/lf and making strings array + if "\r" in bigString: + ret = bigString.split("\r") + else: + ret = [bigString] + + return ret + + def commandAndStdResult(self, commandText, maxWaitTime = 5000, possibleResults = None): + self.lastResult = None + + #setting up standard results + if possibleResults is None: + possibleResults = ["OK", "ERROR"] + + start = time.time() + buffer = bytearray() + + self.flush() + + #sending command + self.simpleWriteLn(commandText) + + try: + while True: + if timeDelta(start) >= maxWaitTime: + break + + readBytesQty = 0 + while True: + b = self.__serial.read(100) + + if (b is not None) and (len(b) >= 1): + buffer += bytearray(b) + self.logger.debug("{0}: buffer = {1}".format(inspect.stack()[0][3], buffer)) + + readBytesQty += len(b) + continue + else: + break + + #if we have no data - let's go sleep for tiny amount of time + if readBytesQty == 0: + time.sleep(0.005) + continue + + #parsing result strings + strings = SimGsm.parseStrings(buffer[:]) + self.logger.debug("{0}: strings = {1}".format(inspect.stack()[0][3], strings)) + + if strings is None: + time.sleep(0.01) + continue + + #if we have some strings let's parse it + if len(strings) > 0: + lastString = SimGsm.getLastNonEmptyString(strings[:]) + + if lastString in possibleResults: + self.lastResult = lastString + return SimGsm.removeEndResult(strings[:], lastString) + + time.sleep(0.05) + + return None + except Exception as e: + self.setError(e) + return None + except: + self.setError("reading error...") + return None + + def execSimpleCommand(self, commandText, result, timeout = 500): + ret = self.commandAndStdResult(commandText, timeout, [result]) + if (ret is None) or (self.lastResult != result): + return False + + return True + + def execSimpleOkCommand(self, commandText, timeout = 500): + self.logger.debug("executing command '{0}'".format(commandText)) + + ret = self.commandAndStdResult(commandText, timeout, ["OK", "ERROR"]) + if (ret is None) or (self.lastResult != "OK"): + return False + + return True + + def execSimpleCommandsList(self, commandsList): + for command in commandsList: + if not self.execSimpleOkCommand(command[0], command[1]): + return False + + return True + +class SimGsm(SimGsmSerialPortHandler): + def __init__(self, serial, logger = None): + SimGsmSerialPortHandler.__init__(self, serial, logger) + + self.__state = SimGsmState.UNKNOWN + self.pinState = SimGsmPinRequestState.UNKNOWN + + def begin(self, numberOfAttempts = 5): + ok = False + + self.flush() + + needDisableEcho = False + + for i in range(numberOfAttempts): + self.printLn("AT") + line = self.readDataLine(2000, "ascii") + + #if we do not have something in input - let's go sleep + if line is None: + time.sleep(0.2) + continue + + #we have echo, need to reconfigure + if line == "AT": + #we have ECHO, need reconfigure + needDisableEcho = True + line = self.readDataLine(500, "ascii") + if line == "OK": + ok = True + break + + elif line == "OK": + ok = True + break + + if not ok: + return False + + #disabling echo if needed + if needDisableEcho: + self.logger.info("Disabling echo, calling 'ATE0'") + self.simpleWriteLn("ATE0") + time.sleep(0.5) + self.flush() + + commands = [ + ["ATV1", 500], #short answer for commands + ["AT+CMEE=0", 500], #disabling error report + ["AT", 5000] #checking state + ] + + for cmd in commands: + self.logger.debug("configuring, calling: {0}".format(cmd[0])) + if not self.execSimpleOkCommand(commandText=cmd[0],timeout=cmd[1]): + return False + + #checking PIN state + if not self.__checkPin(): + return False + + return True + + def __checkPin(self): + msg = self.commandAndStdResult("AT+CPIN?") + if msg is None: + return False + + if self.lastResult != "OK": + return False + + msg = str(msg).strip() + + values = splitAndFilter(msg, ":") + msg.split(":") + + if len(values) < 2: + self.setError("Wrong response for PIN state request") + return False + + if values[0] != "+CPIN": + self.setError("Wrong response for PIN state request. First value = '{0}'".format(values[0])) + return False + + v = " ".join([v for v in values[1:]]) + + if v == "READY": + self.pinState = SimGsmPinRequestState.NOPINNEEDED + elif v == "SIM PIN": + self.pinState = SimGsmPinRequestState.SIM_PIN + elif v == "SIM PUK": + self.pinState = SimGsmPinRequestState.SIM_PUK + elif v == "PH_SIM PIN": + self.pinState = SimGsmPinRequestState.PH_SIM_PIN + elif v == "PH_SIM PUK": + self.pinState = SimGsmPinRequestState.PH_SIM_PUK + elif v == "SIM PIN2": + self.pinState = SimGsmPinRequestState.SIM_PIN2 + elif v == "SIM PUK2": + self.pinState = SimGsmPinRequestState.SIM_PUK2 + else: + self.pinState = SimGsmPinRequestState.UNKNOWN + self.setError("Unknown PIN request answer: {0}".format(v)) + return False + + return True + + def enterPin(self, pinCode): + return self.execSimpleOkCommand("AT+CPIN=\"{0}\"".format(pinCode)) diff --git a/lib/sim900/imei.py b/lib/sim900/imei.py new file mode 100644 index 0000000..343f93b --- /dev/null +++ b/lib/sim900/imei.py @@ -0,0 +1,45 @@ +#The MIT License (MIT) +# +#Copyright (c) 2014-2015 Bohdan Danishevsky ( dbn@aminis.com.ua ) +# +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: +# +#The above copyright notice and this permission notice shall be included in all +#copies or substantial portions of the Software. +# +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +#SOFTWARE. + +""" +This file is part of sim-module package. Can be used for SIM900 module IMEI retrieving + +sim-module package allows to communicate with SIM 900 modules: send SMS, make HTTP requests and use other +functions of SIM 900 modules. + +Copyright (C) 2014-2015 Bohdan Danishevsky ( dbn@aminis.com.ua ) All Rights Reserved. +""" + +from lib.sim900.gsm import SimGsm + +class SimImeiRetriever(SimGsm): + def __init__(self, port, logger): + SimGsm.__init__(self, port, logger) + + def getIMEI(self): + self.logger.debug("retrieving IMEI") + + data = self.commandAndStdResult("AT+GSN", 1000) + if data is None: + return None + + return str(data).strip() \ No newline at end of file diff --git a/lib/sim900/inetgsm.py b/lib/sim900/inetgsm.py new file mode 100644 index 0000000..c62c165 --- /dev/null +++ b/lib/sim900/inetgsm.py @@ -0,0 +1,621 @@ +#The MIT License (MIT) +# +#Copyright (c) 2014-2015 Bohdan Danishevsky ( dbn@aminis.com.ua ) +# +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: +# +#The above copyright notice and this permission notice shall be included in all +#copies or substantial portions of the Software. +# +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +#SOFTWARE. + +""" +This file is part of sim-module package. Can be used for HTTP requests making. + +sim-module package allows to communicate with SIM 900 modules: send SMS, make HTTP requests and use other +functions of SIM 900 modules. + +Copyright (C) 2014-2015 Bohdan Danishevsky ( dbn@aminis.com.ua ) All Rights Reserved. +""" + +from lib.sim900.gsm import * + +class SimInetGSMConnection: + inetUnknown = -1 + inetConnecting = 0 + inetConnected = 1 + inetClosing = 2 + inetClosed = 3 + +class SimInetGSM(SimGsm): + def __init__(self, port, logger): + SimGsm.__init__(self, port, logger) + + self.__ip = None + + #user agent + self.__userAgent = "Aminis SIM-900 module client (version 0.1)" + self.__connectionState = SimInetGSMConnection.inetUnknown + self.__httpResult = 0 + self.__httpResponse = None + + @property + def connectionState(self): + return self.__connectionState + + @property + def httpResult(self): + return self.__httpResult + + @property + def httpResponse(self): + return self.__httpResponse + + @property + def ip(self): + return self.__ip + + @property + def userAgent(self): + return self.__userAgent + + @userAgent.setter + def userAgent(self, value): + self.__userAgent = value + + def checkGprsBearer(self, bearerNumber = 1): + """ + Checks GPRS connection. After calling of this method + + :param bearerNumber: bearer number + :return: True if checking was without mistakes, otherwise returns False + """ + self.logger.debug("checking GPRS bearer connection") + + ret = self.commandAndStdResult( + "AT+SAPBR=2,{0}".format(bearerNumber), + 1000, + ["OK"] + ) + if (ret is None) or (self.lastResult != "OK"): + self.setError("{0}: error, lastResult={1}, ret={2}".format(inspect.stack()[0][3], self.lastResult, ret)) + return False + + ret = str(ret).strip() + self.logger.debug("{0}: result = {1}".format(inspect.stack()[0][3], ret)) + + response = str(ret).split(":") + + if len(response) < 2: + self.setError("{0}:error, wrong response length, ret = {1}".format(inspect.stack()[0][3], ret)) + return False + + #parsing string like: + # +SAPBR: 1,1,"100.80.75.124" - when connected (channel 1) + # +SAPBR: 1,3,"0.0.0.0" - when disconnected (channel 1) + + if response[0] != "+SAPBR": + self.setWarn("{0}: warning, response is not '+SAPBR', response = {1}".format(inspect.stack()[0][3], response[0])) + return False + + response = splitAndFilter(response[1], ",") + self.logger.debug("{0}: sapbr result = \"{1}\"".format(inspect.stack()[0][3], response)) + + if len(response) < 3: + self.setError("{0}: wrong SAPBR result length, (sapbr result = '{1}')".format(inspect.stack()[0][3], response[1])) + return False + + if response[0] != str(bearerNumber): + return + + self.__ip = None + if response[1] == "0": + self.__connectionState = SimInetGSMConnection.inetConnecting + elif response[1] == "1": + self.__connectionState = SimInetGSMConnection.inetConnected + self.__ip = response[2].strip("\"").strip() + elif response[1] == "2": + self.__connectionState = SimInetGSMConnection.inetClosing + elif response[1] == "3": + self.__connectionState = SimInetGSMConnection.inetClosed + else: + self.__connectionState = SimInetGSMConnection.inetUnknown + + return True + + def attachGPRS(self, apn, user=None, password=None, bearerNumber = 1): + """ + Attaches GPRS connection for SIM module + + :param apn: Access Point Name + :param user: User name (Login) + :param password: Password + :param bearerNumber: Bearer number + :return: True if everything was OK, otherwise returns False + """ + + #checking current connection state + if not self.checkGprsBearer(bearerNumber): + return False + + #going out if already connected + if self.connectionState == SimInetGSMConnection.inetConnected: + return True + + #Closing the GPRS PDP context. We dont care of result + self.execSimpleOkCommand("AT+CIPSHUT", 500) + + #initialization sequence for GPRS attaching + commands = [ + ["AT+SAPBR=3,{0},\"CONTYPE\",\"GPRS\"".format(bearerNumber), 1000 ], + ["AT+SAPBR=3,{0},\"APN\",\"{1}\"".format(bearerNumber, apn), 500 ], + ["AT+SAPBR=3,{0},\"USER\",\"{1}\"".format(bearerNumber, user), 500 ], + ["AT+SAPBR=3,{0},\"PWD\",\"{1}\"".format(bearerNumber, password), 500 ], + ["AT+SAPBR=1,{0}".format(bearerNumber), 10000 ] + ] + + #executing commands sequence + if not self.execSimpleCommandsList(commands): + return False + + #returning GPRS checking sequence + return self.checkGprsBearer() + + def disconnectTcp(self): + """ + Disconnects TCP connection + :return: + """ + + return self.commandAndStdResult("AT+CIPCLOSE", 1000, ["OK"]) + + def dettachGPRS(self, bearerNumber = 1): + """ + Detaches GPRS connection + :param bearerNumber: bearer number + :return: True if de + """ + + #Disconnecting TCP. Ignoring result + self.disconnectTcp() + + #checking current GPRS connection state + if self.checkGprsBearer(bearerNumber): + if self.connectionState == SimInetGSMConnection.inetClosed: + return True + + #disconnecting GPRS connection for given bearer number + return self.execSimpleOkCommand("AT+SAPBR=0,{0}".format(bearerNumber), 1000) + + def terminateHttpRequest(self): + """ + Terminates current HTTP request. + + :return: True if when operation processing was without errors, otherwise returns False + """ + return self.execSimpleOkCommand("AT+HTTPTERM", 500) + + def __parseHttpResult(self, httpResult, bearerChannel = None): + """ + Parses http result string. + :param httpResult: string to parse + :param bearerChannel: bearer channel + :return: returns http result code and response length + """ + self.logger.debug("{0}: dataLine = {1}".format(inspect.stack()[0][3], httpResult)) + + response = splitAndFilter(httpResult, ":") + if len(response) < 2: + self.setWarn("{0}: wrong HTTP response length, length = {1}".format(inspect.stack()[0][3], len(response))) + return None + + if response[0] != "+HTTPACTION": + self.setWarn("{0}: http response is not a '+HTTPACTION', response = '{1}'".format(inspect.stack()[0][3], response[0])) + return None + + response = splitAndFilter(response[1], ",") + + if len(response) < 3: + self.setWarn("{0}: wrong response length".format(inspect.stack()[0][3])) + return None + + #checking bearer channel if necessary + if bearerChannel is not None: + if response[0] != str(bearerChannel): + self.setWarn("{0}: bad bearer number".format(inspect.stack()[0][3])) + return None + + httpResultCode = str(response[1]) + if not httpResultCode.isnumeric(): + self.setWarn("{0}: response code is not numeric!".format(inspect.stack()[0][3])) + return None + + httpResultCode = int(httpResultCode) + if httpResultCode != 200: + return [httpResultCode, 0] + + responseLength = str(response[2]) + if not responseLength.isnumeric(): + self.setWarn("{0}: response length is not numeric".format(inspect.stack()[0][3])) + return False + + return [httpResultCode, int(responseLength)] + + def __readHttpResponse(self, httpMethodCode, responseLength): + """ + Reads http response data from SIM module buffer + + :param httpMethodCode: ? + :param responseLength: response length + :return: True if reading was successful, otherwise returns false + """ + self.logger.debug("asking for http response (length = {0})".format(responseLength)) + + #trying to read HTTP response data + ret = self.commandAndStdResult( + "AT+HTTPREAD={0},{1}".format(httpMethodCode, responseLength), + 10000, + ["OK"] + ) + + if (ret is None) or (self.lastResult != "OK"): + self.setError("{0}: error reading http response data".format(inspect.stack()[0][3])) + return False + + #removing leading \n symbols + #TODO: we must remove only 1 \n, not all! Fix it! + ret = str(ret).strip() + + #reading first string in response (it must be "+HTTPREAD") + httpReadResultString = "" + while True: + if len(ret) == 0: + break + + httpReadResultString += ret[0] + ret = ret[1:] + + if "\n" in httpReadResultString: + break + + httpReadResultString = str(httpReadResultString).strip() + if len(httpReadResultString) == 0: + self.setError("{0}: wrong http response. Result is empty".format(inspect.stack()[0][3])) + return False + + httpReadResult = str(httpReadResultString).strip() + self.logger.debug("{0}: httpReadResult = {1}".format(inspect.stack()[0][3], httpReadResult)) + + httpReadResult = splitAndFilter(httpReadResult, ":") + if (len(httpReadResult) < 2) or (httpReadResult[0] != "+HTTPREAD"): + self.setError("{0}: bad response (cant find '+HTTPREAD'".format(inspect.stack()[0][3])) + return False + + if int(httpReadResult[1]) != responseLength: + self.setWarn("{0}: bad response, wrong responseLength = {1}".format(inspect.stack()[0][3], responseLength)) + return False + + self.__httpResponse = ret + return True + + @staticmethod + def ___isOkHttpResponseCode(code): + """ + Checks that given HTTP return code is successful result code + + :param code: http result code for checking + :return: true if given code is HTTP operation successful + """ + return code in [200, 201, 202, 203, 204, 205, 206, 207, 226] + + @staticmethod + def __isNoContentResponse(code): + """ + Checks that HTTP result code is 'NO CONTENT' result code + :param code: code for analysis + :return: true when code is 'NO CONTENT' code, otherwise returns false + """ + return code == 204 + + @staticmethod + def ___isHttpResponseCodeReturnsData(code): + """ + Checks that http operation returns data by given http result code + + :param code: given http call result code + :return: true if http request must return data, otherwise returns false + """ + + return code in [200, 206] + + def httpGet(self, server, port = 80, path = "/", bearerChannel = 1): + """ + Makes HTTP GET request to the given server and script + + :param server: server (host) address + :param port: http port + :param path: path to the script + :param bearerChannel: bearer channel number + :return: true if operation was successfully finished. Otherwise returns false + """ + self.__clearHttpResponse() + + #TODO: close only when opened + self.terminateHttpRequest() + + #HTTP GET request sequence + simpleCommands = [ + [ "AT+HTTPINIT", 2000 ], + [ "AT+HTTPPARA=\"CID\",\"{0}\"".format(bearerChannel), 1000 ], + [ "AT+HTTPPARA=\"URL\",\"{0}:{2}{1}\"".format(server, path,port), 500 ], + [ "AT+HTTPPARA=\"UA\",\"{0}\"".format(self.userAgent), 500 ], + [ "AT+HTTPPARA=\"REDIR\",\"1\"", 500 ], + [ "AT+HTTPPARA=\"TIMEOUT\",\"45\"", 500 ], + [ "AT+HTTPACTION=0", 10000 ] + ] + + #executing http get sequence + if not self.execSimpleCommandsList(simpleCommands): + self.setError("error executing HTTP GET sequence") + return False + + #reading HTTP request result + dataLine = self.readDataLine(10000) + + if dataLine is None: + return False + + #parsing string like this "+HTTPACTION:0,200,15" + httpResult = self.__parseHttpResult(dataLine, 0) + if httpResult is None: + return False + + #assigning HTTP result code + self.__httpResult = httpResult[0] + + #it's can be bad http code, let's check it + if not self.___isOkHttpResponseCode(self.httpResult): + self.terminateHttpRequest() + return True + + #when no data from server we just want go out, everything if OK + if not self.___isHttpResponseCodeReturnsData(self.httpResult): + self.terminateHttpRequest() + return True + + responseLength = httpResult[1] + if responseLength == 0: + self.terminateHttpRequest() + return True + + self.logger.debug("reading http response data") + if not self.__readHttpResponse(0, responseLength): + return False + + return True + + def __clearHttpResponse(self): + self.__httpResponse = None + self.__httpResult = 0 + + def httpPOST(self, server, port, path, parameters, bearerChannel = 1): + """ + Makes HTTP POST request to the given server and script + + :param server: server (host) address + :param port: server port + :param path: path to the script + :param parameters: POST parameters + :param bearerChannel: bearer channel number + :return: true if operation was successfully finished. Otherwise returns false + """ + + self.__clearHttpResponse() + + #TODO: close only when opened + self.terminateHttpRequest() + + #HTTP POST request commands sequence + simpleCommands = [ + [ "AT+HTTPINIT", 2000 ], + [ "AT+HTTPPARA=\"CID\",\"{0}\"".format(bearerChannel), 1000 ], + [ "AT+HTTPPARA=\"URL\",\"{0}:{1}{2}\"".format(server, port, path), 500 ], + [ "AT+HTTPPARA=\"CONTENT\",\"application/x-www-form-urlencoded\"", 500 ], + [ "AT+HTTPPARA=\"UA\",\"{0}\"".format(self.userAgent), 500 ], + [ "AT+HTTPPARA=\"REDIR\",\"1\"", 500 ], + [ "AT+HTTPPARA=\"TIMEOUT\",\"45\"", 500 ] + ] + + #executing commands sequence + if not self.execSimpleCommandsList(simpleCommands): + return False + + + #uploading data + self.logger.debug("uploading HTTP POST data") + ret = self.commandAndStdResult( + "AT+HTTPDATA={0},10000".format(len(parameters)), + 7000, + ["DOWNLOAD", "ERROR"] + ) + + if (ret is None) or (self.lastResult != "DOWNLOAD"): + self.setError("{0}: can't upload HTTP POST data".format(inspect.stack()[0][3])) + return False + + self.simpleWriteLn(parameters) + + dataLine = self.readDataLine(500) + if (dataLine is None) or (dataLine != "OK"): + self.setError("{0}: can't upload HTTP POST data".format(inspect.stack()[0][3])) + return + + self.logger.debug("actually making request") + + #TODO: check CPU utilization + if not self.execSimpleOkCommand("AT+HTTPACTION=1", 15000): + return False + + #reading HTTP request result + dataLine = self.readDataLine(15000) + + if dataLine is None: + self.setError("{0}: empty HTTP request result string".format(inspect.stack()[0][3])) + return False + + #parsing string like this "+HTTPACTION:0,200,15" + httpResult = self.__parseHttpResult(dataLine, bearerChannel) + if httpResult is None: + return False + + #assigning HTTP result code + self.__httpResult = httpResult[0] + + #it's can be bad http code, let's check it + if not self.___isOkHttpResponseCode(self.httpResult): + self.terminateHttpRequest() + return True + + #when no data from server we just want go out, everything if OK + if ( + (self.__isNoContentResponse(self.httpResult)) or + (not self.___isHttpResponseCodeReturnsData(self.httpResult)) + ): + self.terminateHttpRequest() + return True + + responseLength = httpResult[1] + if responseLength == 0: + self.terminateHttpRequest() + return True + + self.logger.debug("reading http request response data") + + if not self.__readHttpResponse(0, responseLength): + return False + + return True + + + # self.disconnectTcp() + # + # return True + + def httpPOST_DATA(self, server, port, path, parameters, bearerChannel = 1): + """ + Makes HTTP POST request to the given server and script + + :param server: server (host) address + :param port: server port + :param path: path to the script + :param parameters: POST parameters + :param bearerChannel: bearer channel number + :return: true if operation was successfully finished. Otherwise returns false + """ + + self.__clearHttpResponse() + + #TODO: close only when opened + self.terminateHttpRequest() + + #HTTP POST request commands sequence + simpleCommands = [ + [ "AT+HTTPINIT", 2000 ], + [ "AT+HTTPPARA=\"CID\",\"{0}\"".format(bearerChannel), 1000 ], + [ "AT+HTTPPARA=\"URL\",\"{0}:{1}{2}\"".format(server, port, path), 500 ], + [ "AT+HTTPPARA=\"CONTENT\",\"multipart/form-data\"", 500 ], + [ "AT+HTTPPARA=\"UA\",\"{0}\"".format(self.userAgent), 500 ], + [ "AT+HTTPPARA=\"REDIR\",\"1\"", 500 ], + [ "AT+HTTPPARA=\"TIMEOUT\",\"45\"", 500 ] + ] + + #executing commands sequence + if not self.execSimpleCommandsList(simpleCommands): + return False + + #uploading data + self.logger.debug("uploading HTTP POST data") + ret = self.commandAndStdResult( + "AT+HTTPDATA={0},10000".format(len(parameters)), + 7000, + ["DOWNLOAD", "ERROR"] + ) + + if (ret is None) or (self.lastResult != "DOWNLOAD"): + self.setError("{0}: can't upload HTTP POST data".format(inspect.stack()[0][3])) + return False + + self.simpleWriteLn(parameters) + + dataLine = self.readDataLine(500) + if (dataLine is None) or (dataLine != "OK"): + self.setError("{0}: can't upload HTTP POST data".format(inspect.stack()[0][3])) + return + + self.logger.debug("actually making request") + + #TODO: check CPU utilization + if not self.execSimpleOkCommand("AT+HTTPACTION=1", 15000): + return False + + #reading HTTP request result + dataLine = self.readDataLine(15000) + + if dataLine is None: + self.setError("{0}: empty HTTP request result string".format(inspect.stack()[0][3])) + return False + + #parsing string like this "+HTTPACTION:0,200,15" + httpResult = self.__parseHttpResult(dataLine, bearerChannel) + if httpResult is None: + return False + + #assigning HTTP result code + self.__httpResult = httpResult[0] + + #it's can be bad http code, let's check it + if not self.___isOkHttpResponseCode(self.httpResult): + self.terminateHttpRequest() + return True + + #when no data from server we just want go out, everything if OK + if ( + (self.__isNoContentResponse(self.httpResult)) or + (not self.___isHttpResponseCodeReturnsData(self.httpResult)) + ): + self.terminateHttpRequest() + return True + + responseLength = httpResult[1] + if responseLength == 0: + self.terminateHttpRequest() + return True + + self.logger.debug("reading http request response data") + + if not self.__readHttpResponse(0, responseLength): + return False + + return True + + + # self.disconnectTcp() + # + # return True + + + # + # int res= gsm.read(result, resultlength); + # //gsm.disconnectTCP(); + # return res; \ No newline at end of file diff --git a/lib/sim900/simshared.py b/lib/sim900/simshared.py new file mode 100644 index 0000000..6576ab7 --- /dev/null +++ b/lib/sim900/simshared.py @@ -0,0 +1,62 @@ +#The MIT License (MIT) +# +#Copyright (c) 2014-2015 Bohdan Danishevsky ( dbn@aminis.com.ua ) +# +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: +# +#The above copyright notice and this permission notice shall be included in all +#copies or substantial portions of the Software. +# +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +#SOFTWARE. + +""" +This file is part of sim-module package. Shared functions for sim-module package. + +sim-module package allows to communicate with SIM 900 modules: send SMS, make HTTP requests and use other +functions of SIM 900 modules. + +Copyright (C) 2014-2015 Bohdan Danishevsky ( dbn@aminis.com.ua ) All Rights Reserved. +""" + +import os +import logging +import inspect + +### conditional import ### + +# our company uses big file amshared.py which is not needed for this library. so here we will import only needful +# functions +if os.path.exists(os.path.abspath(os.path.join(os.path.dirname(__file__), "__stand_alone__.py"))): + from lib.sim900.amsharedmini import * +else: + from lib.amshared import * + +class AminisLastErrorHolderWithLogging(AminisLastErrorHolder): + def __init__(self, logger = None): + AminisLastErrorHolder.__init__(self) + + self.logger = logger + if self.logger is None: + self.logger = logging.getLogger(__name__) + + def setError(self, value): + AminisLastErrorHolder.setError(self, value) + self.logger.error(value) + + def setWarn(self, value): + AminisLastErrorHolder.setError(self, value) + self.logger.warn(value) + +def noneToEmptyString(value): + return '' if value is None else value \ No newline at end of file diff --git a/lib/sim900/smshandler.py b/lib/sim900/smshandler.py new file mode 100644 index 0000000..97e3c0e --- /dev/null +++ b/lib/sim900/smshandler.py @@ -0,0 +1,653 @@ +#The MIT License (MIT) +# +#Copyright (c) 2014-2015 Bohdan Danishevsky ( dbn@aminis.com.ua ) +# +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: +# +#The above copyright notice and this permission notice shall be included in all +#copies or substantial portions of the Software. +# +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +#SOFTWARE. + +""" +This file is part of sim-module package. SMS processing classes and functions. + +sim-module package allows to communicate with SIM 900 modules: send SMS, make HTTP requests and use other +functions of SIM 900 modules. + +Copyright (C) 2014-2015 Bohdan Danishevsky ( dbn@aminis.com.ua ) All Rights Reserved. +""" + +from lib.sim900.gsm import SimGsm +from lib.sim900.simshared import * +import binascii +import random + +class SimSmsPduCompiler(AminisLastErrorHolder): + def __init__(self, smsCenterNumber="", targetPhoneNumber="", smsTextMessage=""): + AminisLastErrorHolder.__init__(self) + + #sms center number + self.__smsCenterNumber = self.__preprocessPhoneNumber(smsCenterNumber) + + #sms recipient number + self.__smsRecipientNumber = self.__preprocessPhoneNumber(targetPhoneNumber) + + #sms text + self.smsText = smsTextMessage + + self.flashMessage = False + + #validation period for message + self.__validationPeriod = None + + def clear(self): + """ + Clears all internal buffers + + :return: nothing + """ + self.clear() + + self.__smsCenterNumber = "" + self.__smsRecipientNumber = "" + self.smsText = "" + self.flashMessage = False + + self.__validationPeriod = None + + @property + def smsCenterNumber(self): + """ + SMS center number + + :return: returns SMS center number + """ + return self.__smsCenterNumber + + @staticmethod + def __preprocessPhoneNumber(value): + value = noneToEmptyString(value) + value = str(value).strip() + value = value.replace(" ", "") + + return value.replace("\t", "") + + @smsCenterNumber.setter + def smsCenterNumber(self, value): + """ + Sets SMS center number + + :param value: new SMS center number + :return: nothing + """ + self.__smsCenterNumber = self.__preprocessPhoneNumber(value) + + @property + def smsRecipientNumber(self): + """ + Returns SMS recipient number + + :return: SMS recipient number + """ + return self.__smsRecipientNumber + + @smsRecipientNumber.setter + def smsRecipientNumber(self, value): + """ + Sets SMS recipient number + + :param value: SMS recipient number + :return: nothig + """ + self.__smsRecipientNumber = self.__preprocessPhoneNumber(value) + + @staticmethod + def __clientPhoneNumberLength(number): + """ + Returns phone number without '+' symbol and without padding 'F' at end + + :param number: number for length calculation + :return: number length + """ + + num = str(number).strip() + num = num.replace("+", "") + + return len(num) + + @staticmethod + def __encodePhoneNumber(number): + """ + Encodes phone number according to PDU rules + + :param number: phone number for encoding + :return: encoded phone number + """ + + num = str(number).strip() + num = num.replace("+", "") + + #adding pad byte + if (len(num) % 2) != 0: + num += 'F' + + #calculating reverted result, according to the + result = "" + i = 0 + while i < len(num): + result += num[i+1] + num[i] + i += 2 + + return result + + def __compileScaPart(self): + """ + Compiles SCA part of PDU request. + + :return: compiled request + """ + if len(self.smsCenterNumber) == 0: + return "00" + + smsCenterNumber = SimSmsPduCompiler.__encodePhoneNumber(self.smsCenterNumber) + sca = SimSmsPduCompiler.__byteToHex ( ((len(smsCenterNumber) // 2) + 1)) + "91" + smsCenterNumber + return sca + + def __canUse7BitsEncoding(self, text = None): + """ + Checks that message can be encoded in 7 bits. + + :param text: optional argument - text for checking, when not specified whole sms text will be checked + :return: true when text can be encoded in 7 bits, otherwise returns false + """ + + if text is None: + return all(ord(c) < 128 for c in self.smsText) + + return all(ord(c) < 128 for c in text) + + @staticmethod + def __encodeMessageIn7Bits(text): + """ + Encodes ASCII text message block with 7 bit's encoding. So, each 8 symbols of message will be encoded in 7 bytes + + :param text: text for encoding + :return: 7-bit encoded message + """ + + data = bytearray(text.encode("ascii")) + + #encoding + i = 1 + while i < len(data): + j = len(data) - 1 + + while j>=i: + firstBit = 0x80 if ((data[j] % 2) > 0) else 0x00 + + data[j-1] = (data[j-1] & 0x7f) | firstBit + data[j] = data[j] >> 1 + + j -= 1 + + i += 1 + + #looking for first 0x00 byte + index = 0 + for b in data: + if b == 0x00: + break + + index += 1 + + data = data[:index] + + # 'hellohello' must be encoded as "E8329BFD4697D9EC37" + return binascii.hexlify(data).decode("ascii").upper() + + def __encodeMessageAsUcs2(self, text): + """ + Encodes message with UCS2 encoding + + :param text: text for encoding + :return: UCS2 encoded message + """ + + try: + d = binascii.hexlify(text.encode("utf-16-be")) + return d.decode("ascii").upper() + except Exception as e: + self.setError("error encoding text: {0}".format(e)) + + return None + + def __compilePduTypePart(self, isMultupartMessage): + """ + Returns PDU Type part. + + :param isMultupartMessage: must be true when message is multupart + :return: encoded PDU-Type + """ + + #returning PDU-Type when validation period is not specified + if self.__validationPeriod is None: + if not isMultupartMessage: + return "01" + + return "41" + + #special value when multi-part message + if isMultupartMessage: + return "51" + + return "11" + + def __compilePduTpVpPart(self): + """ + Returns TP-VP part (validity period for SMS) + :return: + """ + # TP- VP — TP-Validity-Period/ "AA" means 4 days. Note: This octet is optional, see bits 4 and 3 of the first octet + return self.__validationPeriod + + def setValidationPeriodInMinutes(self, value): + """ + Set message validation period in minutes interval. Up to 12 hours. + + :param value: minutes count + :return: true if everything is OK, otherwise returns false + """ + + #0-143 (TP-VP + 1) x 5 minutes 5, 10, 15 minutes ... 11:55, 12:00 hours + count = value // 5 + + if count > 143: + self.setError("Wrong interval, must be between 1 and 720 minutes") + return False + + self.__validationPeriod = self.__byteToHex(count) + return True + + def setValidationPeriodInHours(self, value): + """ + Set validation period in hours (up to 24 hours) with 0.5 hour step + + :param value: hours count (float), must be >= 12 and <= 24 + :return: true if everything is OK, otherwise returns false + """ + #144-167 (12 + (TP-VP - 143) / 2 ) hours 12:30, 13:00, ... 23:30, 24:00 hours + + if (value < 12) or (value > 24): + self.setError("Value must be between 12 and 24 hours") + return False + + value = value - 12 + + count = int(value) + if (value - count) >= 0.5: + count = count*2 + 1 + else: + count = count*2 + + if count>23: + count = 23 + + self.__validationPeriod = self.__byteToHex(count + 144) + return True + + def setValidationPeriodInDays(self, value): + """ + Can set message validation period in days (2-30 days) + + :param value: days count (must be >=2 and <=30) + :return: true when value is OK, otherwise returns false + """ + + #168-196 (TP-VP - 166) days 2, 3, 4, ... 30 days + + if (value < 2) or (value > 30): + self.setError("Bad interval, value must be >= 2 days and <= 30 days") + return False + + self.__validationPeriod = self.__byteToHex(value + 166) + return True + + def setValidationPeriodInWeeks(self, value): + """ + Set validation period in weeks (from 5 to 63 weeks) + + :param value: weeks count (must be >=5 and <= 63) + :return: true if everything is OK, otherwise returns false + """ + + # 197-255 (TP-VP - 192) weeks 5, 6, 7, ... 63 weeks + if (value < 5) or (value > 63): + self.setError("Wrong value, value must be >= 5 and <= 63 weeks") + return False + + value = value - 5 + self.__validationPeriod = self.__byteToHex(value + 197) + return True + + def __compileTpdu(self, pieceNumber, totalPiecesCount, pieceText, messageId = None): + """ + Compiles TPDU part of PDU message request. + :return: compiled TPDU + """ + # TPDU = "PDU-Type" + "TP-MR" + "TP-DA" + "TP-PID" + "TP-DCS" + "TP-VP" + "TP-UDL" + "TP-UD" + # PDU-Type is the same as SMS-SUBMIT-PDU + + ret = "" + #checking that message have more than one part + isMultipartMessage = totalPiecesCount > 1 + + #adding PDU-Type + ret += self.__compilePduTypePart(isMultipartMessage) + + #adding TP-MR (TP-Message-Reference). + ret += self.__byteToHex(pieceNumber+100) + # if totalPiecesCount > 1: + # #setting message reference manually + # ret += self.__byteToHex(pieceNumber) + # else: + # #The "00" value here lets the phone set the message reference number itself. + # ret += "00" + + #encoding TP-DA (TP-Destination-Address - recipient address) + ret += self.__byteToHex(self.__clientPhoneNumberLength(self.smsRecipientNumber)) + "91" + self.__encodePhoneNumber(self.smsRecipientNumber) + + #adding TP-PID (TP-Protocol ID) + ret += "00" + + #adding TP-DCS (TP-Data-Coding-Scheme) + #00h: 7-bit encoding (160 symbols [after packing], but only ASCII) + #08h: UCS2 encoding (Unicode), 70 symbols, 2 bytes per symbol + + #If first octet is "1" message will not be saved in mobile but only flashed on the screen + #10h: Flash-message with 7-bit encoding + #18h: Flash-message with UCS2 encoding + + #checking that message CAN be encoded in 7 bits encoding + canBe7BitsEncoded = self.__canUse7BitsEncoding() + + if canBe7BitsEncoded: + tpDcs = "00" + else: + tpDcs = "08" + + if self.flashMessage: + tpDcs[0] = "1" + + ret += tpDcs + + #adding TP-VP (TP-Validity-Period) is it's specified + if self.__validationPeriod is not None: + ret += self.__compilePduTpVpPart() + + #encoding message (7-bit or UCS2) + if canBe7BitsEncoded: + encodedMessage = self.__encodeMessageIn7Bits(pieceText) + else: + encodedMessage = self.__encodeMessageAsUcs2(pieceText) + + #checking that message was encoded correctly + if encodedMessage is None: + self.setError("error encoding message: {0}".format(self.errorText)) + return None + + #adding TP-UDL (TP-User-Data-Length - message length) + if not isMultipartMessage: + if canBe7BitsEncoded: + #adding TEXT LENGTH IN SYMBOLS + ret += self.__byteToHex(len(self.smsText)) + else: + ret += self.__byteToHex(len(encodedMessage)//2) + else: + if canBe7BitsEncoded: + ret += self.__byteToHex(len(pieceText) + 8) + else: + ret += self.__byteToHex(len(encodedMessage)//2 + 6) + + #adding UDHL + UDH for multipart messages + if isMultipartMessage: + if canBe7BitsEncoded: + #length of UDH + udhl = bytearray([0x06]) + + #UDI IED entry type + iei = bytearray([0x08]) + + #length of UDH IED + iedl = bytearray([0x04]) + + # messageId + ied1Lo = messageId & 0xff + ied1Hi = ((messageId & 0xff00) >> 8) + ied1 = bytearray([ied1Hi, ied1Lo]) + + #total pieces count + ied2 = bytearray([totalPiecesCount]) + + #piece number + ied3 = bytearray([pieceNumber]) + + #compiling IED + ied = ied1 + ied2 + ied3 + + #compiling UDH + udh = iei + iedl + ied + else: + #length of UDH + udhl = bytearray([0x05]) + + #UDI IED entry type + iei = bytearray([0x00]) + + #length of UDH IED + iedl = bytearray([0x03]) + + #message id + ied1Lo = messageId & 0xff + ied1 = bytearray([ied1Lo]) + + #total pieces count + ied2 = bytearray([totalPiecesCount]) + + #piece number + ied3 = bytearray([pieceNumber]) + + #compiling IED + ied = ied1 + ied2 + ied3 + + #compiling UDH + udh = iei + iedl + ied + + cudh = binascii.hexlify(udhl + udh).decode("ascii").upper() + print("cudh = '{0}'".format(cudh)) + + ret += cudh + + #adding TP-UD (TP-User-Data - SMS message encoded as described in TP-DCS) + ret += encodedMessage + return ret + + def messagesCount(self): + if self.__canUse7BitsEncoding(): + symbolsCount = len(self.smsText) + + if symbolsCount <= 160: + return 1 + + messagesCount = symbolsCount // 152 + if symbolsCount % 152: + messagesCount += 1 + + return messagesCount + else: + symbolsCount = len(self.smsText) + + if symbolsCount <= 70: + return 1 + + messagesCount = symbolsCount // 67 + if symbolsCount % 67: + messagesCount += 1 + + return messagesCount + + @staticmethod + def __byteToHex(value): + """ + Returns two-symbold hex-string representation of byte. + + :param value: byte for encoding + :return: encoded value + """ + return "{:02X}".format(value) + + def compile(self): + """ + Compiles PDU request (SCA + TPDU) + + :return: SMS request in PDU format + """ + ret = [] + + symbolsCount = len(self.smsText) + msgCount = self.messagesCount() + isUcs2 = not self.__canUse7BitsEncoding() + + if isUcs2: + symbolsInPiece = 67 + else: + symbolsInPiece = 152 + + #generating message id for multi-part messages + messageId = None + if msgCount > 1: + messageId = random.randint(0, 65535) + + for i in range(msgCount): + + if msgCount == 1: + textPiece = self.smsText + else: + minIndex = i * symbolsInPiece + maxIndex = (minIndex + symbolsInPiece) if (minIndex + symbolsInPiece) < symbolsCount else (symbolsCount) + textPiece = self.smsText[minIndex : maxIndex] + + ret += [(self.__compileScaPart(), self.__compileTpdu(i+1, msgCount, textPiece, messageId),)] + + return ret + +class SimGsmSmsHandler(SimGsm): + def __init__(self, port, logger): + SimGsm.__init__(self, port, logger) + + self.sendingResult = "" + + def clear(self): + SimGsm.clearError(self) + self.sendingResult = "" + + def sendSms(self, phoneNumber, messageText, numberOfAttempts = 3): + tuneCommands = [ + ["AT+CMGS=?", 300], #checking that sms supported + ["AT+CMGF=1", 1000] + ] + + self.logger.debug("initializing SIM module for SMS sending") + for cmd in tuneCommands: + if not self.execSimpleOkCommand(commandText=cmd[0],timeout=cmd[1]): + return False + + for i in range(numberOfAttempts): + ret = self.commandAndStdResult( + "AT+CMGS=\"{0}\"".format(phoneNumber), + 1000, + [">"] + ) + + if (ret is None) or (self.lastResult != ">"): + continue + + ret = self.commandAndStdResult( + "{0}\n\x1a".format(messageText), + 10000, + ["ERROR", "OK"] + ) + if (ret is None) or (self.lastResult not in ["OK"]): + continue + + return True + + self.setError("error sending sms...") + return False + + def __sendPduMessageLow(self, sca, pdu, numberOfAttempts = 3): + tuneCommands = [ + ["AT+CSCS=\"GSM\"", 500], + # ["AT+CMGS?", 500], #checking that sms supported + ["AT+CMGF=0", 1000] + ] + + self.logger.debug("initializing SIM module for SMS sending in PDU mode") + + for cmd in tuneCommands: + if not self.execSimpleOkCommand(commandText=cmd[0], timeout=cmd[1]): + self.setError("error tuning module for sms sending") + return False + + for i in range(numberOfAttempts): + ret = self.commandAndStdResult( + "AT+CMGS={0}".format(len(pdu) // 2), + 1000, + [">"] + ) + + if (ret is None) or (self.lastResult != ">"): + continue + + ret = self.commandAndStdResult( + "{0}\x1a".format(sca + pdu), + 10000, + ["ERROR", "OK"] + ) + if (ret is None) or (self.lastResult != "OK"): + continue + + self.sendingResult = ret.strip() + return True + + return False + + + def sendPduMessage(self, pduHelper, numberOfAttempts = 3): + d = pduHelper.compile() + if d is None: + self.setError("error compiling PDU sms") + return False + + piece = 1 + for (sca, pdu,) in d: + self.logger.info("sendSms(): sca + pdu = \"{0}\"".format(sca + pdu)) + if not self.__sendPduMessageLow(sca, pdu, numberOfAttempts): + return False + + + self.logger.info("Sending result = {0}".format(self.sendingResult)) + + + return True diff --git a/lib/sim900/ussdhandler.py b/lib/sim900/ussdhandler.py new file mode 100644 index 0000000..b39023b --- /dev/null +++ b/lib/sim900/ussdhandler.py @@ -0,0 +1,126 @@ +#The MIT License (MIT) +# +#Copyright (c) 2014-2015 Bohdan Danishevsky ( dbn@aminis.com.ua ) +# +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: +# +#The above copyright notice and this permission notice shall be included in all +#copies or substantial portions of the Software. +# +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +#SOFTWARE. + +""" +This file is part of sim-module package. USSD requests processing classes and functions. + +sim-module package allows to communicate with SIM 900 modules: send SMS, make HTTP requests and use other +functions of SIM 900 modules. + +Copyright (C) 2014-2015 Bohdan Danishevsky ( dbn@aminis.com.ua ) All Rights Reserved. +""" + +from lib.sim900.gsm import SimGsm +from lib.sim900.simshared import * + +class SimUssdHandler(SimGsm): + def __init__(self, port, logger): + SimGsm.__init__(self, port, logger) + self.lastUssdResult = None + + @staticmethod + def __parseResult(value): + #parsing strings like '+CUSD: 0,"data string"' + + #searching and removing '+CUSD' prefix + idx = value.find(":") + if idx == -1: + return None + + left = value[:idx] + left = str(left).strip() + if left != "+CUSD": + return None + + data = value[(idx+1):] + data = str(data).strip() + + #searching and removing numeric parameter + idx = data.find(",") + if idx == -1: + return None + + #also, we can use this code. But I dont know how + code = data[:idx] + data = data[(idx+1):] + + data = str(data).strip() + + data = data.rstrip(',') + data = data.strip('"') + + return data + + def runUssdCode(self, ussdCode): + cmd = "AT+CUSD=1,\"{0}\",15".format(ussdCode) + self.logger.info("running command = '{0}'".format(cmd)) + + #executing command, also we can retrieve result right here + result = self.commandAndStdResult(cmd, 20000) + + if (result is None) or (self.lastResult != 'OK'): + self.setWarn("error running USSD command '{0}'".format(ussdCode)) + return False + + result = str(result).strip() + + #checking that we have result here + if len(result) > 0: + self.lastUssdResult = self.__parseResult(result) + + if self.lastUssdResult is None: + self.setWarn("error parsing USSD command result") + return False + + return True + + #reading data line + dataLine = self.readNullTerminatedLn(20000) + + if dataLine is None: + self.setWarn("error waiting for USSD command result") + return False + + dataLine = str(dataLine).strip() + + #reading bytes in the end of response + data = self.readFixedSzieByteArray(1, 500) + if data == bytes([0xff]): + data = None + + endLine = self.readLn(500) + + if (data is not None) or (endLine is not None): + endLine = noneToEmptyString(data) + noneToEmptyString(endLine) + endLine = str(endLine).strip() + + if len(endLine) > 0: + dataLine += endLine + + #parsing CUSD result + self.lastUssdResult = self.__parseResult(dataLine) + + if self.lastUssdResult is None: + return False + + return True + diff --git a/main.py b/main.py index 0e9454e..13b7bbd 100644 --- a/main.py +++ b/main.py @@ -1,113 +1,94 @@ __author__ = 'asc' DEBUG = True +import logging if DEBUG: - from instruments import Barometer_Debug as Barometer - from instruments import Camera_Debug as Camera + from instruments import Barometer_Debug2 as Barometer + from instruments import Camera_Debug2 as Camera from datahandling import Datalogger_Debug as Datalogger from datahandling import Datareporter_Debug2 as Datareporter from system import System_Debug as System import threading import datetime -# else: -# from instruments import Barometer, Camera -# from datahandling import Datalogger, Datareporter -# from system import System -# import threading +import configparser + +# logging.basicConfig(level=logging.DEBUG, format=("%(asctime)s - %(name)s - %(levelname)s - %(message)s")) +# log = logging.getLogger("instruments") +# log.setLevel(logging.DEBUG) +# log. +# # log.setFo + +log = logging.getLogger("instruments") +handler = logging.StreamHandler() + +formatter = logging.Formatter('%(asctime)-15s %(name)-5s %(levelname)-8s %(message)s') +handler.setFormatter(formatter) +log.addHandler(handler) +log.setLevel(logging.DEBUG) #start-up #log denotes write to local, report denotes sending to server #TODO startup message +#loadconfig +config = configparser.ConfigParser() +config.sections() +config.read('config.ini') +refresh_camera_local = float(config['refresh rates']['refresh camera local']) +refresh_camera_transmit = float(config['refresh rates']['refresh camera transmit']) + +refresh_barometer_local = float(config['refresh rates']['refresh barometer local']) +refresh_barometer_transmit = float(config['refresh rates']['refresh barometer transmit']) + +refresh_gps_local = float(config['refresh rates']['refresh gps local']) +refresh_gps_transmit = float(config['refresh rates']['refresh gps transmit']) + +low_quality_resolution = eval(config['camera settings']['low quality resolution']) +low_quality_compression_pct = int(config['camera settings']['low quality compression pct']) + +report_url = config['report settings']['report url'] +report_image_url = config['report settings']['report image url'] + +com_port_name = config['modem settings']['com port name'] +baud_rate = config['modem settings']['baud rate'] + logger = Datalogger () -logger.log ("System Startup") -logger.log("Log Intiated") +log.debug ("System started") +# logger.log("Log Intiated") #system check system = System() system_status = system.status -logger.log (system_status[1]) +# logger.log (system_status[1]) #todo test cell connection, log, report reporter = Datareporter() reporter_status = reporter.status - -logger.log (reporter_status[1]) +# logger.log (reporter_status[1]) reporter.send(reporter_status[1]) reporter.send(system_status[1]) #TODO test camera, log, report -camera = Camera () +camera = Camera (low_quality_compression_pct=low_quality_compression_pct, + low_quality_resolution=low_quality_resolution) camera_status = camera.status -logger.log (camera_status[1]) -reporter.send(camera_status[1]) #todo test barometer, log, report barometer = Barometer() -barometer_status = barometer.status -logger.log (barometer_status[1]) -reporter.send(barometer_status[1]) +# barometer_status = barometer.status +# logger.log (barometer_status[1]) +# reporter.send(barometer_status[1]) #todo test GPS, log, report -#check for errors, throw exception if error +#todo check for errors, throw exception if error + if(system_status[0] or reporter_status[0] or camera_status[0]): raise Exception ('Error') -if DEBUG: - #rate refresh hi-res camera images, saved locally, in seconds - refresh_camera_local = 2 - refresh_camera_transmit = 5 - - refresh_barometer_local = 1 - refresh_barometer_transmit = 5 - - refresh_gps_local = 2 - refresh_gps_transmit = 5 - -else: - refresh_camera_local = 10 - refresh_camera_transmit= 60 - - refresh_barometer_local = 10 - refresh_barometer_transmit= 60 - - refresh_gps_local = 10 - refresh_gps_transmit= 60 - -def update_barometer(): - #refresh each instrument - alt = barometer.altitude - press = barometer.pressure - temp = barometer.temperature - - #log instrument info - logger.log({"altitude":alt, - "temperature":temp, - "pressure":press, - "sent":datetime.datetime.now() - }) - - #report instrument info - reporter.send({"altitude":alt, - "temperature": temp, - "pressure":press, - "sent":datetime.datetime.now() - },type="data") - - - -def update_camera(): - image = camera.capture() - #log image - logger.log(image.get('file'), type="image") - #report image - reporter.send(image.get('file'), type="image") - pass - def scheduler (interval, worker_func, iterations = 0): if iterations != 1: threading.Timer ( @@ -119,9 +100,17 @@ def scheduler (interval, worker_func, iterations = 0): #while 1: -update_camera() +image = camera.capture() +data = barometer.read() + +# logger.log(image.get('hi'), type='image') +# logger.log(barometer, type='data') +reporter.send(image.get('lo'), type='data') +reporter.send(barometer, type = 'data') + + #todo break apart log and report refresh -scheduler(refresh_barometer_local, update_barometer, 20) -scheduler(refresh_camera_local, update_camera, 1) +# scheduler(refresh_barometer_local, update_barometer, 2) +# scheduler(refresh_camera_local, update_camera, 2) pass diff --git a/system.py b/system.py index e772030..c671657 100644 --- a/system.py +++ b/system.py @@ -1,4 +1,7 @@ __author__ = 'asc' +import os +import psutil + class System_Debug(): @@ -9,4 +12,45 @@ class System_Debug(): @property def status (self): #TODO status check - return (0, "System functioning properly") \ No newline at end of file + return (0, "System functioning properly") + + import os + + # Return CPU temperature as a character string + def getCPUtemperature(): + res = os.popen('vcgencmd measure_temp').readline() + return(res.replace("temp=","").replace("'C\n","")) + + def getCPUusage(self): + p=psutil.cpu_percent(interval=1) + return p + + def getRAMinfo(): + p = psutil.virtual_memory() + p = psutil.swap_memory() + return p + + def getDiskSpace(): + p = psutil.disk_usage('/') + return p + + @property + def stats (self): + #courtesy of Phillipe + # https://www.raspberrypi.org/forums/memberlist.php?mode=viewprofile&u=40834&sid=dd38cc12161ac10b324ed2a2238972d3 + # CPU informatiom + CPU_temp = self.getCPUtemperature() + CPU_usage = self.getCPUuse() + + # RAM information + # Output is in kb, here I convert it in Mb for readability + RAM_stats = self.getRAMinfo() + + + # Disk information + DISK_stats = self.getDiskSpace() + + return {"cpu_temp":CPU_temp, + "cpu_usage":CPU_usage, + "ram_stats":RAM_stats, + "disk_stats":DISK_stats} \ No newline at end of file diff --git a/test.py b/test.py index be8a91d..277a9d3 100644 --- a/test.py +++ b/test.py @@ -1,6 +1,8 @@ import csv, requests -from instruments import Camera_Debug as Camera -from datahandling import Datareporter_Debug2 as Datareporter +from instruments import Camera_Debug2 as Camera +from datahandling import Datareporter_Debug3 as Datareporter +import serial +import sys REPORTIMAGETOURL = "http://10.0.1.4:5010/photo" @@ -8,14 +10,19 @@ LOG_FILE = "log2.txt" data = {"temp":1,"press":3,"altitude":2,"cheetas":"just enough"} -camera = Camera () + reporter = Datareporter () - +camera = Camera(low_quality_resolution=(320, 180), + low_quality_compression_pct=50) image = camera.capture() -response = requests.post(REPORTIMAGETOURL, files={'file': image.get('file')}) +print (image) + +pass + #report image -reporter.send(image.get('file'), type="image") +# reporter.send(image.get('file'), type="image") + pass \ No newline at end of file diff --git a/test_retrieve_data.py b/test_retrieve_data.py deleted file mode 100644 index e7c1edc..0000000 --- a/test_retrieve_data.py +++ /dev/null @@ -1,12 +0,0 @@ -__author__ = 'asc' - -import json -from urllib import request - - -req = request.Request("http://home.ascorrea.com:5010/retrieve-report") -# req.add_header('Content-Type', 'application/json') -response = request.urlopen(req) -r = response.read() -json.loads(r.decode()).get('data') -pass