ESP32, RFID und MQTT

Viele Arduino oder ESP Projekte starten mit "ich schau mir mal das Tutorial an" und enden auch dort. Denn wenn du mehrere Tutorials zusammennimmst, funktioniert es plötzlich nicht mehr.

ESP32, RFID und MQTT

Viele Arduino oder ESP Projekte starten mit "ich schau mir mal das Tutorial an" und enden auch dort. Denn wenn du mehrere Tutorials zusammennimmst, funktioniert es plötzlich nicht mehr.

Den Glue-Code, der die einzelnen Komponenten zusammenhält, den habe ich nirgendwo gefunden. Deshalb hier ein schrittweiser Blogpost, der einem ESP beibringt per Wifi konfigurierbar die UID eines RFID-Tags auf ein MQTT-Topic zu veröffentlichen, auf das du an anderer Stelle (nicht Teil dieses Posts) reagieren kannst. Jedes Kapitel endet mit dem Link zum aktuellen Gesamtstand des Codes auf Github.

Begriffe

MQTT: Nachrichtensystem mit Server-Subscriber-Infrastruktur (Message Queuing Telemetry Transport)
RFID oder NFC: Funkbasierte ID-Technologie, die auf kurze Distanz (meist wenige Zentimeter) funktioniert (radio-frequency identification, near-field communication)
IDE: Entwicklungsumgebung um Code zu schreiben und auf das Gerät zu bringen (integrated development environment)
Baud-Rate: Einheit für Übertragungsgeschwindigkeit benannt nach Baudot
SPI: Protokoll zur Übertragung von Daten zwischen Mikrochips (Serial Peripheral Interface)
SSID: Name des WLAN Access Points (Service Set Identifier)

Ziele

  • Umgang mit der Arduino IDE
  • Einblick in Wifimanager für ESP
  • Einblick in MQTT auf dem ESP
  • Einblick in MFRC522 RFID-Reader auf dem ESP
  • Konfigurationsmöglichkeiten des Wifimanagers

Zeitaufwand 2-4 Stunden

Hardware-Voraussetzungen

Die Links zu Amazon sind Beispiele, damit du nicht suchen musst. Die normalen Links sind keine Affiliate Links, die Unterstützerlinks schon.

  • ESP32 dev Board (dieses Tutorial nutzt das hier) die meisten anderen werden auch funktionieren, haben aber gegebenenfalls ein anderes PinOut (am häufigsten ein zusätzlicher GND Pin neben Pin 23)
    Unterstützerlink
  • MFRC522 RFID-Reader mit mindestens einem Tag (Ich habe meinen aus dem Arduino Starterset mit über 100 Komponenten, der war vorgelötet. Wenn du die Stiftleiste selber lötest, gibt es die hier sehr günstig)
    Unterstützerlink
  • Eine handvoll Jumperwire (bei den oben genannten Boards Buchse auf Buchse, erhältlich hier)
    Unterstützerlink
  • Zusätzliche bzw. Ersatz-Tags als Sticker, Schlüsselanhänger oder als Karte
    Unterstützerlinks: Sticker, Schlüsselanhänger, Karte

Als Amazon-Partner verdiene ich an qualifizierten Verkäufen, wenn du die Unterstützerlinks verwendest. Das hilft mir mehr Zeit für coole Tutorials aufzubringen.

Software-Voraussetzungen und Installation

*Änderung vom 30.04.2021

