DELOARTS

http requests

Um was geht's? Ich will eine Anmeldestation (Stempeluhr) bauen, um Arbeitszeiten festzuhalten. Die Station selbst, der Time Client, fungiert dabei nur als solches - als Client. Der User soll lediglich seinen RFID-Chip an das Gehäuse halten und dann einen Taster drücken, abhängig davon, ob er sich an- oder abmelden mag. Der Client schleust also nur Informationen weiter an den Time Server, der die Zeit in einer Datenbank speichert.

Ein paar Worte zur Übersicht. Das ganze läuft im Netzwerk über WLAN als klassische Server-Client-Kommunikation. Mehr dazu später im Code-Bereich. Der Code soll als Case-Study für Metis dienen, soll beudeuten, dass vielleicht manches für dieses 'Projekt' überflüssig ist. Generell lässt es sich in die folgenden Punkte zusammenfassen:

  • Der Time Client ist ein NodeMCU-Controller (auf Basis des ESP8266-12e), mit einem RFID-Modul, sowie einem LCD-Modul. Dazu kommen noch zwei Taster mit LEDs. Alles verpackt in einem 3D gedruckten Gehäuse, gedacht zur Wandmontage.
  • Der Time Server ist ein einfaches Python-Script, wobei ich Flask als Framework nutze. Die Daten für die Zeiterfassung werden in einer Datenbank (sqlite3) gesichert.
  • Der User Access kann jedes Gerät sein, dass Websites darstellen kann. Der Benutzer soll einfach nur die Adresse des Servers ansurfen und dann seine geloggten Stunden sehen können.
  • Zusätzlich gibt es noch einen Admin-Access, wo der Administrator die Benutzer verwalten kann. Das läuft dann auch über den Web-Zugang.

Um gleich zu Beginn Sicherheitsfragen zu beantorten: Ich habe mich zwar bei der Datenbank an die Konvention der Syntax für Python gehalten, dennoch ist das Projekt als solches nicht vor Angriffen gefeit. Die sicherste Art es zu nutzen ist in einem eigenen Netzwerk ohne Internetzugriff.

Wie aufwändig ist's? Ich würde mal sagen: Ja, schon sehr. Zu Beginn war es noch recht überschaulich, aber dann ist doch immer mehr hinzugekommen. Vor allem, weil ich am Beginn selbst nicht so genau wusste, was ich jetzt alles zum Coden habe. Wahrscheinlich werde ich es nicht ganz schaffen, alles im Code zu erklären (css Styles, html Pages, etc.), aber ich gebe mein Bestes um es übersichtlich zu halten. Bei Fragen: Einfach fragen, jeder kann helfen, das Thema für Neulinge einfacher zugänglich zu machen.

Und los geht's.

1 Der elektrische Aufbau

Grundsätzlich ist nicht viel dran, da ich eigentlich nur fertige Module zusammengebaut habe. Da wäre zum einen das I2C-Display, über welches ich bereits im Nunchuk-Post geschrieben habe, und zum anderen das RFID-Modul. Alle Posts haben eines gemeinsam: Sie sind für den ATMega328 (Arduino Uno oder Nano) geschrieben, und nicht für den ESP8266-12e (NodeMCU mit Xtensa LX106 Prozessor).

Das macht aber nichts, denn man kann den ESP auch mit der Arduino-Software programmieren. Ein Tutorial für den ESP-01, wo man tatsächlich mehr machen muss, als einfach nur ein USB-Kabel anzuschließen, findet sich hier. Für dieses Projekt ist das aber obsolet, weil ich den ESP8266 auf Basis des NodeMCU nutze, ihn aber dennoch mit der Arduino IDE programmiere.

1.1 Stückliste

In letzter Zeit habe ich vermehrt Mails bekommen, wo ich gefragt werde, ob ich nicht Links zu den gekauften Komponenten auf die Website gebe. Das habe ich zu Beginn auch gemacht, doch mit der Zeit ist das zu viel Arbeit geworden, diese Links zu hosten. Deshalb: Ich kaufe meistens bei Aliexpress oder eBay. Dort sind die Preise günstig, aber die Angebote sind oft nur wenige Wochen online, weshalb Links schnell mal tot sind.

Stk. Komponente Ausführung Beschreibung
1 ESP8266 NodeMCU
1 I2C LCD HD44780 16x2 Weißer Text auf schwarzem Hintergrund
1 RFID Modul RC522 Inklusive RFID-Tags
1 Taster mit LED Rotes Licht M16x1 Gewinde
1 Taster mit LED Grünes Licht M16x1 Gewinde
2 Widerstand 10kΩ
n Kabel 0.25mm² Diverse Farben

