Montag, 24. Dezember 2018

Zeitpläne und Generics

Hallo zusammen,

Wie jeden Winter wurde bei uns wieder Anfang Dezember die Weihnachtsdekoration ausgepackt, dazu gehört auch mein Lichterbogen. Diesen hatte ich auch schon die beiden letzten Jahre über mein HomeAutomation-System zeitgesteuert integriert. Durch die Veränderungen der letzten Monate hat sich aber nun die Möglichkeit gegeben, die Integration zu verbessern.

Zunächst möchte ich einmal zeigen, wie die Zeitsteuerung bisher gelöst war. Dies ist im ersten untenstehenden Bild dargestellt. Die Steuerung funktioniert nach dem im Folgenden beschriebenen Prinzip.

Es existieren zunächst zwei globale Ganzzahl-Variablen. Dieses Ganzzahlen (14 und 1) stellen die Stunden von Uhrzeiten dar, genauer von einer Start-Uhrzeit und einer End-Uhrzeit. Beide zusammen ergeben einen Zeitbereich von 14Uhr-1Uhr. Diese globalen Variablen werden über zwei "Globale-Variable"-Blöcke ausgelesen und jeweils in einen "Time Generator"-Block übergeben.

Diese wandeln Werte für Stunde, Minute und Sekunde in eine Uhrzeit in Millisekunden um, wobei alle drei Eingangswerte optional sind. Die beiden Uhrzeiten in Millisekunden werden nun an einen "InDailyTime"-Block übergeben.

Dieser gibt einen Boole'schen Wert aus (also ja/nein), welcher angibt, ob sich die aktuelle Zeit innerhalb oder außerhalb der beiden Uhrzeiten befindet. Mit fortlaufender aktueller Zeit ändert sich natürlich der Ausgabewert des letzten Blocks.

Alte Zeitsteuerung
Diese Variante lief so (naja fast, die globalen Variablen waren ursprünglich mal lokale Konstanten) schon die beiden letzten Jahre. Dies hat einwandfrei funktioniert, hat aber diverse Nachteile. Erstens werden mehrere Variablen für die Zeitangabe benötigt, zweitens ist das Blockschema so schon unübersichtlich und drittens wird bei die Programmierung für weitere Zeitbereiche sehr schnell deutlich unübersichtlicher, da diese jeweils das gezeigte Schema benötigen.

Durch meine letzten Anpassungen, welche ich in den letzten Blogeinträgen beschrieben habe, hat sich nun die Möglichkeit ergeben, diesen Bereich zu optimieren. Bisher war das System auf die Grunddatentypen (Integer, Bool, Float, String, etc.) beschränkt. Jetzt gibt es aber die Möglichkeit  weitere Datentypen anzulegen und zu verwenden. Dadurch hat sich das Blockschema auf folgendes Bild vereinfacht.

Neue Zeitsteuerung
Das Schema hat nicht nur den Vorteil übersichtlicher zu sein, sondern ist dabei auch gleichzeitig deutlich funktionaler. Es verwendet den neuen Datentyp "TimeSchedule" (Zeitplan). Diesem Datentyp ist ein neuer Editor zugeordnet. Dieser ist genau auf den Datentyp zugeschnitten. Ein Zeitplan besteht zunächst aus einer beliebigen Anzahl von Gruppen. Jede Gruppe (im Bild "Woche" und "Wochenende") besitzt eine Zuordnung, an welchen Tagen die Gruppe gültig ist, sowie eine beliebige Anzahl an Zeitbereichen. Solange mindestens ein Zeitbereich einer beliebigen Gruppe aktiv ist, ist der Ausgabewert des Zeitplans "wahr", ansonsten "falsch".

Zeitplan-Editor
Die Angabe von Zeitplänen ist dadurch nun deutlich einfacher. Die neuen Zeitpläne nutze ich nun beispielsweise auch, um dem System mitzuteilen, wann es Tag ist und wann Nacht. Daran ist wiederum eine Vielzahl an Dingen gekoppelt.