Die Installation der oben genannten Softwarepakete und -bibliotheken ist nicht Fokus dieser Anleitung und wird nur sehr kurz abgehandelt. Wenn du Fragen hast, kontaktiere mich (zum Beispiel auf Twitter)

  1. Um die Arduino IDE zu installieren musst du nur dem Link oben folgen, den passenden Downloadlink zu deinem Betriebssystem anklicken und nach dem Download ausführen. Die Arduino IDE bietet grundsätzlich alles, was du brauchst, um deinen ESP zu programmieren.
  2. Damit die Arduino IDE weiß, um welche Hardware es sich bei deinem ESP handelt, musst du eine Liste von "Boards" hinzufügen. Eine gute Anleitung von heise findest du hier. Im Wesentlichen besteht es daraus, eine URL in die Preferences einzutragen und dann die Definitionen für ESP32 Boards herunterzuladen.
  3. Jetzt musst du in der Arduino IDE die Kommunikation einrichten. Verbinde dazu dein ESP Board per USB mit deinem Computer. Als erstes musst du wissen, unter welcher Adresse das Gerät erreichbar ist. Dafür öffnest du unter Systemsteuerung->System->Gerätemanager (auf modernen Systemen reicht es nach Gerätemanager zu suchen). In der Liste solltest du (wenn der ESP angeschlossen ist) den Eintrag Anschlüsse(COM&LPT) sehen. Wenn du diesen aufklappst siehst du dort den angeschlossenen ESP und in Klammern einen Port z.B. COM3. Den brauchst du gleich.
  4. Öffne in der Arduino IDE unter File->Examples->ESP32->WifiScan. Mit diesem Beispiel wollen wir die Verbindung überprüfen. Wenn der Code geöffnet ist, schau dir im Menü "Tools" die Einträge für Boards genau an. Bei Board kannst du deinen ESP auswählen, in meinem Beispiel ist es ESP32 Dev Module. Unter Port wähle den eben herausgefundenen COM-Port aus (z.B. COM3). Es gibt noch mehr Einstellungen, die gegebenenfalls später angepasst werden müssen (s. Abbildung 1), für den Moment sollte aber ein Klick auf Upload (in der Toolbar zweites Icon von links) und etwas Geduld reichen, damit der Beispielcode auf dem ESP installiert wird und läuft. Wenn du sehen möchtest was passiert, kannst du den Serial Monitor unter Tools anschalten. Hier musst du die Übertragungsgeschwindigkeit(Baud-Rate) konfigurieren, damit die eingehenden Daten richtig interpretiert werden. Wenn du im Code eine Zeile findest, die "Serial.begin(#zahl#);" enthält, dann ist diese Zahl das, was du einstellen musst. Steht dort nichts funktioniert 115200 häufig. Du kannst natürlich auch einfach durchprobieren, es wird nichts kaputt gehen – so machen es die Profis. 😛
  5. Wenn das alles funktioniert, kannst du die ganzen Libraries herunterladen, indem du auf die Github-Seiten gehst und auf den grünen Download button klickst. Wenn du gefragt wirst wähle "Download as .zip". In der Arduino IDE kannst du unter Sketch->Include Library->Add .ZIP Library diese heruntergeladenen Zip-Files (ohne Entpacken) hinzufügen.
  6. Zum Testen des Codes ab dem MQTT-Kapitel wird ein MQTT server(broker) benötigt, der Nachrichten entgegennimmt und weiterverteilt. Unter Windows ist die einfachste Lösung, mosquitto zu installieren. Der Nachteil ist: man muss den Service wieder ausstellen, wenn man ihn nicht mehr braucht. Die schönste Lösung ist sicherlich ein Raspberry Pi im Netzwerk mit mosquitto unter raspbian, das würde hier aber den Rahmen sprengen. Deswegen gibt es dazu bald ein eigenes Tutorial. (Update: Das "Mosquitto auf dem Raspberry Pi"-Tutorial findest du hier)
Abb. 1: Einstellungen im Tools-Menü um erfolgreich Sketches auf den ESP zu laden.

Autoconnect und Config

Die autoconnect-Funktion des WiFiManagers erlaubt es, direkt zum letzten bekannten WLAN zu verbinden. Falls das fehlschlägt, wird automatisch ein WLAN Access Point und ein Webserver auf dem ESP gestartet. Wenn du dann deinen Computer / dein Phone mit dem ESP-WLAN verbindest, landest du auf einer Konfigurationsseite, auf der du neue WLAN-Zugangsdaten eingeben kannst. Diese werden dann so abgespeichert, dass sie auch einen Neustart des ESPs überleben.

#include <WiFi.h>
#include <DNSServer.h>
#include <WebServer.h>
#include <WiFiManager.h>
//
// ...
//
// Wifimanager variables
uint64_t chip_id = ESP.getEfuseMac();
uint16_t chip = (uint16_t)(chip_id >> 32);
const char* CONFIG_PASSWORD = "correct-horse-battery-staple";
char ssid[17];

void setup(){
  Serial.begin(115200);
  
  // Set ssid to ESP-123456789ABC
  snprintf(ssid, 17, "ESP-%04X%08X", chip, (uint32_t) chip_id);

  WiFiManager wifimanager;
  
  if (!wifimanager.autoConnect(ssid, CONFIG_PASSWORD)) {
    Serial.println("Could not start AP");
    Serial.println(ssid);
    delay(3000);
    ESP.restart();
    delay(5000);
  }
}

void loop(){
  
}

Um einen Access Point aufzumachen braucht es idealerweise eine einzigartige ID und ein Passwort, deshalb holen wir in den ersten beiden Zeilen nach den include statements die Hardware-Chip-ID vom ESP. Wir setzen außerdem ein Passwort, damit andere nicht die Konfiguration kapern können. Bitte setze ein eigenes Passwort und nutze nicht das hier gezeigte! Die Variable bssid wird mit 17 chars vorbereitet um "ESP-", 12 Zeichen ID und ein null-byte enthalten zu können. Im Setup wird mit Serial.begin() die serielle Verbindung gestartet (hauptsächlich damit wir im Serial Monitor Debug-Meldungen bekommen). Die Zahl in den Klammern ist die Baud-Rate.
Mit snprintf(...) wird der Formatstring "ESP-%04...." mit den entsprechenden Werten der IDs gefüllt und in die Variable ssid gespeichert. Nachdem wir eine Variable vom Typ WiFiManager angelegt haben, können wir von dieser die Funktion .autoconnect() aufrufen. Der Funktion können die SSID des Access Points und das notwendige WLAN-Verbindungspasswort übergeben werden. Die Funktion blockiert den Programmfluss und wartet auf Konfiguration, wenn sie es nicht schafft sich zu einem bekannten WLAN zu verbinden. Außerdem gibt die Funktion einen boolean zurück, der false ist, falls die neukonfigurierte Verbindung auch nicht funktioniert. In dem Fall geben wir den Fehler auf dem Serial Monitor aus, warten etwas mit delay() (Achtung: delay nutzt Millisekunden, wir warten also nur 3 Sekunden) und starten den ESP danach neu. Das delay nach dem restart ist dafür da, dass der ESP nicht während des Neustarts noch etwas Unerwünschtes tut. Wenn du den ESP stattdessen in Tiefschlaf versetzen möchtest, suche bei einer Suchmaschine deines Vertrauens nach esp_deep_sleep_start().