Für den Testaufbau auf dem Steckbrett habe ich statt den beiden Tastern 2 LEDs und zwei Kurzhubtaster genutzt (weil ich zum Zeitpunkt des Artikel-Schreibens noch immer auf die Bauteile gewartet habe).

1.2 Schaltplan

Weil ich gefragt worden bin, welche Software ich für das Erstellen meiner Schaltpläne nutze: Autodesk Eagle. Für all' jene die die Eagle nur von Zeit zu Zeit nutzen: Es gibt auch eine kostenlose, nicht-kommerziell nutzbare Lizenz mit ein paar Einschränkungen. Früher habe ich Fritzing genutzt, das ist mir mittlerweile aber zu un-umfangreich geworden.

Worauf muss man achten? Die digitalen Ein- und Ausgänge sind zwar recht sauber am Board nummeriert (D1, D2, ..., Dn), aber das passt leider nicht mit der GPIO-Nummer zusammen. Im Code habe ich deshalb immer dazugeschrieben, welcher D-Channel welchem GPIO-Channel entspricht.

Jetzt braucht man nur noch zu wissen, welcher Pin-Header zu welchem Anschluss an den Modulen gehört. Das habe ich in der folgenden Tabelle zusammengefasst.

Modul PCB NodeMCU Verbindung Anmerkung
RFID SV1.1 D3 RST
SV1.2 D4 SDA/SS
SV1.3 3V Vcc
SV1.4 GND GND
SV1.5 D5 SCK
SV1.6 D6 MISO
SV1.7 D7 MOSI
LCD SV2.1 5V 5V
SV2.2 GND GND
SV2.3 D1 SCL
SV2.4 D2 SDA
Button Login SV3.1 3V COM
SV3.2 GND LED-
SV3.3 D8 NO Pull-Down Widerstand
SV3.4 D9 LED+
Button Logout SV3.5 D10 LED+
SV3.6 D0 NO Pull-Down Widerstand
SV3.7 GND LED-
SV3.8 3V COM

Wer dem Schaltplan und der Pinliste folgt, der kann sich dann aus einer Lochrasterplatine und ein paar Kabel die Platine nachlöten. Die Platine hat die Maße 50mm x 45mm bei einem Rastermaß von 2.54mm.

Achtet beim Löten auf die Ausrichtung des NodeMCU auf der Platine. Im Bild oben ist jene Seite, die auf dem Tisch aufliegt, die mit 45mm Länge.

2 Der mechanische Aufbau

Ja, hierbei handelt es sich sicherlich um eines der umfangreichsten Scripts zum nachmachen, aber es dient ja auch, wie bereits erwähnt als Basis für Metis. Weiters habe ich mich endlich dazu gerungen, mich etwas näher mit Freiformlächenkonstruktion zu beschäftigen, anstatt einfach nur einen Block zu konstruieren, weshalb das Gehäuse auch eine eher 'ausgefallene' Form hat. Hut ab vor Designern, das braucht viel Übung!

Die STL-Daten gibt's hier zum Download.

Das Gehäuse wird einfach mit der Bodenplatte an die Wand geschraubt. Der Gehäusedeckel hält dann über vier Positionierzapfen und vier Neodymmagneten am Boden. So sieht man keine Schrauben, hat aber dennoch einfachen Zugang zum Innenleben der Konstruktion.

Fertig verkabelt sieht's dann so aus, vielleicht hätte ich doch kleinere Nylon-Steckverbinder nutzen sollen, dann wäre es nicht so eng im Gehäuse.

2.1 Stückliste

Jetzt kommen wir zur Stückliste. Viel braucht man nicht, nur Zeit, weil die 3D-gedruckten Teile doch recht groß sind.

Stk. Teilenummer Anmerkung
1 Gehäusedeckel 3D Druck (PA 6)
1 Gehäuseboden 3D Druck (PA 6)
1 Trägerplatte 3D Druck (PA 6)
1 PCB Siehe Punkt 1.1
10 Zylinderkopfschraube M3x6 8.8 ISO 4762
4 Senkkopfschraube M4x8 8.8 ISO 10642
4 Neodymmagnet 10x10x4 mm

Damit ist auch schon alles zum Thema Gehäuse & co gesagt. Weiter geht's mit der Bedienung.

3 Die Bedienung

Um den Code später besser folgen zu können: Hier ein paar Worte zur Bedienung. Womit fängt man an? Mit dem Server:

3.1 Webserver & Server einrichten