Bei der Programmierung der Logik-Elemente (Die Blöcke in den ersten beiden Bildern) habe ich nun allerdings ein Problem festgestellt. Für jeden Datentyp, den ich anlege, muss ich einen "Konstante"-Block erstellen. Dies ist zwar prinzipiell möglich, zieht aber nach sich, dass mit jedem Datentyp die Anzahl der Blöcke steigt. Sollte ich mich dazu entscheiden, dass Projekt für weitere Entwickler freizugeben, müssten auch diese für jeden neuen Datentyp dieser Regelung folgen.

Konstanten für jeden Datentyp
Das ist zwar so schon nicht schön, es hätte aber vorübergehend funktioniert und ich hätte mich auch später noch darum kümmern können. Allerdings hat sich in Verbindung mit dem "Globale Variablen"-System gezeigt, dass ich es mir nicht so einfach machen kann. Denn ich hätte für jeden Datentypen auch noch einen Block für die globale Variable erstellen müssen.

Das ist nun gleich noch unschöner, hätte aber auch noch funktioniert. Nun ist aber seit kurzem jedes System als Plug-In realsiert. Das heist, das System "Globale Variablen" steht für sich, das System "Zeitplan" steht für sich, etc. Um nun also einen Block "Globale Variable (Typ Zeitplan)" zu erstellen, hätte ich die Spaltung in eigene Systeme auftrennen müssen. Das wollte ich dann doch vermeiden. Daher habe ich mir was intelligenteres einfallen lassen.

