Arduino/ESP + SIMCom A7670E LTE-Modul

Auch in Deutschland ist vermutlich Mitte 2028 Schluß mit dem 2G GSM-Netz: Sowohl Telekom als auch Vodafone haben das Datum zur Abschaltung ihrer ersten digitalen Mobilfunknetze vor einiger Zeit verkündet. Module wie das SIM800L, die nur das 2G-Netz mit GPRS und EDGE unterstützen, werden dann nicht mehr funktionieren. Zeit sich nach Alternativen umzusehen. Das A7670E von SIMCom könnte eine solche Altnative sein. Es ist mit knapp über 10,- Euro von Aliexpress und co. relativ günstig zu haben und via UART-Schnittstelle auch leicht mit 2 Kabeln anzubinden. Allerdings ist nicht alles Gold was glänzt. Hier meine schnelle Pros-/Cons-Liste, bevor wir uns das Board und Software-Beispiele genauer ansehen.

+ relativ günstig
+ kompakt
+ überschaubarer Stromverbrauch
+ optional mit GPS und Bluetooth
+ USB-Anschluss für schnellere Datenübertragung

min. 5 V Versorgungsspannung (SIM800L lief auch mit 3.7 V)
Status LED ungenau (moniert nicht verbunden zu sein, obwohl Daten gesendet und empfangen werden)

Hardware

Für mein Test-Setup verwende ich ein ESP32-WROOM-32D-Board. Andere Boards sind möglich. Ich verwende dieses Board aufgrund der Hardware-Serial, über die die Kommunikation zwischen ESP und A7670E-LTE-Modem läuft. Die Beschriftung auf dem Modem ist ein bisschen unübersichtlich. Auf meinem Board von oben nach unten:
G: GND, R: RXD, T: TXT, K: PWRKEY, V: VCC, G: GND, S-SLEEP

Für mein Setup genügt es, vom ESP TX/RX über Kreuz zu verbinden: Also: TX vom ESP auf RX vom Modem und umgekehrt. Außerdem teilen sich ESP und A7670E einen Ground. Das wars dann aber auch. Ich hatte zwischenzeitlich einen 2200μF-Kondensator verbaut, bis ich gesehen habe das genau für diesen Zweck bereits ein 4700μF-Kondensator auf dem Modem verbaut ist. Trotzdem kackte das Modem beim Einbuchen ins LTE-Netz ab und an ab – die Stromversorgung scheint da aber wohl dann nicht der Grund gewesen zu sein?!

Software

Getestet wurde das hier mit dem A7670E-Modul. Allerdings sollten die meisten Sketches auch mit anderen Boards von SIMCom funktionieren, sofern sie den gleichen Befehlssatz unterstützen.

Basis Sketch

Um es auch für Einsteiger verständlich zu halten, teile ich die einzelnen Funktion in kleine Snippets auf. So könnt ihr euch das raussuchen, was für euer Szenario am besten passt. Diese basieren auf folgender Basis, die zunächst nichts weiter macht als die Kommunikation mit dem A7670E sicherzustellen und die Antworten des Moduls im Serial Monitor euer (Arduino-)IDE anzuzeigen.

#include <Arduino.h>

#define RX_PIN 17 // RX-Pin des ESP-Boards
#define TX_PIN 16 // TX-Pin des ESP-Boards
HardwareSerial modem(2);

void setup() {
  Serial.begin(115200);

  delay(1000);
  Serial.println("Serial connection ready");

  modem.begin(115200, SERIAL_8N1, RX_PIN, TX_PIN);
  modem.println("AT"); // AT = Attention, Modem muss mit "OK" antworten
}

void loop() {
  while (modem.available()) {
    String resp = modem.readStringUntil('\n');
    resp.trim();
    if (resp.length() > 0) {
      Serial.println("Modem: " + resp); // Jede Antwort des Modems in der Serial gekennzeichnet ausgeben
    }
  }
}
Serial connection ready
Modem: AT
Modem: OK
Mobilfunkstatus Sketch

Um zu prüfen, ob das A7670E-Modul mit dem Mobilfunknetz verbunden ist, können wir es einfach selbst fragen.

#include <Arduino.h>

#define RX_PIN 17
#define TX_PIN 16
HardwareSerial modem(2);

