ESP8266 Websocket Server für Echtzeitkommunikation

von cooper.bin
veröffentlicht am 05.05.2024 aktualisiert am 05.05.2024

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.

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.

ESP8266 Websocket Server Beispiel Startseite ><

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:

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");
            
        }

    }

}

Die Themen server und javascript gibt es auch auf dem makesmart Discord-Server! Trete bei, um dich auszutauschen.

mehr erfahren

Teile diesen Beitrag



Diese Artikel könnten dich auch interessieren

ESP8266 Webserver mit echten HTML Dateien

Mit dem ESPAsyncWebserver kann man einen dateibasierten Webserver auf dem ESP8266 realisieren. Dieser kann HTML, CSS, Javascript und jede andere Art von Datei ausliefern.

Titelbild ersetzen Bearbeiten

cooper.bin am 28.04.2024

ESP8266 - Ein einfacher Webserver mit mDNS

Während Webserver wie Apache2 oder NGNIX auf Rechnern laufen, kann man auch auf einem ESP8266 einen Webserver verwenden. In diesem Tutorial werden wir ein Grundgerüst implementieren.

Titelbild ersetzen Bearbeiten

cooper.bin am 13.02.2024

ESP8266 D1 Mini WLAN-Relais mit HTTP API

Mit diesem einfachem Webserver auf dem ESP8266 mit HTTP API kannst du ein Relais smart machen! Steuere das Relais über deinen Webbrowser oder automatisiert durch andere Dienste.

Titelbild ersetzen Bearbeiten

cooper.bin am 20.02.2024

ESP8266 Webserver: GET-Variablen auslesen

GET-Parameter sind Variablen, die über die URL übergeben werden. Diese Variablen können auf einem ESP8266 ausgelesen werden, um bestimmte Aktionen in der Software auszulösen.

Bearbeiten

cooper.bin am 17.02.2024

Der DS18B20 Temperatursensor am ESP8266 D1 Mini - Einfache Temperaturmessung

Lerne, wie du mit der Arduino IDE, dem ESP8266 und einem DS18B20 Temperatursensor einfache und schnelle Temperaturmessungen durchführen kannst.

Titelbild ersetzen Bearbeiten

cooper.bin am 05.03.2024