Echtzeitkommunikation mit ESP32/ESP8266

Echtzeitkommunikation mit ESP32/ESP8266

Was ist Kommunikation?

Jeder kennt sie, jeder nutzt sie, egal ob mit Menschen oder Maschinen. Kommunikation ist wichtig, nützlich und unerlässlich. Kommunikation (lat. communicatio, „Mitteilung“) ist der Austauch oder die Übertragung von Informationen zwischen Empfängern. Dabei ist es egal ob die Informationen unidirektional oder bidirektional ausgetauscht werden. Wir nutzen sie für zwischenmenschliche Beziehungen oder zur Bedienung von Maschinen oder Computern. Kommunikation und Information sind mittlerweile, in der Zeit des Internets, noch viel wichtiger geworden, als sie sowieso schon einmal waren.

Kommunikation im Internet

Im Internet wird auch fleißig kommuniziert, selbst dann schon, wenn wir nur eine URL aufrufen. Hier laufen dann Abfragen gegen einen DNS-Server und einiges mehr.

Der ganz große Nachteil ist, der leider historisch bedingt beim Internet liegt, also quasi „Made by Design“, ist die Tatsache, dass jegliche Anfragen über die Protokolle HTTP und HTTPS eine Verbindung öffnen und diese nach Empfang einer Rückmeldung wieder schließen. Das nachfolgende Diagramm soll dies etwas veranschaulichen, welche Vorgänge bei einem Request via HTTP passiert:

 

Und genau in diesem Ablauf, liegt der Hund begraben, warum wir im Internet mit HTTP keine Echtzeitkommunikation haben. In den Fällen, wo eine Echtzeitkommunikation gewünscht ist, z.B. in einem Chat-Client, hat man dann auf andere Protokolle gesetzt, unter anderem XMPP oder ähnliche.

Wozu also Echtzeit, wenn es bisher immer geklappt hat?

Durchaus ist es sinnvoll, eine Kommunikation in Echtzeit durchzuführen. Das simpelste Beispiel ist z.B. ein Chat, der doch recht zeitkritisch ist und daher eine Kommunikation in Echtzeit passieren sollte oder, wie in meinem konkreten Anwendungsfall, Sensordaten sollen in der Adminoberfläche direkt angezeigt werden, wenn sie kommen, ohne, dass ein Benutzer die Seite aktualisieren muss.

Um genau zu sein ist es nicht mal Echtzeit, da hier immer noch die Latenz zwischen Client und Server bedacht werden muss. Da diese aber in der Regel in Millisekundenbereich ist, kann man schon von einer (quasi) Echtzeit sprechen.

Man kann natürlich auch so etwas über das realisieren. Das würde aber nach sich ziehen, dass der Client immer wieder Anfragen an den Server stellt um zu erfahren, ob sich Daten verändert haben oder neue dazu gekommen sind. Dieses Verfahren nennt sich HTTP-Long polling.

Das hat aber einen entschiedenen Nachteil: Ressourcen und Zeit. Dadurch, dass man immer wieder an den Server Anfragen stellt, muss er diese auch immer wieder bearbeiten, also immer wieder das ganze HTTP-Protokoll abarbeiten und Inhalte senden oder nicht. Das erhöht natürlich auch den Traffic und die last am Server und am Client. Auch werden immer wieder redundante Daten an den Server geschickt, nämlich der HTTP Header. Im einfachsten Fall, das Öffnen der Google-Seite, sähe ein Header so aus:

> GET / HTTP/1.1
> Host: google.de
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Location: http://www.google.de/
< Content-Type: text/html; charset=UTF-8
< Date: Mon, 29 Jan 2018 13:36:51 GMT
< Expires: Wed, 28 Feb 2018 13:36:51 GMT
< Cache-Control: public, max-age=2592000
< Server: gws
< Content-Length: 218
< X-XSS-Protection: 1; mode=block
< X-Frame-Options: SAMEORIGIN

Diesen Header kann man ganz simpel mit cURL prüfen: curl google.de -v

Hierzu nutze ich die Ubuntu-Bash unter Windows 10.

Dieser Header ist ganz genau 404 bytes groß und wird immer wieder an den Server übertragen. Kommen jetzt Cookies oder andere Header dazu, wird er natürlich immer größer. Das heißt, es werden in diesem Falle, immer 404 bytes übertragen, obwohl der Server schon weiß, was eigentlich kommen sollte, da er ja mindestens schon eine Verbindung mit dem Client hatte. Wozu also immer und immer wieder den ganzen Overhead übertragen?

Gerade im Hinblick auf meine Hausautomation, die ich aktuell baue, ist es fast unausweislich, den Overhead so gering wie möglich zu halten. Das minimiert dann die Durchlaufzeiten der Mikrocontroller und so auch den Batterieverbrauch im Deep-Sleep und maximiert damit die Laufzeit der Akkus.

Genau dem Problem nimmt sich die RFC 6455 an und hat einen neuen Standard entworfen: Websockets.

Sockets? Steckdosen im Internet oder wie?

Prinzipiell sind Websockets genau das, Steckdosen. Das Websocket-Protokoll ist ein auf TCP basierendes Netzwerkprotokoll, somit also komplett (mehr oder weniger) unabhängig von HTTP.  Eine Verbindung läuft, vereinfacht, wie folgt ab:

Wie man sieht, bleibt die Verbindung, im Gegensatz zu HTTP, solange bestehen, bis der Client oder der Server die Verbindung schließt. Der Header wird nur beim Handshake übertragen und ist dann für die ganze Verbindung gültig. Somit kann der Server und der Client auf bestimmte Events hören oder selber Events abfeuern, auf die der Server hört.

Was sehr cool ist, wie ich finde, ist, dass Websockets und HTTP parallel laufen können und nur einmal der Port 80 bzw. 443 (SSL) belegt werden. Durch den Handshake wird automatisch festgelegt, dass der aktuelle Request ein Request gegen die Websockets ist. Durch einen Upgrade-Befehl wird dann die HTTP-Verbindung zu einer Websocket-Verbindung „heraufgestuft“.

Ein Header mit Handshake bei Websockets sieht wie folgt aus:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

Eine Antwort des Servers sähe wie folgt aus:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

Der HTTP-Statuscode 101 zeigt hier an, dass das Protokoll gewechselt werden soll, nämlich zu Websockets. Upgrade: websocketund Connection: Upgradestehen für die Bewilligung des Servers, dass der Protokollwechsel durchgeführt werden darf.

Unterstützung der Websockets. Stand 29.01.2018

Diese Websockets werden von allen modernen Browsern (Stand: 29.01.2018) unterstützt:

Das macht es dann einfach, diese Techniken umzusetzen, da schlichtweg jedes Gerät sie unterstützen kann. Selbst unsere Mikrocontroller (in meinem Falle ein oder ) können es. Hierzu gibt es auch bereits einige fertige Bibliotheken.

Die Umsetzung

Bevor man nun die Websockets nutzen kann, muss man sich Gedanken darüber machen, was man mit Websockets machen möchte, denn nicht alles ist sinnvoll oder sicher. Daher sollte man sich genau überlegen, was muss man wirklich in Echtzeit haben und was kann man nach wie vor z.B. über eine REST-API machen. Ein normales Holen von Konfigurationsdaten oder Metadaten ist nicht zwingend sinnvoll, dies über Websockets zu erledigen.

Als Wrapper der Websockets nutze ich, da ich damit gut Erfahrungen gemacht habe und die Handhabung recht einfach ist, Socket.IO.

Detalierte Vor-und Nachteile der Websockets kannst du im Blog von Paul Banks nachlesen: https://banksco.de/p/state-of-realtime-web-2016.html

Serverseitig

Kompatibilität und Voraussetzungen

Serverseitig setze ich hierzu einen NodeJS-Server ein, hier gibt es auch entsprechende Bibliotheken für Socket.IO. Die Website von Socket.IO bietet hierzu ein sehr gutes Tutorial an, was zeigt, wie man einen Server für einen Echtzeit-Chat-Client aufsetzt. Voraussetzung ist hierfür NodeJS. Wichtig ist hierbei zu sagen, dass sehr wenige Webhoster NodeJS unterstützten, bei All-inkl habe ich damit kein Glück. Hierfür nutze ich dann doch einen kleinen vServer bei Uberspace bzw. meinen RaspberryPi 3, der damit auch vollkommen verträglich läuft. Günstige Startersets oder nur den RaspberryPi bekommst du schon für kleines Geld auf Amazon.

Wenn du keinen Server mit NodeJS hast, gibt es auch ganz nützliche Bibliotheken für PHP:

Natürlich musst du nicht auf Socket.IO aufsetzen und kannst direkt mit den „blanken“ Websockets starten. Der Einfachheithalber setze ich hier aber auf Socket.IO.

Zu NodeJS und und dem RaspberryPi gibt es auch sehr gute Literatur, die ich hier gerne empfehlen möchte:

Der Code

Bei den Websockets gibt es prinzipiell zwei wichtige Befehle, mit denen du alles machen kannst, was eine Kommunikation ausmacht. Diese Befehle wären:

  • .on()
  • .emit()

