ESP32 WebSockets
Es geht um die regelmäßige Aktualisierung von Web-Seiteninhalten. Die frühere Methode, die Seite automatisch in kurzen Abständen neu zu laden (Polling) führt zu einer hohen Belastung des Servers, der die Seiten bei jeder Anfrage neu aufbauen und versenden muss.
Im Jahre 2011 wurde mit dem RFC 6455 das WebSocket-Protokoll offiziell eingeführt, mit dem ein asynchroner, bidirektionaler Datenaustausch zwischen dem Browser und dem Web-Server ermöglicht wird.
Mit dem RFC 6455 wurde im Jahr 2011 das WebSocket-Protokoll offiziell eingeführt. Es ermöglicht einen asynchronen, bidirektionalen Datenaustausch zwischen Browser und Web-Server. Dies befähigt beide Seiten, statt der ganzen Web-Seite einzelne Daten zu versenden und zu empfangen, ohne die ganze Seite neu aufzubereiten und zu versenden.
Typische Einsatzbereiche
Inhaltsverzeichnis
Interface IColorChangeListener
Member-Funktionen als Callback-Funktion
Das Beispiel-Projekt ESP32S3-WebSocket-Demo zum Download. Das ZIP-Archiv enthält ein VisualStudio(22)-Projekt mit der VisualMicro-Extension. Benutzer der Arduino-IDE verwenden nur die Quelldateien (.ino, .cpp, .h).
Die gestellte Aufgabe fällt in den Bereich „Dashboard” (Instrumententafel). Die Farbe einer WS2812-RGB-LED (oft als „NeoPixel” bezeichnet) soll auf mehreren Wegen eingestellt werden können. Alle Wege sollen von einer Farbänderung erfahren und die aktuelle Farbe visualisieren. Einer der Wege soll eine Website sein. Die Aufgabe wurde so gewählt, dass sie mit möglichst wenig Hardware erledigt werden kann. Im einfachsten Fall reicht ein ESP32-S3-DevKit mit integrierter RGB-LED. Für andere ESP32-Varianten ist eine externe RGB-LED notwendig. Das Beispiel lässt sich auch vereinfachen, indem eine einfache LED ein- und ausgeschaltet wird.
Der ESP32(-S3) stellt für die Farbauswahl eine Webseite bereit, die Schaltflächen für die Farben Rot, Gelb und Blau enthält. Die aktuell eingestellte Farbe soll auf der Webseite angezeigt werden. Als zweite Möglichkeit zur Farbauswahl dient die serielle Schnittstelle, über die Kommandos zur Farbeinstellung empfangen werden. Ein Farbwechsel soll ebenfalls über die serielle Schnittstelle gemeldet werden.
In diesem Beispiel sind die verschiedenen Teilaufgaben in unabhängigen Klassen gekapselt. Dadurch fällt das Verständnis für die Funktionsweise deutlich leichter. Außerdem können einzelne Komponenten einfach ausgetauscht werden, ohne den Rest des Projekts zu beeinflussen. Ein Beispiel hierfür ist der Austausch der RDB-LED durch einen anderen Typ.
Der notwendige Austausch der Daten zwischen den verschiedenen Objekten soll, wenn immer möglich, über Ereignisse erfolgen (Observer-Pattern). Dies sorgt für eine weitere Entkopplung der verschiedenen Komponenten.
Die Funktionen des Beispielprojekts in vier Hauptklassen aufgeteilt:

Die Klasse RgbLedDriver kapselt diese interne RGB-LED. Die Klasse verwendet die Adafruit NeoPixel-Bibliothek. Neben der Hauptklasse werden

