Time tracking with Wemos D1 and RFID

FunduinoLast updated on

An- und Abmeldung per RFID-Tag, Anzeige im Browser

Karte vor das Lesemodul halten, fertig – der Wemos D1 merkt sich, wer sich wann an- oder abgemeldet hat, und zeigt die Übersicht als Webseite im lokalen Netz an.

Das Projekt bringt drei Themen zusammen: RFID zum Erkennen der Tags, die Bibliothek time.h für die Uhrzeit aus dem Internet und einen kleinen Webserver, der die HTML-Seite selbst erzeugt.

Der Sketch ist in vier Abschnitte geteilt, die nacheinander erklärt werden. Wer will, erweitert die Liste der erfassten Personen anschließend selbst.

Browserseite mit Anmeldestatus dreier Mitarbeiter und Aktualisieren-Button
So sieht die fertige Seite im Browser aus

Was das Projekt macht

Jeder RFID-Tag – ob Karte oder Schlüsselanhänger – trägt einen eigenen Code. Das Projekt ordnet diese Codes Namen zu und hält fest, wann sich jemand an- oder abgemeldet hat.

Die aktuelle Uhrzeit holt sich der Wemos D1 über NTP aus dem Internet, die Übersicht liefert er als Webseite im lokalen Netz aus. Sie aktualisiert sich alle 60 Sekunden selbst; zusätzlich gibt es einen Knopf zum sofortigen Neuladen.

Browserseite mit Anmeldestatus dreier Mitarbeiter und Aktualisieren-Button
So sieht die fertige Seite im Browser aus

Das Board installieren

Trage unter Datei → Einstellungen eine zusätzliche Boardverwalter-URL ein:

https://arduino.esp8266.com/stable/package_esp8266com_index.json

Installiere dann über den Boardverwalter das Paket esp8266. Wird das Board nicht automatisch erkannt, wählst du es über „Wähle ein anderes Board und einen anderen Port“ manuell aus.

Eingabefeld für zusätzliche Boardverwalter-URLs in den Einstellungen
Datei → Einstellungen → Boardverwalter-URL
ESP8266-Paket im Boardverwalter
Das Paket esp8266 installieren
1 / 2

Das RFID-Modul anschließen

Das RFID-Kit besteht aus dem Lesemodul und den Tags. Hältst du einen Tag davor, gibt das Modul dessen Code aus.

Im Sketch sind SDA an D8 und RST an D3 festgelegt; die übrigen Leitungen laufen über SPI. Die genaue Belegung zeigen Schaltplan und Anschlusstabelle.

Installiere anschließend die Bibliothek MFRC522.

Achte auf die Frequenz der Tags: Der MFRC522 arbeitet mit 13,56 MHz. Die in der Bauteilliste verlinkte Karte im Scheckkartenformat ist ein 125-kHz-Tag und lässt sich mit diesem Lesemodul nicht lesen.

RFID-Lesemodul mit Karte im Scheckkartenformat und Schlüsselanhänger
Lesemodul und die beiden Tag-Bauformen
Pinbelegung des RFID-Moduls
Die Pins des RFID-Moduls
Verkabelung von RFID-Modul und Wemos D1 Mini
Der Schaltplan
Tabelle der Anschlüsse zwischen RFID-Modul und Wemos D1
Anschlüsse des RFID-Moduls
Bibliothek MFRC522 in der Bibliotheksverwaltung
Die Bibliothek MFRC522
1 / 5

Die Codes der Tags auslesen

Bevor du Namen zuordnen kannst, musst du wissen, welcher Tag welchen Code hat. Dieses kleine Programm gibt den Code jedes aufgelegten Tags als Folge von Dezimalzahlen im Seriellen Monitor aus.

Notiere dir die Werte – du brauchst sie gleich im Hauptprogramm.

Dezimalwert einer RFID-Karte im Seriellen Monitor
Der ausgelesene Code eines Tags
Den Code eines Tags auslesen
#include <MFRC522.h>

// Anschlüsse RFID definieren
#define SDA D8
#define RST D3

// RFID-Empfänger benennen
MFRC522 mfrc522(SDA, RST);

// vom RFID ermttelter Wert der Karte als Folge von Dezimalzahlen
String WertDEZ;

void setup() 
{
  Serial.begin(9600);
  delay(500);
  SPI.begin();

  // Initialisierung des RFID-Empfängers
  mfrc522.PCD_Init();
}