void setup() {
  Serial.begin(115200);

  delay(1000);
  Serial.println("Serial connection ready");

  modem.begin(115200, SERIAL_8N1, RX_PIN, TX_PIN);
  modem.println("AT");
}

void loop() {
  while (modem.available()) {
    String resp = modem.readStringUntil('\n');
    resp.trim();
    if (resp.length() > 0) {
      Serial.println("Modem: " + resp);
    }
  }

  static unsigned long lastRun = millis() - 18000;
  if (millis() - lastRun > 20000) { // Nach 2 Sekunden alle 20 Sekunden aktualisieren
    lastRun = millis();

    modem.println("AT+CSQ");      // Signalstärke
    modem.println("AT+CEREG?");   // LTE Netzregistrierung
    modem.println("AT+CGATT?");   // Datenregistrierung
    modem.println("AT+CGACT?");   // PDP Kontext aktiv? (Datenverbindung)
    modem.println("AT+CGPADDR");  // IP-Adresse
    modem.println("AT+NETOPEN?"); // Socket-Service Status
  }
}
Modem: AT+CSQ
Modem: +CSQ: 26,99
Modem: OK
Modem: AT+CEREG?
Modem: +CEREG: 0,1
Modem: OK
Modem: AT+CGATT?
Modem: +CGATT: 1
Modem: OK
Modem: AT+CGACT?
Modem: +CGACT: 1,1
Modem: +CGACT: 8,1
Modem: OK
Modem: AT+CGPADDR
Modem: +CGPADDR: 1,10.76.56.42
Modem: +CGPADDR: 8,0.0.0.0,254.128.0.0.0.0.0.0.194.209.178.131.245.194.29.154
Modem: OK
Modem: AT+NETOPEN?
Modem: +NETOPEN: 0
Modem: OK

+CSQ: 26,99: 26 ist die Signalqualität, Skala 0-31, 26 = ziemlich guter Empfang
+CEREG: 0,1: Erfolgreich im LTE-Netz eingebucht
+CGATT: 1: Datenverbindung aktiv
+CGACT: 1,1 & +CGACT: 8,1: Zwei Datenkontexte offen
+CGPADDR: 1,10.76.56.42
+CGPADDR: 8,0.0.0.0,254.128.0.0.0.0.0.0.194.209.178.131.245.194.29.154: IP-Adressen, die das Board erhalten hat

Ping Sketch

Ein grundlegender Test von Netzwerk-Kommunikation ist ein „ping“: Ein kleines Datenpaket wird an einen Server gesendet. Kommt das Paket an antwortet der Server und der Nutzer sieht, wie lange das Datenpaket auf Reise war.

#include <Arduino.h>

#define RX_PIN 17
#define TX_PIN 16
HardwareSerial modem(2);

void setup() {
  Serial.begin(115200);

  delay(1000);
  Serial.println("Serial connection ready");

  modem.begin(115200, SERIAL_8N1, RX_PIN, TX_PIN);
  modem.println("AT");
}

void loop() {
  while (modem.available()) {
    String resp = modem.readStringUntil('\n');
    resp.trim();
    if (resp.length() > 0) {
      Serial.println("Modem: " + resp);
    }
  }

  static unsigned long lastRun = millis() - 18000;
  if (millis() - lastRun > 20000) {
    lastRun = millis();

    modem.println("AT+CPING=\"8.8.8.8\",1,4"); // 4 Pings an Google DNS senden
  }
}
Modem: AT+CPING="8.8.8.8",1,4
Modem: OK
Modem: +CPING: 1,8.8.8.8,92,130,114
Modem: +CPING: 1,8.8.8.8,92,40,114
Modem: +CPING: 1,8.8.8.8,92,15,114
Modem: +CPING: 1,8.8.8.8,92,30,114
Modem: +CPING: 3,4,4,0,15,130,40

AT+CPING=“8.8.8.8″,1,4: 8.8.8.8 ist die Ziel-IP-Adresse, 1 steht für IPv4 (2 wäre IPv6), 4 ist die Anzahl der Wiederholungen
+CPING: 1,8.8.8.8,92,130,114: 1: Ping erfolgreich, der vorletzte Wert (hier 130) ist die Laufzeit in ms, die anderen Werte bitte dem Manual entnehmen
+CPING: 3,4,4,0,15,130,40: Zusammenfassung über die 4 Pings

