Ein Webserver auf dem ESP8266 öffnet viele Türen zu ansprechener Visualisierung - sei es zum Steuern von Geräten oder der Anzeige von Sensordaten - ein Webserver mit einem passendem
Frontend verleiht jedem Projekt einen gewissen Mehrwert. Auch wenn ich schon mehrere Tutorials über Webserver auf dem ESP8266 geschrieben habe, gibt es noch viel mehr zu Entdecken - zum Beispiel Websockets.
Produktempfehlungen und -suche in Verbindung mit dem Amazon Partnerprogramm:
¹ Angaben ohne Gewähr. Bei einem Kauf über den Link erhalten wir eine Provision.
Websockets sind im Gegensatz zu HTTP bidirektional. Das heißt Client und Server können in nahezu Echtzeit miteinander kommunizieren. Nachdem die Websockets Verbindung einmal hergestellt wurde, kann der Client Daten den den Server senden ohne die Seite neu zu laden. Was aber noch viel besser ist: Der Server kann den Client ebenfalls Daten schicken. Besonders praktisch ist das, wenn man Sensoren oder andere Daten auswerten will, ohne dass wir eine neue Anfrage an den Server stellen. Denn mit Websockets kann der Server den neuen Status direkt an uns übermitteln. Und genau darum handelt dieses Tutorial.
Wir werden eine ganz einfache Websocket Implementierung vornehmen, um einen Sensor in Echtzeit zu aktualisieren. Ohne weitere Anfragen und ohne Neuladen der Seite.
Es wäre von Vorteil, wenn ihr euch vorher mein Tutorial ESP8266 Webserver mit echten HTML-Dateien durchlest. Dort erkläre ich, wie man einen Webserver auf dem ESP erstellt, der echte Dateien ausliefern kann. Das Grundgerüst für diess Tutorial. Denn hier werden wir uns nur noch um die Server- und Clientseitige Implementierung der Websockets kümmern, und so eine Echtzeitkommunikation ermöglichen.
Zum Kapitel springen HTTP vs. Websockets
Bei der Kommunikation über HTTP muss vor jeder Antwort des Servers eine Anfrage durch den Client erfolgen. Es wird jedes mal eine neue Verbindung aufgebaut, die Daten werden gesendet, danach wird die Verbindung wieder geschlossen. Bei Websockets ist das anders. Dort wird zwar initial auch eine Verbindung hergestellt, diese bleibt jedoch bestehen. Das macht die gesamte Kommunkation erstens schlanker, aber was noch besser ist, der Server kann auch ohne vorherige Anfrage Daten an den Client senden. Besonders häufig kommen Websocket in Verbindung mit Chat- oder Benachrichtigungsdiensten einher. Diese sind meist darauf ausgelegt, Daten in Echtzeit zu publishen um den User auf neue Nachrichten bzw. Benachrichtungen aufmerksam zu machen. Genauso kann man sich das bei einem ESP8266 vorstellen.
Zum Kapitel springen HTML-Template
Wer bereits das Tutorial ESP8266 Webserver mit echten HTML-Dateien gelesen hat, wird sich hier sehr gut und schnell zurechtfinden. Denn die Implementierung der Websockets Client- sowie Serverseitig sind eigentlich ein Kinderspiel. Natürlich habe ich auch hier wieder ein Template vorbereitet. Im Gegensatz zum Webserver mit echten HTML Dateien wurder der Inhalt der HTML-Datei hier wieder auf das Bare-Minimum gekürzt. Sodass es hier hoffentlich wieder sehr einfach ist, die Bestandteile zu verstehen. Das Script im Javascript ist auch nur rein auf die Websockets-Kommunikation beschränkt.
Download: webserver-websockets-template.zip
Im Ordner data
ist das komplette Frontend der Webseite verankert. Damit der Webserver dieses Frontend auch ordnungsgemäß ausliefern kann, muss dieses zunächst auf den ESP8266 hochgeladen werden. Und zwar direkt in dessen Dateisystem.
Eine genaue Anleitung wie das funktioniert, könnt ihr in meinem Tutorial Dateien auf dem ESP8266 speichern und lesen: Das LittleFS Dateisystem nachlesen. Besser gesagt braucht ihr für den Upload von Dateien in das Dateisystem des ESPs nur das Kapitel Dateiupload vom PC auf den ESP8266.
Die Anzeige von ON / OFF ist auf den Pin D8
beschränkt. Wenn man diesen mit 3V3
verbindet, sollte sich die Anzeige auf der Webseite in nahezu Echtzeit aktualisieren. Hintergrund dafür ist, dass der Server bei jeder Änderung des Pins ein Websocket-Event an die Clients sendet. So kann jeder verbundene Client direkt die Änderung sehen. Im Arduino Programm ist das dieser Abschnitt. Er befindet sich im loop()
des Programms:
// Start Logik; Event senden an alle verbundenen Clients
static bool state = false;
if ( state != digitalRead(15) ){
state = digitalRead(15);
notifyClients( String(state) );
}
// Ende Logik; Event senden an alle verbundenen Clients
Auf der Client-Seite also im HTML / Javascript Code, wird diese Nachricht vom Server folgendermaßen aufgewertet:
Produktempfehlungen und -suche in Verbindung mit dem Amazon Partnerprogramm:
¹ Angaben ohne Gewähr. Bei einem Kauf über den Link erhalten wir eine Provision.
socket.onmessage = function (event) {
console.log('Nachricht vom Server erhalten:', event.data);
if ( event.data == "1" ) {
document.getElementById("state").innerText = "ON";
}
if ( event.data == "0" ) {
document.getElementById("state").innerText = "OFF";
}
};
Zum Kapitel springen Programmcode
Auch wenn ich bereits gesagt habe, dass der Programmcode ein Kinderspiel ist, ist dieser logischerweise ein bisschen umfangreicher. Aber wenn man sich mal in Ruhe mit den einzelnen Bestandteilen auseinandersetzt sollte es kein Problem sein, eigene Projeke umszusetzen.
Nach dem Programmcode kommen ein paar Erklärungen zu den einzelnen Code-Teilen.
/* ############################################################################################## */
/* Bibliotheken einbinden */
/* ############################################################################################## */
// WLAN und Multicast-DNS
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
// Webserver
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
// Filesystem
#include <LittleFS.h>
/* ############################################################################################## */
/* Instanzen erstellen */
/* ############################################################################################## */
// Webserver Standard HTTP-Port 80
AsyncWebServer server(80);
// Websocket-Server
AsyncWebSocket ws("/ws");
/* ############################################################################################## */
/* Globale Konstanten */
/* ############################################################################################## */
// WLAN-Zugangsdaten
const char *ssid = "SSID";
const char *password = "PASSWORD";
// Hostname -> http://makesmartesp.local/
const char *espHostname = "makesmartesp";
/* ############################################################################################## */
/* Vordeklarieren der Funktionen */
/* ############################################################################################## */
// Allgemeine Server und Datei-Routen
void setupServerRoutes();
void setupFilePaths();
// Websockets
void setupWebsockets();
void onWebsocketsEvent( AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len );
void handleWebSocketMessage( AsyncWebSocketClient *client, void *arg, uint8_t *data, size_t len );
void notifyClients( String msg );
// Was passiert, wenn eine Seite nicht gefunden werden kann
// Es wird die Datei `404.html` ausgeliefert
void notFound(AsyncWebServerRequest *request) {
AsyncWebServerResponse* response =
request -> beginResponse(LittleFS, "/404.html", "text/html");
response -> setCode(404);
request -> send(response);
}
void setup(){
delay(500);
Serial.begin(115200);
Serial.println();
delay(500);
// Dateisystem initialisieren
if (LittleFS.begin()){
Serial.println("Dateisystem: Initialisiert");
}else{
Serial.println("Dateisystem: Fehler beim Initialisieren");
}
WiFi.mode( WIFI_STA );
WiFi.begin( ssid, password );
if ( WiFi.waitForConnectResult() != WL_CONNECTED){
Serial.println("WLAN Verbindung fehlgeschlagen");
return;
}
Serial.print( "Verbunden! IP-Adresse: " );
Serial.println( WiFi.localIP() );
// Starten des mDNS-Servers
if (!MDNS.begin(espHostname)){
Serial.println("Fehler beim Staren des mDNS-Servers!");
}
// Aufsetzen der Routen und des Websocket-Servers
// Datei-Routen
setupFilePaths();
// Server-Routen
setupServerRoutes();
// Websockets
setupWebsockets();
server.begin();
}
void loop(){
MDNS.update();
ws.cleanupClients();
// Start Logik; Event senden an alle verbundenen Clients
static bool state = false;
if ( state != digitalRead(15) ){
state = digitalRead(15);
notifyClients( String(state) );
}
// Ende Logik; Event senden an alle verbundenen Clients
}
void setupServerRoutes(){
// Auf dem Pfad `/` wird die Datei `index.html` aus dem `data` Ordner ausgeliefert
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(LittleFS, "/index.html");
});
// Wenn die angeforderte Seite nicht vorhanden ist, `notFound()` aufrufen
server.onNotFound(notFound);
}
void setupFilePaths(){
// Hier werden nur die Pfade der Dateien bestimmt und welche Dateien
// aus dem Dateisystem ausgeliefert werden sollen
server.on("/assets/css/style.css", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(LittleFS, "/assets/css/style.css", "text/css");
});
server.on("/assets/js/script.js", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(LittleFS, "/assets/js/script.js", "text/javascript");
});
// Gleiches mit einem PNG-Bild
server.on("/assets/images/favicon-96.png", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(LittleFS, "/assets/images/favicon-96.png", "image/png");
});
}
void setupWebsockets(){
ws.onEvent( onWebsocketsEvent );
server.addHandler( &ws );
}
void onWebsocketsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len){
if ( type == WS_EVT_CONNECT ){
// Eine neue Websockets Verbindung wurde hergestellt
// Serial.println("Client Verbinung wurde hergestellt!");
// Der Server sendet eine Nachricht an diesen einen neuen Client
client->text("user:welcome");
}
if ( type == WS_EVT_DISCONNECT ){
// Eine Websockets verbindung wurde getrennt
// Serial.println("Client Verbindung wurde beendet!");
}
if ( type == WS_EVT_DATA ){
// Eine Websocket-Nachricht wurde empfangen
// Serial.println("Websocket-Nachricht empfangen");
handleWebSocketMessage( client, arg, data, len );
}
}
// Die zwei wichtigsten Funktionen um auf eine Nachricht zu antworten:
// notifyClients( "Diese Nachricht geht an alle Clients!" );
// client->text( "Diese Nachricht geht nur an den Empfänger zurück!" );
void notifyClients( String msg ){ ws.textAll(msg); }
void handleWebSocketMessage(AsyncWebSocketClient *client, void *arg, uint8_t *data, size_t len){
Serial.println("handleWebSocketMessage();");
AwsFrameInfo *info = ( AwsFrameInfo * )arg;
if ( info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT ){
// Enthält den Inhalt der Nachricht vom Client
String message = String( (char *) data );
if ( message.indexOf("connection:new") >= 0 ){
// Serial.println("Ein neuer Client hat sich verbunden");
notifyClients("user:new");
}
}
}
Zum Kapitel springen Erklärungen im Code
Zum Kapitel springen Allgemeine Events
Es gibt eine Reihe an allgemeinen "vorgefertigten" Events. Diese befinden sich im Server-Code unter onWebsocketsEvent()
.
void onWebsocketsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len){
if ( type == WS_EVT_CONNECT ){
// Eine neue Websockets Verbindung wurde hergestellt
// Serial.println("Client Verbinung wurde hergestellt!");
// Der Server sendet eine Nachricht an diesen einen neuen Client
client->text("user:welcome");
}
if ( type == WS_EVT_DISCONNECT ){
// Eine Websockets verbindung wurde getrennt
// Serial.println("Client Verbindung wurde beendet!");
}
if ( type == WS_EVT_DATA ){
// Eine Websocket-Nachricht wurde empfangen
// Serial.println("Websocket-Nachricht empfangen");
// Die erhaltenen Daten werden in der Funktion handleWebSocketMessage() verarbeitet
handleWebSocketMessage( client, arg, data, len );
}
}
Zum Kapitel springen Servernachricht an Client
Der folgende Code-Abschnitt auf dem Server, also dem ESP8266, sendet eine Benachrichtung an alle Clients:
if ( state != digitalRead(15) ){
state = digitalRead(15);
notifyClients( String(state) );
}
Genauer gesagt, die Funktion notifyClients()
. Diese sieht wie folgt aus:
void notifyClients( String msg ){ ws.textAll(msg); }
// So sieht sie aus, wenn man sie nicht in eine Zeile schreibt:
void notifyClients( String msg ){
ws.textAll(msg);
}
Wir machen mit ws.textAll("Meine Nachricht")
also eine Benachrichtung an alle Clients. Im Beispiel mit dem Taster ist die Nachricht der Status. Also 0
oder 1
. Und diese Nachricht wird dann auf der Client-Seite ausgewertet.
socket.onmessage = function (event) {
console.log('Nachricht vom Server erhalten:', event.data);
if ( event.data == "1" ) {
document.getElementById("state").innerText = "ON";
}
if ( event.data == "0" ) {
document.getElementById("state").innerText = "OFF";
}
};
Die Funktion socket.onmessage
wird immer ausgeführt, wenn eine neue Nachricht vom Server erhalten wurde. event.data
enthält dabei dann immer die Nachricht, welche mithilfe von ws.textAll("Meine Nachricht")
gesendet wurde. In diesem Fall würde event.data
= "Meine Nachricht"
enthalten. Im Beispiel des Tasters ist dies Logischerweise direkt der Status - 0 oder 1. Aus diesem Grund wird die Nachricht auch demensprechend ausgewertet.
Zum Kapitel springen Clientnachricht an den Server
Wenn der Client eine Nachricht an den Server sendet, würde das im Javascript-Code so aussehen:
socket.send("connection:new");
Der Server wertet die Nachricht dann entsprechend in der Funkion handleWebSocketMessage()
aus:
void handleWebSocketMessage(AsyncWebSocketClient *client, void *arg, uint8_t *data, size_t len){
Serial.println("handleWebSocketMessage();");
AwsFrameInfo *info = ( AwsFrameInfo * )arg;
if ( info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT ){
// Enthält den Inhalt der Nachricht vom Client
String message = String( (char *) data );
if ( message.indexOf("connection:new") >= 0 ){
// Serial.println("Ein neuer Client hat sich verbunden");
// Alle Clients-benachrichten
notifyClients("user:new");
// Nur den Client-Benachrichtigen von dem das Event kam
// client->text("user:welcome");
}
}
}