Freitag, 12. Oktober 2018

IoT-System: Update Geräteprotokoll

Hallo zusammen,

heute möchte Ich näher auf das Verbindungsprotokoll eingehen, welches von meinem IoT-System zur Kommunikation zwischen dem Server und den Geräten verwendet wird. Der Grund dafür ist, dass ich dieses System in den letzten Wochen überarbeitet habe.

Bisheriger Zustand


Zunächst möchte ich darauf eingehen, wie das Protokoll bisher funktioniert hat.

Sogut wie alle meiner bisher eingebundenen Geräte sind über WLAN mit dem Server verbunden. Dieser ist über LAN mit einem Router gekoppelt, welcher das WLAN zur Verfügung stellt. Da den Geräten anfangs die Zugangsdaten zum WLAN nicht bekannt sind, spannen sie zunächst einen eigenen AccessPoint (AP) auf. Zu diesem muss sich nun mit einem Rechner, Tablet, etc. verbunden werden. Die Geräte stellen anschließend eine HTML-Seite zur Verfügung, über welche die Netzwerk-Verbindungsdaten eingegeben werden können. Dafür nutze ich den ein Modul namens WiFiManager, welches über GitHub zur Verfügung steht.

Ist die WLAN-Verbindung erfolgreich, so wird eine TCP-Verbindung zu einer fest eingetragenen Server-IP + Port aufgebaut. Dies ist auch der erste Hauptgrund, warum ich das System überarbeiten wollte. Ändert sich die IP oder der Port des Servers, so musste auf alle Geräte eine neue Software aufgespielt werden. Auch wenn dies per OTA-Update möglich ist, stellt es doch einen nicht geringen Aufwand dar.

Nachrichtenformat


Nach erfolgreichem Aufbau der TCP-Verbindung beginnt die eigentliche Kommunikation. Da TCP-Verbindungen lediglich aus einem Bytestrom ohne definiertes Datenformat bestehen, werden zunächst vier Bytes geschicht, welche eine 32-Bit Integerzahl darstellen. Diese teilen den Bytestrom in definierte Nachrichten auf.

Die Nachrichten bestehen nun aus 4 Byte SenderID, 4 Byte ZielID, 4 Byte BefehlsID und optionalen, befehlsabhängigen Daten. Deren Länge ergibt sich aus der Nachrichtenlänge minus den 12 Bytes an eben genannten Daten.

Datenformat altes Geräteprotokoll
Bevor ich nun das eigentliche Protokoll beschreiben kann, muss ich kurz auf die Verbindungsstruktur der Geräte eingehen. Auch wenn es sich aktuell um eins-zu-eins-Verbindungen zwischen Server und Geräten handelt, ist es vorgesehen, dass Geräte auch Sub-Geräte anbinden können. Beispielsweise könnte so ein Gerät als Bridge zwischen LAN/WLAN und einem anderen Bus-System, beispielsweise SPI, fungieren.

Parent Discovery


Da der Server später zur Übertragung von Nachrichten wissen muss, an welches Gerät er die Nachricht schicken muss, ist es notwendig, dass die Geräte bei der späteren Anmeldung ihr übergeordnetes Gerät (Parent-Device) mitteilen. Dafür ist es wiederum notwendig, dass sie wissen, welches Gerät ihr Parent-Device ist. Daher senden sie als erstes eine Nachricht mit dem Befehl "GetParent" an ihr ParentDevice. Da sie hier noch nicht wissen, welche ZielID sie dafür verwenden müssen, nutzen sie die dafür reservierte ID -2. Das übergeordnete Gerät antwortet nun mit einer Nachricht "SetParent", wobei im SenderID-Feld nun die ID des Geräts enthalten ist. Ist das Gerät direkt mit dem Server verbunden, so ändert dies nichts an der Vorgehensweise. Das SenderID-Feld der "SetParent"-Nachricht enthält dann die ID 0, welches die eindeutige ID des Servers darstellt.

Nachrichten GetParent/SetParent

Device Registration


Nun kann sich das Gerät am Server anmelden. Dafür gibt es zwei Varianten. Welche Variante genutzt wird, hängt davon ab, ob das Gerät bereits einmal mit dem Server verbunden war oder nicht. Sollte es noch nicht verbunden gewesen sein, so besitzt es noch keine eindeutige DeviceID. In diesem Fall wird zunächst eine DefaultDeviceID verwendet, für welche die ID -1 reserviert ist. Auch für die eben beschriebene Anforderung der ParentID wird dann diese ID verwendet. Das Gerät fragt nun eine Registrierung als neues Gerät an, wobei die ParentID im Feld für optionale Daten angehängt ist. Der Server antwortet darauf mit einer neuen, eindeutigen DeviceID.

