diff --git a/README.md b/README.md index fc73924..93ae20e 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # gsmHat - Waveshare GSM/GPRS/GNSS HAT for Raspberry Pi with Python -With gsmHat, you can easily use the functionality of the Waveshare GSM/GPRS/GNSS HAT for Raspberry Pi ([Link to HAT](https://www.waveshare.com/gsm-gprs-gnss-hat.htm)). On this module a SIM868 Controller is doing the job too connect your Raspberry Pi with the world just by using a sim card. +With gsmHat, you can easily use the functionality of the Waveshare GSM/GPRS/GNSS HAT for Raspberry Pi ([Link to HAT](https://www.waveshare.com/gsm-gprs-gnss-hat.htm)). On this module a SIM868 Controller is doing the job to connect your Raspberry Pi with the world just by using a sim card. + +## Update on Wed Oct 21st, 2020 +:point_right: Internet functionality added! ## Overview gsmHat was written for Python 3. It provides the following features @@ -8,6 +11,7 @@ gsmHat was written for Python 3. It provides the following features - Non-blocking receiving and sending SMS in background - Non-blocking calling - Non-blocking refreshing of actual gps position + - Non-blocking URL Call and receiving of response ## Usage @@ -88,7 +92,7 @@ gsm.HangUp() # Or you can HangUp by yourself earlier gsm.Call(Number, 60) # Or lets change the timeout to 60 seconds. This call hangs up automatically after 60 seconds ``` -7. Lets see, where your Raspberry Pi (in a car or in a motocycle or on a cat?) is positioned on earth +7. Lets see, where your Raspberry Pi (in a car or on a motocycle or on a cat?) is positioned on earth ```Python # Get actual GPS position @@ -114,16 +118,49 @@ print('Signal: %s' % str(GPSObj.Signal)) 8. Calculate the distance between two Points on earth ```Python - GPSObj1 = GPS() # You can also use gsm.GetActualGPS() to get an GPS object - GPSObj1.Latitude = 52.266949 # Location of Braunschweig, Germany - GPSObj1.Longitude = 10.524822 +GPSObj1 = GPS() # You can also use gsm.GetActualGPS() to get an GPS object +GPSObj1.Latitude = 52.266949 # Location of Braunschweig, Germany +GPSObj1.Longitude = 10.524822 - GPSObj2 = GPS() - GPSObj2.Latitude = 36.720005 # Location of Manavgat, Turkey - GPSObj2.Longitude = 31.546094 +GPSObj2 = GPS() +GPSObj2.Latitude = 36.720005 # Location of Manavgat, Turkey +GPSObj2.Longitude = 31.546094 - print('Distance from Braunschweig to Manavgat is [m]:') - print(GPS.CalculateDeltaP(GPSObj1, GPSObj2)) # this will print 2384660.7 metres +print('Distance from Braunschweig to Manavgat in metres:') +print(GPS.CalculateDeltaP(GPSObj1, GPSObj2)) # this will print 2384660.7 metres +``` + +9. Call URL to send some data + +```Python +# Init gsmHat +gsm = GSMHat('/dev/ttyS0', 115200) + +# Set the APN Connection data. You will get this from your provider +# e.g. German Provider 'Congstar' +gsm.SetGPRSconnection('internet.telekom', 'congstar', 'cs') + +# Get actual GPS position +GPSObj = gsm.GetActualGPS() + +# Build url string with data +url = 'www.someserver.de/myscript.php' +url += '?time='+str(int(GPSObj.UTC.timestamp())) +url += '&lat='+str(GPSObj.Latitude) +url += '&lon='+str(GPSObj.Longitude) +url += '&alt='+str(GPSObj.Altitude) + +gsm.CallUrl(url) # Send actual position to a webserver +``` + +10. Get the Response from a previous URL call + +```Python +# Check, if new Response Data is available +if gsm.UrlResponse_available() > 0: + # Read the Response + newResponse = gsm.UrlResponse_read() + # Do something with it ``` ## What will come in the future? @@ -168,4 +205,4 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -contact me: +contact me: \ No newline at end of file diff --git a/gsmHat/gsmHat.py b/gsmHat/gsmHat.py index 706f835..30ac43f 100755 --- a/gsmHat/gsmHat.py +++ b/gsmHat/gsmHat.py @@ -52,21 +52,23 @@ class GSMHat: """GSM Hat Backend with SMS Functionality (for now)""" regexGetSingleValue = r'([+][a-zA-Z\ ]+(:\ ))([\d]+)' - regexGetAllValues = r'([+][a-zA-Z:\s]+)([\w\",\s+\/:.]+)' + regexGetAllValues = r'([+][a-zA-Z:\s]+)([\w\",\s+-\/:.]+)' timeoutSerial = 5 timeoutGPSActive = 1 - timeoutGPSInactive = 5 + timeoutGPSInactive = 2000 + cSMSwaittime = 2500 # milliseconds + cGPRSstatusWaittime = 5000 # milliseconds - def __init__(self, SerialPort, Baudrate): + def __init__(self, SerialPort, Baudrate, Logpath='gmsHat.log'): self.__baudrate = Baudrate self.__port = SerialPort self.__logger = logging.getLogger(__name__) - self.__logger.setLevel(logging.INFO) - self.__loggerFileHandle = logging.FileHandler('gsmHat.log') + self.__logger.setLevel(logging.DEBUG) + self.__loggerFileHandle = logging.FileHandler(Logpath) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') self.__loggerFileHandle.setFormatter(formatter) - self.__loggerFileHandle.setLevel(logging.INFO ) + self.__loggerFileHandle.setLevel(logging.DEBUG) self.__logger.addHandler(self.__loggerFileHandle) self.__connect() @@ -85,21 +87,35 @@ class GSMHat: def __startWorking(self): self.__working = True self.__state = 1 + self.__nextState = 0 self.__smsToRead = 0 + self.__retryAfterTimeout = False + self.__retryAfterTimeoutCount = 0 self.__init = False - self.__readRAW = False + self.__lastCommandSentString = '' + self.__readRAW = 0 self.__smsToBuild = None self.__smsList = [] self.__smsSendList = [] + self.__SMSwaittime = 0 self.__numberToCall = '' self.__sendHangUp = False self.__startGPS = False + self.__GPRSwaittimeStatus = 0 + self.__GPRSIPaddress = '' + self.__GPRSready = False + self.__GPRSuserAPN = None + self.__GPRSuserUSER = None + self.__GPRSuserPWD = None + self.__GPRScallUrlList = [] + self.__GPRSdataReceived = [] + self.__GPRSwaitForData = False self.__GPSstarted = False self.__GPSstartSending = False self.__GPSstopSending = False self.__GPScollectData = False self.__GPSactualData = GPS() - self.__GPStimeout = self.timeoutGPSInactive * 1000 + self.__GPStimeout = self.timeoutGPSInactive self.__GPSwaittime = 0 self.__workerThread = threading.Thread(target=self.__workerThread, daemon=True) self.__workerThread.start() @@ -110,7 +126,7 @@ class GSMHat: def __sendToHat(self, string): if self.__writeLock == False: - self.__lastCommand = string + self.__lastCommandSentString = string string = string + '\n' self.__ser.write(string.encode('iso-8859-1')) self.__writeLock = True @@ -118,7 +134,7 @@ class GSMHat: self.__logger.debug('Sent to hat: %s' % string) return True else: - self.__logger.debug('Wait for Lock...') + self.__logger.debug('Wait for Lock... state: ' + str(self.__state) + ' senddata: ' + string) time.sleep(1) return False @@ -164,6 +180,29 @@ class GSMHat: def GetActualGPS(self): return self.__GPSactualData + def UrlResponse_available(self): + return len(self.__GPRSdataReceived) + + def UrlResponse_read(self): + if self.UrlResponse_available() > 0: + retResponse = self.__GPRSdataReceived[0] + del self.__GPRSdataReceived[0] + return retResponse + + return None + + def CallUrl(self, url): + self.__GPRScallUrlList.append(url) + self.__logger.debug('Got new URL call') + + def PendingUrlCalls(self): + return len(self.__GPRScallUrlList) + + def SetGPRSconnection(self, APN, Username, Password): + self.__GPRSuserAPN = APN + self.__GPRSuserUSER = Username + self.__GPRSuserPWD = Password + def __startGPSUnit(self): self.__startGPS = True @@ -186,22 +225,40 @@ class GSMHat: def __processData(self): if self.__serData != '': - if self.__readRAW: + if self.__readRAW > 0: self.__logger.debug('Received Raw Data: %s' % self.__serData) - if self.__serData == 'OK\r\n': - self.__smsToBuild.Message = self.__smsToBuild.Message.rstrip('\r\n') - self.__smsList.append(self.__smsToBuild) - self.__readRAW = False - self.__writeLock = False - else: - #self.__smsToBuild.Message = bytearray.fromhex(self.__serData).decode() - self.__smsToBuild.Message = self.__smsToBuild.Message + self.__serData + if self.__readRAW == 1: + # Handle SMS + if self.__serData == 'OK\r\n': + self.__smsToBuild.Message = self.__smsToBuild.Message.rstrip('\r\n') + self.__smsList.append(self.__smsToBuild) + self.__readRAW = 0 + self.__writeLock = False + else: + self.__smsToBuild.Message = self.__smsToBuild.Message + self.__serData + elif self.__readRAW == 2: + # Handle HTTP Response + if self.__serData == 'OK\r\n': + self.__readRAW = 0 + self.__writeLock = False + self.__GPRSdataToBuild = self.__GPRSdataToBuild.rstrip('\r\n') + self.__GPRSdataReceived.append(self.__GPRSdataToBuild) + else: + self.__GPRSdataToBuild = self.__GPRSdataToBuild + self.__serData else: self.__logger.debug('Received Data: %s' % self.__serData) if 'OK' in self.__serData: self.__writeLock = False self.__logger.debug('Lock Off') + if 'ERROR' in self.__serData: + # ERROR Handling here + if self.__state == 71: + # Error after sending AT+HTTPINIT + # Lets terminate request before starting new one + self.__logger.info('Error after starting new HTTP Request.') + self.__writeLock = False + self.__state = 75 elif '+CME ERROR:' in self.__serData: self.__writeLock = False @@ -228,7 +285,7 @@ class GSMHat: # read SMS content match = re.findall(self.regexGetAllValues, self.__serData) rawData = match[0][1].split('","') - self.__readRAW = True + self.__readRAW = 1 self.__smsToBuild = SMS() #self.__smsToBuild.Sender = bytearray.fromhex(rawData[1]).decode() self.__smsToBuild.Sender = rawData[1] @@ -236,6 +293,42 @@ class GSMHat: self.__smsToBuild.Date = datetime.strptime(rawData[3].replace('"', '')[:-3], '%y/%m/%d,%H:%M:%S') self.__smsToBuild.Message = '' + elif '+SAPBR:' in self.__serData: + # check if IP is valid + # Return value looks like: +SAPBR: 1,3,"0.0.0.0" + match = re.findall(self.regexGetAllValues, self.__serData) + rawData = match[0][1].split(',') + self.__GPRSIPaddress = rawData[2].replace('"', '') + + if self.__GPRSIPaddress != '0.0.0.0': + self.__GPRSready = True + else: + self.__GPRSready = False + + elif '+HTTPREAD:' in self.__serData: + # read HTTP content + self.__GPRSdataToBuild = '' + self.__readRAW = 2 + + elif '+HTTPACTION:' in self.__serData: + # Return value looks like: +HTTPACTION: 0,200,0 + match = re.findall(self.regexGetAllValues, self.__serData) + rawData = match[0][1].split(',') + self.__GPRSgotHttpResponse = True + if len(rawData) == 3: + requestMethod = int(rawData[0]) + httpStatus = int(rawData[1]) + recvDataLength = int(rawData[2]) + if httpStatus == 200: # Successful request + self.__GPRSnewDataReceived = True + elif httpStatus == 601: # Successful request + self.__logger.info('HTTPACTION Network Error ' + str(httpStatus)) + else: + self.__logger.info('HTTPACTION Unhandled Error ' + str(httpStatus)) + + else: + self.__logger.info('HTTPACTION return value is not expected: ' + match[0][1]) + # unannounced data reception below (e.g. new SMS oder phone call) elif '+CMTI:' in self.__serData: self.__logger.info('Received new SMS') @@ -251,87 +344,137 @@ class GSMHat: self.__logger.debug('New GPS Data:') match = re.findall(self.regexGetAllValues, self.__serData) rawData = match[0][1].split(',') - - newGPS = GPS() + if len(rawData) == 21: + newGPS = GPS() + goodPosition = True - try: - newGPS.GNSS_status = int(rawData[0]) - except: - pass - try: - newGPS.Fix_status = int(rawData[1]) - except: - pass - try: - newGPS.UTC = datetime.strptime(rawData[2][:-4], '%Y%m%d%H%M%S') - except: - pass - try: - newGPS.Latitude = float(rawData[3]) - except: - pass - try: - newGPS.Longitude = float(rawData[4]) - except: - pass - try: - newGPS.Altitude = float(rawData[5]) - except: - pass - try: - newGPS.Speed = float(rawData[6]) - except: - pass - try: - newGPS.Course = float(rawData[7]) - except: - pass - try: - newGPS.HDOP = float(rawData[10]) - except: - pass - try: - newGPS.PDOP = float(rawData[11]) - except: - pass - try: - newGPS.VDOP = float(rawData[12]) - except: - pass - try: - newGPS.GPS_satellites = int(rawData[14]) - except: - pass - try: - newGPS.GNSS_satellites = int(rawData[15]) - except: - pass - try: - newGPS.Signal = float(rawData[18])/55.0 - except: - pass + try: + newGPS.GNSS_status = int(rawData[0]) + except: + self.__logger.debug('GNSS_status: Could not convert ' + rawData[0] + ' to int.') - self.__GPSactualData = newGPS + try: + # Fix_status: GPS connection + newGPS.Fix_status = int(rawData[1]) + if newGPS.Fix_status == 0: + goodPosition = False + except: + self.__logger.debug('Fix_status: Could not convert ' + rawData[1] + ' to int.') + + try: + newGPS.UTC = datetime.strptime(rawData[2][:-4], '%Y%m%d%H%M%S') + except: + self.__logger.debug('UTC: Could not convert ' + rawData[2][:-4] + ' to int.') + + try: + newGPS.Latitude = float(rawData[3]) + if newGPS.Latitude == 0.0: + goodPosition = False + except: + self.__logger.debug('Latitude: Could not convert ' + rawData[3] + ' to float.') + + try: + newGPS.Longitude = float(rawData[4]) + if newGPS.Longitude == 0.0: + goodPosition = False + except: + self.__logger.debug('Longitude: Could not convert ' + rawData[4] + ' to float.') + + try: + newGPS.Altitude = float(rawData[5]) + if newGPS.Altitude == 0.0: + goodPosition = False + except: + self.__logger.debug('Altitude: Could not convert ' + rawData[5] + ' to float.') + + try: + newGPS.Speed = float(rawData[6]) + except: + self.__logger.debug('Speed: Could not convert ' + rawData[6] + ' to float.') + + try: + newGPS.Course = float(rawData[7]) + except: + self.__logger.debug('Course: Could not convert ' + rawData[7] + ' to float.') + + try: + newGPS.HDOP = float(rawData[10]) + except: + self.__logger.debug('HDOP: Could not convert ' + rawData[10] + ' to float.') + + try: + newGPS.PDOP = float(rawData[11]) + if newGPS.PDOP == 0.0: + goodPosition = False + except: + self.__logger.debug('PDOP: Could not convert ' + rawData[11] + ' to float.') + + try: + newGPS.VDOP = float(rawData[12]) + except: + self.__logger.debug('VDOP: Could not convert ' + rawData[12] + ' to float.') + + try: + newGPS.GPS_satellites = int(rawData[14]) + except: + self.__logger.debug('GPS_satellites: Could not convert ' + rawData[14] + ' to int.') + + try: + newGPS.GNSS_satellites = int(rawData[15]) + except: + self.__logger.debug('GNSS_satellites: Could not convert ' + rawData[15] + ' to int.') + + try: + newGPS.Signal = float(rawData[18])/55.0 + except: + self.__logger.debug('Signal: Could not convert ' + rawData[18] + ' to float.') + + if goodPosition: + self.__GPSactualData = newGPS self.__serData = '' + def __restartProcedure(self): + self.__logger.error('Try to restart gsm module') + self.__pressPowerKey() + self.__state = 1 + self.__writeLock = False + self.__retryAfterTimeout = False + self.__sentTimeout = 0 + def __waitForUnlock(self): actTime = int(round(time.time())) if self.__sentTimeout > 0 and actTime > self.__sentTimeout: # Timeout self.__logger.error('Timeout during data reception') + self.__logger.info('Command sent: ' + self.__lastCommandSentString) + self.__logger.info('Actual state of programme: ' + str(self.__state)) - if self.__state == 2: + if self.__state == 2 or self.__state == 97: # It might be that the gsm module is not powered on - # So let's try - self.__logger.error('Try to restart gsm module') - self.__pressPowerKey() - self.__state = 1 + # So let's try to restart + self.__restartProcedure() + return False + elif self.__state == 3: + # Tried to check for new SMS + # Retry 3 times + if self.__retryAfterTimeout: + if self.__retryAfterTimeoutCount > 0: + self.__retryAfterTimeoutCount -= 1 + else: + self.__restartProcedure() + return False + else: + self.__retryAfterTimeout = True + self.__retryAfterTimeoutCount = 3 + + self.__state = 2 self.__writeLock = False self.__sentTimeout = 0 return False else: + self.__logger.critical('Exception: Unhandled timeout during data reception') raise 'Unhandled timeout during data reception' if self.__writeLock: @@ -350,12 +493,12 @@ class GSMHat: newChar = self.__ser.read().decode('iso-8859-1') if newChar == '\n': - if self.__readRAW == True: + if self.__readRAW > 0: self.__serData += newChar self.__processData() else: if newChar == '\r': - if self.__readRAW == True: + if self.__readRAW > 0: self.__serData += newChar else: self.__serData += newChar @@ -374,8 +517,6 @@ class GSMHat: elif self.__state == 3: if self.__waitForUnlock(): self.__state = 97 - self.__nextState = 2 - self.__waitTime = actTime + 5000 elif self.__state == 20: # Read SMS if self.__sendToHat('AT+CMGR='+str(self.__smsToRead)): @@ -387,7 +528,8 @@ class GSMHat: pass else: # Es gab eine neue SMS - self.__smsToBuild = None + self.__logger.info('New Message from ' + self.__smsToBuild.Sender + ' was received') + self.__smsToBuild = None # Lösche die behandelte SMS an der Stelle if self.__sendToHat('AT+CMGD='+str(self.__smsToRead)): @@ -401,8 +543,6 @@ class GSMHat: self.__smsToRead = self.__smsToRead + 1 self.__state = 97 - self.__nextState = 2 - self.__waitTime = actTime + 5000 elif self.__state == 30: # SMS versenden @@ -420,8 +560,6 @@ class GSMHat: self.timeoutSerial = 5 self.__state = 97 - self.__nextState = 2 - self.__waitTime = actTime + 5000 elif self.__state == 40: if self.__sendToHat('ATD' + self.__numberToCall + ';'): @@ -438,8 +576,6 @@ class GSMHat: self.__numberToCall = '' self.__sendHangUp = True self.__state = 97 - self.__nextState = 2 - self.__waitTime = actTime + 5000 elif self.__state == 43: if self.__sendToHat('AT+CHUP'): @@ -449,8 +585,6 @@ class GSMHat: if self.__waitForUnlock(): self.__sendHangUp = False self.__state = 97 - self.__nextState = 2 - self.__waitTime = actTime + 5000 elif self.__state == 50: if self.__sendToHat('AT+CGNSPWR=1'): @@ -461,8 +595,6 @@ class GSMHat: self.__logger.debug('GPS powered on') self.__startGPS = False self.__state = 97 - self.__nextState = 2 - self.__waitTime = actTime + 5000 elif self.__state == 52: if self.__sendToHat('AT+CGNSTST=1'): @@ -483,8 +615,89 @@ class GSMHat: elif self.__state == 55: if self.__waitForUnlock(): self.__state = 97 - self.__nextState = 2 - self.__waitTime = actTime + 5000 + + elif self.__state == 60: + if self.__sendToHat('AT+SAPBR=2,1'): + self.__state = 61 + + elif self.__state == 61: + if self.__waitForUnlock(): + tempState = 97 + if self.__GPRSready == True: + pass + else: + if self.__GPRSuserAPN != None and self.__GPRSuserUSER != None and self.__GPRSuserPWD != None: + # try to connect + tempState = 62 + + self.__state = tempState + + elif self.__state == 62: + if self.__sendToHat('AT+SAPBR=3,1,"Contype","GPRS"'): + self.__state = self.__state + 1 + + elif self.__state == 63: + if self.__waitForUnlock(): + if self.__sendToHat('AT+SAPBR=3,1,"APN","' + self.__GPRSuserAPN + '"'): + self.__state = self.__state + 1 + + elif self.__state == 64: + if self.__waitForUnlock(): + if self.__sendToHat('AT+SAPBR=3,1,"USER","' + self.__GPRSuserUSER + '"'): + self.__state = self.__state + 1 + + elif self.__state == 65: + if self.__waitForUnlock(): + if self.__sendToHat('AT+SAPBR=3,1,"PWD","' + self.__GPRSuserPWD + '"'): + self.__state = self.__state + 1 + + elif self.__state == 66: + if self.__waitForUnlock(): + if self.__sendToHat('AT+SAPBR=1,1'): + self.__state = self.__state + 1 + + elif self.__state == 67: + if self.__waitForUnlock(): + self.__state = 97 + + elif self.__state == 70: + # Call the first URL in List + if self.__sendToHat('AT+HTTPINIT'): + self.__state = self.__state + 1 + + elif self.__state == 71: + if self.__waitForUnlock(): + if self.__sendToHat('AT+HTTPPARA="CID",1'): + self.__state = self.__state + 1 + + elif self.__state == 72: + if self.__waitForUnlock(): + getUrl = self.__GPRScallUrlList[0] + if self.__sendToHat('AT+HTTPPARA="URL","' + getUrl + '"'): + del self.__GPRScallUrlList[0] + self.__GPRSwaitForData = True + self.__GPRSnewDataReceived = False + self.__GPRSgotHttpResponse = False + self.__state = self.__state + 1 + + elif self.__state == 73: + if self.__waitForUnlock(): + if self.__sendToHat('AT+HTTPACTION=0'): + self.__state = 97 + #self.__state = self.__state + 1 + + elif self.__state == 74: + if self.__waitForUnlock(): + if self.__sendToHat('AT+HTTPREAD'): + self.__state = self.__state + 1 + + elif self.__state == 75: + # Close HTTP Request + if self.__waitForUnlock(): + if self.__sendToHat('AT+HTTPTERM'): + self.__state = 97 + self.__GPRSwaitForData = False + self.__GPRSnewDataReceived = False elif self.__state == 97: # Check if new SMS to send is there @@ -503,6 +716,16 @@ class GSMHat: elif self.__smsToRead > 0: self.__state = 20 + # Check if we should call some Urls + elif len(self.__GPRScallUrlList) > 0 and self.__GPRSready and self.__GPRSwaitForData == False: + self.__state = 70 + + elif self.__GPRSwaitForData and self.__GPRSgotHttpResponse: + if self.__GPRSnewDataReceived: + self.__state = 74 + else: + self.__state = 75 + # Check if GPS Unit should start elif self.__startGPS: self.__state = 50 @@ -522,10 +745,20 @@ class GSMHat: elif actTime > self.__GPSwaittime: self.__GPScollectData = True self.__GPSwaittime = actTime + self.__GPStimeout + + elif actTime > self.__SMSwaittime: + self.__state = 2 + self.__SMSwaittime = actTime + self.cSMSwaittime + + elif actTime > self.__GPRSwaittimeStatus: + self.__state = 60 + self.__GPRSwaittimeStatus = actTime + self.cGPRSstatusWaittime # Wait x Seconds elif actTime > self.__waitTime: - self.__state = self.__nextState + if self.__nextState > 0: + self.__state = self.__nextState + self.__nextState = 0 elif self.__state == 98: #Check if alive self.__logger.debug('Check if alive 98') diff --git a/setup.py b/setup.py index 1acaf98..97e5081 100755 --- a/setup.py +++ b/setup.py @@ -2,13 +2,13 @@ from distutils.core import setup setup( name = 'gsmHat', packages = ['gsmHat'], - version = '0.3', + version = '0.4', license='MIT', description = 'Using the Waveshare GSM/GPRS/GNSS Hat for Raspberry Pi with Python', author = 'Tarek Tounsi', author_email = 'software@tounsi.de', url = 'https://github.com/Civlo85/gsmHat', - download_url = 'https://github.com/Civlo85/gsmHat/archive/v_03.tar.gz', + download_url = 'https://github.com/Civlo85/gsmHat/archive/v_04.tar.gz', keywords = ['Waveshare', 'GSM', 'GPS', 'Raspberry', 'Pi'], install_requires=[ 'serial',