void loop() 
{
  // Wert der Karte zurücksetzen
  WertDEZ = "";

  // wenn eine Karte aufgelegt wird startet die Abfrage
  if (mfrc522.PICC_IsNewCardPresent()) 
  {
    mfrc522.PICC_ReadCardSerial();

    // Dezimalwerte der Karte in String schreiben
    for (byte i = 0; i < mfrc522.uid.size; i++) 
    {
      WertDEZ = WertDEZ + String(mfrc522.uid.uidByte[i], DEC) + " ";
    }

    // Wartezeit anpassen, wenn mehrere Werte gelesen werden
    delay(1000);

    // dezimalen Wert im Seriellen Monitor anzeigen
    Serial.println("dezimaler Wert: " + WertDEZ);
  }
}

Variablen, Zeitzone und Zeitserver

Im ersten Programmabschnitt legst du fest, woher die Zeit kommt und wer erfasst wird.

Als Zeitquelle dient ein Pool von NTP-Servern – fällt einer aus, springt automatisch der nächste ein. Die Zeitzonenangabe regelt die Umstellung zwischen Sommer- und Winterzeit selbsttätig.

Trage SSID und Passwort deines Routers ein. Willst du weitere Personen erfassen, ergänzt du sie im Array MitarbeiterInnen – und musst die Größe der Arrays AnmeldeStatus und AnmeldeZeit entsprechend anpassen.

SSID und Passwort stehen im Klartext im Sketch. Teile ihn also nicht unverändert weiter.

Globale Variablen und Bibliotheken
// Bibiotheken hinzufügen
#include <ESP8266WebServer.h>
#include <time.h>
#include <MFRC522.h>

// Anschlüsse RFID definieren
#define SDA D8
#define RST D3

// RFID-Empfänger benennen
MFRC522 mfrc522(SDA, RST);

char Router[] = "Router_SSID";
char Passwort[] = "xxxxxxxx";

// vom RFID ermttelter Wert der Karte als Folge von Dezimalzahlen
String WertDEZ;

/*
  NTP-Server aus Pool 
  # define Zeitserver "de.pool.ntp.org"
  https://www.pool.ntp.org/zone/de
  oder z.B. Zeitserver der Physikalisch-technische Bundesanstalt
  # define Zeitserver "ptbtime1.ptb.de"
  wenn eine statische IP-Adresse verwendet kann der Eintrag auch direkt erfolgen
  NTP-Server als IP-Adresse angeben
  192.53.103.108 -> Physikalisch-technische Bundesanstalt
*/
#define Zeitserver "de.pool.ntp.org"

/*
  Liste der Zeitzonen
  https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
  Zeitzone CET = Central European Time -1 -> 1 Stunde zurück
  CEST = Central European Summer Time von
  M3 = März, 5.0 = Sonntag 5. Woche, 02 = 2 Uhr
  bis M10 = Oktober, 5.0 = Sonntag 5. Woche 03 = 3 Uhr
*/
#define Zeitzone "CET-1CEST,M3.5.0/02,M10.5.0/03"

// time_t enthält die Anzahl der Sekunden seit dem 1.1.1970 0 Uhr
time_t aktuelleZeit;

/* 
  Struktur tm wandelt die ermittelte Zeit in ein "lesbares" Format um:
  tm_hour -> Stunde: 0 bis 23
  tm_min -> Minuten: 0 bis 59
  tm_sec -> Sekunden 0 bis 59
  tm_mday -> Tag 1 bis 31
  tm_mon -> Monat: 0 (Januar) bis 11 (Dezember)
  tm_year -> Jahre seit 1900
  tm_yday -> vergangene Tage seit 1. Januar des Jahres
  tm_isdst -> Wert > 0 = Sommerzeit (dst = daylight saving time)
*/
tm Zeit;

WiFiServer Server(80);
WiFiClient Client;

// Namen der Mitarbeiter*innen
String MitarbeiterInnen[3] = { "Klaus Drolshagen", "Eva Hilger", "Claudia Kruse" };

// Anmeldestatus: false = abgemeldet
bool AnmeldeStatus[3] = { false, false, false };

// Zeit der Anmeldung/Abmeldung
String AnmeldeZeit[3];

Der setup-Teil

Das setup initialisiert das RFID-Modul, setzt Zeitzone und Zeitserver, baut die WLAN-Verbindung auf und startet den Webserver.

Sobald die Verbindung steht, zeigt der Serielle Monitor den Namen des Routers und die IP-Adresse des Wemos D1. Diese Adresse tippst du in die Adresszeile deines Browsers.

IP-Adresse des Wemos D1 im Seriellen Monitor
Die IP-Adresse des Wemos D1
Eingabe der IP-Adresse in die Adresszeile des Browsers
IP-Adresse im Browser eingeben
1 / 2
Der setup-Teil
void setup() 
{
  Serial.begin(9600);
  delay(500);
  SPI.begin();

  // Initialisierung des RFID-Empfängers
  mfrc522.PCD_Init();

  // Parameter für die zu ermittelnde Zeit
  configTime(Zeitzone, Zeitserver);

  WiFi.begin(Router, Passwort);

  // Verbindung herstellen
  while (WiFi.status() != WL_CONNECTED) 
  {
    delay(200);
    Serial.print(".");
  }

  Server.begin();

  // SSID des Routers anzeigen
  Serial.println();
  Serial.print("Verbunden mit ");
  Serial.println(WiFi.SSID());

  // IP des D! anzeigen
  Serial.println(WiFi.localIP());
}