Nachrichten RegisterNew / AcceptNew
Erhält das Gerät die Nachricht AcceptNew, so beginnt es, die einzelnen Ein- und Ausgänge (IOs) anzumelden. Darauf möchte ich an dieser Stelle jedoch nicht eingehen, da es sich bei der Überarbeitung nicht geändert hat und da es den Rahmen hier sprengen würde.

Sollte das Gerät zuvor schon einmal verbunden gewesen sein, so besitzt es bereits eine DeviceID. Diese wird in einem lokalen Festspeicher (EEPROM, SPIFFS, etc.) gespeichert. Stellt das Gerät beim Start fest, dass es bereits eine DeviceID besitzt, so lädt es diese. Das Gerät geht nun davon aus, dass es sich bei dem Server schon einmal angemeldet hat und ihm dabei diese ID zugeteilt wurde. Dies ist ein weiterer Grund, warum ich das Protokoll überarbeiten wollte, aber dazu später mehr.

Für die Anmeldung am Server verwendet das Gerät nun seine eigene DeviceID und registriert sich auch nicht als neues, sondern als bekanntest Gerät. Nach der Annahme der Registrierung durch den Server ist es nun nichtmehr notwendig, die IOs anzumelden, da diese dem Server bereits bekannt sein sollten.

Nachrichten RegisterKnown / AcceptKnown

Zusammenfassung altes Protokoll


Ich möchte nun noch einmal die Punkte auflisten, welche mich an dem bisherigen Protokoll gestört haben, bzw. welche zu Problemen führen könnten und zum Teil auch geführt haben.
  1. Die IP und der Port des Serves sind fest eingetragen.
  2. Bei der Anfrage als neues Gerät wird eine nicht-eindeutige DeviceID verwendet. Dies könnte bei gleichzeitiger Anmeldung von mehreren Geräten dazu führen, dass die AcceptNew-Nachricht dem falschen Gerät zugestellt wird, welchem dadurch Serverseitig die falsche ParentID zugeordnet sein könnte.
  3. Eine Anmeldung eines Geräts an verschiedenen Servern ist nicht möglich. Da der Server den Geräten die DeviceID zuteilt, sind diese nur für diesen Server gültig. Versucht sich das Gerät nun an einem alternativen Server anzumelden, so kennt dieser das Gerät entweder nicht, oder kennt unter der DeviceID bereits ein anderes Gerät. Beides führt zu Problemen. Kenn der Server das Gerät nicht, so muss er es ablehnen. Das Gerät muss darauf reagieren. In diesem Fall hat es bisher seine DeviceID gelöscht und sich neu angemeldet. Sollte es sich später erneut beim Original-Server anmelden, so beginnt das Problem von vorn. Kennt der Alternativserver unter der DeviceID bereits ein Gerät, so besitzen zwei Geräte die gleiche ID, was zwangsweise zu Problemen führt.
  4. Nur das Gerät ist in der Lage, seine IOs anzumelden. Dies geschieht nur bei einer Registrierung als neues Gerät. Wird das Gerät baulich verändert, so muss es komplett neu registriert werden.
Gesamtübersicht altes Protokoll

Neues Protokoll


Um die genannten Probleme zu lösen, habe ich das System in zwei Schritten verändert.

Server Discovery


Zunächst habe ich die festeingetragenen Serverdaten entfernt. Der Server teilt nun seine möglichen Verbindungen über einen UDP-Multicast mit. Dafür sendet er eine Nachricht mit den möglichen Protokollen an eine UDP-Multicast-Adresse. Die Geräte lauschen auf dieser Adresse und erhalten alle dahin geschickten Nachrichten.

Neben den Protokoll-Informationen enthält die Nachricht natürlich auch die IP des Servers und zusätzlich noch zwei Bytes an Flags. Aktuell werden nur zwei Bits verwendet. Ein Bit gibt an, ob es sich um ein Standard oder ein Backup-Server handelt. Die Geräte bevorzugen dabei Standard-Server. Ist kein Standart-Server vorhanden, so nutzen sie, falls vorhanden, auch Backup-Server. Das zweite Bit gibt an, ob es sich um einen Entwicklungs-Server handelt. Diese Server werden nur verwendet, wenn auch die Geräte im Entwicklungsmodus betrieben werden, anderenfalls werden die ignoriert. Die weiteren 14 Bits werden aktuell nicht verwendet.

Protokoll-Veränderungen


Zunächst hat sich das Datenformat der Nachrichten geändert. Dies ist darin begründet, dass als DeviceID nun keine fortlaufende 32-Bit-Integerzahl mehr verwendet wird, sondern eine GUID. Diese umfasst 16 Bytes, also 128 Bit, und wird zufällig generiert.