Die Enumeration Color legt die möglichen Werte für die Farben fest.
// Farb-Definition für die RGB-LED
enum class Color {
Red,
Green,
Blue
};
Einige andere Objekte müssen darüber informiert werden, wenn sich die angezeigte Farbe ändert. Diese müssen dazu das Interface (abstrakte Klasse) IColorChangeListener implementieren, d.h. die Funktion onColorChanged bereitstellen, und sich beim RgbLed-Objekt über die Methode subscribeColorChangeEvent registrieren.
// Interface für Farbänderungs-Listener
class IColorChangeListener {
public:
virtual ~IColorChangeListener() {}
// Callback-Funktion (Ereignis-Handler)
virtual void onColorChanged(Color color, String colorName) = 0;
};
| Methode | Funktion | Anmerkung |
|---|---|---|
| RgbLedDriver(uint8_t pin) | Konstruktor für die Klasse. pin ist die GPIO-Nummer an die die interne RGB-LED angeschlossen ist. | Das Framework stellt dazu die Konstante PIN_RGB_LED bereit. |
| void begin() | Initialisiert die NeoPixel-Bibliothek. | Die initiale Farbe ist rot. |
| void setColor(Color color) | Setzt die Farbe der RGB-LED und löst das Ereignis für Farbänderungen aus. | |
| Color getColor() | Gibt die aktuelle Farbe der RGB-LED zurück. | |
| String getColorString() | Gibt die Bezeichnung der aktuellen Farbe der RGB-LED als String zurück. | |
| String colorToString(Color color) | Liefert die Bezeichnung der angegebenen Farbe als String zurück. | |
| void subscribeColorChangeEvent( IColorChangeListener* colorChangeListener) |
Registriert einen Callback für Farbänderungen. |
Die meisten Methoden sind recht einfach zu verstehen. setColor muss neben dem Einstellen der LED auch die registrieren Ereignis-Handler bedienen. Pointer auf die registrierten Objekte, werden im std::vector colorChangeCallbacks gespeichert. Nachdem die RDB-LED eingestellt wurde, iteriert setColor über den Vektor und ruft für jedes registrierte Objekt die Methode onColorChanged auf.
void RgbLed::setColor(Color color) {
currentColor = color;
// Setze die Farbe der RGB-LED
switch (color) {
case Color::Red:
pixels.setPixelColor(0, pixels.Color(brightness, 0, 0));
break;
case Color::Yellow:
pixels.setPixelColor(0, pixels.Color(brightness, brightness, 0));
break;
case Color::Blue:
pixels.setPixelColor(0, pixels.Color(0, 0, brightness));
break;
}
pixels.show();
// Ruft die Callback-Funktionen auf, um die Farbänderung zu melden
for ( IColorChangeListener* listener : colorChangeCallbacks) {
listener->onColorChanged(color, colorToString(color));
}
}
Über die serielle Schnittstelle werden ein Zeichen lange Kommandos empfangen, die zur Farbsteuerung der LED verwendet werden. Definiert sind die Codes 'r', 'g' und 'b' für die entsprechenden Farben.
Dem Konstruktor für die Klasse wird eine Referenz auf die Instanz der Klasse RgbLed übergeben, die angesteuert werden soll. Die Funktion handleSerialInput prüft, ob Kommandos gesendet wurden und wertet si aus. Dazu muss sie regelmäßig (in loop) aufgerufen werden.
#pragma once
#include <Arduino.h>
#include "RgbLedDriver.h"
class SerialHandler {
public:
SerialHandler(RgbLedDriver& ledDriver) : rgbLed(ledDriver) {}
void handleSerialInput();
protected:
RgbLedDriver& rgbLed; // RgbLed-Objekt für die LED-Steuerung
};
handleSerialInput liest jeweils ein Zeichen ein und steuert die RGB-LED an. Über das Callback in RgbLed können weitere Maßnahmen ausgelöst werden.
void SerialHandler::handleSerialInput() {
if (Serial.available() > 0) {
char input = Serial.read();
switch (input) {
case 'r':
case 'R':
rgbLed.setColor(Color::Red);
Serial.println("LED-Farbe auf Rot gesetzt.");
break;
case 'g':
case 'G':
rgbLed.setColor(Color::Yellow);
Serial.println("LED-Farbe auf Gelb gesetzt.");
break;
case 'b':
case 'B':
rgbLed.setColor(Color::Blue);
Serial.println("LED-Farbe auf Blau gesetzt.");
break;
default:
Serial.println("Ungueltiger Befehl. Bitte 'r', 'g' oder 'b' eingeben.");
break;
}
}
}
Der Web-Server stellt eine einzige Seite bereit:

Unter der Überschrift „ESP WebSocket Server” befindet sich ein Label, in dem die aktuelle Farbe der RDB-LED angezeigt wird. Drei Schaltflächen für die Farbauswahl sind darunter zu sehen: Rot, Gelb und Blau. Wenn eine der Schaltflächen gedrückt wird, verändert sich der Label-Text in „Farbe angefordert: <Farbe>” und es wird per WebSocket eine entsprechende Nachricht an den Web(Socket)-Server auf dem ESP32 gesendet. Dieser schaltet wie angefordert die LED und meldet die neu eingestellte Farbe zurück. Nach Erhalt dieser Meldung im Browser wird der Label-Text wieder in „Aktuelle Farbe: <Farbe>” abgeändert.
Viele der verwendeten Bibliotheken, wie z.B. die Bibliothek ESPAsyncWebServer, unterstützen diese Vorgehensweise, in dem sie Callback-Funktionen anbieten. Leider erwarten sie hierfür statische Methoden. Diese haben aber keinen Bezug zu der entsprechenden Klasse und können nicht auf deren anderen Member zugreifen. Member-Funktion verwenden intern einen zusätzlichen Zeiger auf die zugehöre Instanz der Klasse und sind inkompatibel mit statischen Methoden.
Abhilfe schafft ein Lambda-Ausdruck (auch anonyme Funktion genannt). Dies ist eine Funktion ohne festen Namen. Man schreibt sie direkt dorthin, wo man sie im Code braucht. Ein Lambda-Ausdruck ermöglicht es, eine anonyme Funktion innerhalb einer anderen Funktion zu definieren. Also auch in einer Member-Funktion.
Die Capture-Klausel ermöglicht einem Lambda-Ausdruck (indirekt) Zugriff auf Variablen im umgebenden Gültigkeitsbereich zu gewähren, auf die er normalerweise keinen Zugriff hätte. Dazu müssen lediglich die Variablen, auf die innerhalb des Lambda-Ausdrucks zugreifen werden soll, als Teil der Capture-Klausel aufgelistet werden. In den meisten Fällen möchten wir unserem Lambda-Ausdruck Zugriff auf die Member der Klasseninstanz gewähren.
Hier ein Beispiel:
Zunächst wird ein Funktionstyp für eine Callback-Funktion festgelegt. Dies muss über das std::function-Template geschehen, weil sonst das Capture zu einem Fehler beim Kompilieren Führt.
typedef std::function<int(int value)> MyCallback_t;
Die Klasse Foo benötigt eine Callback-Funktion vom Type MyCallback_t um ihre Aufgabe in der Methode doSomething erledigen zu können. Die notwendige Callback-Funktion kann über subscribeCallback registriert werden.
class Foo {
public:
void subscribeCallback(MyCallback_t cb) {
fooCallbackStorage = cb;
}
int doSomething(int value) const {
if (fooCallbackStorage) {
return fooCallbackStorage(value);
}
}
private:
MyCallback_t fooCallbackStorage;
};
Die Klasse Bar stellt eine passende Funktion bereit (barCallbackFunction). Dies muss allerdings eine Member-Funktion sein, weil Zugriff auf die private Variable factor zugegriffen werden muss. barCallbackFunction lässt sich somit nicht direkt an Foo::subscribeCallback übergeben, sondern muss über einen Lambda-Ausdruck mit Verweis (Capture-Klausel) auf die Klasseninstanz gekapselt werden.
class Bar {
public:
Bar(Foo& foo) {
foo.subscribeCallback([this] (int value) { return this->barCallbackFunction(value); });
}
int barCallbackFunction(int value) const {
// Do something with the value
return value * factor;
}
private:
int factor = 2;
Der Lambda-Ausdruck im Konstruktor von Bar hat folgenden Aufbau:
// Capture Params Code, return legt den Rückgabetyp fest.
// vvvv vvvvv vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
foo.subscribeCallback([this] (int value) { return barCallbackFunction(value); });
Ein Programm, das die Klassen benutzt, könnte so aussehen:
int main() {
Serial.begin(115200);
Foo foo;
Bar bar(foo);
Serial.println(foo.doSomething(5)); // Dadurch wird die Funktion barCallbackFunction von Bar(!) mit dem Wert 5 aufgerufen.
}
Die Klasse WebServer besitzt neben dem Konstruktor nur die eine öffentliche Methode begin, die den Web-Server einrichtet und startet. Die Klasse kapselt eine Instanz der Klasse AsyncWebServer. Weiterhin wird der Zugriff auf die ausführende Instanz der Klasse RgbLed benötigt um die die aktuell angezeigte Farbe zu ermitteln.
#pragma once
#include <Arduino.h>
#include <ESPAsyncWebServer.h> //Quelle: https://github.com/ESP32Async/ESPAsyncWebServer
#include "RgbLedDriver.h"
#include "WebSocketServer.h"
class WebServer {
public:
// Konstruktor für die WebServer-Klasse
// rgbLed: Referenz auf das RgbLedDriver-Objekt für die LED-Steuerung
// ws: Referenz auf das WebSocketServer-Objekt für die WebSocket-Kommunikation
// port: Portnummer für den Webserver (Standard: 80)
WebServer(RgbLedDriver& rgbLed, WebSocketServer& ws, uint16_t port = 80) : server(port), led(rgbLed);
// Initialisiert den Webserver und startet ihn
void begin();
protected:
RgbLedDriver& led; // RgbLed-Objekt für die LED-Steuerung
String preProcessor(const String& var); // Funktion zum Ersetzen von Platzhaltern in HTML-Vorlagen
AsyncWebServer server; // AsyncWebServer-Objekt
};
Im Konstruktor wird nur die übergebene Instanz der Klasse WebSocketServer (s.u.) registriert, die die Kommunikation mit dem Browser per WebSockets übernimmt:
WebServer::WebServer(RgbLedDriver& rgbLed, WebSocketServer& ws, uint16_t port) : server(port), led(rgbLed){
server.addHandler(&ws);
}
Die Methode begin legt mit dem Aufruf von server.on die Methode fest, mit der HTML-Response erzeugt wird, der nach einer HTTP-Anfrage an die Root-Seite ("/") zurückgesendet werden soll. Der zurückgemeldete Text kann Platzhalter, eingefasst in Prozentzeichen (%<Platzhalter>%), enthalten. Diese können beim Aufbereiten des HTTP-Response durch aktuelle Werte ersetzt werden. Dies wird von der geschützten Funktion preProcessor erledigt.
void WebServer::begin() {
// Route for root / web page
server.on("/", HTTP_GET, [this](AsyncWebServerRequest* request) {
request->send(200, "text/html", index_html, [this](const String& var) {
return preProcessor(var);
}); // Ende Lambda
}); // Ende server.on
server.begin(); // Start server
}
// Die Signatur von on ist:
AsyncCallbackWebHandler &on(
AsyncURIMatcher uri, // URI, für die dieser Handler zuständig ist ("/" = Root)
WebRequestMethodComposite method, // Soll auf HTTP_GET reagieren
ArRequestHandlerFunction onRequest, // Muss den HTTP-Response versenden
ArUploadHandlerFunction onUpload = nullptr,
ArBodyHandlerFunction onBody = nullptr
);
Für die Bedienung der Methode on wird ein doppelt gestaffelter Lambda-Ausdruck benötigt, jeweils mit einer Capture-Klausel (s. Member-Funktionen als Callback-Funktion). Der dritte Parameter ist ein Pointer auf eine statische(!) Funktion, die die Antwort (Response) auf die HTTP-Anforderung (Request) übermittelt. Die Signatur für die erwarte Funktion ist:
typedef std::function<void(AsyncWebServerRequest *request)> ArRequestHandlerFunction;
Für die angeforderte Funktion wird ein Lambda-Ausdruck verwendet, der die Methode AsyncWebServerRequest::send verwendet, um eine Antwort zu versenden. Hier wird ein Capture ([this]) auf die aktuell ausführende Instanz der WebServer-Klasse benötigt. Die Signatur von send ist:
void send(int code,
const char *contentType = asyncsrv::empty,
const char *content = asyncsrv::empty,
AwsTemplateProcessor callback = nullptr);
// Die Signatur von AwsTemplateProcessor ist:
typedef std::function<String(const String &)> AwsTemplateProcessor;
index_html ist ein Zeiger auf eine Zeichenfolge (const char*) die den HTML-Code der Web-Site darstellt (Beschreibung s.u.). Diese enthält den Platzhalter %COLOR%, der vor dem Versand des Response durch die aktuell angezeigte Farbe ersetzt werden muss. Der dritte Parameter von send ist ein Pointer auf eine statische(!) Funktion, die die Werte für die Platzhalter liefert. Im Beispiel übernimmt diese Aufgabe die Member(!)-Funktion preProzecessor. Deshalb ist auch hier ein Capture ([this]) auf die aktuell ausführende Instanz der WebServer-Klasse notwendig:
//...
request->send(200, "text/html", index_html, [this](const String& var) {
return preProcessor(var);
})
//...
Im Konstruktor der WebServer-Klasse wird eine Referenz auf die Instanz der RdgLed-Klasse übergeben, über die die Methode preProcessor den aktuellen Wert für den Platzhalter %COLOR% ermitteln kann:
String WebServer::preProcessor(const String& var) {
if (var == "COLOR")
return led.getColorString();
return String();
}
Die Klasse WebSocketServer ist von AsyncWebSocket abgeleitet und implementiert das IColorChangeListener-Interface:
class WebSocketServer : public AsyncWebSocket, public IColorChangeListener {
public:
// Konstruktor für die WebSocketServer-Klasse
// url: WebSocket-URL, z.B. "/ws"
// ledDriver: Referenz auf das RgbLedDriver-Objekt, um Farbänderungen zu überwachen
WebSocketServer(const String& url, RgbLedDriver& ledDriver);
protected:
RgbLedDriver& ledDriver; // Referenz auf das RgbLedDriver-Objekt
// WebSocket-Ereignis-Handler
void onWsEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
void handleWebSocketMessage(void* arg, uint8_t* data, size_t len); // Funktion zum Verarbeiten von WebSocket-Nachrichten
void onColorChanged(Color color, String colorName) override; // Callback-Funktion für Farbänderungen
};
Dem Konstruktor wird die vorgesehene URL (Pfad-Komponenten, siehe HTTP-Adressierung) und ein Verweis auf die ausführende Instanz der RgbLed-Klasse übergeben.
WebSocketServer::WebSocketServer(const String& url, RgbLedDriver& ledDriver) : AsyncWebSocket(url), ledDriver(ledDriver) {
onEvent([this](AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) {
onWsEvent(server, client, type, arg, data, len);
}
);
}
Die geschützte Methode onWsEvent reagiert auf die verschiedenen Ereignistypen, die beim WebSockets auftreten können. Im Falle einer Übertragung von Daten werden diese an die Methode handleWebSocketMessage weitergegeben:
void WebSocketServer::onWsEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len){
switch (type) {
case WS_EVT_CONNECT:
Serial.printf("WebSocket client #%lu connected from %s\n", client->id(), client->remoteIP().toString().c_str());
break;
case WS_EVT_DISCONNECT:
Serial.printf("WebSocket client #%lu disconnected\n", client->id());
break;
case WS_EVT_DATA:
handleWebSocketMessage(arg, data, len);
break;
case WS_EVT_PONG:
case WS_EVT_PING:
case WS_EVT_ERROR:
break;
}
}
Die Methode handleWebSocketMessage analysiert die erhalten Daten (Kommandos) und steuert die gespeicherte Instanz von RgbLedDriver für die Farbänderung an.
Über Farbänderungen erfährt der WebSocketServer das Interface IColorChangeListener mit der Methode onColorChanged:
void WebSocketServer::onColorChanged(Color color, String colorName){
Serial.printf("Farbe umgestellt auf %s, sende an alle WebSocket-Clients\n", colorName.c_str());
textAll(colorName);}
AsyncWebSocket::txtall versendet die übergebenen Daten, hier der Name der neuen Farbe an alle verbunden Clients.
Der HTML-Code ist in der Klasse als statisches, unformatierten Zeichenfolgeliteral angelegt (Raw-String). Damit lassen sich mehrzeilige Zeichenketten schreiben und Sonderzeichen wie Anführungszeichen oder Backslashes ohne Escape-Zeichen einfügen. Diese Syntax wird häufig in Arduino- und ESP32-Projekten zum Speichern von HTML- oder JSON-Code verwendet. Die Syntax für Raw-String-Literale in C++ ist
R"rawliteral(<Text>)rawliteral"
So funktioniert es:
Beispiel-Code (C++):
R"rawliteral(
<html>
<body>
<h1>Hello World</h1>
</body>
</html>
)rawliteral"
Die Web-Site enthält unter der Titelzeile ein Label (Tag <p>) zur Anzeige der aktuellen Farbe (id=color). Darunter befinden sich drei Schaltflächen mit den drei auszuwählenden Farben. Wird eine der Schaltflächen angeklickt, wird die JavaScript-Funktion setColor mit den entsprechenden Argumenten aufgerufen.
Beim Tag <body> wird über das Attribut onLoad die Funktion initWebSocket aufgerufen. Der WebSocket-Client wird hier nicht direkt initialisiert, weil der entsprechende Code auch noch an anderer Stelle benötigt wird.
<!DOCTYPE HTML>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
<title>EPS32S3-WebSocket-Demo</title>
</head>
<body style="text-align: center;" onload="initWebSocket()>
<h1>ESP WebSocket Server</h1>
<p id="color">Aktuelle Farbe: %COLOR%</p>
<p><button onclick="setColor('R', 'Rot')" style="background-color: red; width: 8em">Rot</button></p>
<p><button onclick="setColor('G', 'Gelb')" style="background-color: yellow; width: 8em">Gelb</button></p>
<p><button onclick="setColor('B', 'Blau')" style="background-color: blue; width: 8em">Blau</button></p>
</body>
</html>
Die Funktion initWebSocket initialisiert die Variable webSocket, die den WebSocket-Client für diese Seite darstellt.
var websocket; // WebSocket-Objekt
function initWebSocket() {
console.log('Trying to open a WebSocket connection...');
var gateway = 'ws://' + window.location.hostname + '/ws'; // WebSocket-Gateway-URL, z.B.: ws://192.168.178.70/ws
websocket = new WebSocket(gateway);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage;
}
Die Eigenschaft hostname der location-Schnittstelle ist eine Zeichenkette, die entweder den Domänennamen oder die IP-Adresse der Standort-URL enthält. Wenn die URL keinen Hostnamen enthält, enthält diese Eigenschaft eine leere Zeichenkette, „“. IPv4- und IPv6-Adressen werden normalisiert, beispielsweise durch Entfernen führender Nullen, und Domänennamen werden in IDN (Internationalisierter Domainname) umgewandelt.
Für die WebSocket-Ereignisse open, close und message wird auf eigenständige Funktionen verwiesen. Das Ereignis error wird nicht behandelt.
initWebSocket wird beim Laden der Web-Site über das Attribut onLoad des Tags <body> aufgerufen:
<body style="text-align: center;" onload="initWebSocket()">
function onOpen(event) {
console.log('Connection opened');
}
function onClose(event) {
console.log('Connection closed');
setTimeout(initWebSocket, 2000);
}
function onMessage(event) {
document.getElementById("color").innerHTML = 'Aktuelle Farbe: ' + event.data ;
console.log('Color got: ' + event.data);
}
Jedes Ereignis wird auf der Konsole protokolliert. Zugriff auf die Browser-Konsole enthält man bei den gängigen Browsern über die Taste F12. Es öffnet sich ein Fenster mit mehreren Reitern, einer davon ist die Konsole.

Im Fall des Verbindungsverlustes wir im Abstand von zwei Sekunden durch Aufruf von initWebSocket versucht, eine neue Verbindung herzustellen:
Wenn eine der Schaltflächen gedrückt wird, wird die Funktion setColor aufgerufen:
function setColor(color, colorName) {
document.getElementById("color").innerHTML = 'Farbe angefordert: ' + colorName;
websocket.send(color);
console.log('Color requested: ' + color);
}
Zunächst wird der Text des Labes in "Farbe angefordert: ..." geändert. Danach wird mit websocket.send die Farbänderungsanforderung an den WebSocket-Server gesendet. Dieser ändert die Farbe gemäß der Anforderung und sendet eine Bestätigung zurück. Die Rückmeldung wird im Ereignis-Handler onMessage verarbeitet und führt dazu das der Text des Labels auf "Aktuelle Farbe: ..." geändert wird.
Einen detaillierten Ablauf zeigt das folgende Kapitel.
Auch wenn der WebSocket-Server über eine andere Quelle -im Beispiel über die serielle Schnittstelle- eine Farbänderungsanforderung erhält, sendet er eine Meldung an den Browser und löst damit ebenfalls das Ereignis onMessage aus.