This episode is also available as:
Heute geht es um das Senden von Textnachrichten über das Mobilfunknetz via SMS. ESP32 und SIM808 bilden eine Art Server oder , an den zur Steuerung Textnachrichten gesendet werden können, der aber auch selbst in der Lage ist, Nachrichten zeit- oder eventgesteuert abzusetzen. Als Schalt- und Empfangszentrale könnt ihr dann das Handy, den PC oder einen Mikrocontroller verwenden. Und weil die Strecke zwischen SIM808 und Handy keine Rolle spielt, gibt es auch kein Entfernungslimit, ausreichende Netzabdeckung vorausgesetzt. Das LCD-Keypad wird eigentlich überflüssig, weil eine direkte Steuerung der entfernten ESP32-Einheit sowieso nicht in Frage kommt. Dennoch leistet ein Display gute Dienste beim Start der Relaiseinheit und für weitere Statusmeldungen beim Debuggen. Interessanter als ein LCD ist da vielleicht ein kleines OLED-Display, rein zu Wartungszwecken, aber auch das kann über SMS laufen. Damit herzlich willkommen zum dritten Teil von
Hardwarezuwachs – die SIM-Karte
An Hardware kommt im Vergleich zu Tei2 wenig dazu. Wie? Sie haben die Teile 1 und 2 nicht gelesen und sind neu hier? Na gut, überredet, in der folgenden Liste finden Sie alle Teile für das aktuelle Projekt. Fast alles davon wurde bereits in und eingesetzt und dort natürlich auch ausführlich beschrieben. Diese Bauteile verwenden wir selbstverständlich wieder. Neu hinzugekommen ist eine SIM-Karte, denn ohne diese werden Sie keine SMS-Nachricht versenden oder empfangen können. Natürlich kann es sein, dass nicht alle Sensoren und/oder Aktoren wegen der Kabellänge direkt an der Relaisstation angeschlossen werden können. Dann wäre es doch nicht schlecht, wenn es dafür eine Funkstrecke gäbe. Deshalb zeige ich Ihnen in der nächsten Folge, wie man so etwas ganz unkompliziert mit dem UDP-Protokoll über WLAN-Verbindungen umsetzen kann. Für diesen Zweck werde ich als Beispiel einen ESP8266 mit einem LDR-Widerstand als Funksensor verwenden. Andere Sensoren wie DS18B20 (Temperatur), DHT22 AM2302(Feuchte und Temperatur), GY-302 BH1750 (Licht Sensor), etc. können dank der diversen MicroPython-Module, die es dafür gibt, ebenso gut hergenommen werden. Doch jetzt erst einmal zur GSM-Verbindung, wir wollen mit unserem ESP32 ein bisschen simsen.
1 |
ESP32 Dev Kit C V4 unverlötet oder ähnlich |
1 |
LCD1602 Display Keypad Shield HD44780 1602 Modul mit 2x16 Zeichen |
1 |
|
1 |
|
1 |
Li-Akku Typ 18650 |
1 |
I2C IIC Adapter serielle Schnittstelle für LCD Display 1602 und 2004 |
4 |
Widerstand 10kΩ |
1 |
|
1 |
SIM-Karte (beliebiger Anbieter) |
Die Schaltung für das Projekt wird erst einmal 1:1 von übernommen. Später entscheiden Sie selbst, welche Teile Sie weglassen, ersetzen oder neu hinzunehmen. Die programmtechnischen Möglichkeiten für die Umsetzung finden Sie in diesem Beitrag.
Abbildung 2: gps-teil2
Ein besser lesbares Exemplar der Darstellung in DIN A4 bekommen Sie mit dem .
Die Software
Verwendete Software:
Fürs Flashen und die Programmierung des ESP:
oder
Verwendete Firmware:
Download MicroPython-Module und Programme
für SIM808 und GPS6MV2(U-Blocks)
zum LCD-Modul
für standardisierten Zugriff auf den Bus
zum Testen der Tastendecodierung des LCD-Keypads
Tricks und Infos zu MicroPython
In diesem Projekt wird die Interpretersprache MicroPython benutzt. Der Hauptunterschied zur Arduino-IDE ist, dass Sie die MicroPython-Firmware auf den ESP32 flashen müssen, bevor der Controller MicroPython-Anweisungen versteht. Sie können dazu Thonny, µPyCraft oder esptool.py benutzen. Für Thonny habe ich den Vorgang im zu diesem Thema beschrieben.
Nachdem die Firmware geflasht ist, können Sie sich zwanglos mit Ihrem Controller im Zwiegespräch unterhalten, einzelne Befehle testen und sofort die Antwort sehen, ohne vorher ein ganzes Programm compilieren und übertragen zu müssen. Bei der Entwicklung der Software für diesen Blog habe ich davon wieder reichlich Gebrauch gemacht. Das Spektrum reicht von einfachen Tests der Syntax bis zum Ausprobieren und Verfeinern von Funktionen und ganzen Programmteilen. Zu diesem Zweck werde ich, wie schon bei den vorangegangenen Folgen, kleine Testprogramme erstellen. Sie bilden eine Art Macro, weil sie wiederkehrende Befehle zusammenfassen.
Gestartet werden solche Programme aus dem aktuellen Editorfenster in der Thonny-IDE über die Taste F5, das geht schneller als der Mausklick auf den Startbutton oder über das Menü Run. Die Installation von Thonny habe ich auch im genau beschrieben.
Die Klassen GPS, SIM808 und GSM
Gut, das Wichtigste für eine GPS-Anwendung ist: Wie spreche ich die GPS-Dienste des SIM808 an? Ach ja, richtig – es soll in diesem Beitrag ja nicht (allein) um GPS sondern vorrangig um GSM gehen. Die Frage muss also wohl anders lauten. Vielleicht: Wie kann man mit dem SIM808 und dem ESP32 simsen? Genau das wollen wir uns jetzt anschauen. Die AT-Befehle erlauben uns eine einfache Handhabung der vielfältigen Eigenschaften des SIM808-Moduls.Einen sehr kleinen Teil habe ich benutzt, um daraus die GSM-Klasse für mein Projekt zu basteln. Zusammen mit der Klasse GPS für die Ortung und der Klasse SIM808 für die Hardwareansteuerung finden Sie GSM im Modul gps.py friedlich vereint.
Für neu dazugestoßene Leser, so nehmen Sie das SIM808-Modul in Betrieb. Sie müssen den kleinen Schiebeschalter gleich neben der Rohrbuchse für die Spannungsversorgung, 5V bis 12V, in Richtung SIM808-Chip schieben. Eine rote LED leuchtet neben der GSM-Antennenbuchse auf. Das finden Sie sicher ganz leicht, denn alle Lötpunkte und Schnittstellen sind auf dem Board gut dokumentiert.
Ein Stück weiter links von der GSM-Antennenbuchse befindet sich die Starttaste. Sie können zur Orientierung auch die obige Schaltskizze zu Hilfe nehmen. Drücken Sie die Starttaste ca. 1 Sekunde lang, dann leuchten zwischen den anderen beiden Antennenbuchsen zwei weitere LEDs auf, die rechte davon blinkt im Sekundenrhythmus. An die linke Schraubbuchse sollte bereits die aktive GPS-Antenne angeschlossen sein. Diese legen Sie am besten in die Nähe eines Fensters.
Damit Sie jetzt nicht jedes Mal das Gehäuse ihres GPS-Empfängers öffnen müssen, um das SIM808 zu starten, empfehle ich Ihnen, es mir gleich zu tun und ein Kabel an den heißen Anschluss des Starttasters zu löten. Von oben betrachtet ist es der rechte, wenn die Rohrbuchse ebenfalls nach rechts zeigt. Sie können nun das SIM808 starten, indem Sie einen GPIO-Pin des ESP32 als Ausgang definieren und für eine Sekunde von High nach Low und zurück auf High schalten. Ich habe dafür den Pin 4 vorgesehen.
Abbildung 3: SIM808unten
Beim Aufruf des Konstruktors für das GPS-Objekt wird die Nummer des Pins zusammen mit dem Displayobjekt als Parameter übergeben. Bevor Sie die nachfolgenden Kommandos an den ESP32 schicken, laden Sie bitte die eingangs verlinkten Module in den Flash-Speicher des Controllers hoch. Die Befehle werden über die Kommandozeile im Terminalbereich eingegeben.
>>> from gps import GPS,SIM808,GSM
>>> from lcd import LCD
>>> from machine import ADC, Pin, I2C
>>> from keypad import KEYPAD
>>> i2c=I2C(-1,Pin(21),Pin(22))
>>> d=LCD(i2c,0x27,cols=16,lines=2)
>>> g=GSM(4,d)
>>> k=KEYPAD(35)
Wird kein Displayobjekt (d) übergeben, gibt es natürlich auch keine Ausgabe auf LCD oder OLED. Es erfolgt aber keine Fehlermeldung, die Tastensteuerung arbeitet normal. Bei fast allen wichtigen Ergebnissen erfolgt eine Ausgabe im Terminalfenster.
Die Klasse GPS erledigt die Hauptarbeit. Der Konstruktor erwartet, wie erwähnt, ein Displayobjekt, das im aufrufenden Programm definiert oder bereits bekannt sein muss. Es wird ein serieller Kanal zum SIM808 auf 9600 Baud, 8,0,1 geöffnet, dann werden die Instanzvariablen für die Aufnahme der GPS-Daten eingerichtet.
Im Überblick hier die wichtigsten Methoden der GPS-Klasse
Die Methode waitForLine() tut, was ihr Name sagt, sie wartet auf einen vom SIM808. Als Parameter wird der Typ des NMEA-Satzes angegeben, der erwartet wird. Ist der Satz vollständig und fehlerfrei, wird er an das aufrufende Programm zurückgegeben. Es können in der gegenwärtigen Ausbaustufe des Programms $GPRMC- und $GPGGA-Sätze empfangen werden. Sie enthalten alle relevanten Daten wie Gültigkeit, Datum, Zeit, geographische Breite (Latitude, vom Äquator aus bis zu den Polen in Grad) und Länge (Longitude vom Null-Meridian aus +/- 180°) sowie Höhe über NN in Metern. Analog zu dem bestehenden Code können leicht weitere Datensätze vom SIM808 aufgenommen und decodiert werden.
Die Methode decodeLine() nimmt den empfangenen Satz und versucht ihn zu . Diese Methode enthält eine lokale Funktion, die nach Vorgabe des Attributs Mode die Winkelangaben in die Formate Grad Minuten Sekunden und Bruchteile, Grad und Bruchteile oder Grad Minuten und Bruchteile umwandelt.
Die Methode printData() gibt einen Datensatz im Terminalfenster aus. showData() liefert das Ergebnis an das Display. Weil mit dem LCD-Keypad nur ein zweizeiliges Display verwendet wird, muss die Anzeige in mehrere Abschnitte aufgeteilt werden. Die Tasten des Keypads übernehmen die Steuerung.
Weil die UART0-Schnittstelle für REPL reserviert ist, muss eine zweite Schnittstelle für die Kommunikation mit dem SIM808 vorhanden sein. Der ESP32 stellt eine solche als UART2 bereit. Die Anschlüsse für RXD (Empfang) und TXD (Sendung) können sogar frei gewählt werden. Für einen Vollduplexbetrieb (senden und empfangen gleichzeitig) müssen die Anschlüsse RXD und TXD vom ESP32 zum SIM808 gekreuzt werden. Sie können das am nachvollziehen. Die Defaultwerte am ESP32 sind RXD=16 und TXD=17. Die Organisation des Anschlusses übernimmt die Klasse gps.GPS.
Das beginnt mit dem Einschalten des SIM808. Wenn Sie meiner Empfehlung gefolgt sind und ein Kabel an den Einschalttaster gelötet haben, können Sie das SIM808 jetzt mit folgendem Befehl einschalten, vorausgesetzt, dass dieses Kabel am Pin 4 des ESP32 liegt.
>>> g.SIMOn()
Befehle an das SIM808 werden im AT-Format übermittelt. Es gibt eine riesige Auswahl von Befehlen, die in einer nachgelesen werden können. Aber keine Sorge, für unser Projekt reichen wenige Befehle. Zwei davon sind in den Methoden init808() und deinit808() zusammengefasst, ein paar weitere werden im GSM-Kapitel vorgestellt.
def init808(self):
self.u.write("AT+CGNSPWR=1\r\n")
self.u.write("AT+CGNSTST=1\r\n")
def deinit808(self):
self.u.write("AT+CGNSPWR=0\r\n")
self.u.write("AT+CGNSTST=0\r\n")
AT+CGNSPWR=1 aktiviert das GPS-Modul und AT+CGNSTST=1 aktiviert die Übertragung der NMEA-Sätze zum ESP32 über die serielle Schnittstelle UART2. Der Controller empfängt die Informationen des SIM808 und stellt sie in der oben beschriebenen Weise via Terminal und LCD bereit.
Das Modul gps.py enthält neben der Hardwaresteuerung des SIM808 auch noch die nötigen Befehle für das kleinere GPS-System GPS6MV2 mit dem Chip Neo 6M von UBLOX. Die Steuerung dieses Moduls erfolgt nicht über AT-Befehle, sondern über eine eigene Syntax. Die Klassen SIM808 und GPS6MV2 sind nicht gegen einander austauschbar, weil sie über unterschiedliche APIs verfügen.
Zum genaueren Studium des gps-Moduls folgt nun das Listing. Die drei enthaltenen Klassen GPS, SIM808(GPS) und GSM(SIM808) bauen durch Vererbung einen einheitlichen Namensraum auf. Daher sind alle Methoden in einem Objekt der Klasse GSM verfügbar. Wird GSM nicht gebraucht (kein SMS-Transfer), kann man auch über die Klasse SIM808 einsteigen, wie es im gemacht wurde.
Die Klasse SIM808 kümmert sich um die Hardwaresteuerung und um den Datentransfer zum ESP32. Die Klasse GPS enthält Methoden zur Decodierung der NMEA-Sätze vom SIM808, zu deren Darstellung auf dem Display und zur Kursberechnung. Die GSM-Klasse schließlich, stellt die Methoden für den SMS-Transfer und die Verwaltung von Nachrichten zur Verfügung. Diese Nachrichten müssen sich nicht allein auf GPS-Daten beziehen. Vielmehr habe ich die Klasse gps.GSM neutral gehalten, sodass sie auch in anderen Projekten einsetzbar ist. Auch im Programm relais.py finden Sie nicht nur einen GPS-Ansatz, sondern auch die Umsetzung einer BMP280-Abfrage via GSM als Beispiel für die Einbindung weiterer Sensoren. Die dafür erforderlichen Klassen BMP280 und I2CBus wurden bereits in vorgestellt.
"""
File: gps.py
Author: J. Grzesina
Rev. 1.0: AVR-Assembler
Rev. 2.0: Adaption auf Micropython
------------------
Die enthaltenen Klassen sprechen einen ESP32 als Controller an.
Dieses Modul beherbergt die Klassen GPS, GPS6MV2 und SIM808
GPS stellt Methoden zur Decodierung und Verarbeitung der NMEA-Saetze
$GPGAA und $GPRMC bereit, welche die wesentlichen Infos zur
Position, Hoehe und Zeit einer Position liefern. Sie werden dann
angezeigt, wenn die Datensaetze als "gueltig" gemeldet werden.
Eine Skalierung auf weitere NMEA-Sätze ist jederzeit möglich.
GPS6MV2 und SIM808 beziehen sich auf die entsprechende Hardware.
"""
from machine import UART,I2C,Pin
import sys
from time import sleep, time, ticks_ms
from math import *
# *********************** Beginn GSM ****************************PS
class GPS:
#
gDeg=const(0)
gFdeg=const(1)
gMin=const(1)
gFmin=const(2)
gSec=const(2)
gFsec=const(3)
gHemi=const(4)
#DEFAULT_TIMEOUT=500
#CHAR_TIMEOUT=200
def __init__(self,disp=None,key=None): # display mit OLED-API
self.u=UART(2,9600)
# u=UART(2,9600,tx=19,rx=18) # mit alternativen Pins
self.display=disp
self.key=key
self.timecorr=2
self.Latitude=""
self.Longitude=""
self.Time=""
self.Date=""
self.Height=""
self.Valid=""
self.Mode="DMF" # default
self.AngleModes=["DDF","DMS","DMF"]
self.displayModes=["time","height","pos"]
self.DMode="pos"
# DDF = Degrees + DegreeFractions
# DMS = Degrees + Minutes + Seconds + Fractions
# DMF = Degrees + Minutes + MinuteFraktions
self.DDLat=49.28868056 # aktuelle Position
self.DDLon=11.47506105
self.DDLatOld=49.3223 # vorige Position
self.DDLonOld=11.5000
self.zielPtr=0
self.course=0
self.distance=0
print("GPS initialized, Position:{},{}".format(self.DDLat,self.DDLon))
def decodeLine(self,zeile):
latitude=["","","","","N"]
longitude=["","","","","E"]
angleDecimal=0
def formatAngle(angle): # Eingabe ist Deg:Min:Fmin
nonlocal angleDecimal
minute=int(angle[1]) # min als int
minFrac=float("0."+angle[2]) # minfrac als float
angleDecimal=int(angle[0])+(float(minute)+minFrac)/60
if self.Mode == "DMS":
seconds=minFrac*60
secInt=int(seconds)
secFrac=str(seconds - secInt)
a=str(int(angle[0]))+"*"+angle[1]+"'"+str(secInt)+secFrac[1:6]+'"'+angle[4]
elif self.Mode == "DDF":
minutes=minute+minFrac
degFrac=str(minutes/60)
a=str(int(angle[0]))+degFrac[1:]+"* "+angle[4]
else:
a=str(int(angle[0]))+"*"+angle[1]+"."+angle[2]+"' "+angle[4]
return a
# GPGGA-Fields
nmea=[0]*16
name=const(0)
time=const(1)
lati=const(2)
hemi=const(3)
long=const(4)
part=const(5)
qual=const(6)
sats=const(7)
hdop=const(8)
alti=const(9)
auni=const(10)
geos=const(11)
geou=const(12)
aged=const(13)
trash=const(14)
nmea=zeile.split(",")
lineStart=nmea[0]
if lineStart == "$GPGGA":
self.Time=str((int(nmea[time][:2])+self.timecorr)%24)+":"+nmea[time][2:4]+":"+nmea[time][4:6]
latitude[gDeg]=nmea[lati][:2]
latitude[gMin]=nmea[lati][2:4]
latitude[gFmin]=nmea[lati][5:]
latitude[gHemi]=nmea[hemi]
longitude[gDeg]=nmea[long][:3]
longitude[gMin]=nmea[long][3:5]
longitude[gFmin]=nmea[long][6:]
longitude[gHemi]=nmea[part]
self.Height,despose=nmea[alti].split(".")
self.Latitude=formatAngle(latitude) # mode = Zielmodus Winkelangabe
self.DDLat=angleDecimal
self.Longitude=formatAngle(longitude)
self.DDLon=angleDecimal
if lineStart == "$GPRMC":
date=nmea[9]
self.Date=date[:2]+"."+date[2:4]+"."+date[4:]
try:
self.Valid=nmea[2]
except:
self.Valid="V"
def waitForLine(self,title,delay=2000):
line=""
c=""
d=delay
if delay < 1000: d=1000
start = ticks_ms()
end=start+d
current=start
while current <= end:
#print(end-current)
if self.u.any():
c=self.u.read(1)
if ord(c) <=126:
c=c.decode()
if c == "\n":
test=line[0:6]
if test==title:
#print(line)
return line
else:
line=""
else:
if c != "\r":
line +=c
current = ticks_ms()
sleep(0.05)
return ""
def showData(self):
if self.display:
if self.DMode=="time":
self.display.writeAt("Date:{}".format(self.Date),0,0)
self.display.writeAt("Time:{}".format(self.Time),0,1)
if self.DMode=="height":
self.display.writeAt("Height: {}m ".format(self.Height),0,0)
self.display.writeAt("Time:{}".format(self.Time),0,1)
if self.DMode=="pos":
self.display.writeAt(self.Latitude+" "*(16-len(self.Latitude)),0,0)
self.display.writeAt(self.Longitude+" "*(16-len(self.Longitude)),0,1)
def printData(self):
print(self.Date,self.Time,sep="_")
print("LAT",self.Latitude)
print("LON",self.Longitude)
print("ALT",self.Height)
def showError(self,msg):
if self.display:
self.display.clearAll()
self.display.writeAt(msg,0,0)
print(msg)
def storePosition(self): # aktuelle Position als DD.dddd merken
lat=str(self.DDLat)+","
lon=str(self.DDLon)+"\n"
try:
D=open("stored.pos","wt")
D.write(lat)
D.write(lon)
D.close()
if self.display:
self.display.clearAll()
self.display.writeAt("Pos. stored",0,0)
sleep(3)
self.display.clearAll()
except (OSError) as e:
enumber=e.args[0]
if enumber==2:
print("Not stored")
if self.display:
self.display.clearAll()
self.display.writeAt("act. Position",0,0)
self.display.writeAt("not stored",0,0)
sleep(3)
self.display.clearAll()
def chooseDestination(self, wait=3):
if not self.display: return None
self.display.clearAll()
self.display.writeAt("ENTER=RST-Button",0,0)
n="positions.pos"
try:
D=open(n,"rt")
ziel=D.readlines()
D.close()
i = 0
while 1:
lat,lon=(ziel[i].rstrip("\r\n")).split(",")
self.display.clearAll()
self.display.writeAt("{}. {}".format(i,lat),0,0)
self.display.writeAt(" {}".format(lon),0,1)
sleep(wait)
if self.key.value()==0: break
i+=1
if i>=len(ziel): i=0
self.zielPtr=i
self.display.clearAll()
self.display.writeAt("picked: {}".format(i),0,0)
sleep(wait)
self.display.clearAll()
lat,lon=ziel[i].split(",")
lon=lon.strip("\r\n")
print("{}. Lat,Lon: {}, {}".format(i,lat,lon))
lat=float(lat)
lon=float(lon)
return (lat,lon)
except (OSError) as e:
enumber=e.args[0]
if enumber==2: print("File not found")
self.display.clearAll()
self.display.writeAt("There is no",0,0)
self.display.writeAt("Positionfile",0,1)
sleep(3)
self.display.clearAll()
return (0,0)
def calcNewCourse(self,delay=3): # von letzter Position bis hier
lat,lon=self.chooseDestination(delay)
if lat==0 and lon==0: return
dy=(lat-self.DDLat)*60*1852
dx=(lon-self.DDLon)*60*1852*cos(radians(self.DDLatOld))
print("Start {},{}".format(self.DDLat,self.DDLon)," Ziel {},{}".format(lat,lon))
#print("Ziel-Start",self.DDLon-self.DDLonOld, self.DDLat-self.DDLatOld)
return self.calcCourse(dx,dy)
def calcLastCourse(self): # von letzter Position bis hier
try:
D=open("stored.pos","rt")
lat,lon=(D.readline()).split(",")
D.close()
self.DDLatOld=float(lat)
self.DDLonOld=float(lon)
except:
self.DDLatOld=49.3223
self.DDLonOld=11.50
dy=(self.DDLat-self.DDLatOld)*60*1852
dx=(self.DDLon-self.DDLonOld)*60*1852*cos(radians(self.DDLatOld))
print("Start {},{}".format(self.DDLonOld,self.DDLatOld)," Ziel {},{}".format(self.DDLon,self.DDLat))
#print("Ziel-Start",self.DDLon-self.DDLonOld, self.DDLat-self.DDLatOld)
return self.calcCourse(dx,dy)
def calcCourse(self,dx,dy): # von letzter Position bis hier
course=0
distance=0
#print(dx,dy,degrees(atan2(dy,dx)))
if abs(dx) < 0.0002:
if dy > 0:
course=0
#print("Trace: 1")
if dy < 0:
course=180
#print("Trace: 2")
if abs(dy) < 0.0002:
course=None
#print("Trace: 3")
else: # dx >= 0.0002
if abs(dy) < 0.0002:
if dx > 0:
course=90
#print("Trace: 4")
if dx < 0:
course=270
#print("Trace: 5")
else: ## dy > 0.0002
course=90-degrees(atan2(dy,dx))
#print("Trace: 6")
if course > 360:
course -= 360
#print("Trace: 7")
if course < 0:
course += 360
print("Trace: 8")
self.course=int(course)
self.distance=int(sqrt(dx*dx+dy*dy))
print("Distance: {}, Course: {}".format(self.distance,self.course))
return (self.distance,self.course)
# *********************** Ende GPS ******************************
# ********************* Beginn SIM808 ***************************
class SIM808(GPS):
DEFAULT_TIMEOUT=const(500)
CHAR_TIMEOUT=const(100)
CMD=const(1)
DATA=const(0)
def __init__(self,switch=4,disp=None,key=None):
self.switch=Pin(switch,Pin.OUT)
self.switch.on()
super().__init__(disp,key)
self.display=disp
self.key=key
print("SIM808 initialized")
def simOn(self):
self.switch.off()
sleep(1)
self.switch.on()
sleep(3)
def simOff(self):
self.switch.off()
sleep(3)
self.switch.on()
sleep(3)
def simStartPhone():
pass
def simGPSInit(self):
self.u.write("AT+CGNSPWR=1\r\n")
self.u.write("AT+CGNSTST=1\r\n")
def simGPSDeinit(self):
self.u.write("AT+CGNSPWR=0\r\n")
self.u.write("AT+CGNSTST=0\r\n")
def simStopGPSTransmitting(self):
self.u.write("AT+CGNSTST=0\r\n")
def simStartGPSTransmitting(self):
self.u.write("AT+CGNSTST=1\r\n")
def simFlushUART(self):
while self.u.any():
self.u.read()
# Wartet auf Zeichen an UART -> 0: keine Zeichen bis Timeout
def simWaitForData(self,delay=CHAR_TIMEOUT):
noOfBytes=0
start=ticks_ms()
end=start+delay
current=start
while current <= end:
sleep(0.1)
noOfBytes=self.u.any()
if noOfBytes>0:
break
return noOfBytes
def simReadBuffer(self,cnt,tout=DEFAULT_TIMEOUT,ctout=CHAR_TIMEOUT):
i=0
strbuffer=""
start=ticks_ms()
prevchar=0
while 1:
while self.u.any():
c=self.u.read(1)
c=chr(ord(c))
prevchar=ticks_ms()
strbuffer+=c
i+=1
if i>=cnt: break
if i>= cnt:break
if ticks_ms()-start > tout: break
if ticks_ms()-prevchar > ctout: break
return (i,strbuffer) # gelesene Zeichen
def simSendByte(self,data):
return self.u.write(data.to_bytes(1,"little"))
def simSendChar(self,data):
return self.u.write(data)
def simSendCommand(self,cmd):
self.u.write(cmd)
def simSendCommandCRLF(self,cmd):
self.u.write(cmd+"\r\n")
def simSendAT(self):
return self.simSendCmdChecked("AT","OK",CMD)
def simSendEndMark(self):
self.simSendChar(chr(26))
def simWaitForResponse(self,resp,typ=DATA,tout=DEFAULT_TIMEOUT,ctout=CHAR_TIMEOUT):
l=len(resp)
s=0
self.simWaitForData(300)
start=ticks_ms()
prevchar=0
while 1:
if self.u.any():
c=self.u.read(1)
if ord(c) < 126:
c=c.decode()
prevchar=ticks_ms()
s=(s+1 if c==resp[s] else 0)
if s == l: break
if ticks_ms()-start > tout: return False
if ticks_ms()-prevchar > ctout: return False
if type==CMD:
self.simFlushUART()
return True
def simSendCmdChecked(self,cmd,response,typ,tout=DEFAULT_TIMEOUT,ctout=CHAR_TIMEOUT):
self.simSendCommand(cmd)
return self.simWaitForResponse(response,typ,tout,ctout)
# ********************** Ende SIM808 ***************************
# *********************** Beginn GSM ****************************
class GSM(SIM808):
def __init__(self, switch=4, disp=None, key=None):
super().__init__(switch,disp,key)
self.gsmInit()
print("GSM module initialized")
def gsmInit(self):
if not self.simSendCmdChecked("AT","OK\r\n",CMD):
return False
if not self.simSendCmdChecked("AT+CFUN=1","OK\r\n",CMD):
return False
if not self.gsmCheckSimStatus():
return False
return True
def gsmIsPowerUp(self):
return self.simSendAT()
def gsmPowerOn(self):
self.switch.off()
sleep(3)
self.switch.on()
sleep(3)
def gsmPowerReset(self):
self.switch.off()
sleep(3)
self.switch.on()
sleep(3)
self.switch.off()
sleep(1)
self.switch.on()
sleep(3)
def gsmCheckSimStatus(self):
n=0
a=""
self.simFlushUART()
while n < 3:
self.simSendCommand("AT+CPIN?\r\n")
a=self.simReadBuffer(32)
if "+CPIN: READY" in a[1]:
break
n+=1
sleep(0.3)
if n == 3:
return False
return True
def gsmSendSMS(self,phoneNbr,mesg):
if not self.simSendCmdChecked("AT+CMGF=1\r\n","OK\r\n",CMD):
print("SMS-Mode not selcted")
return False
sleep(0.5)
self.simFlushUART()
if not self.simSendCmdChecked('AT+CMGS="'+phoneNbr+'"\r\n',">",CMD):
print("Phonenumber Problem")
return False
sleep(1)
self.simSendCommand(mesg)
sleep(0.5)
self.simSendEndMark()
sleep(1)
return self.simWaitForResponse(mesg,CMD)
#return self.simReadBuffer(50)
def gsmAreThereSMS(self,stat):
buf=""
# SMS-Modus aktivieren
self.simSendCmdChecked("AT+CMGF=1\r\n","OK\r\n",CMD)
sleep(1)
self.simFlushUART()
# ungelesene SMS listen ohne Statusänderung ",1"
#print("SMS-Status",stat)
self.simSendCommand('AT+CMGL="{}",1\r\n'.format(stat))
sleep(2)
# OK findet sich in den ersten 30 Zeichen nur, wenn
# keine ungelesene SMS vorliegt
a=self.simReadBuffer(30)[1]
#print(30,a)
if "OK" in a:
sleep(0.1)
return 0
else:
# restliche Zeichen im UART-Buffer entsorgen
self.simFlushUART()
# erneuter Aufruf zum Einlesen
self.simSendCommand('AT+CMGL="{}",1\r\n'.format(stat))
sleep(2)
a=self.simReadBuffer(48)[1]
#print(48,a)
# suche nach der Position von "+CMGL:"
p=a.find("+CMGL:")
if p != -1:
pkomma=a.find(",",p)
#print("gefunden",a[p+6:pkomma])
return int(a[p+6:pkomma])
else:
#print("CMGL not found", a)
return None
#print("Kein 'OK'")
return None
def gsmReadAll(self,stat,cnt=500):
# SMS-Modus aktivieren
self.simSendCmdChecked("AT+CMGF=1\r\n","OK\r\n",CMD)
sleep(1)
self.simFlushUART()
# SMS listen ohne Statusänderung ",1"
self.simSendCommand('AT+CMGL="{}",1\r\n'.format(stat))
sleep(2)
a=self.simReadBuffer(cnt)
#print("BUFFER:\n",a[1],"\n")
return a
def gsmFindSMS(self,stat="ALL",cnt=150):
Liste=[]
i=0
p0=0
again=-1
self.simFlushUART()
m,a=self.gsmReadAll(stat,cnt)
while again == -1:
again=a.find("OK") # wenn OK gefunden, letzter Durchgang
#print("again",again) # only for debugging
#print("@@@@",a,"@@@@") # only for debugging
while 1:
merker=p0
#print("merker: ",merker) # only for debugging
p0=a.find("+CMGL:",p0)
#print("While1_p0",p0)
if p0 == -1:
p0=merker
#print("@p0_break",p0) # only for debugging
break
p1=a.find(",",p0)
#print("p0 und p1: ",p0,p1) # only for debugging
if p1 == -1:
p0=merker
#print("@p1_break",p0) # only for debugging
break
n=int(a[p0+6:p1])
#print("Liste: ",Liste,"neu: ",n) # only for debugging
Liste.append(n)
p0=p1
m,x=self.simReadBuffer(cnt)
#print(p0,": Buffer: \n",a[p0:],"*****",x,"<<<<<") # only for debugging
a=a[p0:]+x
p0=0
if m == 0:
break
return Liste
def gsmReadSMS(self,index,mode=0): # mode=1: Status nicht veraendern
# SMS-Modus einschalten
self.simSendCmdChecked("AT+CMGF=1\r\n","OK\r\n",CMD)
sleep(1)
self.simFlushUART()
try:
if mode==1:
self.simSendCommand("AT+CMGR={},1\r\n".format(index))
elif mode==0:
self.simSendCommand("AT+CMGR={}\r\n".format(index))
sleep(1)
a=self.simReadBuffer(250) # Antwort=(Anzahl, Zeichen)
#print(a[1]) # only for debugging
if a[0] != 0:
a=a[1].split(',',4+mode)
#print(a) # only for debugging
p0=a[0+mode].find('"')
p1=a[0+mode].find('"',p0+1)
Status=a[0+mode][p0+1:p1]
Phone=a[1+mode].strip('"')
Date=a[3+mode].lstrip('"')
Time=a[4+mode].split('"')[0]
Message=a[4+mode].split('"')[1].strip("\r\n").rstrip("\r\nOK")
return (Status,Phone,Date,Time,Message)
else:
return None
except (IndexError,TypeError) as e:
print("Index out if range",e.args[0])
return None
def gsmShowAndDelete(self,stat,disp=None,delete=None):
SMSlist=self.gsmFindSMS(stat, cnt=100)
while 1:
if len(SMSlist)==0:
print("nothing to do")
break
for i in SMSlist:
response=self.gsmReadSMS(i,mode=1)
print(i,response[0],"\n",response[4])
if disp:
disp.clearAll()
disp.writeAt("{}. {}".format(i,response[0]),0,0)
disp.writeAt(response[4],0,1)
sleep(2)
if delete or not(response[1]=='+49xxxxxxxxxxx'): #evtl. weitere PhoneNbr + stati
if self.gsmDeleteSMS(i):
print("!!! deleted",response[1])
first=SMSlist[0]
SMSlist=self.gsmFindSMS(stat,cnt=100)
if len(SMSlist)==0 or SMSlist[0] == first: break
if disp: disp.clearAll()
def gsmDeleteSMS(self,index):
print("SMS[{}] deleted".format(index))
return self.simSendCmdChecked("AT+CMGD={},0\r\n".format(index),"OK\r\n",CMD)
# *********************** Ende GSM ******************************
# ******************** Beginn GPS6MV2 ***************************
class GPS6MV2(GPS):
# Befehlscodes setzen
GPGLLcmd=const(0x01)
GPGSVcmd=const(0x03)
GPGSAcmd=const(0x02)
GPVTGcmd=const(0x05)
GPRMCcmd=const(0x04)
def __init__(self,delay=1,disp=None,key=None):
super().__init__(disp,key)
self.display=disp
self.delay=delay # GPS sendet im delay Sekunden Abstand
period=delay*1000
SetPeriod=bytearray([0x06,0x08,0x06,0x00,period&0xFF,(period>>8)&0xFF,0x01,0x00,0x01,0x00])
self.sendCommand(SetPeriod)
self.sendScanOff(bytes([GPGLLcmd]))
self.sendScanOff(bytes([GPGSVcmd]))
self.sendScanOff(bytes([GPGSAcmd]))
self.sendScanOff(bytes([GPVTGcmd]))
self.sendScanOn(bytes([GPRMCcmd]))
print("GPS6MV2 initialized")
def sendCommand(self,comnd): # comnd ist ein bytearray
self.u.write(b'\xB5\x62')
a=0; b=0
for i in range(len(comnd)):
c=comnd[i]
a+=c # Fletcher Algorithmus
b+=a
self.u.write(bytes([c]))
self.u.write(bytes([a&0xff]))
self.u.write(bytes([b&0xff]))
def sendScanOff(self,item): # item ist ein bytes-objekt
shutoff=b'\x06\x01\x03\x00\xF0'+item+b'\x00'
self.sendCommand(shutoff)
def sendScanOn(self,item):
turnon=b'\x06\x01\x03\x00\xF0'+item+b'\x01'
self.sendCommand(turnon)
GSM – oder Simsen für den ESP32
Zu den AT-Befehlen für das Einschalten der GPS-Einheit und das Öffnen der UART-Verbindung auf dem SIM808 gesellen sich für den SMS-Betrieb einige weitere, auf die ich kurz eingehen werde. Grundsätzlich sind AT-Befehle mit einem \r\n (Wagenrücklauf und Zeilenvorschub) abzuschließen, damit sie vom SIM808 als solche erkannt werden. Das SIM808 sendet Antworten oder Ergebnisse zurück. Außer bei Nutzdaten, wie Nachrichtentexten, ist es hier nur wichtig, ob die Antwort der erwarteten entspricht. Es gibt daher eine gsm-Methode, die genau diese Überprüfung durchführt. Das Klassenattribut CMD (Wert = 1) kennzeichnet den AT-Befehl als Kommando. Für Kommandos gilt, dass nach dem Einlesen der Antwort vom SIM808 der Rest des UART-Buffers automatisch geleert wird.
simSendCmdChecked("AT+CMGF=1\r\n","OK\r\n",CMD)
AT+CMGF=1: schaltet auf Textbetrieb um, und ermöglich so den SMS-Betrieb
Der Status von SMS-Nachrichten erlaubt deren Auswahl zum Beispiel beim Listbefehl. stat enthält demnach einen der folgenden Strings: "ALL", "REC UNREAD" oder "REC_READ". Dieser Wert wird durch die Formatanweisung in den Befehl eingebaut.
simSendCommand('AT+CMGL="{}",1\r\n'.format(stat))
Die '1' sorgt dafür, dass sich beim Auflisten der Status nicht ändert.
Beim Versenden von SMS-Nachrichten muss dem Befehl als erstes die Nummer des Anschlusses mitgeteilt werden. Das SIM808 antwortet darauf mit einem ">".
simSendCmdChecked('AT+CMGS="'+phoneNbr+'"\r\n',">",CMD)
Wurde ">" erkannt, kann die Nachricht gesendet werden.
simSendCommand(mesg)
Das Ende der Nachricht wird der Gegenstation durch Senden eines Textendezeichens (chr(26)) bekannt gegeben.
Um eine SMS-Nachricht aus dem Speicher des SIM808 zu lesen, muss deren Index bekannt sein. Nach Absetzen des Lesebefehls kann der UART-Puffer des SIM808 ausgelesen werden. Er enthält den Status, Datum, Uhrzeit und den Text der Nachricht.
"AT+CMGR={}\r\n".format(index))
Nachrichten können durch Angabe des Index auch gelöscht werden.
simSendCmdChecked("AT+CMGD={},0\r\n".format(index),"OK\r\n",CMD)
Erst nachdem eine SIM-Karte erkannt wurde, ist der SMS-Betrieb möglich. Der Befehl
simSendCommand("AT+CPIN?\r\n")
überprüft das. Die SIM-Karte darf dabei nicht durch eine PIN gesichert sein.
Ein Teil der GSM-Methoden dient der internen Verarbeitung und Steuerung. Das Hauptprogramm relais.py zeigt die Anwendung der Methoden der Klasse.
Das Anwendungsprogramm relais.py ist im Vergleich zur reinen GPS-Anwendung aus Teil2 ein wenig umfangreicher geworden. Das liegt an den verschiedenen Beispielen für die Abfrage von Sensoren und dem SMS-Nachrichtenverkehr. Das Programm hat schon einiges zu bieten. Im Einzelnen demonstriert es:
-
zeitgesteuerte SMS (alle x Stunden, Minuten…)
-
eventgesteuerte SMS (Temperatur außerhalb eines Bereichs)
-
SMS on demand (Antwort auf eine SMS)
-
GPS-Tracker (Entfernung überschritten oder Wegpunktübermittlung)
-
Messwert übermitteln
# File: relais.py
# put contents into boot.py after successful testing
# Purpose: Booting SMS-Relais Station
# Author: J. Grzesina
# Rev.:1.0 - 2021-04-29
#********************** Beginn Bootsequenz ************************
# Dieser Teil geht 1:1 an boot.py fuer autonomen Start
#************************ Importgeschaeft *************************
# Hier werden grundlegende Importe erledigt
import os,sys # System- und Dateianweisungen
import esp # nervige Systemmeldungen aus
esp.osdebug(None)
import gc # Platz fuer Variablen schaffen
gc.collect()
#
from machine import ADC, Pin, I2C
from button import BUTTONS, BUTTON32
from time import sleep,time,sleep_ms, ticks_ms
from gps import GPS,SIM808, GSM
from lcd import LCD
from keypad import KEYPAD
from bmp280 import BMP280
#***************** Variablen/Objekte deklarieren *****************
# ----------------------------------------------------------------
# ***************** create essential objects *********************
# ----------------------------------------------------------------
rstNbr=25
rst=BUTTON32(rstNbr,True,"RST")
ctrl=Pin(rstNbr,Pin.IN,Pin.PULL_UP)
t=BUTTONS() # Methoden für Buttons bereitstellen
i2c=I2C(-1,Pin(21),Pin(22))
k=KEYPAD(35)
d=LCD(i2c,0x27,cols=16,lines=2)
d.printAt("SIM808-GSM",0,0)
d.printAt("RELAY booting",0,1)
sleep(1)
g=GSM(switch=4,disp=d,key=ctrl)
print("************************")
g.simGPSInit()
g.simOn()
g.mode="DDF"
b=BMP280(i2c)
#sleep(10)
timeFlag =0b00000001 # Intervallsteuerung
distanceFlag=0b00000010 # Streckenalarm
tempFlag =0b00000100 # Tempraturalarm
orderFlag =0b00001000 # SMS Anforderung per SMS
Trigger=orderFlag # Hier die Flags der Dienste eintragen
distLimit=100 # Entfernungsrichtwert in Metern
tempMax=25 # *C
tempMin=20 # *C
heartbeat=0 # Actionblinker
timeBase=3600*1 # Zeitintervall ins Sekunden
distanceBase=3600*1#3600*2 # Sekunden
tempBase=20#3600*2 # Sekunden
timeEnde=time()+5
distanceEnde=time()+5
tempEnde=time()+5
# Die Dictionarystruktur (dict) erlaubt spaeter die Klartextausgabe
# des Verbindungsstatus anstelle der Zahlencodes
#********************Funktionen deklarieren ***********************
def timeJob():
t=str(b.calcTemperature())+"*C"
p=str(b.calcPressureNN())+"hPa"
print("sende Wetterdaten")
g.simFlushUART()
g.gsmSendSMS("+49xxxxxxxxxxx","Wetterwerte:\n{},\n{}".format(t,p))
return True
def tempJob(temp):
t=str(temp)+"*C"
return g.gsmSendSMS("+49xxxxxxxxxxx","WARNUNG!!:\n{},\nTemperatur nicht im Limit!!".format(t))
def positionJob(dist):
g.gsmSendSMS("+49xxxxxxxxxxx","Entfernung: {},\n Positionswerte\n{},\n{}".format(dist,g.Latitude,g.Longitude))
print("Position gemeldet")
def orderJob(Nachrichten):
# decode message
if "wetter" in Nachrichten.lower():
print("Wetterdaten angefordert")
return timeJob()
# do jobs
# send results
# return True # if no errors occured
def getDistance():
rmc=g.waitForLine("$GPRMC",delay=1000)
if rmc:
try:
g.decodeLine(rmc)
if g.Valid == "A":
try:
gga=g.waitForLine("$GPGGA",delay=2)
g.decodeLine(gga)
g.printData()
if d: g.showData()
entfernung=g.calcLastCourse()[0]
print("Distanz: {}".format(entfernung))
return entfernung
except:
g.showError("Invalid GGA-set!")
except:
g.showError("Invalid RMC-set!")
# ******************** Funktionen Ende ***********************
print("Check SMS")
g.gsmShowAndDelete("REC_READ",d,delete=True)
g.gsmShowAndDelete("ALL",d,delete=False)
# *********************** Main Loop **************************
while 1:
getDistance()
if d:
heartbeat+=1
if heartbeat % 2:
d.writeAt("X ",14,0)
else:
d.writeAt(" "+g.Valid,14,0)
if (Trigger & timeFlag) == timeFlag and time() >= timeEnde:
timeJob()
timeEnde=time()+timeBase
if (Trigger & distanceFlag) == distanceFlag and time() >= distanceEnde:
g.calcLastCourse()
w=g.distance
print(w)
if w > distLimit:
positionJob(w)
## naechsten Befehl aus kommentieren für Kreisfläche
g.storePosition()
distanceEnde = time()+ distanceBase
if (Trigger & orderFlag) == orderFlag:
L=g.gsmFindSMS("REC UNREAD")
if L:
Nachricht=g.gsmReadSMS(L[0],mode=1)[4]
print("neue SMS gefunden", L)
orderJob(Nachricht)
print("Job erledigt, mache jetzt Kaffepause")
sleep(5)
g.gsmDeleteSMS(L[0])
sleep(2)
if (Trigger & tempFlag)==tempFlag and time() >= tempEnde:
t=b.calcTemperature()
#print("Temperatur: {} ## {}".format(t,tempEnde))
if t < tempMin or t > tempMax:
tempJob(t)
tempEnde=time()+tempBase
#print(tempEnde)
# Job-Schleifenende
if ctrl.value() == 0: break
sleep(0.1)
Hinweis:
Damit Sie auch Nachrichten auf Ihr Handy bekommen, setzen Sie bitte an anstatt der +49xxxxxxxxxxx Ihre eigene Handynummer ein.
Für die einzelnen Jobs gibt es jeweils ein Steuerflag. damit wird die Funktion scharf geschaltet. Alle Funktionen bis auf SMS on demand haben neben dem Steuerflag noch eine zusätzliche Zeitsperre. Diese bewirkt, dass zum Beispiel nach dem Überschreiten der Temperatur nicht pausenlos SMS versandt werden, bis der Wert wieder im Rahmen ist. Die Zeitsperre wird in Sekunden angegeben, kann aber durch entsprechende Faktoren fast beliebig gedehnt werden.
Im Eventmodus wird in festen Zeitabschnitten ein Sensor abgefragt. Nur wenn der Sensorwert nicht den Vorgaben entspricht, wird eine Nachricht versandt.
Der rein zeitgesteuerte Modus verschickt unabhängig von einem Sensorwert das Ergebnis einer Messung. Hier wird per Voreinstellung auf die Messergebnisse des BMP280 zugegriffen, stellvertretend für beliebige weitere Sensoren.
Bei der SMS on demand können Codeworte an den ESP32 via SMS gesendet werden. Ungelesene SMS-Leichen werden bei jedem Neustart gelöscht. Dann informiert das System über noch im Speicher befindliche andere Nachrichten.
in der Jobschleife wird auf eingetroffene Nachrichten geprüft. Die Jobfunktion muss den Inhalt decodieren und gegebenenfalls entsprechende Aktionen einleiten oder Messungen durchführen. Denkbar sind auch Aktionen, die direkt auf das SIM808 wirken und zum Beispiel alle SMS löschen. In jedem Fall automatisch gelöscht wird auch die Mail, welche die Aktion ausgelöst hat. Diese Betriebsart ist voreingestellt.
Weitere Nachrichten warten im Eingangspuffer des SIM808 und werden der Reihe nach abgearbeitet. Sehr wichtig sind die Wartezeiten (fett). werden die weggelassen oder zu kurz angesetzt, dann gehen SMS im Dauerlauf raus, weil der nächste Schleifendurchlauf die Nachricht erneut auffindet und bearbeitet.
if (Trigger & orderFlag) == orderFlag:
L=g.gsmFindSMS("REC UNREAD")
if L:
Nachricht=g.gsmReadSMS(L[0],mode=1)[4]
print("neue SMS gefunden", L)
orderJob(Nachricht)
print("Job erledigt, mache jetzt Kaffepause")
sleep(5)
g.gsmDeleteSMS(L[0])
sleep(2)
Sie sehen, die Einsatzmöglichkeiten sind sehr vielfältig. Dazu kommt, dass niemand außer Ihnen diese Steuerung unberechtigt benutzen kann, es sei denn, Sie hängen die Telefonnummer Ihrer SIM-Karte ans Schwarze Brett, zusammen mit all Ihren Steuercodes und dem Handy. Denn selbst Zugriffe von anderen Telefonnummern außer Ihrer eigenen sind im Programm geblockt.
So, und damit das alles autonom mit dem Einschalten des ESP32 startet, müssen Sie das Programm einfach in die Datei boot.py kopieren und diese zum ESP32 hochladen, Neustart, das war's. Sollte nachträglich etwas schief gehen und der ESP32 von Thonny oder einem anderen Terminalprogramm aus nicht mehr ansprechbar sein, dann ziehen Sie einfach die Notbremse, die das Programm abbricht. Es ist die RST-Taste am LCD-Keypad, die Sie so lange drücken, bis im Terminal der REPL-Prompt >>> erscheint. Jetzt können Sie Änderungen durchführen.
Ich wünsche viel Vergnügen beim Simsen mit Ihrem ESP32. In der nächsten Folge erlauben wir unserem Controller, zusammen mit dem SIM808, fremdzugehen. Ein oder auch mehrere Funkmodule, die mit ESP8266 oder ESP32 bestückt sein können, werden den Aktionsradius des Controllers erweitern. Das Übertragungsprotokoll wird UDP sein, das reicht für unsere Zwecke locker aus und ist viel schneller und simpler als TCP. Bleiben Sie dran!
Viel Spaß bei der Umsetzung des Projekts!
Weitere Downloadlinks: