Mit Java ins Internet der Dinge - Teil 2

Amazon IoT für Java: Device Shadows & Device Twins

Tam Hanna

© LuckyStep/Shutterstock.com

Device Shadows bzw. Device Twins sind ein interessanter Weg, um IoT-Verbünde auch beim Verlust von Teilen der Internetverbindung am Leben zu erhalten. Wie das funktioniert, klärt der folgende Artikel.

Bevor wir in medias res gehen, wollen wir noch eine Runde Unterschiedsbingo spielen. In der heutigen Welt hört man nämlich immer wieder vom Thema des Digital Twin. Ein Digital Twin ist im Grunde genommen ein neuartiger Begriff für eine umfangreiche Softwaresimulation eines realen Systems, die einem Consulting- oder Beratungsunternehmen große Umsätze bringen soll.

Unsere hier verwendeten Device Shadows arbeiten auf einem wesentlich „niedrigeren“ Level. Sie fungieren als eine Art Cache zwischen Gerät und Logik, um bei kurzfristigen Verbindungsausfällen das Weiterfunktionieren des Systems zu garantieren.

Wie funktioniert ein Device Shadow?

Wie so oft in der Welt der Cloud-getriebenen Informatik gilt auch hier, dass 5 000 Wege nach Rom führen. Im Hause Amazon setzt man zur Realisierung der als Device Shadow bezeichneten Funktion auf eine Gruppe von MQTT-Kanälen, über die sowohl Server als auch Client Änderungswünsche miteinander austauschen.

Hinter dem in der Dokumentation im Detail beschriebenen Verfahren steckt der Gedanke, dass das Serversystem beim Nichtbestehen einer Verbindung zum Endgerät mit den im Device Shadow vorliegenden Informationen arbeitet. Kommt das Gerät später wieder online, prüft es Änderungen am Server und aktualisiert den Shadow mit seinen aktuellen Messdaten. Zudem kann das Gerät auch nach Zustandsinformationen befragen, die es auf seine Umgebung anwendet.

Ein Klassiker sind alle Arten von langsam verändernden Zuständen wie beispielsweise eine Solltemperatur für einen Raum – dass Sie einen Device Shadow nicht zum Steuern einer latenzkritischen Aufgabe wie beispielsweise einer Turbinendrehzahl einsetzen sollten, nimmt der Autor als logisch an und führt es hier primär zur Vermeidung rechtlicher oder tödlicher Probleme durch Dummheit an.

Beschwörung eines Schattens

Im Artikel „AWS IoT Device SDK for Java“ [1] in der letzten Ausgabe haben wir festgestellt, dass das SDK vergleichsweise umfangreich ist. Als Lösung nutzten wir den Apache-Maven-Paketmanager, der die notwendigen .jar- und sonstigen Dateien für uns automatisch bereitstellte. Öffnen Sie die Eclipse IDE, und kehren Sie in das im letzten Artikel erzeugte Projektskelett zurück. Ersetzen Sie den im Einsprungpunkt befindlichen Code danach durch die in Listing 1 gezeigten Passagen.