Datenformat neues Geräteprotokoll
Diese Änderung liegt darin begründet, dass die Vergabe einer DeviceGUID nun unabhängig von der Registrierung als Gerät stattfindet. Stellt eine Gerät beim Start fest, dass es noch keine GUID besitzt, so fordert es nun nach erfolgreichem Verbindungsaufbau als erstes eine GUID von seinem ParentDevice an. Sollte das ParentDevice nicht der Server sein, so fordert es wiederum eine GUID von seinem ParentDevice an. Die ParentDevices müssen sich nur merken, welches ChildDevice als letztes nach einer GUID gefragt haben und nach Erhalt der GUID diese an das ChildDevice weiterleiten. Dies löst auch das Problem der gleichzeitigen Anmeldung mehrerer Geräte. In diesem Fall erhält das Gerät, welches zuletzt angefragt hat die neue GUID. Das erste Gerät erhält daher keine Antwort auf seine GUID-Anfrage. Mit Hilfe eines TimeOuts stellt das übergangene Gerät später die Anfrage erneut.

Nachrichten RequestGUID / SendGUID
Im Anschluss an die GUID-Anfrage, beziehungsweise wenn das Gerät bereits eine GUID besitzt, folgt die Anfrage des ParentDevice. Im Vergleich zum bisherigen Protokoll hat sich lediglich geändert, dass das Gerät nun bereits eine GUID besitzt.

Nachrichten Get/Set Parent
Der Vorteil der vorgestellten GUID-Anfrage zeigt sich nun in der Anmeldung beim Server. Hier entscheidet nun nicht mehr das Gerät, ob es sich als neues oder bekanntes Gerät anmeldet, sondern der Server. Kennt dieser die DeviceGUID bereits, so akzeptiert er die Registrierung. Kennt er die DeviceGUID noch nicht, so fragt er zunächst die IOs des Gerätes an. Im Anschluss an die IO-Registrierung akzeptiert er dann die Registrierung

Nachrichten RegisterDevice / AcceptDevice
Sollte sich die Gerätekonfiguration später einmal ändern, so ist es jederzeit möglich über das Server-Interface eine neue Abfrage der Geräte-IOs zu starten.

Ich möchte nun noch einmal darauf zurückkommen, warum es nötig war, die DeviceID von Int32 auf GUID umzustellen. Da die Vergabe von IDs durch verschiedene Server durchgeführt werden könnte, müssen die IDs zufällig generiert werden. Bei den bisherigen fortlaufenden Zahlen würde es zwangsweise zu Überschneidungen kommen. Auch wenn die Anzahl an Möglichkeiten bei 32 Bit schon 2^32 ~ 4.3 Milliarden beträgt,  besteht doch die Möglichkeit, dass IDs zufällig mehrfach belegt werden. Bei der Verwendung einer GUID gibt es 2^128 ~ 3.4*10^38 Möglichkeiten. Dies schließt eine Mehrfachbelegung selbst bei hunderten Geräten innerhalb eines Systems nahezu aus.

Ein weiterer Grund der Umstellung liegt darin, dass GUIDs überall im gesamten restlichen System verwendet werden. Da ich für die Erweiterung des Systems plane, dass beliebigen Objekten (Geräte, Logikstrukturen, Logikinstanzen,...) beliebige Daten angehängt werden können, ist es sinnvoll, wenn alle Objekte eine einheitliche Referenzierung ermöglichen. Die Verwendung von GUIDs liegt hier nah.

Gesamtübersicht neues Protokoll

Portierung

Die Portierung des Systems vom alten auf das neue Protokoll musste sowohl Serverseitig, als auch Geräteseitig durchgeführt werden. Da ich nicht verhindern wollte, alle Geräte wieder neu anlernen und verknüpfen zu müssen, habe ich auf beiden Seiten Update-Routinen erstellt. Diese updaten die Daten der Systeme auf die neuen Versionen. Im Zuge dessen habe ich in allen Systemen eine Angabe einer Dateiversion hinzugefügt. Dies wird es erleichtern, bei späteren Updates herauszufinden, ob die Daten bereits geupdated wurden, oder nicht. In den aktuellen Update-Routinen musste ich dies über Umwege feststellen.

Leider ist mir beim Überarbeiten der Quellcodes auf Geräteseite ein Fehler unterlaufen, sodass ich das OTA-Update außer Kraft gesetzt habe. Daher musste ich nach Behebung des Fehlers alle bereits geupdateten Geräte vom Netz trennen, öffnen und per Hand updaten. Da dies ein vergleichsweise hoher Aufwand ist, kann ich daher anderen nur empfehlen und werde mich hoffentlich in Zukunft auch daran halten, bei neuen Updates das OTA-Update zu testen, bevor das Update groß verteils wird.

Damit bin ich für heute am Ende angelangt und bedanke mich für das Lesen!

Keine Kommentare:

Kommentar veröffentlichen