Den Programmierern unter den Lesern wird der Begriff "generic" bekannt sein. Für alle anderen möchte ich kurz erklären, worum es sich dabei handelt. Als generisch wird ein Objekt bezeichnet, welches mit verschiedenen Typen umgehen kann. Als Beispiel möchte ich eine Liste (C#) nennen. Eine Liste ist ein Objekt, welches mehrere Objekte eines Types enthält. Die Liste kann dabei mit allen möglichen Datentypen umgehen und besitzt verschiedene Funktionen, welche unabhängig vom Datentyp der enthaltenen Objekte sind. In C# erkennt man solche generischen Elemente an den Pfeilen, welchen den generischen Typ angeben. Im Beispiel der Liste mit dem enthaltenen Typ einer Ganzzahl würde dies so aussehen: List<int>

Um jetzt mal zum Thema zurückzukehren, möchte ich zeigen, was ich eingentlich getan habe. Es gibt nun einen neuen generischen Konstanten-Block, welcher alle bisherigen Konstanten-Blöcke ersetzt. Je nachdem, mit welchem Datentyp dieser Block verbunden wird, ändert sich der enthaltene Datentyp. Nach dem Verbinden kann anschließend der Wert der Konstante festgelegt werden.

Der Datentyp des neuen "Konstante"-Blocks ändert sich je nach angeschlossenen Typ.
Nach dem Verbinden kann der Wert festgelegt werden.
Der Typ des Blocks lässt sich jedoch nicht nur durch den angeschlossenen Datentyp bestimmen. Ist noch kein Datentyp angeschlossen, so bietet das Element eine Auswahl der Datentypen, welche alle nötigen Kriterien für Konstanten erfüllen. Diese Kriterien sind: Der Typ muss ich serialisieren (~speichern) und deserialisieren (~laden) lassen, und ihm muss ein Editor zugeordnet sein.

Das generische Element bietet eine Auswahl, welcher Datentyp verwendet werden soll.
Es kann natürlich auch eine Mischung aus beiden Fällen auftreten. Beispielsweise beim Verbinden mit dem Additions-Element. Dieses unterstützt Eingänge vom Typ "Ganzzahl" und "Gleitkommazahl". Da dem Element nach dem Verbinden noch nicht bekannt ist, welchen Datentyp es enthalten soll, fragt es auch hier nach.

Das Element fragt bei einer Auswahl an Datentypen nach, welcher verwendet werden soll.
Hinter dem Konstanten-Element steht ein System, welches es ermöglicht den Ausgabetyp eines Logikelements genau zu spezifizieren. Dieses System kann natürlich auch von allen anderen Logikelementen verwendet werden. Dadurch habe ich nun auch das"Globale Variablen"-Plug-In überarbeitet, sodass auch hier ein neues generisches Element existiert. Jeder Datentyp, der nun also die oben genannten Kriterien erfüllt, kann nun automatisch für globale und lokale Konstanten verwendet werden, ohne dass ein eigenes Logikelement erstellt werden muss.

Damit bin ich für heute auch schon wieder am Ende. Ich bedanke mich bei allen Lesern und wünsche schöne Weihnachten, sowie einen guten Start ins neue Jahr.

Markus

Samstag, 10. November 2018

Umstrukturierung SmartHome-System

Hallo zusammen,

heute möchte ich über die Umgestaltung meines IoT-Systems sprechen. Dazu möchte ich zunächst auf die bisherige Struktur eingehen.

Alte Struktur


Bei der Entwicklung der Struktur 2015 bin ich von einer einfachen EVA-Struktur ausgegangen. Das heißt es gibt die Schritte Eingabe, Verarbeitung und Ausgabe. Für die Ein- und Ausgabe hatte ich physische Geräte vorgesehen, welche sich mit einem Server verbinden und sich dort registrieren. Dabei teilen sie diesem ihre Ein- und Ausgänge (IOs) mit, welche vom Server gespeichert werden. Zur Verbindung dieser virtuellen IOs waren Logikstrukturen gedacht. Diese enthalten auch wieder Ein- und Ausgänge, sowie Logikelemente, welche vom Benutzer über einen Editor zusammengestellt werden können. Sämtliche Elemente, also Eingänge, Ausgänge und Logikelemente können schließlich über den Editor logisch verbunden werden. Die dadurch entstandenen Strukturen stellen Vorlagen dar, von welchen nun Instanzen erzeugt werden können. Diese enthalten nun die definierten IOs, welche mit den IOs der Geräte verbunden werden können.

Das ganze System wurde mit der Zeit Schritt für Schritt erweitert. Eine Erweiterung stellt die Möglichkeit dar, instanzabhängige Variablen einzufügen. Während es schon von Beginn an möglich war Konstanten in die Struktur zu integrieren, welche für alle Instanzen einer Struktur gleich sind, so ist es damit möglich Konstanten zu definieren, welche für jede Instanz einzeln einen Wert erhalten.

Eine andere Erweiterung ist die Möglichkeit, eigene Logikelemente programmieren und als Plug-In integrieren zu können. Beispiele dafür sind Plug-Ins für Alexa, sowie ein Plug-In für globale Variablen. Damit ist es möglich Konstanten zu definieren, welche Struktur- und Instanzübergreifend verwendet werden können.

Mit der stetigen Erweiterung des Systems wurde für jede Möglichkeit die verschiedenen IOs zu verbinden jeweils eigener Code und passende Nutzerinterfaces erstellt. Daher existieren mehrere Quellcodebereiche, die praktisch die gleichen Aufgaben erfüllen, jedoch alle leicht unterschiedlich sind. Dabei ist jedoch immernoch die starre Struktur Eingang-Logik-Ausgang vorhanden.

Alte Systemstruktur: Alle Module sind direkt verbunden
Das Ziel der Überarbeitung ist es daher, den gesamten Prozess des Verbindens zu vereinheitlichen. Dadurch soll die Menge des Quellcodes reduziert und der Prozess flexibler gestaltet werden.

Neue Struktur


Als Lösung verwende ich einheitliche Datenpunkte „DataNodes“. Jeder Ein- und Ausgang im System erstellt nun für sich ein DataNode. Die DataNodes werden global verwaltet und können über ein einheitliches Interface verbunden werden. Daher bezeichne ich diese Systemstruktur als „Unified Node Architecture“ („UNA“).

Der Vorteil von UNA besteht darin, dass die verschiedenen Module (Logikmodul, Gerätemodul), welche bisher hart über den Quellcode verbunden wurden, nun als Plug-Ins ausgelegt werden können, welche ihre IOs flexibel registrieren. Weiterhin können einfach zusätzliche Plugins hinzugefügt werden, welche mit den bestehenden Plug-Ins verbunden werden können. Zudem entfällt die starre Eingang-Logik-Augang-Struktur, da nun auch beispielsweise Geräte direkt ohne Logik verknüpft werden können, oder auch Ausgänge von Logikstrukturen als Eingang von weiteren Strukturen genutzt werden können.

Neue Systemstruktur: Die Module sind als Plug-Ins (blau) ausgelegt, die Verbindung erfolgt universell.

Nutzerinterface


Damit die neuen UNA-Nodes auch verbunden werden können, wurde ein neues Form erstellt. Das Verbinden von IOs war bisher ein vergleichsweise unübersichtlicher Prozess. Das verwendete Form war für die vier Kombinationen von Ein- und Ausgängen aus Logik- und Gerätesicht ausgelegt.

-    Eingang aus Logiksicht
-    Eingang aus Gerätesicht
-    Ausgang aus Logiksicht
-    Ausgang aus Gerätesicht

Für jede Variante war eigener Code vorhanden, welcher je Anwendung ausgewählt wurde. Das führte leider zu viel und zu unübersichtlichen Code, sowie zu einem unübersichtlichen Form.

Altes Verbindungsform:
Anzeige vorhandener Verbindungen
Altes Verbindungsform:
Auswahl möglicher Geräte
Altes Verbindungsform:
Erstellen einer Verbindung
Aus diesem Grund bin ich froh, dieses Verbindungsform durch ein neues ersetzen zu können. Da es nun nur noch DataNodes gibt, gestaltet sich das neue Form übersichtlicher. Es besteht aus zwei (TreeView-)Listen, welche alle existierenden Nodes anzeigen. Dabei wird unterschieden, ob ein Node zum Lesen oder zum Schreiben gedacht ist. Generell können alle Schreib-Nodes auch gelesen werden, jedoch nicht anders herum. Entsprechend dieser Information erscheint das Node auf der linken und/oder der rechten Seite.

Auf beiden Seiten können Nodes ausgewählt werden (türkis im Bild). Entsprechend wird die jeweils andere Liste nach Kompatibilität mit dem gewählten Node gefiltert. Gleichzeitig werden bereits verbundene Nodes farblich (grün im Bild) hervorgehoben. Ist auf beiden Seiten ein Node ausgewählt, so können diese über Buttons verbunden oder getrennt werden.

Neues Verbindungsform für UNA-Nodes
Möglicherweise ist dem ein oder anderen beim Betrachten des Bildes aufgefallen, dass sich dieses im Design von allen anderen Forms unterscheidet, die ich bisher in diesem und in anderen Einträgen gepostet habe. Dies ist darin begründet, dass ich dieses Form erstmals mit WPF („Windows-Presentation-Foundation“) erstellt und programmiert habe, während es sich bei allen anderen Forms um Windows-Forms handelt. Darauf möchte ich an dieser Stelle jedoch nicht näher eingehen.

Nutzerinterface-PlugIns


Neben der Neuerstellung des Verbindungs-Forms ist es durch die Umarbeitung des Geräte-Handlers und des Logik-Handlers zu serverseitigen Plug-Ins parallel nötig, die jeweiligen Bereiche in der Bedienoberfläche zu überarbeiten. Diese Module waren bisher fest in die Software integriert und konnten über einen Kommunikations-Kern (UI-Core) entsprechende Funktionen auf dem Server auslösen und Daten abrufen.

Alte Kommunikationsstruktur mit direkten Verbindungen
Da die Module serverseitig nun Plug-Ins sind, kann der Connection-Handler auf Serverseite eintreffende Requests nicht mehr einfach an die Module weitergeben. Daher habe ich die Module auf Client-Seite ebenfalls in Plug-Ins ausgelagert. Zur Kommunikation können diese nun Requests an einen Plugin-Handler weiterreichen. Dieser verpackt die Anfrage und schickt sie an den Server. Der Connection-Handler kann die Anfrage nun an den Plugin-Handler übergeben. Dieser entpackt die Anfrage wieder und sucht anhand einer angehängten Plug-In-ID  ein passendes Plug-In. Diesem übergibt er die Anfrage, welche dort verarbeitet wird.

Neue Kommunikationsstruktur mit Plug-Ins (blau)
Da die Umstrukturierung nun abgeschlossen ist, überlege ich aktuell, an welchen Stellen das System weiter ausgebaut und verbessert werden kann. Gleichzeitig sind bei mir weitere Geräte eingetroffen, die ich gern in das System einbinden möchte. Weiterhin prüfe ich derzeit, ob das System bereits weit genug entwickelt ist, um es als Anwendung oder als Quellcode auf GitHub zu veröffentlichen.

Über all diese Themen werde ich jedoch hier berichten, wenn die Zeit gekommen ist. Bis dahin bleibt mir nur, mich fürs Lesen zu bedanken und mich für heute zu verabschieden.

Vielen Dank!

Montag, 29. Oktober 2018

Sonoff S26

Hallo zusammen,

heute erreichte mich ein weiteres Päckchen aus China, welches zwei Sonoff S26 (F) enthielt. Dabei handelt es sich um schaltbare Zwischenstecker, welche per WLAN kommunizieren. Die Geräte kommen in einer einfach gehaltenen Pappbox, welche - wie bei allen anderen Sonoff-Geräten auch - nicht viel größer ist als die Geräte selbst.

Sonoff S26 in Originalverpackung
Sonoff S26 Vorder- und Rückansicht

Da ich nicht die Original-Firmware verwenden möchte, habe ich zunächst ein Gerät geöffnet. Dazu müssen einfach drei Schrauben auf der Rückseite entfernt werden. Dadurch lässt sich die Front abnehmen. Anschließend kann der TypF-Aufsatz abgenommen werden, sodass die blanken Kontakte sichtbar sind.

Sonoff S26 offen Sonoff S26 mit Typ-F Einsatz Sonoff S26 ohne Typ-F Einsatz
Als nächstes lässt sich die Rückseite abnehmen. Übrig bleibt dann nur noch die Platine und der Typ-F Stecker. Beim Betrachten der Platine fällt auf, dass der verbaute ESP8266 auf einer eigenen Platine untergebracht wurde. Leider fallen deswegen die Kontakte zum Flashen des ESP sehr klein aus und es lassen sich auch keine Stiftleisten anlöten.

Unterseite Platine Sonoff S26 Seitenansicht Platine Sonoff S26 mit Platine für ESP8266 Oberseite Platine Sonoff S26

Da der S26 die gleichen Pins wie der Standard-Sonoff zu verwenden scheint, konnte ich das dafür geschriebene Programm übernehmen, lediglich der Name des Gerätetyps musste angepasst werden. Auch die Parameter des verwendeten ESP8266 sind die gleichen wie beim Standard-Sonoff (1MB Flash). Als problematisch gestaltet sich das Flashen des Programms auf den S26. Nach verschiedenen Versuchen mit gebogenen Stiften als Federelemente habe ich letztendlich doch zum Lötkolben gegriffen und kurze Drähte angelötet.

Drähte zum Flashen des Sonoff S26
Das Flashen ist nun unproblematisch. Allerdings müssen diese Drähte nun bei jedem neuen Gerät erneut angebracht werden. Schöner wäre ein Adapter mit Federkontakten gewesen.

Letztendlich habe ich den S26 natürlich auch noch ausprobiert. Die Funktionen sind dieselben wie beim Standard-Sonoff. Als einziger Unterschied gibt es beim S26 zwei LEDs statt einer (blauen), welche einen Ring um den Taster an der Frontseite beleuchten. Die zusätzliche (rote) LED zeigt den Status des Relays an und ist daher fest mit diesem verbunden.

Die schaltbare Leistung des Gerätes ist mit 2200W (entspricht etwa 230V/10A) angegeben, die verwendeten Leitungen scheinen dafür auch gut dimensioniert zu sein. Beim Standard-Sonoff wäre ich dagegen mit einer solchen Leistung vorsichtig. Ein weiterer Vorteil gegenüber dem Standard-Sonoff ist das Vorhandensein eine Schutzkontaktes. Diese kann zwar beim Standard-Sonoff über eine dritte Ader, welche am Gerät vorbei geführt wird, realisiert werden, die hier verwendete Lösung ist aber natürlich besser.

Damit möchte ich mich auch für heute schon wieder verabschieden. Im nächsten Post werde ich voraussichlicht über die Umstrukturierung meines IoT-Systems berichten, da diese gut voranschreitet.

Markus

Sonntag, 21. Oktober 2018

IoT-System: Überarbeitung Datentypen

Hallo zusammen,

da ich aktuell ein wenig Zeit für mich zur Verfügung habe, ist es mir möglich mein IoT-System zu überarbeiten und auszubauen. Dafür habe ich mir überlegt, das Gesamtkonzept umzustellen, worauf ich jedoch heute noch nicht eingehen möchte. Bevor ich mit der Umstrukturierung beginnen kann, möchte ich noch ein paar Altlasten loswerden, welche ich bei der ersten Programmierung des Systems erzeugt habe. Auf eine dieser Lasten möchte ich heute eingehen.

Enumerationen


Als eines der grundlegendsten Elemente in der Programmierung des gesamten Systems existiert die Enumeration "IoTValueType". Eine Enumeration ist - einfach gesagt - ein Datentyp, welcher nur definierte Werte annehmen kann. Ein gutes Beispiel dafür ist ein Wochentag. Eine Enumeration vom Typ "Wochentag" kann logisch nur die Werte "Montag", "Dienstag", ... "Sonntag" annehmen. (mehr) Im Hintergrund kümmert sich dann der Compiler darum, dass jedem Enumerationswert ein passender Ganzzahlwert  zugeordnet wird. Diese Zuordnung kann auch per Hand durchgeführt werden.

Enumerationen können auch dafür genutzt werden, um mehrere der definierten Werte auszuwählen. Um beim gleichen Beispiel zu bleiben, könnte mit Hilfe der Enumeration "Wochentag" eine Auswahl erstellt werden, an welchen Tagen beispielsweise ein Wecker klingeln soll. Dafür muss die Enumeration mit dem "Flags-Attribut" markiert werden. Im Normalfall wird jedem Enumerationswert eine Ganzzahl zugeordnet:

(Montag = 0, Dienstag = 1, Mittwoch = 2, Donnerstag = 3, Freitag = 4, ...)

Mit dem Flags-Attribut wird jedem Wert ein Bit zugeordnet, wodurch sie praktisch Werte der Zweierpotenzreihe annehmen:

(Montag = 1, Dienstag = 2, Mittwoch = 4, Donnerstag = 8, Freitag = 16, ...)

Dadurch ist es nun möglich, Kombinationen zu bilden. Beispielsweise entspricht die Auswahl "Dienstag und Mittwoch" gleich der Zahl 6. Ohne das Flags-Attribut ergibt dieselbe Kombination die Zahl 3, welcher bereits "Donnerstag" zugeordnet ist. Es kann daher nicht unterschieden werden, ob bei einer 3 "Donnerstag" oder "Dienstag und Mittwoch" gemeint ist.

Ich habe nun bisher eine solche Enumeration verwendet, um die möglichen Datentypen im System zu definieren. Dadurch ist es nicht nur möglich gewesen, den genauen Datentyp eines Elements festzulegen, sondern mit Hilfe des Flags-Attributes auch eine Auswahl an Typen anzugeben, welche von einem Element unterstüzt werden. Dies habe ich insbesondere bei der Erstellung der Logikstrukturen verwendet, wie ich in einem früheren Post beschrieben habe.

IoTValueType-Enumeration
Die Verwendung einer Enumeration an dieser Stelle hat jedoch den Nachteil, dass die Auswahl an Datentypen auf die angegebenen Typen begrenzt ist. Eine Erweiterung ist daher nur möglich, indem neue Typen der Enumeration hinzugefügt werden. Ich habe zwar auch einen Datentyp "Objekt" hinzugefügt, welcher einen beliebigen Datentyp beschreibt, dies führt jedoch dazu, dass Elemente mit verschiedenen Objekt-Datentypen verknüpft werden können. Ein weiterer negativer Punkt ist, dass keine Arrays o.Ä. existieren. Arrays können zwar auch über den Objekt-Datentyp übergeben werden, aber das führt wieder zu dem eben genannten Verhalten.

DataType - Klasse


Um die aufgezeigten Probleme zu lösen, habe ich beschlossen die Enumeration "IoTValueType" zu ersetzen. Dafür habe ich mir zunächst überlegt, welche Arten von Datentypen es gibt und wie diese zu differenzieren sind. Grundsätzlich resultieren daraus drei Arten: Ein Grunddatentyp, welcher einfache Werte wie Ganzzahlen, Strings, etc. darstellt, ein Arraydatentyp, welcher eine Auflistung von Werten eines Datentyps darstellt und ein komplexer Datentyp, welcher eine Zusammenstellung verschiedener Datentypen darstellt. Die genannten Datentypen habe ich nun durch Klassen beschrieben, welche alle eine gemeinsame "DataType"-Basisklasse verwenden.

Datenstruktur "DataType"
Neben den im Bild dargestellten Feldern besitzt die DataType-Basisklasse noch weitere Felder und Methoden, um beispielsweise einem Datentyp Methoden zum Serialisieren und Deserialisieren zuzuordnen.

MultiDataType


Die Verwendung der genannten DataType-Klasse hat aber nun den Nachteil, dass jeweils nur exakt ein Datentyp verwendet werden kann. Daher habe ich zusätzlich eine Klasse MultiDataType angelegt, welche es ermöglicht mehrere mögliche Datentypen anzugeben.



using System;
using System.Collections.Generic;
using System.Linq;
using HomeAutomationServerExtensionInterface;
 
namespace LogicStructureEditor
{
 public class MultiDataType
 {

Das Prinzip des MultiDataType besteht darin, dass es zunächst vier Zustände besitzt. Diese sind "Alle", "Keine", "Alle außer" und "Keine außer". Bei der Nutzung einer der letzten beiden Zustände wird außerdem eine Liste verwendet, welche die Datentypen enthält, welche zulässig oder eben nicht zulässig sind.

Zudem definiert die Klasse Operatoren zum Vergleich und die Bitoperatoren OR, AND und die Bitweise-Negation. Diese werden verwendet, um das Verhalten der Enumeration des alten IoTValueType weiterverwenden zu können. Dadurch kann ein Großteil des geschriebenen Quellcodes weiterverwendet werden. Trotzdem war es nötig, große Teile des Quellcodes zu überarbeiten. Die Überarbeitung ist aber nun abgeschlossen und mein System läuft mit der überarbeiteten Variante.

Im nächsten Schritt werde ich die Anfangs erwähnte Umstrukturierung des Systems angehen. Dazu werde ich mich hier äußern, wenn ich dies abgeschlossen habe.

Bis dahin,
Markus

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!