Wenn der ESP bisher noch nicht zu deinem WLAN verbunden war, kannst du das Skript jetzt mit "upload" hochladen und wirst nach ein paar Minuten, wenn der ESP automatisch neugestartet hat, ein neues WLAN namens "ESP-XXXXXXXXXXXX" sehen. Verbinde dich damit und öffne einen Browser, dann wirst du auf ein Konfigurationsmenü weitergeleitet, das in etwa so aussieht (Abbildung 2). Hier kannst du deine WLAN-Zugangsdaten angeben und der ESP verbindet sich beim Start automatisch mit deinem WLAN. Debug-Ausgabe in Abbildung 3.

Abb. 2: Das Konfigurationsmenü des ESP WiFiManagers bietet die Möglichkeit nach WiFis zu scannen oder einfach nur Daten entgegenzunehmen (z.B. für versteckte SSIDs). Im zweiten Screen kann man dann die WLAN-Zugangsdaten angeben.
Abb. 3: Serial Monitor Ausgabe einer erfolgreichen WLAN-Verbindung und Ausgabe der zugewiesenen IP-Adresse

Zum Vergleich der Code auf Github. Jetzt ist ein guter Moment für 5 - 10 Minuten Pause. Wenn noch irgendetwas nicht klappt, schreib mir vor der Pause kurz auf Twitter (mit etwas Glück hab ich bis zum Ende der Pause sogar geantwortet :D )

MQTT

Das absolute Minimum um MQTT zu nutzen besteht aus vier Komponenten; den Teilnehmern an der Kommunikation: Server (früher Broker) und Client; und der Möglichkeit Nachrichten zu senden und zu empfangen.

mosquitto installieren und IP-Adresse herausfinden

Falls noch nicht geschehen, ist jetzt die Zeit mosquitto zu installieren. Wenn der mosquitto MQTT server läuft, musst du noch die IP-Adresse dieses Servers herausfinden. Je nach Windows-Version gibt es unterschiedliche Wege. Der Weg, der immer funktioniert ist Windowstaste + R zu drücken (Oder "Ausführen" im Startmenü) und cmd zu starten. In der Konsole, die sich öffnet, bekommst du nach Eingabe von ipconfig (mit Enter bestätigen) eine Auflistung deiner Netzwerkadapter. Ein Block heißt so etwas wie "Wireless LAN adapter WiFi". In dem Block gibt es den Eintrag IPv4-Address und dahinter die Adresse z.B. 10.0.0.60 (manchmal sehen die Adressen auch eher nach 192.168.0.X aus)

Client einrichten

Ausgehend von dem bereits bestehenden Code brauchst du ein paar Variablen, um die Konfiguration des MQTT Servers zu speichern. Die Bibliotheken werden wie folgt im include Block (egal ob Anfang oder Ende der include-Statements) eingebunden

#include <IPStack.h>
#include <Countdown.h>
#include <MQTTClient.h>

Außerdem bereitest du die Verbindung vor, indem du den Bibliotheken sagst, wie sie sie aufbauen können. Das kann zum Beispiel so aussehen:

// MQTT variables
char mqtt_host[] = "10.0.0.60";
char mqtt_port[] = "1883";
char mqtt_topic[] = "devices/esp/status";

// Initialize MQTT client
WiFiClient wifi;
IPStack ipstack(wifi);
int MMMS = 50; // MAX_MQTT_MESSAGE_SIZE
int MMPS = 1;  // MAX_MQTT_PARALLEL_SESSIONS
MQTT::Client<IPStack, Countdown, MMMS, MMPS> client = MQTT::Client<IPStack, Countdown, MMMS, MMPS>(ipstack);
MQTT::Message message; 

//void setup(){
//  ...

Später werden wir den Variablen-Teil umprogrammieren, damit die Variablen auch im WiFi-Config Menü gesetzt werden können. Die Verbindung zum MQTT-Server vorzubereiten sieht etwas umständlich aus, ist aber im Wesentlichen so aus dem Beispiel der MQTT-Bibliothek kopiert. IPStack beinhaltet alles was nötig ist, um Direktverbindungen zwischen Computern aufzubauen. Die maximale Nachrichtengröße ist mit einem Limit von 50 bytes normalerweise ausreichend, der Wert kann bei Bedarf aber auf bis zu 255 erhöht werden. In der letzten neuen Zeile wird noch eine Variable message angelegt, damit du Teile der Konfiguration einmal setzen und dann wiederverwenden kannst.

Innerhalb der setup-Routine fügst du als letzten Eintrag die gleichbleibenden Eigenschaften der message hinzu:

  message.qos = MQTT::QOS1;
  message.retained = false;