Der loop-Teil mit dem Webserver

Der loop liest zuerst die aktuelle Zeit und dann den aufgelegten Tag. Stimmt dessen Code mit einem der hinterlegten überein, kippt der Anmeldestatus der zugehörigen Person um und die aktuelle Uhrzeit wird festgehalten.

Danach baut das Programm die HTML-Seite Zeile für Zeile auf und schickt sie an den Browser: Datum mit Wochentag, Uhrzeit, die Liste aller Personen mit Status und Zeitstempel sowie der Aktualisieren-Knopf.

Die drei Beispielcodes im Sketch musst du durch die Werte deiner eigenen Tags ersetzen.

Drei Fehler im loop sind zu beachten. Erstens überschreibt die Minutenergänzung die bereits zusammengesetzte Stunde, statt sie zu ergänzen – bei Minuten unter zehn steht deshalb nur die Minute da. Zweitens prüft die Monatsergänzung den nullbasierten Monat, gibt aber den einsbasierten aus, was im Oktober zu „010“ führt. Drittens sendet der Aktualisieren-Knopf die Zeichenfolge WiFi.localIP() statt der IP-Adresse und funktioniert deshalb nicht.

Beschriftete RFID-Karten mit Portraitbildern der Mitarbeiter
Beispiel für die Beschriftung der Karten
Der loop-Teil mit dem Webserver
void loop() 
{
  // aktuelle Zeit lesen
  time(&aktuelleZeit);

  // localtime_r -> ermittelte Zeit in die lokale Zeitzone setzen
  localtime_r(&aktuelleZeit, &Zeit);
  
  // Wert der Karte zurücksetzen
  WertDEZ = "";

  // wenn eine Karte aufgelegt wird startet die Abfrage
  if (mfrc522.PICC_IsNewCardPresent()) 
  {
    mfrc522.PICC_ReadCardSerial();

    // Dezimalwerte der Karte in String schreiben
    for (byte i = 0; i < mfrc522.uid.size; i++) 
    {
      WertDEZ = WertDEZ + String(mfrc522.uid.uidByte[i], DEC) + " ";
    }

    // Wartezeit anpassen, wenn mehrere Werte gelesen werden
    delay(1000);

    // dezimalen Wert im Seriellen Monitor anzeigen
    Serial.println("dezimaler Wert: " + WertDEZ);
  }

  // Leerzeichen am Ende entfernen
  WertDEZ.trim();

  // Karten abfragen und Namen der Mitarbeiter*innen und Zeit zuordnen
  if (WertDEZ.compareTo("195 106 18 23") == 0) 
  {
    AnmeldeStatus[0] = !AnmeldeStatus[0];

    // AnmeldeZeit zurücksetzen
    AnmeldeZeit[0] = "";

    // wenn Stunde < 10 -> 0 voranstellen
    if (Zeit.tm_hour < 10) AnmeldeZeit[0] = "0";
    AnmeldeZeit[0] = AnmeldeZeit[0] + Zeit.tm_hour + ":";

    // wenn Minute < 10 -> 0 voranstellen
    if (Zeit.tm_min < 10) AnmeldeZeit[0] = "0";
    AnmeldeZeit[0] = AnmeldeZeit[0] + Zeit.tm_min;
  }

  if (WertDEZ.compareTo("227 77 233 22") == 0) 
  {
    AnmeldeStatus[1] = !AnmeldeStatus[1];
    AnmeldeZeit[1] = "";
    if (Zeit.tm_hour < 10) AnmeldeZeit[1] = "0";
    AnmeldeZeit[1] = AnmeldeZeit[1] + Zeit.tm_hour + ":";
    if (Zeit.tm_min < 10) AnmeldeZeit[1] = "0";
    AnmeldeZeit[1] = AnmeldeZeit[1] + Zeit.tm_min;
  }

  if (WertDEZ.compareTo("131 125 21 23") == 0) 
  {
    AnmeldeStatus[2] = !AnmeldeStatus[2];
    AnmeldeZeit[2] = "";

    if (Zeit.tm_hour < 10) AnmeldeZeit[2] = "0";
    AnmeldeZeit[2] = AnmeldeZeit[2] + Zeit.tm_hour + ":";
    if (Zeit.tm_min < 10) AnmeldeZeit[2] = "0";
    AnmeldeZeit[2] = AnmeldeZeit[2] + Zeit.tm_min;
  }

  Client = Server.available();
  if (Client) 
  {
    // Seite aufbauen wenn SeiteAufbauen true ist
    boolean SeiteAufbauen = true;

    while (Client.connected()) 
    {
      if (Client.available()) 
      {
        char Zeichen = Client.read();
        if (Zeichen == '\n') 
        {
          // wenn SeiteAufbauen den Wert true hat
          if (SeiteAufbauen) 
          {
            // HTTP-Anforderung senden
            Client.println("HTTP/1.1 200 OK");
            Client.println("Content-Type: text/html");

            // Leerzeile zwingend erforderlich
            Client.println();

            /*
              HTML-Seite aufbauen
              die folgenden Anweisungen müssen mit print oder println gesendet werden
              println "verschönert" den Quelltext
              " muss mit \" maskiert werden
            */
            Client.println("<!doctype html>");
            Client.println("<html>");
            Client.println("<body>");

            // alle 60 Sekunden aktualisieren mit meta-Tag
            Client.println("<meta http-equiv=\"refresh\" content=\"60\">");
            Client.println("<h1> Anmeldestatus der Mitarbeiter*innen</h1>");
            Client.println("<hr />");

            // aktuelle Zeit anzeigen
            Client.println("<h2>");

            // tm_mday -> Wochentag anzeigen
            switch (Zeit.tm_wday) 
            {
              case 0:
                Client.print(F("Sonntag"));
                break;
              case 1:
                Client.print(F("Montag"));
                break;
              case 2:
                Client.print(F("Dienstag"));
                break;
              case 3:
                Client.print(F("Mittwoch"));
                break;
              case 4:
                Client.print(F("Donnerstag"));
                break;
              case 5:
                Client.print(F("Freitag"));
                break;
              case 6:
                Client.print(F("Samstag"));
                break;
            }
            Client.print(", ");

            // Tag: führende 0 ergänzen
            if (Zeit.tm_mday < 10) Client.print("0");
            Client.print(Zeit.tm_mday);
            Client.print(".");

            // Monat: führende 0 ergänzen
            if (Zeit.tm_mon < 10) Client.print("0");
            Client.print(Zeit.tm_mon + 1);
            Client.print(".");

            // tm_year + 1900
            Client.print(Zeit.tm_year + 1900);

            // Uhrzeit
            Client.print(" Uhrzeit: ");

            // Stunden
            if (Zeit.tm_hour < 10) Client.print("0");
            Client.print(Zeit.tm_hour);
            Client.print(":");

            // Minuten
            if (Zeit.tm_min < 10) Client.print("0");
            Client.print(Zeit.tm_min);
            Client.print(":");

            // Sekunden
            if (Zeit.tm_sec < 10) Client.print("0");
            Client.print(Zeit.tm_sec);

            Client.println("</h2>");
            Client.println("<hr />");

            // Name und Anmeldestatus der Mitarbeiter*innen anzeigen
            for (int i = 0; i < sizeof(MitarbeiterInnen) / sizeof(MitarbeiterInnen[0]); i++) 
            {
              Client.println(MitarbeiterInnen[i]);
              if (AnmeldeStatus[i] == true) Client.println(": angemeldet seit " + AnmeldeZeit[i] + " Uhr");
              else Client.println(": abgemeldet seit " + AnmeldeZeit[i] + " Uhr");
              Client.println("<br>");
            }
            Client.println("<hr>");

            Client.println("<form>");

            // Button aktualisieren formatieren
            Client.print("<input style=\"font-size:16pt; font-weight:bold;");
            Client.print("background-color:#55A96B;");
            Client.print("display:block; cursor:pointer;\"type=\"button\"");
            Client.println(" onClick=\"location.href='WiFi.localIP()'\" value=\"aktualisieren\">");
            Client.println("</form>");
            Client.println("<hr />");

            // IPs anzeigen
            Client.print(F("<b>Eigene IP: "));
            Client.print(Client.remoteIP());
            Client.print(F("</b>"));
            Client.print(F("<br><b>IP Klient (D1): "));
            Client.print(WiFi.localIP());
            Client.print(F("</b>"));
            Client.println("</b>");

            Client.println("</body>");
            Client.print("</html>");

            // HTTP-Antwort endet mit neuer Zeile
            Client.println();

            // Seite vollständig geladen -> loop verlassen
            break;
          }

          // wenn new line (\n) gesendet wurde -> Seite aufbauen
          if (Zeichen == '\n') SeiteAufbauen = true;

          else if (Zeichen != '\r') SeiteAufbauen = false;
          
          delay(1);
          Client.stop();
        }
      }
    }
  }
}
Rate the instructions

Comments

Ask a question or share an experience

Contact us if something doesn’t work. Please include your board and the error message.

We review every comment before publication. Your email address remains with us.