Zuerst braucht man allerdings die Daten. Jetzt muss man sich entscheiden, wo man den Server laufen lassen will. Man kann dazu ohne Probleme einen Raspberry Pi, Orange Pi, irgendeine Ubuntu-Version oder Windows nutzen. Das folgende Beispiel zeige ich anhand eines RPi.

Verbindet euch via SSH und dann updaten + git holen.

sudo apt-get update
sudo apt-get install git

Um die Benutzerdaten darstellen zu können braucht man einen Webserver am Raspberry. Hierzu nutzt man den Apache-Server. Diesen hat man in Sekunden eingerichtet.

Navigiert dann ins home-Verzeichnis und erstellt dort einen neuen Ordner namens github und betretet diesen.

cd ~
sudo mkdir github
cd github

Hier klonen wir das Repo rein.

sudo git clone https://github.com/deloarts/esp-http-requests.git

Solltet ihr das Repo schon mal geklont haben, und es nur updaten wollen, dann nutzt git pull. Aber Achtung: Die womöglich bereits vorhandene Datenbank mit den geloggten Zeiten wird dabei gelöscht. Deshalb vorher sichern!

sudo git pull https://github.com/deloarts/esp-http-requests.git

Kopiert die Website ins home-Verzeichnis des Apache-Servers. Per default:

sudo cp -a ~/github/esp-http-requests/web/. /var/www/html/

Navigiert dann in das Verzeichnis des Servers und führt das Python-Script aus.

cd ~/github/esp-http-requests/server/
sudo python server.py

Der Server sollte jetzt laufen und auf Anfragen warten. Um ihm diese zu liefern bringt man den Time-Client ins Netzwerk.

3.2 Client einrichten

Dazu müssen beide Taster während dem Starten gedrückt werden. Am Display erscheint die Meldung, dass ihr euch mit dem soeben erstellten WLAN Access Point verbinden könnt. Tut dies mit einem Laptop oder einem Smartphone indem ihr euch mit der SSID und dem Passwort am ESP einloggt. Die SSID und das Passwort des Mikrocontrollers könnt ihr selbst im Code verändern, per Default lauten beiden aber: TIME CLIENT und notverysecure.

Neben der SSID des Controllers erscheint am Display auch eine IP-Adresse. Dies ist jene, die ihr ansurfen müsst, um zu den Einstellungen des Controllers zu kommen. In meinem Fall lautet diese http://192.168.0.104/.

Gebt die erforderliche Passphrase ein. Diese ist, genau so wie die SSID und das WLAN-Passwort, hard-gecoded, kann also nicht vom User geändert werden (von euch natürlich schon, weil diese Daten am Begin des Arduino-Codes stehen und verändert werden können). Default: admin und notverysecuretoo.

static char hostname[] = "Time Client";

const char apSSID[] = "TIME CLIENT";
const char apPassword[] = "notverysecure";

const char userName[] = "admin";
const char userPassword[] = "notverysecuretoo";

Gebt die Daten (SSID und Passwort) eures Heimnetzwerks ein. Die Serveradresse ist die IP-Adresse des PCs, auf dem der Server läuft. Mein Pi hat im Netzwerk die Adresse 192.168.0.101, weshalb ich auch diese eingebe. Der Port ist 5000. Wenn bei euch im Netzwerk schon was auf diesen Port hört, dann ändert ihn einfach im Python-Script server.py.

Danach startet der Controller neu und verbindet sich anschließend mit eurem WLAN. Der Controller startet übrigens auch immer um Mitternacht neu, um einen Overflow zu vermeiden.

Und jetzt? Jetzt sollte sich der Controller in euer WLAN einloggen nun sich mit dem Server verbinden können. Sollte das nicht der Fall sein, dann:

  • Tippfehler bei der SSID / PW
  • Tippfehler bei der IP / Port
  • Euer Netzwerk ist nicht mit WPA2 verschlüsselt
  • Das WLAN-Passwort ist weniger als 8 Zeichen lang
  • Ein mir unbekannter Fehler

3.3 Benutzer verwalten

Jetzt könnt ihr euren Chip ans Gehäuse halten, daraufhin wird die Meldung erscheinen, dass dieser nicht registriert ist. Dafür seht ihr aber auch die ID des Chips, die ihr euch notieren solltet!

Startet jetzt das Script admin-client.py. Das sollte selbserklärend sein, fügt den Benutzer hinzu und schon ist dieser als Datenbankeintrag am Server vorhanden.

Jetzt kommt zum ersten mal der Apache-Webserver zum Tragen. Gebt die IP-Adresse des Raspberry Pi (oder jenes Gerätes, wo der Webserver läuft) ein. Nun solltet ihr die Maske des Time Servers sehen, wo ihr die Daten des jeweiligen Benutzers einsehen könnt. Nur: Es gibt noch keine, weshalb wir neue Benutzer anlegen. Hierfür fügt der URL den Pfad /admin.html hinzu.

Fügt euren Nutzer hinzu, fertig.

4 Der Code

Wie oben schon angerissen: GitHub.

Auf GitHub finden sich 3 Verzeichnisse, die ich jetzt nacheinander durchgehe. Grundsätzlich geht der Content der drei Ordner Hand-in-Hand. Wer hier mal die Durchblick verliert, der braucht sich nur an die Übersicht von oben erinnern.

Ein paar Segmente vom Code werden sich sicherlich im Laufe der Zeit ändern, weil ich das Ding ja länger im Einsatz haben werde. Die Grundlogik bleibt aber gleich.

4.1 Arduino IDE

Wir programmieren hier zwar nicht auf einem Arduino, dafür mit der Arduino IDE. Diese müsst ihr zuerst vorbereiten, sofern das nicht bereits passiert ist. Eine Anleitung findet sich hier. Lediglich die Board-Settings müssen mit jenen aus der folgenden Liste abgeglichen werden:

  • Board: "NodeMCU 1.0 (ESP-12E Module)"
  • Flash Size: "4M (3M SPIFFS)"
  • Flash Frequency: "40MHz"
  • CPU Frequency: "80MHz"
  • Upload Speed: "115200"

Im Arduino-Verzeichnis findet ihr zusätzlich die von mir genutzten Libs, die ihr zuerst in das Arduino-Standardverzeichnis für Programmbibliotheken kopieren müsst.

Noch eine Kleinigkeit: Nachdem der Code auf den ESP geflasht wurde muss er per Hand zurückgesetzt werden. Ansonsten funktioniert der Software-Reset nicht. Das ist leider ein kleiner Bug am ESP.

4.2 Python

Der Server ist ein Python-Script. Es hört auf Anfragen von den Microcontrollern und der Website, und verarbeitet diese entsprechend. Als Framework nutze ich Flask, welches ich mir zuvor via pip auf meinem Rechner installiere. Zusätzlich braucht ihr noch flask_cors und dateutil.parser. Beides holt ihr euch wiederum mit pip.

Wie nutzt man Flask? Recht einfach, man routet einfach am Server ankommende http-requests in die entsprechende Funktion im Python-Script.

from flask import Flask, request, g

@app.route('/logout', methods=['POST'])
def routeLogout():
    payload = request.get_json()
    # Do something with the payload

Um gleich mal ein Missverständnis auszuräumen: Viele stellen sich unter einem Server einen physikalischen Rechner vor. Aber hier ist ein Server einfach nur ein Script, das für seine Clients Dienste zur Verügung stellt.

4.3 Json

Um die Daten zwischen dem Client (NodeMCU) und dem Server (Python Script auf eurem PC) auszutauschen benötigt man eine klar definierte Notation. Man kann sich entweder selber etwas ausdenken: Nutzdaten werden über Bindestriche getrennt, etc., oder man nutzt einfach json.

Json nutzt klassische key-value Paare um Daten austauschen zu können. Das ganze schaut dann so aus:

{
    "ID": 1713001,
    "Datum": "04-02-2018",
    "Titel": "http-requests"
}

Möchte man nun die Daten (= Values) haben, so braucht man lediglich im Code diese anhand des Keys herauszupicken.

4.3.1 Json am Arduino

// Get json from http-request
StaticJsonBuffer<50> buffer;
JsonObject& root = jsonBuffer.parseObject(json);

long ID = root["ID"];
String date = root["Datum"];
String title = root["Titel"];

// Format data into json
StaticJsonBuffer<50> buffer;
JsonObject& root = buffer.createObject();

root["ID"] = 5;
root["Datum"] = "04-02-2018";
root["Titel"] = "http-requests";

4.3.2 Json in Python

# Get json from a http-request
payload = request.get_json()
ID = payload['ID']
date = payload['Datum']
title = payload['Titel']

# Format data into json
payload = json.dumps({'ID': 1, 'Datum': '04-02-2018', 'Titel': 'http-requests'})

4.4 Client-Server Kommunikation

Im Ordner client findet ihr die client.ino und die zugehörigen Header-Files. Diese beinhalten die Websites, welche dann am ESP laufen (erinnert ihr euch an Kaptiel 3.2? Darin seht ihr die Maske der Website am Mikrocontroller). In der Grafik unten entspricht dieser Code dem, der auf dem Time-Client läuft.

Im Ordner server gibt es das Script server.py. Dieses entspricht dem Time Server in der Grafik.

Beide kommunizieren miteinander über http-requests, weshalb ich im Folgenden auch zwischen beiden Codes hin- und herspringe.

4.4.1 GET und POST

Ich nutze die beiden Request-Typen GET und POST. In dem Moment wo der User den RFID-Chip zum Gehäuse hält sendet der Microcontroller einen POST-Request zum Server (der Request wird an die URL /detected geschickt. Als Nutzdaten wird hier nur die Chip-Nummer gesendet. Danach schaut der Server, ob es den Nutzer gibt.

@app.route('/detected', methods=['POST'])
def routeDetected():
    payload = request.get_json()
    client = payload['client']
    rfid_id = payload['rfid_id']
    try:
        user = getUserFromRfid(rfid_id)
        surname = getSurnameFromUser(user)
        name = getNameFromUser(user)

        ...

Wenn es ihn gibt, dann kann der Nutzer über die Buttons entscheiden, ob er sich an- oder abmelden mag. Hier schickt der Microcontroller dann nochmals die Tag-Nummer des RFID-Chips, sowie den gewählten Button an die URL /login oder /logout.

@app.route('/login', methods=['POST'])
def routeLogin():
    try:
        payload = request.get_json()
        client = payload['client']
        user = getUserFromRfid(payload['rfid_id'])

        ...

Den GET-Request schickt der Controller zum Beispiel dann, wenn er sich zum ersten mal mit dem Server verbinden will. Er schickt keine Nutzdaten mit (weil GET), sondern erwartet sich nur eine Antwort vom Server. Zusätzlich aber kann der Server bei der Antwort Nutzdaten mitschicken, was er in dem Fall auch tut: Er schickt die Sekunden mit, die noch bis Mitternach übrig sind (wie bereits erwähnt: Der Microcontroller resetet sich immer um Mitternach selbst).

4.5 Web-Client-Server Kommunikation

Die Maske, die der User im Webbrowser sieht, ist auch nur ein Client. Ergo kommuniziert die Website auch mit dem Server via http-requests.

Auf GitHub befinden wir uns jetzt im Verzeichnis web.

Die Funktionalität der Website wird durch JavaScript erledigt. Hier nutze ich vue.js. Vue ist ein echt feines Framework für JavaScript. Zur Kommunikation zwischen Webbrowser und Server nutze ich die Lib axios.

Ein Beispiel: Ihr surft an die URL des Servers und seht dann die Maske, in der ihr den Nutzer auswählt, von dem ihr die Zeitdaten sehen wollt. Von wo bekommt die Website die verfügbaren User? Vom Server! Dazu schickt die Website via axios einen GET-request an den Server.

getUser() {
    const vm = this;
    axios({
        method: 'GET',
        url: this.serverUrl + 'getUser',
    }).then(function(response) {
        if (response.data.status === 'ok') {
            vm.userData = response.data.data;
        }
        else {
            alert("Something went really wrong.")
        }
    }).catch(function(error) {
        console.error(error);
        alert(error.message)
    });
},

Am Server wird der Request dann an der Funktion /getUser behandelt.

@app.route('/getUser', methods=['GET'])
    def routeGetUser():
        # Returns to the admin & web client
        try:
            dbCursor = dbTime().cursor()
            dbSelection = dbCursor.execute('SELECT * FROM ' + tableUsers + ' ORDER BY user')
            dbValue = dbSelection.fetchall()

            ...

4.5.1 Query Parameter

Hat der User dann eine Auswahl getroffen, so schickt die Website erneut einen Request an den Server. Diesmal aber schickt er diesen an die URL /getData. Doch woher weiß der der Server, von welchem Nutzer er die Daten aus der Datenbank holen soll? Hierfür gibt es die Query Parameter.

Kurz erklärt: Die Website schickt die Daten dann nicht einfach nur an die URL, sondern hängt auch noch die gewählte User-Nummer hinten dran. So weiß der Server, von wem die Daten geschickt werden sollen. Als Beispiel sieht das dann etwa so aus:

http://192.168.0.101:8080/getData?user=30

Der Servers stellt dann die Daten vom Benutzer mit der Nummer 30 bereit. Am Server müssen diese Query Parameter ausgewertet werden. Das funktioniert so:

@app.route('/getData', methods=['GET'])
def routeGetData():
    user = request.args.get('user')

    ...

5 Fin

So ... ich denke ich habe alles behandelt, was so angefallen ist. Irgendwie habe ich etwas über mein ursprünglich gestecktes Ziel hinausgeschossen, aber hey: Wenn's Spaß macht soll man nicht aufhören.

Bei Fragen einfach fragen.

Philip Delorenzo | contact@deloarts.com | wordpress | 500px | instagram | English