QOS oder Quality of Service beschreibt das Sendeverhalten des Clients. Null bedeutet: Schick die Nachricht ab und vergiss sie. Eins bedeutet: Schick die Nachricht solange immer wieder ab, bis du einmal eine positive Rückmeldung vom Server bekommen hast (garantierte Auslieferung). Zwei bedeutet: Schick die Nachricht so lange bis garantiert wurde, dass die Nachricht angekommen ist und nur einmal empfangen wurde (Das wird über eine zweistufige Rückmeldung umgesetzt und ist am langsamsten). In den allermeisten Fällen ist Level eins das passende. Bei Clients (z.B. Sensoren), die häufig senden und wo es egal ist, wenn Werte fehlen, kann auch Level null sinnvoll sein. Wenn du dich tiefergehend dafür interessierst, kannst du diesen Artikel lesen, der die Gründe (Netzwerkstabilität) detaillierter beschreibt.

Die retained-Option besagt, ob Nachrichten auf dem Server behalten werden soll, für Clients die später verbinden und dann direkt diese letzte Nachricht nachgesendet bekommen. Im Code hier sagen wir mit message.retained = false, dass die Nachricht nur an alle zum Zeitpunkt der Veröffentlichung verbundenen Clients zugestellt werden soll.

Client verbinden

Um den MQTT-client zu verbinden habe ich eine eigene Funktion vorbereitet. Das empfehle ich dir auch, weil es dann im Falle eines Verbindungsverlusts einfacher ist, die Verbindung wieder neu aufzubauen. Zwischen void setup(){...} und void loop(){...} fügst du dazu folgenden Code ein:

void connect_mqtt(){
  Serial.print("Connecting to: ");
  Serial.print(mqtt_host);
  Serial.print(":");
  Serial.println(mqtt_port);
  int returncode = ipstack.connect(mqtt_host, atoi(mqtt_port));
  if (returncode != 1)
  {
    Serial.print("Returncode from TCP connect is ");
    Serial.println(returncode);
    return;
  }
  MQTTPacket_connectData data = MQTTPacket_connectData_initializer;
  data.MQTTVersion = 3;
  data.clientID.cstring = ssid;
  Serial.print("Connecting as: ");
  Serial.println(ssid);
  returncode = client.connect(data);
  if (returncode != 0)
  {
    Serial.print("Returncode from MQTT connect is ");
    Serial.println(returncode);
    return;
  }
  Serial.println("MQTT connected"); 
}

Das meiste davon sind Ausgaben auf dem Serial Monitor, damit wir nachvollziehen können, was passiert. Die wichtigsten drei Zeilen sind:

  1. ipstack.connect(mqtt_host, atoi(mqtt_port)) um die Netzwerkverbindung aufzubauen mit der IP-Adresse, die wir vorhin abgespeichert haben, und dem Port. Die Portnummer wird mit atoi() zu einer Ganzzahl konvertiert, weil die connect Funktion das so verlangt, der Port in deinem Code aber als Text gespeichert ist (weil er später aus einem Formularfeld kommen wird).
  2. data.clientID.cstring = ssid um eine eindeutige Identifikation des MQTT-Clients zu ermöglichen. Dieser Name darf nur einmal beim Broker registriert sein, weil die subscriptions in MQTT anhand der Client-ID verwaltet werden.
  3. client.connect(data) um innerhalb der Netzwerkverbindung (siehe 1.) das MQTT-Protokoll zu starten.

Die erste Nachricht

Jetzt ist alles vorbereitet. Bleibt nur noch, im loop die connect-Funktion von gerade aufzurufen und eine Nachricht zu veröffentlichen.

Exkurs: Weil es bei mosquitto keine Ausgabe gibt, bietet es sich an einen zweiten Client zu starten, der auf das Topic "#" hört (das bedeutet alle Nachrichten). Ich benutze normalerweise MQTT Explorer es gibt aber auch noch andere (sogar als Browser Plugins: Gute Auflistung von HiveMQ )
void loop(){
  if(!client.isConnected()){
      connect_mqtt();
  }
  char payload[] = "alive";
  message.payload = payload;
  message.payloadlen = strlen(payload);
  client.publish(mqtt_topic, message);
  delay(5000);
}

Wann immer der client nicht verbunden ist, wird versucht die Verbindung neu aufzubauen. Eigentlich sollte hier noch Fehlerbehandlung passieren, wenn das nicht geht. Das wird dann wohl ein Todo für den finalen Code auf github. Danach setzt du die payload und payloadlen Eigenschaften unserer Nachricht erst einmal auf einen fixen Wert. Jetzt kannst mit client.publish() die message in unser mqtt_topic veröffentlichen und alle Clients, die dieses Topic subscribed haben, bekommen die Nachricht. Damit nicht mehrere hundert bis tausend Nachrichten pro Minute gesendet werden, habe ich 5 Sekunden Delay eingebaut.

Du kannst mit dem ESP auch auf Nachrichten reagieren, es sprengt leider den Rahmen in der Tiefe darauf einzugehen. Auch hier wird es bald ein separates Tutorial von mir geben.

char printbuf[100];
void messageArrived(MQTT::MessageData& md)
{
  MQTT::Message &msg = md.message;
  
  sprintf(printbuf, "Message %d arrived: qos %d, retained %d, dup %d, packetid %d\n", 
		++arrivedcount, msg.qos, msg.retained, msg.dup, msg.id);
  Serial.print(printbuf);
  sprintf(printbuf, "Payload %s\n", (char*)msg.payload);
  Serial.print(printbuf);
  // Hier deinen Code einfügen, der z.B. eine LED schaltet
}
  
  // .....
  
void connect_mqtt(){
  // ipstack.connect
  // ...
  // client.connect
  // ...
  returncode = client.subscribe(mqtt_topic, MQTT::QOS1, messageArrived); 
  // ...
}

Gratulation! Jetzt kannst du Nachrichten an deinen MQTT-broker schicken und Endgeräte, die auf das entsprechende Topic subscribed sind, erhalten diese Nachricht. Den gesamten Code bis hier findest du hier auf Github. Als nächstes kümmern wir uns um den Input vom RFID shield. Ich empfehle vorher nochmal 5-10 Minuten Pause.

RFID-Reader MFRC522

Verkabelung

Aus der Erfahrung und den Heldengeschichten zerstörter Boards, möchte ich überliefern:

  1. Verkabele niemals um bei eingeschaltetem Gerät
  2. Schließe als allererstes immer Ground (Erdung) an und vergewissere dich zweimal, dass es stimmt.  
Abb. 4: Anleitung zum Anschluss vom MFRC522 RFID-Shield an ein ESP32-WROOM Entwicklerboard (wegen schlechtem Farbkontrast folgt noch eine Anschlusstabelle)
ESP32 Pin MFRC522 Pin
3V3 3V3
GND GND
D5 SDA
D18 SCK
D19 MISO
D22 RST
D23 MOSI

Programmierung

Um das RFID-Modul zu benutzen, benötigst du die oben verlinkte MFRC522-Bibliothek und musst den ESP entsprechend Abbildung 4 verkabeln. Als nächstes brauchen wir Code, der den Reader initialisiert und im loop abfragt, ob ein neuer RFID-Tag erkannt wurde. Der Code ergänzt den bisher bestehenden an mehreren Stellen. Zur Orientierung findest du Landmarken wie #include-Statements, setup()-Routine und loop()

// add to include block
#include <SPI.h>
#include <MFRC522.h>

// Set Secondary Select and Reset pin to the pins connected
#define SS_PIN 5
#define RST_PIN 22

MFRC522 rfid(SS_PIN, RST_PIN); // Instance of the class
MFRC522::MIFARE_Key key;

void setup(){
  // Start RFID reader
  SPI.begin(); // Init SPI bus
  rfid.PCD_Init(); // Init MFRC522
  for (byte i = 0; i < 6; i++) {
    key.keyByte[i] = 0xFF;
  }
  // ...
}

void array_to_string(byte array[], unsigned int len, char buffer[])
{
  for (unsigned int i = 0; i < len; i++)
  {
    byte nib1 = (array[i] >> 4) & 0x0F;
    byte nib2 = (array[i] >> 0) & 0x0F;
    buffer[i * 2 + 0] = nib1  < 0xA ? '0' + nib1  : 'A' + nib1  - 0xA;
    buffer[i * 2 + 1] = nib2  < 0xA ? '0' + nib2  : 'A' + nib2  - 0xA;
  }
  buffer[len * 2] = '\0';
}

void loop(){
  
  if ( ! rfid.PICC_IsNewCardPresent())
    {return;}
  if ( ! rfid.PICC_ReadCardSerial())
    {return;}
  
  // prepare string buffer 2 chars per byte + '\0'
  char str[rfid.uid.size * 2 + 1]; 
  array_to_string(rfid.uid.uidByte, rfid.uid.size, str);
  Serial.println(str);
  message.retained = false;
  message.payload = str;
  message.payloadlen = strlen(str);
  client.publish(mqtt_topic, message);
  rfid.PICC_HaltA();  // stop card from being read while near reader
}

Als erstes müssen wir der SPI-Bibliothek mitteilen, an welchen Pins wir SS und RST angeschlossen haben. Die restlichen Pins sind standardisiert und müssen nicht explizit gesetzt werden. Dann werden die Variablen für den rfid reader und den key zurechtgelegt.
Im Setup wird zuerst die serielle Verbindung über SPI gestartet. Das ist nicht die gleiche, über die wir Nachrichten auf den Seriellen Monitor bekommen. Mit dem Aufruf von PCD_Init wird der Reader gestartet. Jetzt wird noch der Verschlüsselungskey gesetzt. Wenn es keinen konkreten Key gibt, wird dieser wie hier üblicherweise auf 0xFFFFFFFFFFFF gesetzt.

Die Funktion array_to_string() brauchen wir um die uid vom Chip, die als byte Array bereitgestellt wird, in einen lesbaren HexString zu verwandeln. Hier wird die obere Hälfte und die untere Hälfte vom Byte separat untersucht und wenn sie < 10 ist als Ziffer und sonst als Buchstabe 'A', 'B', 'C', 'D', 'E' oder 'F' in den Ausgabepuffer geschrieben.