public static void main(String[] args) {
  String clientEndpoint = "a3gw153wutrvel-ats.iot.eu-west-1.amazonaws.com";
  String clientId = "TamsGeraetInEclipse";
  
  AWSIotMqttClient client = new AWSIotMqttClient(clientEndpoint, clientId, "AaaaW", "iaaa5", null);
 
  try {
    client.connect();

Auch diesmal müssen wir damit beginnen, eine Instanz der Klasse AWSIotDevice zu erzeugen und unter Verwendung der beiden im letzten Heft generierten IDs am AWS-Server auszuweisen. Die beiden Schlüsselstrings sollten sich nicht geändert haben, wenn Sie im AWS Backend zwischenzeitlich keine Änderungen vorgenommen haben.

Überzeugen Sie sich im ersten Schritt vom korrekten Funktionieren unseres Programms, um danach die folgenden Änderungen durchzuführen:

try {
  ...
  client.connect();
 
  String state = "{\"state\":{\"reported\":{\"sensor\":3.0}}}";
  device.update(state);

Die Klasse AWSIotDevice dient als Dreh- und Angelpunkt zwischen einzelnen im AWS IoT Backend angelegten Geräteinstanzen. Der übergebene String TamsSensorAktorDevice muss dabei nicht unbedingt sprechend sein – wichtig ist aus Sicht von Amazon nur, dass er ein weltweit einzigartiges Gerät beschreibt. Wer beispielsweise mit einem ESP32 arbeitet oder sein Gerät mit einem Sensorchip ausstattet, kann die im Sensor enthaltenen Seriennummerdaten auch als ID für das Gesamtgerät verwenden. Eine dankbare Quelle sind dafür alle per OneWire verbundenen Sensoren – Abbildung 1 zeigt den relevanten Ausschnitt aus dem Datenblatt des weitverbreiteten DS18B20-Temperatursensors.

Abb. 1: OneWire-Peripheriegeräte bringen im Allgemeinen eine Seriennummer mit

Abb. 1: OneWire-Peripheriegeräte bringen im Allgemeinen eine Seriennummer mit

Wer das Programm im vorliegenden Zustand ausführt, stellt fest, dass es durchläuft. Im Backend bemerkt man allerdings nichts vom neu angelegten Geräteprofil.

Zur Umgehung dieses Problems müssen wir unseren Code ein wenig weiter anpassen, um Informationen in den Device Shadow zu schreiben:

try {
  ...
  client.connect();
  
  String state = "{\"state\":{\"reported\":{\"sensor\":3.0}}}";
  device.update(state);

Die Amazon-Dokumentation ist an dieser Stelle übrigens veraltet – die Updatefunktion erwartet nicht mehr ein Date-Objekt, sondern stattdessen einen JSON-String. Diese Vorgehensweise ist vernünftig, da es in Java – je nach Konfiguration Ihrer Arbeitsumgebung – schon eine oder sogar mehrere Stackklassen gibt.

Dieser an sich vernünftig wirkende Code lässt sich in der Cloud ebenfalls problemlos ausführen. Leider sehen wir auch hier im Backend kein Ergebnis. Zur Überprüfung des korrekten Verhaltens ändern wir den in state angelieferten Sensorwert, und schreiben ihn zudem vor der Anpassung des Werts nach außen:

System.out.println(device.get());
 
String state = "{\"state\":{\"reported\":{\"sensor\":9.0}}}";
device.update(state);

Wer die neue Version des Programms zum ersten Mal ausführt, bekommt den alten Sensorwert ausgegeben – es ist erwiesen, dass der Device Shadow die per Updatemethode eingelieferten Werte zwischen verschiedenen Aufrufen unseres Beispielprogramms persistiert.

Misstrauische Naturen „adjustieren“ an dieser Stelle den Wert, der an AWS IoT Device übergeben wird. Im Rahmen des nächsten Starts scheitert das Programm mit der in Abbildung 2 gezeigten Fehlermeldung – findet das AWS Backend keinen zu einem Gerät passenden Shadow, so scheitert der Aufruf der Methode.

Abb. 2: Das „direkte“ Laden des Device Shadow führt zu Exceptions

Abb. 2: Das „direkte“ Laden des Device Shadow führt zu Exceptions

Schatten lesen

Nachdem erwiesen ist, dass die per Deviceklasse in Richtung der Cloud geschriebenen Informationen für das Programm selbst lesbar sind, wollen wir einen primitiven Client-Server-Flow realisieren. Wir hatten im oben genannten Artikel festgestellt, dass jede Verbindung zwischen einer Applikation und dem AWS Backend eine weltweit einzigartige Client-ID aufweisen muss. Der bequemste Weg zur Befriedigung dieser Bedingung besteht darin, in Eclipse ein weiteres Maven-Projekt anzulegen, das AWS SDK abermals zu importieren und eine weitere Instanz von AWS-IoT-MQTT-Client zu erzeugen. Die Authentifizierungsstrings dürfen Sie wie bisher gewohnt eins zu eins übernehmen. Wichtig ist nur, dass der Wert der Client-ID nun anders lautet. Ein kleiner Test des Autors trug beispielsweise folgenden Namen:

public static void main(String[] args) {
  String clientEndpoint = ". . .amazonaws.com";      
  String clientId = "TamsLeserInEclipse";
 
  AWSIotMqttClient client = new AWSIotMqttClient(clientEndpoint, clientId, "AKaaaaaIW", "i2JRaaaaal5", null);

Die zweite Version des Programms unterscheidet sich von der bisher verwendeten Applikation nur dadurch, dass wir in Client-ID nun einen anderen Namen übergeben. Zur Laufzeit bedeutet das, dass die beiden Programme zumindest in der Theorie gleichzeitig mit dem AWS Backend verbunden sein dürfen.

Für einen ersten Test reicht es dann aus, die folgende Payload einzufügen:

try {
  AWSIotDevice device = new AWSIotDevice("TamsSensorAktorDevice");
  client.attach(device);
  client.connect();
  System.out.println(device.get());

AWSIoTDevice bekommt den weiter oben erstmals verwendeten String nochmals übergeben, um eine Verbindung zur selben Geräteinstanz in der Cloud herzustellen. An dieser Stelle können Sie das Leseprogramm auch schon ausführen – es wird in der Konsole den weiter oben in den Device Shadow geschriebenen String ausgeben:

{"state":{"reported":{"sensor":9.0}},

Im Interesse eines besseren Verständnisses des Device Shadow API möchte ich an dieser Stelle einen kurzen Blick auf den vom Server zurückgelieferten String werfen. Er beginnt mit der Sequenz {"state":{", die im allgemeinen den weiter oben übergebenen String mit dem Temperaturattribut aufweist.

Weiter unten findet sich dann allerdings noch der folgende zweite Teil:

"metadata":{"reported":{"sensor":{"timestamp":1580147822}}}, "version":3, "timestamp":1580417922,"clientToken": "481d1504-b23d-4a73-845b-552dd66580a8"}

Amazon realisiert als Teil von AWS für uns eine Art Versionierungssystem, das sich um das Hinzufügen diverser Metadateninformationen kümmert. Neben der Möglichkeit zum Festlegen von reported-Attributen, die von der Gerätehardware ausgezeichnet und für die Verwendung in der Cloud vorgesehen sind, gibt es auch noch die Verwendung von desired-Informationen. Diese gehen – logischerweise – den entgegengesetzten Weg. Wir werden ihre Verwendung in den folgenden Schritten näher ansehen.

Für Sie als Entwickler ist hier vor allem wichtig, dass sich der Inhalt des Strings jederzeit ansehen lässt – wenn sich Ihr Programm seltsam verhält, ist dies eine vernünftige Option zur Problembehebung.

Bequemere Schattenverarbeitung

Unser Device Shadow mag an dieser Stelle als Speicher funktionieren. Wirklich bequem ist das Handling nicht; wenn wir mehr als nur den einen Wert ablegen würden, müssten wir uns beispielsweise mit einer JSON-Deserialisierungsbibliothek rumärgern. Erfreulicherweise bietet das AWS IoT Device SDK for Java einen bequemeren Weg an, der diese auf den ersten Blick lästige Aufgabe wesentlich vereinfacht. Hierzu müssen wir im ersten Schritt in das Schreibprogramm zurückkehren, in dem wir eine von AWSIotDevice abgelegte Klasse anlegen. Die absolute Basisvariante, die sich auf die Erfüllung der Anforderungen des Compilers beschränkt, sieht folgendermaßen aus:

public class ShadowWorker extends AWSIotDevice {
  public ShadowWorker(String thingName) {
    super(thingName);
  }
}

ShadowWorker unterscheidet sich insofern von der gewöhnlichen Programmierung, als wir nun die abgeleitete Klasse verwenden. Zur Korrelation zwischen dem Objekt und der Repräsentation in der Cloud ist natürlich abermals ein Gerätestring erforderlich, der das System bzw. das Endgerät einzigartig identifiziert.

Das eigentliche Anliegen der in der Cloud zu speichernden Elemente erfolgt dann über das Attribut @ AWSIotDeviceProperty. Für einen ersten kleinen Versuch wollen wir unserem soeben angelegten ShadowWorker nach dem in Listing 2 gezeigten Schema zwei Attribute hinzufügen.

import com.amazonaws.services.iot.client.AWSIotDeviceProperty;
 
public class ShadowWorker extends AWSIotDevice {
  ...
  @AWSIotDeviceProperty
  private String myStringVal;
  @AWSIotDeviceProperty
  private float myFloatVal;
}

Für einen ersten Test müssen wir den Zugriffscode dann von der bisher verwendeten Klasse auch auf eine Instanz des Workers umstellen:

try {
  ShadowWorker device = new ShadowWorker("TamsSensorAktorDevice");
  client.attach(device);
  client.connect();

An sich ist das Programm an dieser Stelle zur Ausführung bereit – die Kompilation läuft problemlos durch, und die JVM beginnt auch mit dem Programmstart. Während der eigentlichen Ausführung sehen wir dann allerdings Fehler, die nach der in Listing 3 gezeigten Bauart aufgebaut sind.

com.fasterxml.jackson.databind.JsonMappingException: Unexpected IOException (of type java.io.IOException): java.lang.IllegalArgumentException: java.lang.NoSuchMethodException: ShadowWorker.getMyStringVal()
  at com.fasterxml.jackson.databind.JsonMappingException.fromUnexpectedIOE(JsonMappingException.java:338)

Amazon realisiert das AWS IoT Device SDK for Java unter Verwendung des jackson-JSON-Serialisierers. Er erwartet, dass jede mit dem Attribut AWSIotDeviceProperty ausgestattete Variable durch eine Getter- und eine Setter-Methode flankiert wird. Diese auf den ersten Blick pedantisch erscheinende Vorgehensweise ist insofern gerechtfertigt, als die Amazon Engine diese beiden Methoden zum Anmelden der vom Server ankommenden und zum Ernten von am Gerät befindlichen Informationen einspannen wird.

Zur Behebung der Fehlermeldungen reicht es aus, nach dem in Listing 4 gezeigten Schema sowohl Getter als auch Setter zu realisieren.

public String getMyStringVal() {
  return myStringVal;
}
public void setMyStringVal(String _x) {
  myStringVal = _x;
}
public float getMyFloatVal() {
  return myFloatVal;
}
public void setMyFloatVal(float _x) {
  myFloatVal = _x;
}

Im Interesse der besseren Überwachbarkeit des resultierenden Programms sollten Sie an dieser Stelle sowohl für die Getter als auch für die Setter Logging-Operationen platzieren. Im Interesse der Kompaktheit drucken wir diesen Code an dieser Stelle nicht ab, da er sich aus der Logik ergibt.

Wer das mit Instrumentierung ausgestattete Programm zur Ausführung freigibt, sieht nach dem Start hohe Aktivität. Danach ruft das AWS IoT Device SDK for Java von Haus aus alle fünf Sekunden einmal die Methode getMyStringVal auf. Wenn Sie auch in der für die Float-Variable vorgesehenen Methode Instrumentierung platzieren, finden sich zudem Aufrufe von getMyFloatVal.

Dieses auf den ersten Blick seltsame Eigenleben des AWS IoT Device SDK for Java ist darin begründet, dass die in reported vorgehaltenen Informationen am Server immer ein zumindest halbwegs aktuelles Abbild der physikalischen Realität um das Endgerät darstellen sollen. Von Haus aus legt Amazon dabei eine Übertragung für alle fünf Sekunden fest, was aus Sicht des „Buchhändlers“ einen akzeptablen Kompromiss zwischen Aktualität und Datenverbrauch darstellt.

Erfreulicherweise lässt sich die Aktualisierungsgeschwindigkeit des SDK über die setReportInterval-Methode beeinflussen:

long reportInterval = 15000;            // milliseconds.
device.setReportInterval(reportInterval);

Als besonders haarig erweist sich eigentlich nur, dass das AWS IoT Device SDK for Java den Aktualisierungsgeschwindigkeitswert in Millisekunden erwartet. Schon aufgrund von Netzwerklatenzen, RTC-Ungenauigkeiten und Co. dürfen Sie hier allerdings keine an einen Totalisator mit GPS-Disziplinierung erinnernde Genauigkeit erwarten.

An dieser Stelle können wir das Leseprogramm abermals ausführen. Es wird nun den folgenden String in die Konsole ausgeben, den wir hier im Interesse der Übersichtlichkeit gekürzt abdrucken:

{"state":{"reported":{"sensor":9.0,"myFloatVal":0.0}},"metadata": {"reported":{"sensor":{"timestamp":1580419131},"myFloatVal":  {"timestamp":1580419144}}},"version":14, ...

Auffällig ist, dass der von Haus aus leere Stringwert nicht im Device Shadow auftaucht. Stattdessen erscheint neu nur das Float, da die Software Standarddefaultwerte von null nicht von einer Null unterscheiden kann, die ein Algorithmus im Rahmen einer Messung in die Speicherstelle abgelegt hat.

Zur Herstellung eines konsistenteren Zustands könnten wir das Schreibprogramm folgendermaßen adaptieren:

client.connect();
device.delete();
device.setMyStringVal("Hallo Welt!");
device.setMyFloatVal((float) 2.2);

Die neue Version ruft im ersten Schritt delete auf, um alle schon im Device Shadow befindlichen Informationen zu löschen. Danach aktualisieren wir die Inhalte, um dem Leseprogramm einen konsistenten Zustand anzubieten. Wer es an dieser Stelle abermals zur Ausführung freigibt, bekommt nun das folgende Ergebnis:

{"state":{"reported":{"myStringVal":"Hallo Welt!","myFloatVal":2.2}}

Ergebnisüberprüfung im Backend

An dieser Stelle sind Sie möglicherweise dazu motiviert, fortgeschrittene Versuche durchzuführen. Hierbei ist es wünschenswert, wenn man die Inhalte des Device Shadows bzw. sogar des ganzen Geräts auch im weiter oben verwendeten Browser-Backend verwenden kann.

Amazon erweist sich dabei insofern als unkooperativ, als der Buchhändler die diesbezüglichen Informationen von Haus aus nicht anzeigt. Sie müssen stattdessen im ersten Schritt in die Rubrik Manage | Things wechseln, die Sie bei einem „jungfräulichen“ AWS-Konto mit der Aufforderung „Register a thing“. begrüßt. Klicken Sie den Knopf an, um den in der Fachliteratur auch als an Boarding bezeichneten Geräteanmeldeprozess zu aktivieren.

Cloud-Anbieter liefern sich seit längerer Zeit durchaus erbitterte Gefechte darum, wer die Anmeldung einer größeren Kohorte von Hardwareendgeräten am bequemsten bewerkstelligt. Da wir im Moment allerdings nur die Informationen unseres in Java lebenden Geräts sichtbar machen wollen, entscheiden wir uns für den zur Option „Register a single AWS IoT thing“ gehörenden Aktionsknopf.

Im ersten Schritt erfragt der Assistent vergleichsweise umfangreiche Konfigurationseinstellungen und erlaubt auch, unser Ding einer Gruppe zuzuweisen. Im Interesse der Bequemlichkeit vergeben wir hier als Namen den String TamsSensorAktorDevice, der von weiter oben bekannt sein dürfte. Der Rest des Formulars bleibt unberührt. Im zweiten Schritt entscheiden wir uns für die Option „Skip certificate and create thing“. Sie weist das AWS Backend dazu an, unser Ding ohne fortgeschrittene typografische Zertifikate zur Identitätssicherung zu erzeugen.

An dieser Stelle ist die Arbeit auch schon abgeschlossen, die Ergebnisse des Prozesses lassen sich, wie in Abbildung 3 gezeigt, sofort sehen.

Abb. 3: Nach der Extraeinladung erscheinen die Inhalte am Bildschirm

Abb. 3: Nach der Extraeinladung erscheinen die Inhalte am Bildschirm

Hier bietet sich ein kleiner Test an: Starten Sie das Schreibprogramm und versuchen Sie danach, unter Verwendung des Shadow-State-Werkzeugs eine Änderung des Strings durchzuführen. Der Amazon-Server wird diese zwar im ersten Schritt quittieren, um sie danach aber wieder abzulehnen. Ursache dieses auf den ersten Blick unnatürlich erscheinenden Verhaltens ist, dass das Device-Shadow-Dokument neben dem hier bearbeitenden Block auch einen Desired-Block aufweist, der für die Übertragung von am Server durchgeführten Änderungen in Richtung der physikalischen Hardware verantwortlich ist.

Zur Behebung des Problems müssen wir, wie in Listing 5 gezeigt, den JSON-String adaptieren, um die gewünschte Änderung über das Desired-Feld anzuliefern.

{
  "reported": {
    "myStringVal": "Hallo Welt!",
    "myFloatVal": 2.2
  },
    "desired": {
    "myStringVal": "Hallo SUS!",
    "myFloatVal": 3.3
  }
}

Wundern Sie sich nicht, wenn während der Abarbeitung der Payload kurzfristig ein JSON-String mit einem Deltablock aufscheint. Er liefert Informationen darüber, welche Änderung im Datenbestand gerade erfolgt ist. Der Lohn der Mühen ist die in Abbildung 4 gezeigte Aktualisierung, die sich auch am Endgerät auswirkt.

Abb. 4: Die Änderung vom Server wirkt sofort

Abb. 4: Die Änderung vom Server wirkt sofort

Fazit

Device Shadows mögen bei der Realisierung von Eins-zu-eins-Verbindungen zwischen Cloud-Dienst und Server nicht wirklich hilfreich sein. Ihre Macht spielen sie in dem Moment aus, in dem der Entwickler kompliziertere Verfahren realisieren möchte und sich dabei nicht mit dem Verbindungszustand rumärgern will. Das ist übrigens eine Aufgabe, an der wenig Embedded-erfahrene Entwickler routiniert verzweifeln.

Damit wollen wir unsere Reise in die Welt des AWS API für Java allerdings auch schon beenden. Wir hoffen, dass Sie mit diesem kleinen Exkurs jede Menge Spaß hatten.

Geschrieben von
Tam Hanna
Tam Hanna
Tam Hanna befasst sich seit der Zeit des Palm IIIc mit der Programmierung und Anwendung von Handcomputern. Er entwickelt Programme für diverse Plattformen, betreibt Onlinenewsdienste zum Thema und steht unter tamhan@tamoggemon.com für Fragen, Trainings und Vorträge gern zur Verfügung.
Kommentare

Hinterlasse einen Kommentar

avatar
4000
  Subscribe  
Benachrichtige mich zu: