Öltanküberwachung Mqtt ESP8266 Ultraschall

In diesem Beitrag zeige ich wie ich meine Öltanküberwachung mit Hilfe von Ultraschall und dem Mikrocontroller ESP8266 umgesetzt habe. Ultraschall wird genutzt, um den Füllstand des Öltanks zu messen, während der ESP8266 die Daten erfasst und via MQTT (Message Queuing Telemetry Transport) an einen Empfänger überträgt.

Durch die Verwendung von MQTT können die Daten problemlos an einen Empfänger, Server oder IoT Device übertragen werden, wo die Daten in Echtzeit überwacht und / oder gespeichert werden können.

Achtung: Der Umgang mit brennbaren Flüssigkeiten ist gefährlich. Ich weise auf den Haftungsausschluss am Ende meines Berichts hin. Die ist ein rein informativer Beitrag. Keine Nachbauanleitung.

Prinzip

Ultraschallsensor

Das Prinzip der Messung mit Ultraschall im Öltank beruht auf der Laufzeitmessung von Schallwellen. Der Ultraschallsensor AJ-SR04M sendet dabei Schallwellen aus, die an der Oberfläche des Öls reflektiert werden. Die reflektierten Schallwellen werden vom Sensor erfasst und die Laufzeit der Schallwellen wird gemessen. Aus dieser Laufzeit kann dann der Abstand zwischen Sensor und Öl-Oberfläche berechnet werden. Da sich der Füllstand im Tank ändert, ändert sich auch der Abstand zwischen Sensor und Öl-Oberfläche, was zur Folge hat, dass sich die Laufzeit der Schallwellen verändert. Daraus ergibt sich eine Änderung des Abstandes und somit des Füllstandes im Tank.

Ultraschall Sensor AJ-SR04M
Ultraschall Sensor AJ-SR04M

Mikrocontroller ESP8266 und MQTT

Der ESP8266 ist ein Mikrocontroller, der die Daten des Ultraschallsensors empfangen und verarbeiten kann. Dazu wird der Mikrocontroller mit dem Ultraschallsensor verbunden und es wird ein Programm erstellt, das die Daten des Sensors auswertet und in eine für den Empfänger lesbare Form bringt. Hierbei können zum Beispiel Messwerte oder ein Status-Update übertragen werden.

Die Auswertung der Daten im ESP8266 erfolgt dann in der Regel über ein Programm, das auf dem Mikrocontroller ausgeführt wird. In diesem Programm werden die Daten des Sensors ausgelesen, verarbeitet und ggf. auch gespeichert. Hierbei können verschiedene Methoden zur Datenverarbeitung genutzt werden, wie zum Beispiel die Berechnung von Durchschnittswerten oder die Überwachung von Schwellwerten. Anschließend können die Daten über das Wi-Fi-Modul des ESP8266 an einen Empfänger via MQTT übertragen werden, um sie zu visualisieren oder weitere Aktionen auszulösen.

ESP8266 Mikrocontroller
ESP8266 Mikrocontroller

Umsetzung Ultraschall Öltanküberwachung

Verwendete Komponenten

Schaltplan ESP8266 AJ-SR04M Ultraschall Sensor

Schaltplan ESP8266 AJ-SR04M Ultraschall Senort
Schaltplan ESP8266 AJ-SR04M Ultraschall Senort MQTT

Der Schaltplan zeigt die verwendeten Komponenten. Hauptsächlich ist der ESP8266 und der Ultraschallsensor AJ-SR04M wichtig. Der ESP ist durch die Pin Header J1 und J2 dargestellt. Die Pin Header J3 stellen den AJ-SR04M dar.

Ich erstelle meine Platinen und Schaltpläne gerne mit Pin Headern, da ich auf den fertigen Platinen, dann die Komponenten auswechseln oder wiederverwenden kann.

Zusätzlich verwende ich hier einen 5V Spannungsregler U1, da die Energieversorgung und deren Spannung zum Zeitpunkt der konzeption noch nicht fest lag. Die Spannungsversorgung (Batteriefach, Netzteil) wird hier an die Pin Header J5 angeschlossen.

Der Widerstand R1 470 Ohm dient der Spannungsüberwachung der Energieversorgung an J5. Je nach Energiequelle muss dieser gegebenenfalls angepasst werden. Die Spannungsüberwachung kann auch vernachlässigt werden, wenn das Projekt an einer festen Spannungsquelle wie einem Netzteil versorgt wird oder beim Ausbleiben der Messwerte die Öltanküberwachung überprüft wird. In meinem Fall verwende ich später möglicherweise eine Batteriebox. Da ist es für mich interessant die sinkende Spannung der Batterien zu überwachen um abzuschätzen, wann die Öltanküberwachung ausfallen wird.

Über den Pin Header J4 steuere ich, dass der ESP8266 aus dem Deep Sleep wieder erwachen kann. Soll die Software angepasst werden, muss hier nur ein USB Stecker für das Update eingesteckt werden und der Jumper vom Pin Header J4 entfernt werden.

Platine Ultraschall Sensor mit ESP8266

Platine ESP8266 AJ-SR04M Ultraschall Sensor
Platine ESP8266 AJ-SR04M Ultraschall Sensor

Aus dem Schaltplan, den ich mithilfe von Fritzing erstellt habe, layoutete ich meine Platine. Dabei versuche ich auf eine platzsparendes Layout zu setzten. Dies ist nicht immer leicht, da die Bedienbarkeit oder Erreichbarkeit der Schnittstellen, Jumper oder Buttons für mich auch eine wichtige Rolle spielt. Kreuzungen der Leiterbahnen sind nur durch weitere Schichten auf der Leiterplatte PCB möglich. Im heutigen Produktionsprozess nicht mehr unbedingt ein Kostenfaktor, dennoch möchte ich gerne darauf verzichten. Zu komplex ist das Projekt nun aus meiner Sicht auch nicht.