In der Regel ist die Stromaufnahme sehr gering. Beim Senden und Empfangen kann der Verbrauch kurzzeitig ansteigen. HTTPS-Traffic verbraucht (wenig überraschend) noch etwas mehr Strom
Daten via HTTP abrufen (HTTP GET-Request)
#include <Arduino.h>

#define RX_PIN 17
#define TX_PIN 16
HardwareSerial modem(2);

void setup() {
  Serial.begin(115200);

  delay(1000);
  Serial.println("Serial connection ready");

  modem.begin(115200, SERIAL_8N1, RX_PIN, TX_PIN);
  modem.println("AT");
}

void loop() {
  while (modem.available()) {
    String resp = modem.readStringUntil('\n');
    resp.trim();
    if (resp.length() > 0) {
      Serial.println("Modem: " + resp);
    }
  }

  static unsigned long lastRun = millis() - 18000;
  if (millis() - lastRun > 20000) {
    lastRun = millis();

    modem.println("AT+HTTPINIT"); // HTTP-Service des Modems starten

    
    modem.println("AT+HTTPPARA=\"URL\",http://httpbin.org/get"); // Abzurufende Adresse
    modem.println("AT+HTTPACTION=0"); // 0 = GET, 1 = POST, 2 = HEAD, 3 = DELETE, 4 = PUT

    delay(2500); // Wir gehen davon aus, in 2.5s die Antwort zu erhalten. Vermutlich besser: AT+HTTPACTION auswerten, liefert die Content-Length

    modem.println("AT+HTTPHEAD"); // Ausgabe des Response Headers

    delay(200);

    modem.println("AT+HTTPREAD=99999"); // "Unsaubere Implementierung": Eigentlich AT+HTTPREAD? nutzen um Länge der Antwort zu ermitteln
    modem.println("AT+HTTPTERM"); // HTTP-Service nach Verwendung stoppen
  }
}
Modem: AT+HTTPINIT
Modem: ERROR
Modem: AT+HTTPPARA="URL",http://httpbin.org/get
Modem: OK
Modem: AT+HTTPACTION=0
Modem: OK
Modem: +HTTPACTION: 0,200,255
Modem: AT+HTTPHEAD
Modem: +HTTPHEAD: 226
Modem: HTTP/1.1 200 OK
Modem: Date: Fri, 05 Sep 2025 15:28:01 GMT
Modem: Content-Type: application/json
Modem: Content-Length: 255
Modem: Connection: keep-alive
Modem: Server: gunicorn/19.9.0
Modem: Access-Control-Allow-Origin: *
Modem: Access-Control-Allow-Credentials: true
Modem: OK
Modem: AT+HTTPREAD?
Modem: +HTTPREAD: LEN,255
Modem: OK
Modem: AT+HTTPREAD=99999
Modem: OK
Modem: AT+HTTPTERM
Modem: OK
Modem: +HTTPREAD: 255
Modem: {
Modem: "args": {},
Modem: "headers": {
Modem: "Accept": "*/*",
Modem: "C "origin": "80.187.118.156",
Modem: "url": "http://httpbin.org/get"
Modem: }
Modem: +HTTPREAD: 0

+HTTPACTION: 0,200,255: 0 = Type der Anfrage (GET), 200 = Statuscode des Servers, 255 = Länge der Antwort in Bytes
+HTTPHEAD: 226: Start Ausgabe des Headers, 226 Bytes
+HTTPREAD: LEN,255: Eine Response liegt vor, 255 Bytes
+HTTPREAD: 255: Beginn der Ausgabe des Response bis HTTPREAD: 0

Daten via HTTP senden (HTTP POST, PUT, PATCH)

Im vorangegangenen Beispiel beim Abruf der Daten via HTTP haben wir etwas „gefuttelt“ was die Antworten anging. So haben wir mit delay gearbeitet und gehofft, dass bis dahin eine Antwort da ist. Damit kommen wir jetzt nicht mehr durch. Um den Code weiter lesbar zu halten, habe ich die Logik in einzelne Funktionen aufgeteilt.

#include <Arduino.h>

#define RX_PIN 17
#define TX_PIN 16
HardwareSerial modem(2);

String waitForURC(const char* token, unsigned long timeoutMs = 10000) { // URC: Unsolicited Result Code, Ereignisbasierte Melungen des Modems abfragen. In unserem Fall für die Antwort nach dem POST genutzt
  unsigned long start = millis();
  String buffer;

  while (millis() - start < timeoutMs) {
    while (modem.available()) {
      String line = modem.readStringUntil('\n');
      line.trim();

      if (line.length() > 0) {
        Serial.println("URC: " + line);
      }

      if (line.startsWith(token)) {
        return line;
      }
    }
  }
  Serial.println("Timeout beim Warten auf URC: " + String(token));
  return "";
}

void readHttpResponse() {
  modem.println("AT+HTTPREAD=99999"); // Wir lesen pauschal alles; Hier könnte man auch mit der genauen Zahl an Bytes arbeiten (was sauberer wäre), aber das LTE-Modul liefert nur solange Daten zurück, wie sie vorliegen.
  
  String response;
  unsigned long startTime = millis();

  while (millis() - startTime < 15000) {
    while (modem.available()) {
      String line = modem.readStringUntil('\n');
      line.trim();

      if (line.startsWith("+HTTPREAD:")) continue;
      if (line == "OK") {
        Serial.println("HTTP-Body:");
        Serial.println(response);
        return;
      }

      response += line + "\n";
    }
  }
  Serial.println("Timeout beim Lesen der Response!");
}

void httpPost(const char* url, const char* json) { // Methode speziell für den HTTP POST-Prozess
  modem.println("AT+HTTPINIT");
  modem.printf("AT+HTTPPARA=\"URL\",\"%s\"\r\n", url);
  modem.println("AT+HTTPPARA=\"CONTENT\",\"application/json\""); // Festsetzen des Inhalts auf JSON
  modem.printf("AT+HTTPDATA=%d,5000\r\n", strlen(json)); // Dem Modem mitteilen, dass gleich Daten kommen, die es für die Übermittlung aufzeichnen soll

  String dl = waitForURC("DOWNLOAD", 5000); // Warten bis das Modem mit "DOWNLOAD" antwortet, dann senden unseres JSON
  if (dl.length() > 0) {
    modem.print(json);
    if (waitForURC("OK", 5000).length() == 0) { // Im Anschluss auf die Bestätigung der Übertragung durch das Modem warten
      Serial.println("Modem or connection stuck, too many data sent or check wiring");
      return;
    }
  }

  modem.println("AT+HTTPACTION=1"); // 1 = POST
  String action = waitForURC("+HTTPACTION:", 15000); // Hier optimierter Ablauf: Sobald die Anfrage vom Modem versendet wurde, geht's weiter...

  if (action.length() > 0) {
    int method, status, len;
    if (sscanf(action.c_str(), "+HTTPACTION: %d,%d,%d", &method, &status, &len) == 3) {
      Serial.printf("HTTP Status: %d, Length: %d\n", status, len);

      if (status == 200 || status == 201) { // Status code 200/201 stehen für eine erfolgreiche Anfragen bei POST
        readHttpResponse(); // Auslesen der Antwort in einer eigenen Funktion dafür
      } else {
        Serial.println("Unexpected response");
      }
    }
  }

  modem.println("AT+HTTPTERM");
}

void setup() {
  Serial.begin(115200);

  delay(1000);
  Serial.println("Serial connection ready");

  modem.begin(115200, SERIAL_8N1, RX_PIN, TX_PIN);
  modem.println("AT");
}

void loop() {
  while (modem.available()) {
    String resp = modem.readStringUntil('\n');
    resp.trim();
    if (resp.length() > 0) {
      Serial.println("Modem: " + resp);
    }
  }

  static unsigned long lastRun = millis() - 18000;
  if (millis() - lastRun > 20000) {
    lastRun = millis();
    const char* json = "{\"temperature\":25.3,\"battery_level\":\"83\"}"; // Unsere Daten verpackt in JSON

    httpPost("http://httpbin.org/post", json); // Start des HTTP Post
  }
}
Serial connection ready
Modem: AT
Modem: OK
URC: AT+HTTPINIT
URC: OK
URC: AT+HTTPPARA="URL","http://httpbin.org/post"
URC: OK
URC: AT+HTTPPARA="CONTENT","application/json"
URC: OK
URC: AT+HTTPDATA=41,5000
URC: DOWNLOAD
URC: OK
URC: AT+HTTPACTION=1
URC: OK
URC: +HTTPACTION: 1,200,488
HTTP Status: 200, Length: 488
HTTP-Body:
AT+HTTPREAD=99999

Modem: +HTTPREAD: 488
Modem: {
Modem: "args": {},
Modem: "data": "{\"temperature\":25.3,\"battery_level\":\"83\"}",
Modem: "files": {},
Modem: "form": {},
Modem: "headers": {
Modem: "Accept": "*/*",
Modem: "Cache-Control": "no-cache",
Modem: "Content-Length": "41",
Modem: "Content-Type": "application/json",
Modem: "Host": "httpbin.org",
Modem: "X-Amzn-Trace-Id": "Root=1-68bd430d",
Modem: "url": "http://httpbin.org/post"
Modem: }
Modem: AT+HTTPTERM
Modem: OK
Modem: +HTTPREAD: 0

Dadurch das mit waitForURC auf ein Ereignis warten, verschieben sich hier die Ausgaben ein wenig.
AT+HTTPDATA=41,5000: Unsere zu sendenden Daten sind 41 Bytes lang, Timeout: 5 Sekunden
+HTTPACTION: 1,200,488: Die Übertragung war erfolgreich, 488 Bytes lang ist die Antwort
AT+HTTPREAD=99999: Wir lesen pauschal 99999 Bytes, das Modem liefert aber nur so viel Daten zurück wie vorhanden; Wir hätten hier auch die 488 aus der HTTPACTION-Antwort verwenden können. Das wäre sauberer gewesen, aber in manchen Fällen (bspw. „chunked“-Responses) liegt wieder Wert nicht vor. Wir haben es uns also einfacher gemacht
+HTTPREAD: 488: Beginn der Ausgabe der Antwort des Servers, enthält als Bestätigung unter „data“ das, was wir geposted haben

In der Konsole (hier Plattform IO, „Serieller Monitor“ in Arduino IDE) sieht man die Ausgaben des Modems und kann so nachvollziehen, was passiert
Besonderheiten bei HTTPS

Das 7670E-Modul ist dazu in der Lage Daten unverschlüsselt über HTTP, als auch verschlüsselt via HTTPS zu übertragen. Im besten Fall ist dazu nicht viel mehr nötig, als in den hier genannten Beispielen auf „http://“ ein „https://“ zu machen. Allerdings steckt der Teufel da gerne im Detail, denn HTTPS ≠ HTTPS.

Hier ein Mini-Exkurs: Werden im Netz Daten verschlüsselt übertragen, kommen dazu unterschiedliche Mechanismen zu tragen. So wird zum einen ein Verschlüsselungsalgorithmus zwischen Client und Server ausgehandelt., heutzutage oft TLS 1.2 oder größer. Neben diesem wird ein passendes Zertifikat benötigt. Gerade wenn es günstig sein soll, wird heute oft Lets Encrypt verwendet – mach ich auch so. Damit die Kommunikation aber wirklich sicher ist, müssen Betriebssysteme, Browser oder eben LTE-Module mit HTTPS-Support aber die Stammzertifikate der Anbieter kennen. Ein weiteres Thema ist Server Name Indication (SNI) in Verbindung mit virtuellen Hosts. Kurz um: Viele Dinge, die entweder eine vermeintlich sichere Verbindung unsicher machen können oder sie erst gar nicht zustand kommen lassen.

Wenn die Übertragung nicht gelingt…

…geht’s an Debugging. Folgende Sachen könnt ihr prüfen:

SNI aktivieren
Das Modul hat zur Laufzeit 8 SSL-Konfiguration im Speicher. Per Default ist da wenig konfiguriert, außer die Art der TLS-Verschlüsselung mit der Gegenseite automatisch ausgehandelt werden soll. Was dabei nicht konfiguriert wird ist Server Name Indication. Das kann aber essentiell sein, nämlich dann, wenn euer Server mit Virtual Hosts arbeitet. Aktiviert SNI vor dem AT+HTTPINIT mit:

modem.println("AT+CSSLCFG=\"enableSNI\",0,1")

SNI wird für den SSL-Kontext „0“ mit „1“ aktiviert. SSL-Kontext 0 ist der erste der genannten 8 möglichen und wird per default verwendet. Um die SSL-Kontext-ID anzupassen, kann man den Befehl AT+HTTPPARA=“SSLCFG“,2 nutzen, wobei hier die 2 symbolisch für die ID des Index steht, also 0-7.

HTTPACTION auswerten
Einen GET-Request starten wir ja mit AT+HTTPACTION=0. Das Modul gibt neben einem „OK“ auch weitere Informationen aus, im besten Fall sowas: +HTTPACTION: 0,200,81
Die 0 steht für die Art der Anfrage (GET), die 200 für den vom Server gesendeten Statuscode und 81 ist die Länge der Antwort. Ist die Antwort nicht erfolgreich, ist interessant was dort statt 200 steht. Eine 706 würden auf Probleme beim Lesen der Antwort hindeuten, aber gleichzeitig bekräftigen, dass der TLS-Handshake zuvor erfolgreich war, während eine 715 eben genau auf Probleme bei ebendiesem hinweisen würden. In dem Fall müsste man prüfen, ob der verwendete TLS-Standard auf deinem Server von dem LTE-Modul überhaupt unterstützt wird.

HTTP-Header auslesen
Mit AT+HTTPHEAD lässt sich nach dem Absenden der Anfrage der Antwort-Header auslesen. Hier können auch Hinweise auf das Problem aufgeführt sein, wenn man Glück hat. Das oben angesprochene SNI-Problem könnte beispielsweise hier benannt werden.

HTTPS GET Beispiel Sketch

Die oben genannte Punkte sind in diesem Beispiel berücksichtig, welcher bei mir erfolgreich funktionierte.

#include <Arduino.h>
 
#define RX_PIN 17
#define TX_PIN 16
HardwareSerial modem(2);


String waitForURC(const char* token, unsigned long timeoutMs = 10000) {
  unsigned long start = millis();
  String buffer;
 
  while (millis() - start < timeoutMs) {
    while (modem.available()) {
      String line = modem.readStringUntil('\n');
      line.trim();
 
      if (line.length() > 0) {
        Serial.println("URC: " + line);
      }
 
      if (line.startsWith(token)) {
        return line;
      }
    }
  }
  Serial.println("Timeout beim Warten auf URC: " + String(token));
  return "";
}


void setup() {
  Serial.begin(115200);
 
  delay(1000);
  Serial.println("Serial connection ready");
 
  modem.begin(115200, SERIAL_8N1, RX_PIN, TX_PIN);
  modem.println("AT");

  
}
 
void loop() {
  while (modem.available()) {
    String resp = modem.readStringUntil('\n');
    resp.trim();
    if (resp.length() > 0) {
      Serial.println("Modem: " + resp);
    }
  }
 
  static unsigned long lastRun = millis() - 18000;
  if (millis() - lastRun > 20000) {
    lastRun = millis();
 
    modem.println("AT+CSSLCFG=\"enableSNI\",0,1"); // SNI für SSL config ID 0 aktivieren
    
    modem.println("AT+HTTPINIT");
    
    modem.println("AT+HTTPPARA=\"URL\",https://phil.cossnet.de/lte_modul_test.html"); // Kleine selbstgehostete Test-HTML-Datei
    modem.println("AT+HTTPACTION=0");
 

    String response = waitForURC("+HTTPACTION", 5000); // Auf Antwort warten, offenbar dauert es bei HTTPS länger als bei HTTP
    if (response.length() > 0) {
      modem.println("AT+HTTPHEAD");
      modem.println("AT+HTTPREAD=99999");
    }
 
    delay(200);
    modem.println("AT+HTTPTERM");
  }
}
Total
0
Shares
Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Mit der Nutzung dieses Formulars erklärst Du Dich mit der Speicherung und Verarbeitung Deiner Daten durch datort.de einverstanden. Weitere Informationen findest Du in der Datenschutzerklärung.

Previous Article

Pad One Please - Podcast Episode 1