Mit on() hörst du auf ein bestimmtes Event. Mit emit() emittierst/sendest du ein Event. Natürlich gibt es dann auch noch die Einstellung der Adressaten. Du kannst auch einen .broadcast() machen, der alle verbundene Clients informiert. Auch kann man ein bestimmte Rooms und Namespaces bereitstellen.

Ein einfacher Code für den Server könnte wie folgt aussehen:

var app = require('http').createServer(handler)
var io = require('socket.io')(app);
var fs = require('fs');

app.listen(80);

function handler (req, res) {
  fs.readFile(__dirname + '/index.html',
  function (err, data) {
    if (err) {
      res.writeHead(500);
      return res.end('Error loading index.html');
    }

    res.writeHead(200);
    res.end(data);
  });
}

io.on('connection', function (socket) {
  socket.emit('news', { hello: 'world' });
  socket.on('my other event', function (data) {
    console.log(data);
  });
});

Wie du im Code siehst, werden normale Browser mit der Datei index.html bedient, gleichzeitig hört ein Websocket mit auf dem Port 80. In dem Beispiel wird bei einer erfolgreichen Connection mit einem Client, das Event „news“ emittiert und sendet Daten im JSON-Format. Danach hört der Server auf das Event „my other event“ und stellt eine Callback-Funktion zur Verfügung, die aufgerufen wird, wenn das Event ausgelöst wird. Die Callback-Funktion empfängt dann die entsprechenden Daten, die der Client schickt.

Implementierung im Client/Mikrocontroller

Damit der Mikrocontroller nun eine Verbindung zu deinem neuen Websocket-Server aufbauen kann, musst du auf dem natürlich erst den Befehlssatz kenntlich machen. Auch hierzu gibt es bereits einige Implementierungen, die man auf GitHub findet:

Ich selbst nutze die Bibliothek Socket.io-v1.x-Library, da diese auch gleichzeitig eine REST-API anbietet. Auch hier bietet dir der Entwickler der Bibliothek entsprechende Beispiele, sowohl client- als auch serverseitig an. Diese Bibliothek eignet sich auch für den und den .

Das Einbinden ist dann recht einfach und nur noch Fleissarbeit.

Senden kann man Events mit client.send() und auf Events hört man mit client.monitor() und bekommt dann entsprechende Rückgaben als Variablen:

  • RID (ID des Raums)
  • Rname (Name des Raums)
  • Rcontent (Datenrückgabe des Servers)

Mit Hilfe der Bibliothek ArduinoJSON kann man auch Daten im JSON-Format elegant wegschicken:

JsonObject& root = jsonBuffer.createObject();
root["Device_ID"] = DeviceID;
root["temp"] = temp;
root["humidity"] =  hum;
  
String json = "";
root.printTo(json);

Debug.println("[measerementTemp]: Sending JSON via websockets: "+json);
_socketIOClient.sendJSON("send tempdata", json);

In meinem Code sende ich damit die Temperatur und die Luftfeuchtigkeit (die mit Hilfe eines gemessen werden) an meinen Server und schreibe sie in eine Datenbank:

socket.on('send tempdata', function(data){
        pool.getConnection(function(err,connection){
            if (err) {
                console.log(err);
                return;
            }
            connection.query("INSERT INTO Temperatures (Temp, Humidity, Device_ID) VALUES ("+data.temp+","+data.humidity+","+data.Device_ID+")", function(err,rows){
        if(err) throw err;
        var insertedID = rows.insertId;
        connection.query('SELECT * FROM Temperatures WHERE ID = '+insertedID, function(err,rows){
          if(err) throw err;
          io.emit('temps return', rows);

          var ret = [{
            'Date': rows[0].Date,
            'Text': 'Temperatur gemessen von Device: '+rows[0].Device_ID
          }];
        
        })

        
            });
        });
  });

Im gleichen Zuge hole ich den letzten eingefügten Datensatz aus der Datenbank und sende ihn an meine Weboberfläche, mit dem Event io.emit('temps return', rows);

Abschluss

Im Grossen und Ganzen war es das auch schon. Wenn du einmal hinter die Technik gestiegen bist, kann man damit sehr schnell und zügig schöne Dashboards bauen. Für meine Weboberfläche nutze ich dafür ChartJS um damit dann die Temperatur- und Luftfeuchtigkeitsdaten visualisieren zu können.

Da du nun die Techniken kennst, wünsche ich dir viel Spaß bei der Implementierung! 🙂

Wenn du Fragen oder Anregungen hast, freue ich mich diese in den Kommentaren zu lesen. Gerne kannst du mich auch einfach wissen lassen, welche Projekte du damit umgesetzt hast.