So wurde hier der Ultraschallsensor AJ-SR04M, der mit einem abgewinkelten Pin Header angeliefert wurde, senkrecht auf die Platine gestellt. Die Besonderheit bei der Platine des Ultraschallsensors ist, dass die Komponenten der Platine beidseitig angebracht sind. Der Anschluss des Ultraschallmesskopfs ist auf der Unterseite.

Beim ESP8366 war es mir wichtig den USB Anschluss und den Reset-Button gut zugänglich zu haben.

Hergestellt wurde meine Platine von Aisler, die praktischerweise direkt in der Fritzing Software angeknüpft sind.

Meine Platine habe ich veröffentlicht unter https://aisler.net/p/FBCDSPNR

ChrisBue Platine DIY Oeltankueberwachung Ultraschall
Platine Öltankueberwachung Ultraschall

Source Code ESP8266 MQTT

Ultraschallsensor AJ-SR04M Entfernung messen

Zuerst erstellte ich mit Hilfe von Foren und anderen Bloggern eine Funktion um die Messung der Entfernung über den Ultraschallsensor AJ-SR04M zu bewerkstelligung. Ich baue eine Serielle Kommunikation zwischen ESP8266 und dem Ultraschallmodul auf.

#include <SoftwareSerial.h>

#define rxPin 13   // D6 // send //rxPin // trigger
#define txPin 12   // D7 // receive //txPin // echo

SoftwareSerial jsnSerial;

void setup() {

  pinMode(rxPin, INPUT);
  pinMode(txPin, OUTPUT);
  
  Serial.begin(115200);
  jsnSerial.begin(9600, SWSERIAL_8N1, rxPin, txPin, false);
  Serial.println();
  int value = getMeasurment();
}

Das oben beschriebene Messprinzip wird mit folgenden Funktionen durchgeführt. Ich wiederhole meine Messungen und berechne den Mittelwert der plausiblen Messwerte, um Schwankungen oder Messfehlern vorzubeugen. Es ist darauf zu achten, dass die Laufzeiten der Schallwellen jeder Messung sich nicht überschneiden.

int getMeasurment() {
  
  int sumValues = 0;
  int messurementsNotNull = 0;

  for(int i=0; i < MEASURMENTS; i++){

    if(jsnSerial.available() > 0){
        int inByte = jsnSerial.read();
    }

    jsnSerial.write(0x01);

    delay(50);

    if(jsnSerial.available() > 0){

      int dist = getDistance();
      
      if(dist > 0){
        sumValues = sumValues + dist; 
        messurementsNotNull = messurementsNotNull + 1;
      }
    }
  }
  return sumValues / messurementsNotNull;
}


int getDistance(){

  Serial.println("Start messung");

  unsigned int distance;
  byte startByte, h_data, l_data, sum = 0;
  byte buf[4];

  jsnSerial.readBytes(buf, 4);
  
  startByte = buf[0];

  if(startByte != 255){
      return -1;
  }

  h_data = buf[1];
  l_data = buf[2];
  sum = buf[3];

  distance = (h_data<<8) + l_data;

  if((( h_data + l_data)&0xFF) -1 != sum){

    Serial.printf("Invalid result: %d %d (%d)", buf[1], buf[2], buf[3]);
    Serial.println();

    return -1;
  }
    
  Serial.print("Distance [mm]: "); 
  Serial.println(distance);

  return distance;
}

Verbindungsaufbau ESP8266 mit MQTT zum Server

Wenn die Entfernung zwischen Ultraschallsensor und Öloberfläche bestimmt wurde, sende ich die Daten mittels ESP8266 -> MQTT -> WIFI an meinen Server. Dieser nimmt die Daten entgegen, speichert diese ab und errechnet Füllmengen.

#include <ESP8266WiFi.h>
#include <Ticker.h>
#include <AsyncMqttClient.h>

//WIFI configuration
#define wifi_ssid "ChrisBue_Blog"
#define wifi_password "ChrisBue_geheim_Blog"


//MQTT configuration
#define mqtt_host "192.168.1.77"
#define mqtt_port 1883

#define mqtt_user "ChrisBue_Blog_mqtt_user"
#define mqtt_pass "ChrisBue_geheim_Blog_mqtt_pass"

#define MEASURMENTS 12 // Measurement for calc avg value 
// #define DEEPSLEEP 10000000 // 10 sek 
#define DEEPSLEEP 30 * 60000000 // 30 min

const char* topic = "ChrisBue/Blog/oil_tank";

#define rxPin 13   // D6 // send //rxPin // trigger
#define txPin 12   // D7 // receive //txPin // echo

const int analogInPin = A0;  // ESP8266 Analog Pin ADC0 = A0

AsyncMqttClient mqttClient;
Ticker mqttReconnectTimer;
WiFiEventHandler wifiConnectHandler;
WiFiEventHandler wifiDisconnectHandler;
Ticker wifiReconnectTimer;

SoftwareSerial jsnSerial;

void connectToWifi() {

  delay(10);
  Serial.print("Connecting to ");
  Serial.print(wifi_ssid);

  WiFi.begin(wifi_ssid, wifi_password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
}

void onWifiConnect(const WiFiEventStationModeGotIP& event) {

  Serial.print("IP-Adresse: ");
  Serial.println(WiFi.localIP());
  Serial.print("MAC-Adresse: ");
  Serial.println(WiFi.macAddress());
  Serial.print("Hostname: ");
  Serial.println(WiFi.getHostname());

  connectToMqtt();
}

void connectToMqtt() {

  Serial.println("Connecting to MQTT...");
  mqttClient.connect();
}

void onWifiDisconnect(const WiFiEventStationModeDisconnected& event) {

  Serial.println("Disconnected from Wi-Fi.");

  mqttReconnectTimer.detach(); // ensure we don't reconnect to MQTT while
  wifiReconnectTimer.once(2, connectToWifi);// reconnecting to Wi-Fi

}

void onMqttConnect(bool sessionPresent) {

  Serial.println("Connected to MQTT.");
  Serial.print("Session present: ");
  Serial.println(sessionPresent);
}

void onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {

  Serial.println("Disconnected from MQTT.");
  if (WiFi.isConnected()) {
    mqttReconnectTimer.once(2, connectToMqtt);
  }
}

void onMqttSubscribe(uint16_t packetId, uint8_t qos) {

  Serial.println("Subscribe acknowledged.");
  Serial.print(" packetId: ");
  Serial.println(packetId);
  Serial.print(" qos: ");
  Serial.println(qos);
}
void onMqttUnsubscribe(uint16_t packetId) {

  Serial.println("Unsubscribe acknowledged.");
  Serial.print(" packetId: ");
  Serial.println(packetId);
}


void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) {

  Serial.println("Publish received.");
  Serial.print(" topic: ");
  Serial.println(topic);
  Serial.print(" qos: ");
  Serial.println(properties.qos);
  Serial.print(" dup: ");
  Serial.println(properties.dup);
  Serial.print(" retain: ");
  Serial.println(properties.retain);
  Serial.print(" len: ");
  Serial.println(len);
  Serial.print(" index: ");
  Serial.println(index);
  Serial.print(" total: ");
  Serial.println(total);
}

void onMqttPublish(uint16_t packetId) {
  Serial.println("Publish acknowledged.");

  Serial.println("DeepSleep");
  Serial.println("");

  ESP.deepSleep(DEEPSLEEP);
}

void setup() {

  pinMode(rxPin, INPUT);
  pinMode(txPin, OUTPUT);
  
  Serial.begin(115200);
  jsnSerial.begin(9600, SWSERIAL_8N1, rxPin, txPin, false);
  Serial.println();
  Serial.println();

  wifiConnectHandler = WiFi.onStationModeGotIP(onWifiConnect);
  wifiDisconnectHandler = WiFi.onStationModeDisconnected(onWifiDisconnect);

  mqttClient.onConnect(onMqttConnect);
  mqttClient.onDisconnect(onMqttDisconnect);
  mqttClient.onSubscribe(onMqttSubscribe);
  mqttClient.onUnsubscribe(onMqttUnsubscribe);
  mqttClient.onMessage(onMqttMessage);
  mqttClient.onPublish(onMqttPublish);
  mqttClient.setServer(mqtt_host, mqtt_port);
  mqttClient.setCredentials(mqtt_user, mqtt_pass);
  
  connectToWifi();

  int value = getMeasurment();

  float sensorVolt = analogRead(analogInPin) * 0.0077;

  String messages = String("{ \"volt\" : \"") + String( sensorVolt ) + String("\", \"level\" : \"") + String( value ) + String( "\" }" );

  // Publish an MQTT message on topic 
  uint16_t packetIdPub1 = mqttClient.publish(topic, 1, true, String( messages ).c_str() );

  Serial.printf("Publishing on topic %s at QoS 1, packetId: %i, value: %s ", topic, packetIdPub1, String( messages ).c_str() );
  //Serial.printf("value: %s", String( messages ).c_str());
  Serial.println("");

}

int getMeasurment() {
  
  int sumValues = 0;
  int messurementsNotNull = 0;

  for(int i=0; i < MEASURMENTS; i++){

    if(jsnSerial.available() > 0){
        int inByte = jsnSerial.read();
    }

    jsnSerial.write(0x01);

    delay(50);

    if(jsnSerial.available() > 0){

      int dist = getDistance();
      
      if(dist > 0){
        sumValues = sumValues + dist; 
        messurementsNotNull = messurementsNotNull + 1;
      }
    }

    
  }

  return sumValues / messurementsNotNull;
}

int getDistance(){

  Serial.println("Start messung");

  unsigned int distance;
  byte startByte, h_data, l_data, sum = 0;
  byte buf[4];

  jsnSerial.readBytes(buf, 4);
  
  startByte = buf[0];

  if(startByte != 255){
      return -1;
  }

  h_data = buf[1];
  l_data = buf[2];
  sum = buf[3];

  distance = (h_data<<8) + l_data;

  if((( h_data + l_data)&0xFF) -1 != sum){

    Serial.printf("Invalid result: %d %d (%d)", buf[1], buf[2], buf[3]);
    Serial.println();

    return -1;
  }
    
  Serial.print("Distance [mm]: "); 
  Serial.println(distance);

  return distance;
}

void loop() {
  
}

Die Ermittlung der Daten und das übertragen der Daten ist für mich ausreichend als Aufgabe für den ESP8266. Diese Aufgabe überlasse ich absichtlich dem Server. Der ESP8266 kann natürlich deutlich mehr. Es wäre auch möglich die Füllmengen direkt ausrechnen zu lassen oder die Daten über einen installierten Webserver direkt im Lan oder WAN anzuzeigen.

In meinem Fall wollte ich wenig energieraubende Aufgaben an den ESP8266 abgeben. Dieser ist schon mit WLAN und Ultraschallsensor beschäftigt. Nach erfolgreicher Messung und übertragung der Daten mittels MQTT geht der ESP8266 für 30 Minuten in den Deep Sleep um Energie zu sparen. Die Loop-Funktion bleibt in diesem Fall leer.

Zusammengebaut

Zusammengebaut und mit einem Batteriefach als Spannungsversorgung habe ich das Projekt bei mir im Heizraum aufgebaut. Bei der Anbringung des Sensors sollte die Beschreibung des Herstellers berücksichtigt werden. Es kann Sein, dass Reflektionen der Tankwände die Messwerte verfälschen oder unbrauchbar machen. In meinem Fall ist der Sensor z.B. für kurze Distanzen ungeeignet und misst erst ab einer Entfernung von 20 cm mit einer Genauigkeit von 1-2 cm. In meinem Fall aber ausreichend.

Resümee

Ich muss hier anmerken, dass der Öltank schon älter ist und auch diese Öffnung sowie die „Sensorbefestigung“ nicht optimal sind. Dennoch sind die Messergebnisse doch sehr beeindruckend.

Die Energieversorgung möchte ich noch einmal überarbeiten und auch ein dauerhaft geschlossenes Gehäuse wäre sinnvoll.

Ein weiteres Projekt, bei dem der Ultraschallsensor AJ-SR04M und ein ESP8266 zum Einsatz kommen habe ich hier verlinkt. Beide Projekte sind vergleichbar. Jedes hatte aber kleine, unterschiedliche Herausforderungen.

Achtung: Ich weise hier ausdrücklich darauf hin, dass der Nachbau und/oder Einbau auf eigene Gefahr geschieht. Ich weise ebenfalls ausdrücklich darauf hin, dass ich für durch den Nach- und/oder Einbau der beschriebenen Projekte entstandene Personen oder Sachschäden keine Haftung übernehme.

NFC ESP8266 RFID DIY Stempeluhr

Heute stelle ich meine NFC-Stempeluhr auf Basis eines ESP8266 Mikrocontrollers vor, die eine einfache und effektive Zeiterfassungslösung für jeden bietet. Durch das Scannen von NFC-Tags können Mitarbeiter schnell und einfach ein- und ausstempeln, während die Daten automatisch in einer Datenbank gespeichert werden.

Zielsetzung

Das Ziel dieses Projekts NFC ESP8266 besteht darin, eine eigene NFC-basierte Stempeluhr auf Basis des ESP8266 Mikrocontrollers zu entwickeln. Durch das Scannen von NFC-Tags sollen Mitarbeiter/Kollegen einfach und schnell ein- und ausstempeln können. Die Daten werden automatisch an einen Server gesendet, in einer Datenbank gespeichert und können für die Zeiterfassung genutzt werden. Zusätzlich zur Hardware soll auch die Software für den ESP8266 Microcontroller erstellt werden.

Umsetzung

Ich habe für die Realisierung meiner NFC-basierten Stempeluhr auf Basis des ESP8266 Mikrocontrollers eine eigene Platine entworfen und hergestellt. Dies ermöglichte mir eine optimale Anordnung der Komponenten und eine saubere Verdrahtung. Allerdings ist es nicht unbedingt notwendig, eine eigene Platine herzustellen, um dieses NFC ESP8266 Projekt umzusetzen. Es gibt viele Alternativen wie z.B. Lochrasterplatinen oder Prototyping-Shields, die es Ihnen ermöglichen, die Hardwarekomponenten auf einfache Weise zu verbinden. Letztendlich hängt die Wahl der Platine davon ab, wie anspruchsvoll das Projekt ist und welche Komponenten verwendet werden.

Benötigte Komponenten für NFC ESP8266

  • ESP8266 Microcontroller WEMOS D1 Mini AliExpress
  • NFC-Lesegerät RFID-RC522 (je nach Hersteller unterschiedliche Pinbelegung) AliExpress
  • LED DIP WS2812 (1) AliExpress
  • Buzzer 5V Summer (1)
  • 2N3904 Transistor (1) AliExpress
  • 1k ohm Widerstände (2) AliExpress
  • 4,7k ohm Widerstand (1) AliExpress
  • 5V Stromversorgung über USB Handy Ladegerät
  • Gehäuse für die Hardware (optional)

Schaltplan

Schaltplan NFC Stempeluhr

Als erstes musste ich den Schaltplan erstellen. Hierfür verwendete ich das das kostenlose Layout-Tool Fritzing verwenden. Ich plaziete die Komponenten, einschließlich des ESP8266 Mikrocontrollers, des NFC-Lesegeräts, der NEO LED WS2812, des Summers und des Jumpers auf dem virtuellen Breadboard. Anschließend verknüpfte ich die Komponenten miteinander, um den Schaltplan zu erstellen.

In meinem Schaltplan verwende ich das RFID RC522 Modul von Funduino. Das Pinout von anderen Herstellern dieser fertigen Module kann variieren. In meinem Fall habe ich noch einen anderen Anbieter gefunden, der lediglich die Abfolge der Pins geändert hat.

Als Stromversorgung für mein Projekt verwende ich die USB Schnittstelle, die auf dem ESP8266 Wemos D1 bereits integriert ist. Darüber kann ich die Spannungen 5V und 3,3 Volt für meine Schaltung entnehmen.

Über einen Jumper-Schalter möchte ich den ESP8266 in zwei Modi versetzen können. Einmal um die Kommunikation zum WLan am Aufstellort herzustellen und um den ESP8266 dann in seinen Arbeitsbetrieb zu versetzten.

Die NEO LED WS2812 und der Summer dienen der Benutzerinteraktion, indem sie den Benutzer über erfolgreiche oder fehlgeschlagene Aktionen informieren.

Hinweis: In meinem Schaltplan ist eine RGB LED verbaut. Da zum Zeitpunkt der Erstellung ich keinen NEO LED WS2812 Plan zur Verfügung hatte, der das DIP Lochmuster der WS2812 darstellt.

Platine

Nachdem ich das Design abgeschlossen hatte, habe ich die Platine über die Plattform Aisler herstellen lassen. Aisler ist ein Online-Service, der benutzerdefinierte Platinenherstellung anbietet. Ich habe das Design meiner Platine hochgeladen und konnte innerhalb weniger Tage eine professionell gefertigte Platine erhalten.

Die Verwendung von Aisler bot mir eine effiziente Möglichkeit, meine Platine herzustellen, ohne dass ich eigene Ausrüstung oder Erfahrung mit der Herstellung von Platinen haben musste. Die fertige Platine passte perfekt zu meinem Design und konnte nahtlos in das Projekt integriert werden.

Insgesamt war die Verwendung von Fritzing und Aisler eine großartige Möglichkeit, meine eigene Platine für das NFC Stempeluhr ESP8266 DIY Projekt zu erstellen und sicherzustellen, dass das Projekt auch für mich als Laie umgesetzt werden konnte.

Die passende Platine kannst du hier finden und gleich herstellen lassen.

https://aisler.net/p/VPOHRRVV

Software

Die Software für das NFC Stempeluhr ESP8266 DIY Projekt wurde mit der Arduino Integrated Development Environment (IDE) erstellt. Arduino ist eine Open-Source-Plattform, die häufig für die Entwicklung von Mikrocontroller-basierten Projekten wie diesem verwendet wird.

In der Arduino IDE konnte ich den ESP8266 Mikrocontroller programmieren und die verschiedenen Komponenten des Projekts steuern. Ich habe die notwendigen Bibliotheken und Sketche verwendet, um sicherzustellen, dass die NFC-Lese- und Schreibfunktionen sowie die LED und der Summer korrekt funktionieren.

Darüber hinaus habe ich auch die WLAN-Verbindung des ESP8266 programmiert, die es ermöglicht, die Stempeluhr über das lokale Netzwerk zu verbinden und die erfassten Arbeitszeiten auf einem Server zu speichern.

Konfigurationsmodus über Jumper-Schalter

Um die WLAN-Verbindung des NFC Stempeluhr ESP8266 DIY Projekts zu konfigurieren, wurde ein spezieller Konfigurationsmodus implementiert. Der Konfigurationsmodus wird gestartet, indem ein physischer Jumper-Schalter auf der Platine betätigt wird.

Sobald der Konfigurationsmodus gestartet ist, wird der ESP8266 zu einem Access Point (AP) und erzeugt ein WLAN-Signal. Dieses WLAN-Signal kann vom Benutzer erkannt werden, der sich dann mit dem Access Point verbindet.

Nachdem der Benutzer mit dem Access Point verbunden ist, kann er eine spezielle Webseite aufrufen, auf der er die SSID und das Passwort des vorhandenen WLAN-Netzwerks eingeben kann, mit dem er sich verbinden möchte.

Sobald der Benutzer die entsprechenden Informationen eingegeben hat, speichert der ESP8266 die WLAN-Verbindungsinformationen und verwendet sie bei zukünftigen Verbindungen mit dem Netzwerk.

Durch die Verwendung dieses Konfigurationsmodus können Benutzer die WLAN-Verbindung des NFC Stempeluhr ESP8266 DIY Projekts auf einfache Weise konfigurieren, ohne dass sie zusätzliche Hardware oder komplexe Software-Konfigurationen benötigen.

Normalbetrieb

Im Normalbetrieb erfasst die NFC Stempeluhr ESP8266 DIY die Stempelzeit von Benutzern, die ihre NFC-Karte oder ihr NFC-Tag an die Stempeluhr halten. Die NFC-Lesefunktion wird von einem NFC-Lesemodul bereitgestellt, das an den ESP8266 Mikrokontroller angeschlossen ist.

Wenn ein Benutzer seine NFC-Karte oder seinen NFC-Tag an die Stempeluhr hält, erkennt das NFC-Lesemodul die Karte oder den Tag und liest die darauf gespeicherten Informationen. Die Stempeluhr sendet die gelesenen Informationen an den Server. Dieser vergleicht die Daten mit einer Liste von Benutzern und speichert den Zeitpunkt der Aktion als Arbeitszeit in einer Datenbank.

Die URL des Servers und ein paar wenige Parameter lassen sich über die Oberfläche einstellen.

Netzwerk Konfigurationsparameter
  • ssid -> Name des WLan Netzwerks
  • pass ->Passwort zum WLan Netzwerk
  • IP -> eigene IP Adresse
  • gateway -> Netzwerkdateway
  • subnet -> Subnetzmaske
  • DNS -> Adresse des DNS-Servers
Dynamische Parameter
  • mac -> MAC-Adresse der NFC Stempeluhr
  • card -> ID der Karte, die an das NFC Lesegerät gehalten wurde
  • pass -> Ein Schlüssel der in MD5 übertragen wird zur Berechtigung der Anfrage
  • url -> Anfrage URL Server.

Die Stempeluhr gibt außerdem Feedback an den Benutzer durch die Verwendung einer LED und eines Summer.

#include <SPI.h>
#include <MFRC522.h>
#include <Adafruit_NeoPixel.h>

#include <EEPROM.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266HTTPClient.h>
#include <MD5Builder.h>

// System Status
int configMode = 0;   // Einstellung
int systemStatus = 0; // 0 = OK , 1 = Error, 2 = Notice
int statusCount = 0;  // Zähler für LEDs

// Webserver
const char* config_mode_ssid = "NFC-CardClock";  // Enter SSID here
const char* config_mode_password = "start123";  //Enter Password here

IPAddress config_mode_local_ip(192,168,1,1);
IPAddress config_mode_gateway(192,168,1,1);
IPAddress config_mode_subnet(255,255,255,0);

ESP8266WebServer server(80);

typedef struct{
  char ssid[32];
  char pass[64]; 
  IPAddress ip;
  IPAddress gateway;
  IPAddress subnet;
  IPAddress dns;
  char url[400];
  char urlpost[400];
  char urlpass[100];
} network;

network user_network;

struct URL {
  String protocol = "";
  String host = "";
  String port = "";
  String path = "";
} url;

// Prüft ob alle notwendigen eingegebenen Werte übertragen wurden.
byte user_checkup;

// Buzzer
int freqDeep=2200; // Hz
int freqNormal=2400; // Hz
int freqHeight=2550; // Hz

int buzzPin=16; 

// Button
int buttonPin=4;

// NeoPixel
int neoLedPin=5;

Adafruit_NeoPixel pixels = Adafruit_NeoPixel(1, neoLedPin, NEO_RGB + NEO_KHZ400);

MFRC522 mfrc522(15, 0);

MD5Builder _md5;

void setup() {

  // Terminal
  Serial.begin(115200); 
  delay(10);
  yield();

  // Neoleds
  pixels.begin();
  pixels.clear();
  pixels.setPixelColor(0, pixels.Color(255, 255, 0));
  pixels.show();
  
  delay(10);

  Serial.println();
  Serial.println("Init System");
  Serial.println();

  // Card Reader
  SPI.begin();                        // Init SPI bus
  mfrc522.PCD_Init();                 // Init MFRC522
  Serial.println("MFRC522 Card Reader details");
  mfrc522.PCD_DumpVersionToSerial();  // Show details of PCD - MFRC522 Card Reader details
  Serial.println();

  // EEPROM
  EEPROM.begin(sizeof(user_network));
  EEPROM.get(0, user_network);
  EEPROM.commit();

  // Prüfen ob Config Mode
  pinMode(buttonPin, INPUT_PULLUP);
  configMode = digitalRead(buttonPin);

  // CONFIG MODE
  if(configMode == 0){
    
    Serial.println("Config Mode -> Soft-AP");

    String ssid_config_mode = config_mode_ssid + String( WiFi.macAddress() );

    WiFi.softAPConfig(config_mode_local_ip, config_mode_gateway, config_mode_subnet);
    
    Serial.println(WiFi.softAP(ssid_config_mode, config_mode_password) ? "Ready" : "Failed!");

    server.on("/", handle_Config_Mode_OnConnect);

    server.on("/config", handle_Config_Mode_Config);

    server.on("/clearconfig", handle_Config_Mode_Clear_Config);

  // NORMAL MODE
  } else {

    // ACCESS POINT vergessen.
    WiFi.softAPdisconnect(true);
    WiFi.disconnect();

    Serial.println("Normal Mode -> wLan Network");
    
    delay(200);

    // Konfiguration aus EEPROM 
    // für Server verwenden
    Serial.print("Connecting to ");
    Serial.println(user_network.ssid);

    // eigene IP
    // DNS Server
    // Gateway
    // Subnet
    WiFi.config(user_network.ip, user_network.dns, user_network.gateway, user_network.subnet);

    // Verbindung zum wLan aufbauen. 
    WiFi.begin(user_network.ssid, user_network.pass);

    delay(500);

    while (WiFi.status() != WL_CONNECTED) {
      
      Serial.print(".");
      delay(500);
    }
    Serial.println("WiFi connected");

    WiFi.setAutoReconnect(true);
    WiFi.persistent(true);

    server.on("/", handle_Normal_Mode_OnConnect);
  }

  server.onNotFound(handle_NotFound);
  
  server.begin();

  Serial.println("HTTP server started");

  soundSystemStart();
  
}

void loop() {
  
  delay(50);

  statusCount++;

  systemStatusControll();

  // Webserver Client bedienen
  server.handleClient();

  // Ab hier ConfigMode
  // im config mode keine Karten annehmen
  if(configMode == 0){
    
    systemStatus = 2;
    
    // Look for new cards
    if ( mfrc522.PICC_IsNewCardPresent()) {

      // Select one of the cards
      if ( mfrc522.PICC_ReadCardSerial()) {
        soundWrong();
      }
    }
    return;
  }

  // *********************
  // Ab hier Normalbetrieb
  // *********************
  
  // Look for new cards
  if ( mfrc522.PICC_IsNewCardPresent()) {

    // Select one of the cards
    if ( mfrc522.PICC_ReadCardSerial()) {

      String cardId = "";
      for (byte i = 0; i < mfrc522.uid.size; i++) {

        // Abstand zwischen HEX-Zahlen und führende Null bei Byte < 16
        cardId += String(mfrc522.uid.uidByte[i] < 0x10 ? "0" : "");
        cardId += String(mfrc522.uid.uidByte[i], HEX);
      }

      int result = httpRequest(cardId);

      // 200 Kommen
      // 202 Gehen
      // 204 Kein User gefunden

      if( result == 200 ){

        soundWork();
        
      } else if(result == 202 ){
    
        soundGoHome();
        
      } else {
        
        soundWrong();
      }
    }
  }
}

int httpRequest(String cardId){

  WiFiClientSecure client;

  HTTPClient http;

  // Daten an Server senden
  Serial.println("URL:");
  Serial.println( user_network.url );

  int output = 0;
  int attempts = 0;

  parseURL(user_network.url, &url);

  Serial.println("Host:");
  Serial.println( url.host );

  IPAddress IPresult;
  IPAddress testIpAdress;

  if(testIpAdress.fromString(url.host)){

    Serial.println("Host is IP Adress:");
    Serial.println( url.host );

    Serial.println("No DNS needed");

  } else {

    while (  ( output = WiFi.hostByName( url.host.c_str(), IPresult, (uint32_t) 2000 ) ) != 1 && ( attempts < 5)  ){
        delay(1000);
        attempts++;
    }

    if(output != 1){
      Serial.println("Host konnte nicht ermittelt werden. DNS fehlgeschlagen.");
      return -1;
    }

    Serial.println("IP from DNS:");
    Serial.println( ipToString( IPresult) );
  }
  
  http.begin(client, user_network.url );
  http.addHeader("Content-Type", "application/x-www-form-urlencoded");

  // Create String for Post
  String post = user_network.urlpost;

  post.replace("{pass}", user_network.urlpass);
  post.replace("{card}", cardId);
  post.replace("{mac}", String( WiFi.macAddress() ) );
  
  Serial.println("Post:");
  Serial.println(post);
  
  int httpCode = http.POST(post);

  Serial.println("Response httpCode:");
  Serial.println(httpCode);

  http.end();

  return httpCode;
}

void systemStatusControll(){

  // Alles OK
  if(systemStatus == 0){
    if(statusCount == 75){

      long rssi = 0;
      
      switch (WiFi.status()){
        case WL_NO_SSID_AVAIL:
          Serial.println("Configured SSID cannot be reached");
          break;
        case WL_CONNECTED:
          Serial.println("Connection successfully established");
          rssi = WiFi.RSSI();
          Serial.println( String( WiFi.macAddress() ) + " RSSI: " + rssi );
          break;
        case WL_CONNECT_FAILED:
          Serial.println("Connection failed");
          break;
      }
      
      pixels.setPixelColor(0, pixels.Color(0, 0, 255));
      pixels.show();
      statusCount = 0;
    }
    
  // Error
  } else if( systemStatus == 1){
    if(statusCount == 3){
      pixels.setPixelColor(0, pixels.Color(255, 0, 0));
      pixels.show();
      statusCount = 0;
    }
    
  // Notice
  } else if( systemStatus == 2){
    if(statusCount == 3){
      pixels.setPixelColor(0, pixels.Color(255, 255, 0));
      pixels.show();
      statusCount = 0;
    }
  }

  delay(30);

  pixels.clear();
  pixels.show();
}

void soundOK() {

  pixels.setPixelColor(0, pixels.Color(0, 255, 0));
  pixels.show();
  
  tone(buzzPin, freqNormal, 350);
  delay(350);
  noTone(buzzPin);
  pinMode(buzzPin, INPUT);

  delay(1000);
}

void soundSystemStart() {

  pixels.setPixelColor(0, pixels.Color(0, 0, 255));
  pixels.show();
  
  tone(buzzPin, freqNormal, 150);
  delay(200);
  tone(buzzPin, freqNormal, 150);
  delay(150);
  
  noTone(buzzPin);
  pinMode(buzzPin, INPUT);

  delay(1000);
}

void soundWork() {

  pixels.setPixelColor(0, pixels.Color(0, 255, 0));
  pixels.show();
  
  tone(buzzPin, freqNormal, 250);
  delay(250);
  noTone(buzzPin);

  pinMode(buzzPin, INPUT);

  delay(1000);
}

void soundGoHome() {

  pixels.setPixelColor(0, pixels.Color(0, 255, 0));
  pixels.show();
  
  tone(buzzPin, freqNormal, 250);
  delay(300);
  noTone(buzzPin);

  delay(150);

  tone(buzzPin, freqNormal, 250);
  delay(250);
  noTone(buzzPin);

  pinMode(buzzPin, INPUT);

  delay(1000);
}

void soundWrong() {

  pixels.setPixelColor(0, pixels.Color(255, 0, 0));
  pixels.show();

  tone(buzzPin, freqNormal, 2000);
  delay(1000);
  noTone(buzzPin);
  pinMode(buzzPin, INPUT);
  
  delay(1000);
}

void handle_NotFound(){
  server.send(404, "text/plain", "Not found");
}

void handle_Normal_Mode_OnConnect() {
  long rssi = WiFi.RSSI();
  Serial.println( String( WiFi.macAddress() )+ " RSSI: " + rssi );
  server.send(200, "text/html", String( WiFi.macAddress()) + " RSSI: " + rssi  ); 
  
}

void handle_Config_Mode_OnConnect() {
  server.send(200, "text/html", sendHtmlConfigMode("")); 
}

void handle_Config_Mode_Clear_Config() {
  
  // EEPROM löschen ->ganze konfig weg!!!
  for (int i = 0 ; i < sizeof(user_network) ; i++) {
    EEPROM.write(i, 0);
  }
  EEPROM.commit();

  Serial.println("Konfig gelöscht. Speicher überschrieben.");

  server.send(200, "text/html", sendHtmlConfigMode(""));
}

void handle_Config_Mode_Config(){

  // INIT checkup
  user_checkup = 0;

  for (int i = 0; i < server.args(); i++) {

    if(server.argName(i) == "ssid"){
      server.arg(i).toCharArray(user_network.ssid, 32);
      bitSet(user_checkup, 0);

      Serial.println(server.argName(i));
      Serial.println(server.arg(i));
    }

    if(server.argName(i) == "pass"){
      server.arg(i).toCharArray(user_network.pass, 64);
      bitSet(user_checkup, 1);

      Serial.println(server.argName(i));
      Serial.println("*********");
      
    }
    
    if(server.argName(i) == "ip"){
      user_network.ip.fromString(server.arg(i));
      bitSet(user_checkup, 2);

      Serial.println(server.argName(i));
      Serial.println(server.arg(i));
    }

    if(server.argName(i) == "gateway"){
      user_network.gateway.fromString(server.arg(i));
      bitSet(user_checkup, 3);

      Serial.println(server.argName(i));
      Serial.println(server.arg(i));
    }

    if(server.argName(i) == "subnet"){
      user_network.subnet.fromString(server.arg(i));
      bitSet(user_checkup, 4);

      Serial.println(server.argName(i));
      Serial.println(server.arg(i));
    }

    if(server.argName(i) == "dns"){
      user_network.dns.fromString(server.arg(i));
      bitSet(user_checkup, 5);

      Serial.println(server.argName(i));
      Serial.println(server.arg(i));
    }

    if(server.argName(i) == "url"){
      server.arg(i).toCharArray(user_network.url, 400);
      bitSet(user_checkup, 6);

      Serial.println(server.argName(i));
      Serial.println(server.arg(i));
    }

    if(server.argName(i) == "urlpost"){
      server.arg(i).toCharArray(user_network.urlpost, 400);
      bitSet(user_checkup, 7);

      Serial.println(server.argName(i));
      Serial.println(server.arg(i));
    }

    if(server.argName(i) == "urlpass"){

      // MD5 umwandeln vor dem abspeichern
      String stringMD5 = md5(server.arg(i));
      stringMD5.toCharArray(user_network.urlpass, 100);


      Serial.println(server.argName(i));
      Serial.println("*********");
    }
  } 

  // Prüfen ob alle Daten 
  // Übertagen wurden
  // 8 bit 11111111 = 255
  if(user_checkup == 255){

    Serial.println( "Länge der Daten: "+ String( sizeof(user_network) ) );

    // EEPROM löschen
    for (int i = 0 ; i < sizeof(user_network) ; i++) {
      EEPROM.write(i, 0);
    }
    EEPROM.commit();

    // Write to EEPROM
    EEPROM.put(0, user_network);
    EEPROM.commit();

    server.send(200, "text/html", sendHtmlConfigMode("<span style=\"color: green;\">Daten gesetzt.</span>")); 
    return;
  }  

  server.send(200, "text/html", sendHtmlConfigMode("")); 
}

String sendHtmlConfigMode(String message){

  // read EEPROM
  EEPROM.get(0, user_network);

  String s = "<!DOCTYPE html>";
  s += "<html>\n";
  s += "<head>";
  s += "<meta charset=\"utf-8\"/>";
  s += "</head>";
  s += "<body>";
  s += "<h1>Config \"NFC-CardClock\" => " + String(WiFi.macAddress()) + "</h1>\n";
  s += "<b>" + message + "</b><br />";
  s += "<form method=\"post\" action=\"/config\">";
  s += "<h2>ssid</h2>\n";
  s += "<input type=\"text\" name=\"ssid\" value=\""+ String(user_network.ssid) +"\" required></input><br />";
  s += "<h2>pass</h2>\n";
  s += "<input type=\"password\" name=\"pass\" value=\"\" required></input><br />";
  s += "<h2>ip</h2>\n";
  
  s += "<input type=\"text\" name=\"ip\" value=\"" + ipToString(user_network.ip) + "\" pattern=\"^([0-9]{1,3}\\.){3}[0-9]{1,3}$\" required></input><br />";
  s += "<h2>gateway</h2>\n";
  s += "<input type=\"text\" name=\"gateway\" value=\""+ipToString(user_network.gateway)+"\" pattern=\"^([0-9]{1,3}\\.){3}[0-9]{1,3}$\" required></input><br />";
  s += "<h2>subnet</h2>\n";
  s += "<input type=\"text\" name=\"subnet\" value=\""+ipToString(user_network.subnet)+"\" pattern=\"^([0-9]{1,3}\\.){3}[0-9]{1,3}$\" required></input><br />";
  s += "<h2>DNS</h2>\n";
  s += "<input type=\"text\" name=\"dns\" value=\""+ipToString(user_network.dns)+"\" pattern=\"^([0-9]{1,3}\\.){3}[0-9]{1,3}$\" required></input><br />";

  s += "<h2>URL-Server</h2>\n";
  s += "<p>Über die Variablen {mac}, {card} und/oder {pass} lassen sich Werde in der URL dynamisch einsetzen. Maximal k&ouml;nnen 400 Zeichen.<br><br>Bei Verwendung von {pass} wird das Passwort mit MD5 umgewandelt. Maximal k&ouml;nnen f&uuml;r {pass} 100 Zeichen verwendet werden.</p>\n";
  s += "<input type=\"text\" name=\"url\" value=\""+ String(user_network.url) +"\" maxlength=\"400\" required></input><br />";

  s += "<h3>POST</h3>\n";
  s += "<input type=\"text\" name=\"urlpost\" value=\""+ String(user_network.urlpost) +"\" maxlength=\"400\" required></input><br />";

  s += "<h3>{pass}</h3>\n";
  s += "<input type=\"text\" name=\"urlpass\" value=\"\" maxlength=\"100\"></input><br />";
  
  s += "<br /><input type=\"submit\" value=\"save\"><br />";
  s += "</form></body><html>";

  return s;
}

String md5(String str) {
  _md5.begin();
  _md5.add(String(str));
  _md5.calculate();
  return _md5.toString();
}


String ipToString(IPAddress ip){
  String s="";
  for (int i=0; i<4; i++)
    s += i  ? "." + String(ip[i]) : String(ip[i]);
  return s;
}

void parseURL(String urlString, URL* url) {
  // Assume a valid URL

  enum URLParseState {PROTOCOL, SEPERATOR, HOST, PORT, PATH} state = PROTOCOL;

  url->protocol = "";
  url->host = "";
  url->port = "";
  url->path = "/";


  for (int i = 0; i < urlString.length(); i++) {
    switch(state)
    {
      case PROTOCOL: if (urlString[i] == ':') state = SEPERATOR;
                     else url->protocol += urlString[i];
                     break;
      case SEPERATOR: if (urlString[i] != '/') {
                          state = HOST;
                          url->host += urlString[i];
                      }
                      break;
      case HOST: if (urlString[i] == ':') state = PORT;
                 else {
                   if (urlString[i] == '/') state = PATH;
                   else url->host += urlString[i];
                   }
                 break;
      case PORT: if (urlString[i] == '/') state = PATH;
                 else  url->port += urlString[i];
                 break;
      case PATH: url->path += urlString[i];

    }
  }
}

Zusammengebaut

Resümee

Das Projekt der NFC Stempeluhr ESP8266 DIY hat sich in den letzten zwei Jahren erfolgreich im Feldeinsatz bewährt. Die Kombination aus NFC-Lesemodul und ESP8266 Mikrocontroller ermöglicht eine einfache und effiziente Erfassung von Arbeitszeiten.

Es besteht jedoch immer die Möglichkeit, das Projekt weiter zu verbessern. Durch den Einsatz einer externen Antenne kann die Verbindungsqualität zum Wlan verbessert werden.

Eine HTTPS-Verschlüsselung bei der Übertragung der Daten an den Server kann die Sicherheit erhöhen und die Daten vor unerlaubtem Zugriff schützen.

Die Speicherung der Stempelzeiten bei Netzwerkausfall kann ebenfalls nützliche sein.

Insgesamt hat die NFC Stempeluhr ESP8266 DIY in den letzten Jahren ihre Nützlichkeit und Effektivität bewiesen und bietet weiterhin Raum für Verbesserungen und Anpassungen.