Nachdem der Reader initialisiert ist, gibt es zwei High-Level-Funktionen, die du benutzen kannst, um herauszufinden, ob dem Reader eine neue Karte/Tag präsentiert wurde und wie die Seriennummer ist. PICC_IsNewCardPresent() und PICC_ReadCardSerial(). Wenn ein neuer Tag präsentiert wurde und die Seriennummer gelesen werden konnte, geben beide true zurück und der ESP kann weiter machen. Ganz am Ende wird PICC_HaltA() aufgerufen, damit der Reader dieselbe Karte nicht noch einmal detektiert. Falls das doch nötig ist – es war auch mein Anwendungsfall – befindet sich der Code dafür ganz am Ende des Posts.

Zwischen ReadSerial und HaltA kannst du jetzt mit der uid arbeiten. Da jedes Byte nachher durch 2 Zeichen dargestellt wird, muss der angelegte String-Puffer doppelt so groß sein wie das Array, +1 für das abschließende Null-Byte. Mit dem String kannst du dann eine Ausgabe auf den seriellen Monitor schreiben. Oder du setzt message.payload auf den String und message.payloadlen auf die entsprechende Länge und schickst sie mit MQTT per publish(topic, message) ab. Den Code bis hierher findest du hier auf Github.

Mehr Config

Last but not least, möchtest du vielleicht für deine ESPs mehr als nur das WLAN konfigurierbar machen, damit du nicht für jeden Anwendungsfall den ESP neu flashen musst. Oder falls du diese Anleitung für dein Business benutzt, weil du vorgeflashte ESPs ausliefern möchtest, die beim Kunden nur noch fertig konfiguriert werden müssen. Die Eigenschaften, die konfigurierbar sein sollen, sind:

  • MQTT server (Hostname oder IP)
  • MQTT port
  • MQTT user
  • MQTT password
  • MQTT topic
  • Default Payload (falls keiner angegeben wird)
  • Client ID

Zuerst schauen wir uns an, wie du Daten auf dem ESP abspeichern und von dort wieder laden kannst. Da wir key-value-Paare abspeichern wollen, lohnt es sich auf eine Bibliothek zurückzugreifen, die das kann. Mein Favorit ist JSON, da es Listen (Arrays) und key-value-Paare (Dictionaries) strukturiert als Zeichenkette abbilden kann. Der ESP hat je nach Modell und Firmware ein paar Kilobyte bis Megabyte Speicherplatz im sogenannten SPIFFS, dem SPI Flash File System, zur freien Verfügung. Mehr zu SPIFFS gibt es in diesem externen Blogpost.

Der Code zum Laden von JSON-Daten sieht im Wesentlichen so aus:

#include <FS.h> // includes for using filesystem
#include <SPIFFS.h>
#include <ArduinoJson.h>

// create local variables for configuration
char mqtt_host[40];
char mqtt_port[6];
char mqtt_user[40];
char mqtt_pw[64];
char mqtt_topic[128] = "RFID/Shield1/lastitem";
char mqtt_default_value[40] = "none";
char mqtt_clientID[40];

void load_json(){
  if (SPIFFS.begin()) {
    Serial.println("mounted file system");
    if (SPIFFS.exists("/config.json")) {
      File configFile = SPIFFS.open("/config.json", "r");
      if (configFile) {
        Serial.println("opened config file");
        DynamicJsonDocument jsondoc(566);
        DeserializationError error = deserializeJson(jsondoc, configFile);
        if (error) {
          Serial.println("failed to load json config");
          Serial.println(error.c_str());
          return;
        }

        //Serial.println("\nparsed json");
        //serializeJson(jsondoc, Serial);  //Testoutput to Serial Monitor
        // copy json values into local variables
        strncpy(mqtt_host, jsondoc["mqtt_host"], 39);
        strncpy(mqtt_port, jsondoc["mqtt_port"], 5);
        strncpy(mqtt_user, jsondoc["mqtt_user"], 40);
        strncpy(mqtt_pw, jsondoc["mqtt_pw"], 63);
        strncpy(mqtt_topic, jsondoc["mqtt_topic"], 127);
        strncpy(mqtt_default_value, jsondoc["mqtt_default_value"], 39);
        strncpy(mqtt_clientID, jsondoc["mqtt_clientID"], 39);
        configFile.close();
      }
    } // else { // would be nicer to have file not exists error here}
  } else {
    Serial.println("failed to mount FS");
    delay(1000);
    return;
  }
}

Die #include-Zeilen binden alle notwendigen Bibliotheken ein.

Der nächste Block deklariert für jede Konfiguration, die du nachher im Web-Interface konfigurieren möchtest, eine lokale Variable mit einer festen Größe (gewünschte String-Länge + 1 wegen dem abschließenden Null-Byte).

Als letztes schreiben wir eine Funktion load_json(), die zunächst das File öffnet, falls es existiert (begin(), exists(), open() mit Leserechten "r"). Dann wird ein Json-Puffer vom Typ DynamicJsonDocument mit der Größe 566 bytes[1] angelegt. Das Schöne an dieser JSON-Bibliothek ist, du kannst damit beliebig Daten in Text (serialize) und wieder zurück (deserialize) konvertieren. Dabei gibst du als Parameter immer das JsonDocument an und Ziel (ser.) bzw. Quelle (deser.) als Puffer, Stream oder Datei handle.
Der Block um if(error) ist dafür gedacht, falls die Konvertierung von Text zu Daten schiefgeht (falsche Formatierung zum Beispiel), einen Fehler auszugeben und die load_json() Funktion abzubrechen. In den letzten Zeilen im Hauptblock werden die Werte aus den interpretierten JSON-Daten mittels strncpy() in die lokalen Variablen kopiert und sind für das Skript verfügbar. Du kannst also jetzt schon ein /config.json File einlesen und die vorher hardkodierten Daten für den MQTT-Client damit überschreiben.
Bleiben zwei Fragen offen: wie wird das File gespeichert und wie bekommst du die Daten, die abgespeichert werden sollen aus dem Config Web Interface (in dem du auch die WiFi Daten einspeicherst)?

Das Abspeichern übernimmt der folgende Code:

void save_json() {
  Serial.println("saving config");
  DynamicJsonDocument jsondoc(1024);
  jsondoc["mqtt_host"] = mqtt_host;
  jsondoc["mqtt_port"] = mqtt_port;
  jsondoc["mqtt_user"] = mqtt_user;
  jsondoc["mqtt_pw"] = mqtt_pw;
  jsondoc["mqtt_topic"] = mqtt_topic;
  jsondoc["mqtt_default_value"] = mqtt_default_value;
  jsondoc["mqtt_clientID"] = mqtt_clientID;
  SPIFFS.begin(true);
  File configFile = SPIFFS.open("/config.json", "w");
  if (!configFile) {
    Serial.println("failed to open config file for writing");
  }
  serializeJson(jsondoc, configFile);
  configFile.close();
  //end save
}

Im Prinzip ist diese Funktion sehr ähnlich zur load_json() Funktion. Nur die Reihenfolge ist invertiert. Nachdem du das DynamicJsonDocument angelegt hast, schreibst du alle Werte, die in den lokalen Variablen gespeichert sind, in das jsondoc. Dann öffnest du das Filesystem und das Configfile (dieses Mal mit Schreibrechten "w"). Zuletzt schreibt serializeJson() die Daten als Text in das config File. Achtung: Eventuell vorhandene Inhalte werden dabei komplett überschrieben. Ein Unterschied zu load_json() besteht noch im Aufruf von SPIFFS.begin(). Der erste Parameter true sagt: falls ein Fehler beim Mounten passiert, soll das Filesystem formatiert werden (bei neuen ESPs oder solchen die gelöscht wurden notwendig). Als nächstes sollen die Variablen noch im Web-Interface konfigurierbar sein und die save_json-Methode aufgerufen werden.

bool needs_saving = false;
void save_callback(){
  needs_saving = true;
}

void setup(){  
  // Set ssid to ESP-123456789ABC
  // snprintf...
  
  //Wifi Manager initialization
  WiFiManager wifimanager;
  load_json();
  
  WiFiManagerParameter custom_mqtt_host("server", "mqtt server", mqtt_host, 40);
  WiFiManagerParameter custom_mqtt_port("port", "mqtt port", mqtt_port, 6);
  WiFiManagerParameter custom_mqtt_user("user", "mqtt user", mqtt_user, 40);
  WiFiManagerParameter custom_mqtt_pw("pw", "mqtt password", mqtt_pw, 64);
  WiFiManagerParameter custom_mqtt_topic("topic", "mqtt topic", mqtt_topic, 128);
  WiFiManagerParameter custom_mqtt_default_value("defaultPayload", "mqtt default_payload", mqtt_default_value, 20);
  WiFiManagerParameter custom_mqtt_clientID("clientID", "Client ID", mqtt_clientID, 40);
  wifimanager.addParameter(&custom_mqtt_host);
  wifimanager.addParameter(&custom_mqtt_port);
  wifimanager.addParameter(&custom_mqtt_user);
  wifimanager.addParameter(&custom_mqtt_pw);
  wifimanager.addParameter(&custom_mqtt_topic);
  wifimanager.addParameter(&custom_mqtt_default_value);
  wifimanager.addParameter(&custom_mqtt_clientID);

  wifimanager.setSaveConfigCallback(save_callback);
  

An der Stelle, an der du vorhin schon den wifimanager initialisiert hast und bevor autoconnect() aufgerufen wird, passieren 3 Dinge:

  1. load_json() wird aufgerufen um eventuell vorhandene Configs in die lokalen Variablen zu schreiben.
  2. WiFiManagerParameter myParameter ("schlüssel", "Hilfetext", aktueller_wert, anzahl_zeichen) legt einen neuen Parameter fürs Webinterface an. Wichtig ist hierbei, dass als dritter Parameter der Variablenname der jeweiligen lokalen Variable verwendet wird, damit bestehende Konfigurationen im Formular vorausgefüllt sind. Mit wifimanager.addParameter() werden dann die angelegten Parameter tatsächlich registriert.
  3. setSaveConfigCallback(funktion) bekommt eine Funktion übergeben, die ganz oben samt Variable definiert ist und nur dafür zuständig ist, wenn der wifimanager sagt, dass abgespeichert werden muss, das Flag needs_saving auf true zu setzen).

Zum Abspeichern füge nach dem autoconnect-Block noch folgenden Code ein

//Immer noch in void setup()

if(!wifimanager.autoconnect(...)){
  ...
}

// we are connected
// so copy back values and check if config needs saving
strncpy(mqtt_host, custom_mqtt_host.getValue(), 39);
strncpy(mqtt_port, custom_mqtt_port.getValue(), 5);
strncpy(mqtt_user, custom_mqtt_user.getValue(), 39);
strncpy(mqtt_pw, custom_mqtt_pw.getValue(), 63);
strncpy(mqtt_topic, custom_mqtt_topic.getValue(), 127);
strncpy(mqtt_default_value, custom_mqtt_default_value.getValue(), 39);
strncpy(mqtt_clientID, custom_mqtt_clientID.getValue(), 39);

if (needs_saving) {
  save_json();
  Serial.println("configuration saved");
}
needs_saving = false;

Hier werden jetzt nachdem die Verbindung aufgebaut ist die Werte aus dem Webformular wieder in die lokalen Variablen kopiert. Danach wird überprüft, ob die Konfiguration in das config File gespeichert werden muss und falls ja save_json() aufgerufen. Das Flag wird dann in jedem Fall auf false zurückgesetzt. Der gesamte Projektcode ist hier auf Github erhältlich.

Anmerkung: Falls du nicht mehr auf das Konfigurationsmenü kommst, weil der ESP immer automatisch zum WLAN verbindet, musst du den Chip komplett löschen. Wenn du dich mit der Kommandozeile gut auskennst, öffne eine im Ordner (die Versionsnummern könnten anders sein) "%USERPROFILE%\AppData\Local\Arduino15\packages\esp32\tools\esptool_py\2.6.1" Hier kannst du jetzt .\esptool.py --port COM3 erase_flash ausführen, um den Flash zu löschen. Das geht nicht, wenn der Serial Monitor noch offen ist.
Wenn dir das zu heikel ist, kannst du stattdessen das GUI Tool nutzen. Es heißt Flash Download Tools und ist direkt bei espressif erhältlich.


  1. Die Größe 566 ist hierbei vorberechnet mit diesem Tool. Wenn ich nicht genau weiß, wie groß die Daten werden könnten, nehme ich meist 1024 oder 2048 bytes. Das reicht für das meiste. ↩︎

Das Ergebnis

Dein ESP kann jetzt per WiFi konfiguriert werden, per MQTT den Zustand zurückmelden und in dieser Ausgestaltung hier die Daten des RFID-Readers an MQTT weiterreichen. Die Anwendungsmöglichkeiten dafür sind vielfältig, aber bitte, bitte nutze es nicht zur Zugangskontrolle. In dem vorgestellten Code gibt es null (in Worten NULL! in Zahlen: NULL!) Sicherheitsmechanismen. Anwendungsfälle könnten aber sein:

  • LED-Streifen-Farbe per unterschiedlicher Tags setzen
  • Vielleicht Programmierbare Kaffeemaschinen auf verschiedene Tags trainieren
  • Inventar für Werkzeuge (mit Webinterface das anzeigt welche nicht am richtigen Ort sind -> ausgeliehen oder verbummelt)
  • Oder eine Gäste-Aussortierungsanlage mit großer Steinkugel wie hier in Abbildung 5 vorbereitet:
Abb. 5: "Stein"plattform mit RFID-Reader. Links ohne Idol (mit Tag), rechts mit Idol. Im Hintergrund sieht man einen MQTT-Client subscribed auf das Topic, der dann die ID vom Idol empfängt.

Die Steinplattform gibt es hier und das Idol hier zum selber Ausdrucken.

Zusammenfassung

In diesem Tutorial hast du schrittweise Code zusammengeschrieben, der deinem ESP erlaubt

  1. eine WiFi-Verbindung aufzubauen, die beim ersten Start oder wenn du in einem neuen WLAN bist per Web-Oberfläche konfigurierbar ist
  2. einen MQTT-Client zu starten, der Nachrichten zum Broker schicken kann
  3. Einen RFID-Reader per SPI anzusteuern, um an die UID von RFID-Tags zu kommen
  4. Die MQTT Konfiguration im selben Webinterface wie 1. konfigurierbar zu machen, sodass du ESPs flashen und im Zielnetzwerk fertig konfigurieren kannst, wenn du die Zugangsdaten vom WLAN und die MQTT-Broker-Adresse kennst.

Es gibt noch die Möglichkeit, die Konfiguration per Button zu resetten, wie das geht, ist auf dem Github Repo des Wifimanager beschrieben.

Mein Anwendungsfall war noch etwas anders als der hier illustrierte, ich wollte kontinuierlich prüfen, ob ein Tag in Reichweite ist, und reagieren können, wenn er weggenommen wird. Dafür sind Timer und viele Hilfsvariablen nötig, der Code befindet sich hier auf Github, ich wollte aber dieses Tutorial nicht noch länger machen, deshalb ist er hier nicht erklärt.

Schick mir ein Bild von deinem Projekt! Schick mir deine Fragen, wenn du nicht weiterkommst! Und schick mir dein Feedback zu diesem Blogpost. Am besten erreichst du mich auf Twitter oder per Email unter info@plantprogrammer.de

Bis zum nächsten Mal und keep hacking!