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!