Mobile Technology Digital 31
Author: Dr. Matthias Berning; Dirk Dorsch; Tam Hanna; Christian Haß; Nils Mehlhorn
Date: 2018-04-16T22:00:00+00:00
Keywords: Firebase; Testing; Cross-Plattform; NativeScript; Angular; Arduino; Android; Android Things; Bluetooth LE; App-Entwicklung
Description: Mobile Technology ist der Wegbegleiter für alle, die sich professionell mit der Entwicklung für mobile Devices und den Möglichkeiten, die der Markt des Mobile Business und Marketing bereithält, beschäftigen. Das Magazin deckt dabei die gesamte Bandbreite an Plattformen und Technologien ab und bringt dem Leser nicht nur Development-Themen für die verschiedenen Mobile OS wie Android, iOS oder Windows Phone näher, sondern beleuchtet auch deren Einsatz im Enterprise-Umfeld. Es bietet darüber hinaus einen tiefen Einblick in die Chancen, die der Markt des Mobile Business und Marketing bereithält. **
Publisher: entwickler.press
Rights: © 2018 entwickler.press. Alle Rechte, auch für Übersetzungen, sind vorbehalten. Reproduktion jeglicher Art (Fotokopie, Nachdruck, Mikrofilm, Erfassung auf elektronischen Datenträgern oder andere Verfahren) nur mit schriftlicher Genehmigung des Verlags. Jegliche Haftung fürFirebase, Testing, Cross-Plattform, NativeScript, Angular, Arduino, Android, Android Things, Bluetooth LE, App-Entwicklung die Richtigkeit des gesamten Werks, kann, trotz sorgfältiger Prüfung durch Autor und Verlag, nicht übernommen werden. Die im Buch genannten Produkte, Warenzeichen und Firmennamen sind in der Regel

Mobile Technology Digital
Volume 31
ISBN: 978-3-86802-792-1
© 2018 Software & Support Media GmbH

Eine Publikation des Mobile Technology
in Kooperation mit entwickler.press

Ihr Kontakt zu Verlag und Redaktion:
Software & Support Media GmbH
entwickler.press
Schwedlerstraße 8
60314 Frankfurt
Tel: +49 (0)69 630089-0
Fax: +49 (0)69 630089-69
lektorat@entwickler-press.de
http://www.entwickler-press.de

Redaktion: Hartmut Schlosser
Korrektorat/Schlussredaktion: Nicole Bechtel, Jonas Bergmeister, Frauke Pesch
Satz: meat* – concept and design
Autoren: Dr. Matthias Berning, Dirk Dorsch, Tam Hanna, Christian Haß, Nils Mehlhorn

Alle Rechte, auch für Übersetzungen, sind vorbehalten. Reproduktion jeglicher Art (Fotokopie, Nachdruck, Mikrofilm, Erfassung auf elektronischen Datenträgern oder andere Verfahren) nur mit schriftlicher Genehmigung des Verlags. Jegliche Haftung für die Richtigkeit des gesamten Werks, kann, trotz sorgfältiger Prüfung durch Autor und Verlag, nicht übernommen werden. Die im Kompendium genannten Produkte, Warenzeichen und Firmennamen sind in der Regel durch deren Inhaber geschützt.

Mit Googles Firebase Apps auf Herz und Nieren testen

Nichts anbrennen lassen
von Tam Hanna

Google bietet mit Firebase einen Onlinedienst an, der von Nokias mittlerweile legendärem RDA inspiriert wurde. Die dahinterstehende Idee ist einfach: Ein über das Internet ansprechbares Testlabor hält eine Vielzahl von Geräten vor, die mit der vom Entwickler angelegten Software ausgestattet werden.

Zu Zeiten von Palm OS hatten kleine und große Entwickler einen ganzen Stapel Testgeräte – der Haufen der in Bratislava ansässigen Resco war geradezu episch. Heute ist diese Vorgehensweise nicht wirklich zielführend: Erstens ist die Anzahl der verfügbaren Geräte geradezu erschlagend, zweitens müssen Sie sich als Entwickler zudem mit einer Vielzahl verschiedener Versionen herumschlagen. Wer einige Male verschiedene ROMs auf ein Testgerät spielt, lernt bald mehrsprachiges Fluchen. Die beste Lösung für das Problem ist Spezialisierung, bei der Firebase helfen kann.

Der wichtigste Unterschied zwischen RDA und Firebase ist, dass Firebase ferngesteuert abläuft – der Entwickler muss also nicht, wie einst bei Nokias Plattform, auf den Geräten herumklicken und sich mit langsamer Bildschirmaktualisierung per VNC herumärgern. Was dies in der Praxis bedeutet und wie Sie die Vorteile von Firebase in Anspruch nehmen können, wollen wir in den folgenden Schritten näher analysieren.

Gleich ran

Als erste Aufgabe wollen wir die Firebase-Konsole öffnen [1]. Sie dient als Schnittstelle zwischen dem Entwickler und den diversen im Google Backend angelegten Services. Klicken Sie auf die Option Explore a Demo Project und nicken Sie die Lizenzanfrage ab. Wir wollen in den folgenden Schritten auf von Google bereitgestellte Programmbeispiele setzen, weil wir uns hier auf die eigentliche Vorstellung der Testverfahren beschränken wollen und lange Diskussionen der Testharnische nicht zielführend sind.

Nach dem erstmaligen Einblenden finden Sie sich in der Demoapplikation MechaHamster wieder: Klicken Sie auf die Combobox, um stattdessen in das Programm Flood It! zu wechseln. Am Bildschirm sehen Sie sodann eine Liste von als Matrix bezeichneten Testläufen, die die eigentlichen Ergebnisse der Analyseprozesse zutage fördern. Das Anklicken einer der Testläufe bietet weitere Informationen über die jeweiligen Ergebnisse – im Fall des von Google bereitgestellten Projekts sind einige der Tests nur noch teilweise zugänglich, weil das Backend Daten nach 90 Tagen löscht (Abb. 1).

Abb. 1: Google sollte Flood It! und MechaHamster häufiger testen

Da die beiden von Google bereitgestellten Sample-Projekte von Entwicklern nicht verändert werden dürfen, sollten Sie alle im Studio geöffneten Projekte durch Anklicken von File | Close Project schließen. Wählen Sie dann die Option Import an Android Code Sample aus, um Android Studio zum Herunterladen aller in der IDE verfügbaren Codebeispiele aus dem Hause Google zu animieren.

Wählen Sie im nächsten Schritt Ui | Horizontal Paging und klicken Sie auf Next, um das Herunterladen des Programms anzuweisen. Die einzelnen Arbeitsschritte des Assistenten sind im Großen und Ganzen selbsterklärend – dank des Gradle-Build-Systems kann Android Studio fehlende Komponenten dynamisch aus dem Internet herunterladen.

Klicken Sie im nächsten Schritt auf den Knopf Create a Project, um den Assistenten zum Anlegen eines neuen Projektskeletts auf den Bildschirm zu holen. Google zwingt Ihnen hierbei das Anlegen eines vollwertigen Firebase-Projekts auf; vergeben Sie als Namen, was immer Sie als sinnvoll erachten. Nach dem erfolgreichen Anlegen finden Sie sich auf dem Startbildschirm wieder, der fortgeschrittene Features von Firebase feilbietet. Wer, wie wir, allerdings nur testen möchte, klickt im Menü auf Stability | Test Lab.

Das Anlegen eines einfachen Tests erfolgt dabei ohne komplizierte Interaktionen mit Android Studio. Weisen Sie die IDE einfach dazu an, eine APK-Datei (Android Package Kit) zu erzeugen, und jagen Sie diese per Drag and Drop in Richtung von Googles Servern. Nach dem Hochladen der Datei – sie muss nicht signiert sein, sondern kann einfach durch Anklicken der Menüoption Build APKs erzeugt werden – läuft ein Validierungstest durch, der einige Sekunden in Anspruch nimmt.

Danach finden Sie sich in einer neuen Testmatrix wieder, die die gerade hochgeladene APK-Datei zur Bearbeitung bereitstellt. Zudem weist Google automatisch einen ersten Robo test an, dessen Ausführung durch den von Android bekannten Wartekreisel angezeigt wird und zeitlich einige Minuten in Anspruch nehmen kann. Zur Füllung dieser Totzeit wollen wir hier einige Überlegungen anstellen, um mehr über den ablaufenden Prozess zu erfahren.

Mit der Herrschaft des Zufalls

Vor vielen Jahren setzte man im Hause PalmSource auf ein als „Gremlins“ bezeichnetes Testwerkzeug. Die dahinterstehende Idee war einfach: Ein im Emulator auf der Workstation laufendes Palm-OS-Programm wurde mit vom Zufallsgenerator erzeugten Tapp- und Eingabeereignissen ausgestattet. Je nach Aufbau des Programms konnte man so tief oder weniger tief in die Benutzerschnittstelle eindringen. Regelmäßige Aktivierungen der diversen Systemereignisse (wie des berüchtigten Global Find) sorgten dafür, dass Probleme in der Interaktion zwischen Applikationsprogramm und Betriebssystem untersucht werden konnten. Ärgerlicherweise waren Gremlins bei der Fehlersuche nicht immer effizient. Das lag vor allem daran, dass man nach dem Auftreten eines Fatal Alerts nicht oder nur teilweise wusste, welche Klick- und Eingabesequenz das Programm am Ende über den Jordan schickte.

Google bietet mit Monkey ein ähnliches Werkzeug an, das in der Praxis allerdings unter den soeben besprochenen Problemen leidet. Robo test umgeht dieses Problem durch eine deterministische Vorgehensweise (Kasten: „Besser schlicht und einfach“). Hinter dieser auf den ersten Blick akademisch klingenden Ausdrucksweise verbirgt sich der Gedanke, dass ein Testlauf immer auf dieselbe Art und Weise abgearbeitet wird. Entwicklern stehen nur zwei Parameter zur Verfügung: Erstens Maximum Depth, mit dem sie die Tiefe festlegen dürfen. Ist der Wert weniger als 2, so verbleibt Robo test immer nur am Hauptbildschirm. Allgemein ist die dahinterstehende Vorgehensweise insofern einfach, als dass nach dem ersten Klick vom Hauptbildschirm aus ein Zähler mitläuft, der so lange erhöht wird, bis das Maximum erreicht wird. Danach kehrt Robo test zum Hauptbildschirm zurück, um einen neuen Durchlauf zu starten.

Besser schlicht und einfach

Da Robo test mit den Steuerelementen auf eine intelligente Art und Weise zu interagieren sucht, muss das Programm über den inneren Aufbau der Views Bescheid wissen. Daraus folgt, dass WebViews und andere Elemente, die nicht aus dem GUI-Stack selbst kommen, nicht getestet werden können. Dass das Vorhandensein von Captchas dem Testprozess ebenfalls nicht zuträglich ist, folgt aus der Logik.

Kriterium Nummer zwei ist der Time-out. Zufallsgetriebene Tests können systembedingt nicht feststellen, wann ein Programm komplett getestet wurde – sie rackern einfach so lange, bis es dem Entwickler um die Rechenleistung schade ist. Im Fall von Robo test empfiehlt Google in der Dokumentation zumindest 2 Minuten Rechenzeit für kleine Programme, während komplexere Programme 5 Minuten bekommen sollten. Das ist auch der Standardwert, der von Android Studio beim direkten Hochladen einer APK-Datei angefordert wird.

Haben Sie unser APK vor dem Lesen dieses Abschnitts hochgeladen, so sollte ungefähr nun eine E-Mail von Google eingehen, die sie darüber informiert, dass der Test des Beispiels erfolgreich abgearbeitet wurde. Dies können Sie auch daran erkennen, dass der Wartekreisel durch einen grünen Haken ersetzt wurde.

Er kompiliert nicht?

Google hat die unangenehme Eigenschaft, einmal veröffentlichte Programmbeispiele nicht oder nur sehr leidlich zu warten. Wundern Sie sich deshalb nicht darüber, wenn Sie Targets, minSdk-Version und andere Attribute Ihres Beispielprojekts anpassen müssen, bevor dieses kompilierbar ist.

Da der von Google vorgegebene Test ein beliebiges Telefon auswählt und sonst nicht viel treibt, klicken wir im nächsten Schritt auf die Option Run a Test. Das Backend bietet Ihnen daraufhin die Option an, verschiedene Arten von Test anzulegen. Wir wollen in den folgenden Schritten abermals einen Robo test ausführen und laden die APK-Datei wie gewohnt hoch.

Bei einem nicht von Google angelegten Test dürfen Sie im nächsten Schritt unter dem etwas irreführenden Namen Select dimensions auswählen, welche Geräte zum Test eingesetzt werden sollen. Achten Sie dabei darauf, dass Google physikalische und virtuelle Geräte unterscheidet: Ein physikalisches Gerät ist ein in einem Serverrack stehendes Android-Smartphone, während ein virtuelles Gerät ein von Google gehostetes Android Virtual Device (AVD) ist. Zum Zeitpunkt der Drucklegung bietet Google einige Dutzend verschiedener Geräte an, in manchen Fällen dürfen sie – wie in Abbildung 2 gezeigt – sogar zwischen verschiedenen Versionen des Betriebssystems wählen.

Abb. 2: Die komplette Auswahl passt nicht auf den Bildschirm

Auf der Unterseite des Bildschirms können Sie beim Aktivieren des Schalters zur Bearbeitung fortgeschrittener Optionen auch die weiter oben besprochenen Parameter einstellen. Sodann können Sie den Test ausführen. Ich empfehle für die folgenden Schritte allerdings, dies nicht zu tun. Mit einem kostenlosen Account dürfen Sie pro Tag nämlich nur fünf reale und zehn virtuelle Gerätetests durchführen, danach müssen Sie bis 12 Uhr (PST) warten. Auch im niederen bezahlten Plan bleibt diese Beschränkung aufrecht: Wer unlimitiert testen möchte, muss die sehr teure Blaze-Option buchen. Reale Geräte kosten 5 US-Dollar pro Stunde, während Tests in AVDs mit 1 US-Dollar pro Stunde zu Buche schlagen.

Auswertung der Ergebnisse

Da Google nach dem Hochladen unserer APK-Datei sowieso einen Test durchgeführt hat, klicken wir nun auf den Namen des Tests, um die schon von den Beispielprojekten bekannte Liste der Testmatrizen auf den Bildschirm zu holen. Das von Google durchgeführte Experiment beschränkte sich dabei auf 1 Pixel API-Level 26. Beim Anklicken dieser Option kommen Sie auf den in Abbildung 3 gezeigten Bildschirmabschnitt.

Abb. 3: Die Auswertungsmöglichkeiten von Firebase Test Lab sind erstklassig

Neben der zu erwartenden Ausgabe von LogCat gibt es auch Screenshots, ein Video und sogar eine Aktivitätsübersicht, die eine Art Flussdiagramm der durch das Programm laufenden Bewegungen darstellt. Kurz gesagt ist es nicht sonderlich schwer, den Verlauf zu analysieren.

Zufall mit Regel

Es ist oft wünschenswert, eine Gruppe von Aktionen bei jedem Durchlauf von Robo test abzuarbeiten. Dies lässt sich über Skripte bewerkstelligen. Es handelt sich dabei um eine Anweisungsliste, die vom Runner abgearbeitet werden muss, bevor er sich – so noch Zeit übrig ist – der gewohnten zufälligen Belästigung der Applikation zuwenden kann.

Bevor wir uns mit der eigentlichen Realisierung eines Robo-test-Skripts auseinandersetzen, sei ein allgemeiner Hinweis erlaubt. Robo test beschränkt sich auch beim Hochladen eines Skripts auf jene Elemente, die hausgemacht sind. Enthält Ihr Programm ein Social-Media-SDK, so wird der Test dieses bzw. die von ihm erzeugten Formulare auch dann nicht berühren, wenn Sie die Klickfolge im Skript anlegen.

Kehren Sie zum Anlegen eines neuen Skripts in Android Studio zurück und klicken auf Tools | Firebase. Die IDE reagiert darauf mit dem Einblenden eines Assistentenfensters, in dem sie die Rubrik Test Lab anklicken. Im nächsten Schritt wählen Sie die Option Record Robo Script and use it to guide Robo Test aus, um den dafür vorgesehenen Wizard auf den Bildschirm zu holen.

Die erste Aufgabe ist das Anklicken des Buttons Record Robo Script: Android Studio blendet daraufhin den vom gewöhnlichen Debugger bekannten ADB-Dialog ein, in dem Sie entweder ein virtuelles oder ein reales Gerät auswählen dürfen. Es wird sodann wie gewohnt angeworfen und mit der zum Testen vorgesehenen Applikation beladen. Nach dem erfolgreichen Start des Programms erscheint ein leeres Fenster mit der Überschrift Record your Robo Script. Klicken Sie im Emulator herum, um zu sehen, dass das Fenster wie in Abbildung 4 gezeigt mit Informationen befüllt wird.

Abb. 4: Das Anklicken von Steuerelementen sorgt dafür, dass Ereignisse in der Ereignisliste aufscheinen

Nach dem Anklicken von OK können Sie die Datei an einen beliebigen Platz speichern, um sie später im Rahmen eines automatisierten Anlegens eines neuen Laufs hochzuladen. Da sich ein geskripteter Testlauf im Großen und Ganzen wie seine gewöhnlichen Kollegen verhält, wollen wir auf sein Handling hier schon aus Platzgründen nicht weiter eingehen.

Test in Akkurat

Mögen zufällige Tests besser als nichts sein, so wünscht man sich in der Praxis doch irgendwann die Möglichkeit, komplexere Untersuchungen durchzuführen. Firebase Testing Labs realisiert dies über sogenannte Instrumentierungstests, die auf Basis der Testframeworks Espresso und UI Automator 2.0 durchgeführt werden (Kasten: „IP-Adressbereiche“). Die in früheren Versionen des Produkts enthaltene Unterstützung für Robotium wurde mittlerweile ersatzlos eingestellt, diesbezügliche Tutorien sind nicht mehr anwendbar.

IP-Adressbereiche

Falls Sie virtuelle bzw. im Test-Lab befindliche Geräte zur Laufzeit erkennen möchten, können Sie dies über die von Google veröffentlichten IP-Adressbereiche tun. Zum Zeitpunkt der Drucklegung sind alle realen Geräte im Bereich 108.177.6.0/23 angesiedelt, während virtuelle Maschinen einige Subnetze für sich haben:

Virtual devices
35.192.160.56/29
35.196.166.80/29
35.196.169.240/29
35.203.128.0/28
199.192.115.0/30
199.192.115.8/30
199.192.115.16/29

Der Aufwand für die Einrichtung eines Tests mag nicht unerheblich sein, amortisiert sich in der Praxis aber. Ein Test wird auf einem physikalischen Gerät nämlich mit bis zu 30 Minuten Rechenzeit ausgestattet, während Sie auf einer virtuellen Maschine sogar eine Stunde lang für Durchsatz sorgen dürfen.

Beachten Sie, dass Google in der Dokumentation die Nutzung von Firebase Test für Stresstests des Backends explizit untersagt. Im Moment ist zwar nicht bekannt, wie man dies umsetzt. Da der Suchmaschinenanbieter allerdings auch der Betreiber des größten Distributionskanals ist, ist Paranoia empfehlenswert.

Wer mit aktuellen Versionen von Android Studio ein Projektskelett erzeugt, bekommt zwei Gruppen von automatisierten Tests eingeschrieben. Erstens die im Verzeichnis module-name/src/test/java/ liegenden klassischen Unit-Tests, die mit einer beliebigen VM ausgeführt werden und zum Überprüfen der Korrektheit von Algorithmen vorgesehen sind. Zweitens gibt es die im Ordner module-name/src/androidTest/java/ liegenden Teile, die für das Testen der Benutzerschnittstelle unter Nutzung der diversen Instrumentierungs-APIs auf der Android-Plattform vorgesehen sind. Da Google keine wirklich brauchbaren Beispiele anbietet, erstellen wir an dieser Stelle von Hand ein neues Projektskelett. In den Instrumentierungstests findet sich der in Listing 1 gezeigte Testfall, der die Struktur demonstriert.

@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("com.tamoggemon.susinstrumentation", appContext.getPackageName());
}
}

Listing 1

Instrumentierungstests sind unwissenschaftlich betrachtet Unit-Testfälle, die in einer Android JVM ausgeführt werden. Daraus folgt, dass Sie diverse Android-Systembestandteile in Ihre Unit-Tests einbeziehen können. Wir nutzen beispielsweise assertEquals, um den Paketnamen mit dem Rückgabewert der Android-Systemfunktion appContext.getPackageName() zu vergleichen.

Zum Testen der diversen GUI-Elemente bietet das Testframework eine Gruppe von Unterstützungsklassen an. Zum Überprüfen der Inhalte von Activities empfiehlt sich die etwas seltsam benannte Klasse ActivityInstrumentationTestCase2 – sie erzeugt die Activity bei Bedarf und kann eine am Bildschirm befindliche Instanz allerdings ebenfalls ergreifen. Zu ihrer Nutzung müssen wir im ersten Schritt eine neue Klasse im Testprojekt anlegen, deren Deklaration folgendermaßen aussieht:

public class TestTheActivity extends ActivityInstrumentationTestCase2<MainActivity> {
public TestTheActivity() {
super("com.tamoggemon.susinstrumentation", MainActivity.class);
}

Eine weitere haarige Stelle ist das Anlegen von Testfällen. Das Ausführungsframework erreicht diese nämlich durch Reflektion des jeweiligen Funktionsnamens, der mit dem String test beginnen muss. Unser erster, nicht sonderlich aussagekräftiger Test sieht dann folgendermaßen aus:

public void testTheTitle(){
Activity activity = getActivity();
assertEquals("SUSInstrumentation",activity.getTitle());
}
}

Da die Ausführung von Testfällen in der Cloud die weiter oben genannten Credits verbraucht, ist es empfehlenswert, vor dem Hochladen einen Test unter Nutzung eines Emulators oder eines lokalen physikalischen Geräts loszulassen. Hierzu reicht es aus, den Ordner mit den Testfällen in Android Studio rechts anzuklicken und die Option Run zu wählen – das Produkt kümmert sich dann selbsttätig darum, eine Ausführungsumgebung aufzusetzen und einen ersten Probelauf durchzuführen. Scheitert dieser schon lokal, so wissen Sie, dass Sie ein Problem haben.

Von besonderem Interesse ist die Reaktion des Produkts beim Erzeugen einer APK-Datei: Anstatt wie gewöhnlich ein APK zu generieren, erzeugt Android Studio nun zwei Dateien (Abb. 5).

Abb. 5: Android Studio generiert beim Erzeugen des APK zwei APK-Dateien
app-debug.apk ist hierbei an das gewöhnliche Applikations-APK angehängt, während app-debug-androidTest.apk die diversen Testfälle bereitstellt. Beim Anlegen einer neuen Testmatrix sind beide Dateien erforderlich. Laden Sie die Files hoch und folgen Sie den Anweisungen am Bildschirm, um wie gewohnt Geräte auszuwählen.

Ludologischer Test

Spiele zu testen, war noch nie leicht: Zufällige Eingaben führen nicht zum Ziel, zudem sorgt die Nutzung von Engines wie Unity dafür, dass intelligente Test-Runner wie Robo an der Evaluation des Benutzerinterface scheitern. Das liegt daran, dass die Engine normalerweise direkt in den Framebuffer zeichnet – View, Button, TextView und sonstige per Reflexion sichtbare Klassen sucht man im Allgemeinen vergebens.

Google begegnet diesem Problem durch ein als Game-Loop-Test bezeichnetes Feature, das derzeit allerdings noch in der Betaphase ist. Wir möchten seine Möglichkeiten allerdings trotzdem kurz anschneiden. Die Nutzung eines Game-Loop-Tests beginnt mit dem Einbinden eines speziellen Intent-Filters, der nach dem in Listing 2 gezeigten Schema aufgebaut sein muss.

<intent-filter>
<action android:name="com.google.intent.action.TEST_LOOP"/>

<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="application/javascript"/>
</intent-filter>

Listing 2

Wichtig ist in diesem Zusammenhang nur, dass es ausschließlich für die Verwendung der Game-Loop-Tests vorgesehen sind – es ist nicht erlaubt, die Filter zu kombinieren. Andererseits spricht allerdings nichts dagegen, die Activity mit mehreren unabhängigen Filtern auszustatten. Zur Laufzeit muss die jeweilige Activity überprüfen, ob sie normal oder im Schleifenmodus angeworfen wurde. In zweitem Fall muss Ihr Code einen Selbsttest durchlaufen; über dessen Aufbau macht Google keine Angaben.

Für die eigentliche Ausführung der Game-Loops steht ein als Test-Loop-Manager bezeichnetes Tool zur Verfügung. Es handelt sich dabei um eine Art Runner, mit dem sie die Game-Loop-bezogenen Tests auf der Workstation oder auf einem Testgerät abfeuern können, um sicherzustellen, dass alles wie erwartet funktioniert.

Zu guter Letzt laden Sie auch hier die generierten APKs hoch, um den Testfall auf die schon von den anderen Methoden bekannte Vorgehensweise zur Ausführung bereitzumachen.

Fazit

Informatiker neigen dazu, an die Existenz von Silberkugeln zu glauben. Firebase Test Lab ist dies mit Sicherheit nicht – drei Dutzend Geräte decken den Gesamtmarkt nicht ab. Dies bedeutet allerdings nicht, dass das Produkt keinen Platz in der Werkzeugkiste eines diensterfahrenen Android-Entwicklers haben sollte. Sind Tests erst automatisiert, so können Sie sie mit minimalem Aufwand ausführen. Auf die Art entsteht eine zusätzliche Qualitätssicherung, die den einen oder anderen Fehler einfängt.

Fehlervermeidung bleibt eine olympische Disziplin: Je mehr Sicherheitsmaßnahmen Sie einbauen, desto wahrscheinlicher ist es, dass Sie nicht ob eines dummen Programmierfehlers einige Dutzend Ein-Stern-Bewertungen erhalten.

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.

Links & Literatur

[1] Firebase-Konsole: https://console.firebase.google.com/project/_/testlab/histories/?pli=1

Mobile Cross-Plattform-Entwicklung mit NativeScript und Angular

Native Apps mit Angular
von Nils Mehlhorn

Als Angular-Entwickler ist man nicht nur auf das Web beschränkt. Das zeigt das Framework NativeScript wie kein anderes. Mit Erfahrungen aus dem Web lassen sich plattformübergreifend mobile Anwendungen erstellen, die dem nativen Look and Feel entsprechen. Der Einstieg fällt ungeahnt leicht, doch wenn es ans Eingemachte geht, kann es hier und da auch schon mal hapern.

Erfahrungen aus der Webentwicklung lassen sich auf anderen Plattformen wiederverwenden, ohne auf die User Experience verzichten zu müssen, die eine nativ implementierte App bietet. Um zu zeigen, wie das geht, werden hier zunächst die generellen Konzepte von NativeScript erläutert, um anschließend darzustellen, wie das Zusammenspiel mit Angular funktioniert. Dann soll anhand einer kleinen Beispielanwendung zum Verwalten von Notizen die grundlegende Entwicklung von plattformübergreifenden Apps mit NativeScript und Angular illustriert werden.

NativeScript ist ein Framework zur plattformübergreifenden Entwicklung von mobilen Anwendungen. Die Progress Software Corporation hat in diesem Jahr die dritte Version der Open-Source-Lösung veröffentlicht [1]. Sie wendet das in der Softwareentwicklung klassische Konzept der Abstraktion von zugrunde liegenden Systemen auf die mobile Entwicklung an. Android-Apps werden mit Java entwickelt, für iOS muss Objective-C oder Swift verwendet werden. Will man seine App auf beiden Plattformen veröffentlichen, ist man beim herkömmlichen Vorgehen quasi gezwungen, zweimal zu entwickeln. Sogenannte Cross-Plattform-Lösungen erlauben das Generieren von beiden Anwendungspaketen aus einer einzigen Codebasis. Ähnlich wie bei anderen Frameworks auf dem Gebiet wird diese bei NativeScript mithilfe von Webtechnologien erstellt. Im konkreten Fall fällt jedoch die Markup-Sprache HTML weg. Anders nämlich als bei Lösungen wie PhoneGap, die letztendlich eine Webanwendung im nativen Gewand ausliefern, ist eine NativeScript-App deutlich näher an der eigentlichen Plattform. Statt der Imitation von nativen Benutzerelementen mithilfe von Webelementen werden direkt native Elemente verwendet. Ein Button entspricht beispielsweise bei der Ausführung in einer NativeScript-App jeweils der nativen Implementierung von iOS oder Android. Das ist möglich, da die Architektur des Frameworks Aufrufe an jegliche Systemschnittstellen erlaubt (Abb. 1).

Abb. 1: Architektur von NativeScript-Apps

User Experience und Performance sind die größten Herausforderungen für Cross-Plattform-Ansätze [2]. NativeScript kann hier durch einen hohen Grad an Nativität punkten. Listing 1 zeigt, wie sehr ein Systemaufruf in NativeScript der nativen Variante ähnelt. Im Beispiel wird ein Ansichtselement zwar programmatisch initialisiert, in der eigentlichen Entwicklung werden die Benutzeroberflächen jedoch deklarativ mithilfe von XML definiert. Dort verwendet der Entwickler auch nicht direkt die Komponenten der Plattform. Er hat zwar nun Zugriff auf alles, was die mobilen Systeme bieten, müsste aber trotzdem immer selbst zwischen ihnen differenzieren. Der Vorteil der Cross-Plattform-Entwicklung soll aber ja gerade sein, dass sich der Entwickler nicht zu sehr mit den letztendlichen Systemschnittstellen beschäftigen muss. Hier kommen Module zum Einsatz, die die erwünschte Abstraktion mitbringen und die Verwaltung der eigentlichen Systemreferenzen übernehmen. Der letzte Aufruf in Listing 1 zeigt dies am Beispiel eines Buttons.

/* Erstellung eines iOS-Buttons */
let btn = UIButton(); // iOS (Swift)
var btn = new UIButton(); // NativeScript (JavaScript)
/* Erstellung eines Android-Buttons */
// Android (Java)
Button btn = new Button(getApplicationContext());
// NativeScript (JavaScript)
var btn = new android.widget.Button(getApplicationContext());
/* Erstellung eines plattformunabhängigen Buttons via NativeScript-Module */
var btn = new Button(); // NativeScript (JavaScript)

Listing 1: Vergleich von Aufrufen an Systemschnittstellen

Mit NativeScript lässt sich also unabhängig von konkreten Plattformen entwickeln, ohne große Einbußen in Sachen Performance und User Experience zu haben. Beim Entwickeln des Anwendungscodes kann jede denkbare Webbibliothek herangezogen werden. Gleichzeitig kann für die Ausführung jegliche native Schnittstelle oder Bibliothek benutzt werden. Durch die Systemintegration von NativeScript ergeben sich weitere interessante Vorteile. So bedarf z. B. die Unterstützung einer neuen Systemversion keiner besonderen Aktualisierungen seitens NativeScript – das bedeutet somit Zero-Day-Support [3].

Framework im Framework

Die Macher von NativeScript haben es sich zunutze gemacht, dass die zweite Inkarnation von Googles Webframework Angular nicht mehr direkt an das Document Object Model (DOM) gekoppelt ist. Dadurch ist Angular viel vielseitiger einsetzbar. Die Kombination aus NativeScript und Angular wurde von Progress und Google entwickelt; es handelt sich somit um ein offiziell unterstütztes Projekt [4].

Im Zentrum der Erwägung, ein weiteres Framework hinzuzuziehen, sollte aus Entwicklersicht aber gar nicht Angular selbst stehen. Vielmehr geht es bei größeren Anwendungen um den Bedarf an Wartbarkeit. Eine umfangreiche Anwendung in purem JavaScript zu schreiben, ist eine Herausforderung. Ein Applikationsframework kann hier Abhilfe schaffen und so effektiv zur Wartbarkeit beitragen. Angular bringt vorgefertigte Strukturen – unter anderem für Services, Routing und Data Binding – mit. Außerdem arbeitet man mit TypeScript und damit mit einer statischen Typisierung und Klassen, wie man sie von Java kennt. TypeScript ist quasi ein Überbau und wird vor der Ausführung in JavaScript überführt. Außerdem lässt sich die Erfahrung mit Angular direkt weiterverwenden. Webentwicklern bieten sich hier also gute Einstiegsmöglichkeiten in die mobile Entwicklung. Mittlerweile kann neben Angular beispielsweise auch Vue.js verwendet werden [5].

Einrichtung

Bei NativeScript laufen verschiedene Fäden zusammen. Dementsprechend sind viele Vorbereitungen zu treffen, um mit der Entwicklung zu beginnen. Hier hat es der Entwickler aber inzwischen ziemlich komfortabel. Wie mittlerweile Standard für alles rund um JavaScript, wird das NativeScript-Kommandozeilentool (CLI) über den Node Package Manager ausgeliefert. Also muss dieser erst einmal zusammen mit Node.js installiert werden. Anschließend lassen sich alle weiteren benötigten Komponenten mit nur einem Befehl auf Windows oder macOS installieren. Bevor man den Befehl ausführt, sollte man aber wissen, auf was man sich einlässt. Hier kommt nämlich einiges zusammen:

- Chocolatey Package Manager (nur Windows, zur Installation der folgenden Abhängigkeiten)

- Google Chrome (zum Debuggen von Apps)

- Java Development Kit (als Grundlage für das Android SDK)

- Android SDK (zum Kompilieren von Android-Anwendungspaketen)

- XCode (nur Mac, zum Kompilieren von iOS-Anwendungspaketen)

Außerdem werden alle benötigten Umgebungsvariablen gesetzt und optional auch virtuelle Geräte zum Testen von Anwendungen angelegt. Für Nutzer von Linux gibt es leider kein interaktives Skript, das einem die Einrichtung abnimmt. Die Installationsanleitung ist hier jedoch, wie auch insgesamt, sehr ausführlich [6].

Hallo, gesamte mobile Welt

Ist alles an Ort und Stelle, kann das erste Projekt angelegt werden. Mithilfe des NativeScript CLI ist der Entwickler in der Lage, ein komplett lauffähiges Grundgerüst mit dem Befehl tns create <Projektname> –ng zu erstellen. Das ng-Flag sorgt hier dafür, dass die Vorlage für ein Angular-basiertes Projekt verwendet wird, das in seiner Struktur stark einem regulären Angular-Projekt ähnelt. Abhängigkeiten werden über die package.json gesteuert und in den Ordner node_modules aufgelöst. Im Ordner app sind Angular-spezifische Quelldateien zu finden. Dort schreibt der Entwickler wie gewohnt seine Komponenten und Services in TypeScript-Dateien. Dateien mit der Endung .html beinhalten standardmäßig das XML-Markup für die Benutzeroberflächen. Die Zuordnung ist allerdings fragwürdig, bei NativeScript wird nämlich definitiv nicht mit einem DOM gearbeitet [7]. Um Verwirrung zu vermeiden, werden für die hier genutzten Beispiele stattdessen Dateien mit der Endung .xml verwendet. Das Erscheinungsbild der Oberflächen lässt sich außerdem mithilfe von CSS-Dateien anpassen. In app/App_Resources können plattformspezifische Assets abgelegt werden.

Das zuvor generierte Projekt implementiert bereits eine einfache Master-Detail-Beziehung. Um sich jedoch sukzessiv der Materie zu nähern, sei zunächst die Implementierung eines simplen Hello World vorgestellt. Wer ebenfalls gerne mit diesem Stand beginnen möchte, kann das hier gezeigte Beispielprojekt verwenden, anstatt ein neues Projekt zu generieren (Kasten: „Beispielprojekt: NativeNotes“).

Beispielprojekt: NativeNotes

Der gesamte Quellcode zum Beispielprojekt findet sich unter: https://github.com/nilsmehlhorn/NativeNotes/ Das Projekt wurde mit NativeScript 3.2.1 und Angular 4.2.0 entwickelt und gegen Android 6.0 sowie iOS 11 getestet. Die verschiedenen Entwicklungsstände lassen sich mithilfe von git-Tags betrachten. Mehr dazu findet sich in der Beschreibung des Projekts auf GitHub.

Um eine neue Ansicht für die NativeScript-App zu definieren, legt man eine ganz gewöhnliche Angular-Komponente an (Listing 2). Diese beschreibt das Aussehen und die Funktionalität eines gekapselten Teils der Oberfläche.

const routes: Routes = [
{ path: "", redirectTo: "/hello", pathMatch: "full"},
{ path: "hello", component: HelloComponent }
];
import { Component } from "@angular/core";
@Component({
moduleId: module.id,
selector: "nn-hello",
templateUrl: "hello.component.xml"
})
export class HelloComponent {
public helloText:string = "Hallo Welt!"
}

Listing 2: „hello.component.ts“

Wie erwähnt, kann die Ansicht in einer zugehörigen hello.component.xml deklariert werden. Hier soll nun der Text aus der Klasse HelloComponent ausgegeben werden. Dazu wird dort mit <Label [text]="helloText"></Label> ein Label-Element definiert. Die eckigen Klammern für das text-Attribut sorgen hier dafür, dass dessen Wert mit der Instanzvariable helloText aus der Komponentenklasse verknüpft wird – klassisches Angular also. Damit die Komponente auch entsprechend von Angular aufgegriffen wird, muss sie noch für die App importiert und als Standardroute definiert werden. Das Vorgehen gleicht dem in der Entwicklung von Webanwendungen. Ersteres lässt sich durch eine Deklaration in app.module.ts erreichen, das Routing in app.routing.ts kann wie folgt erweitert werden:

Ist ein geeigneter Emulator eingerichtet oder ein echtes Gerät an die Entwicklungsmaschine angeschlossen, kann die NativeScript-App mit tns run <android|ios> installiert und ausgeführt werden. Im Hintergrund werden dazu zuvor die benötigten Quellen für die Zielplattform erstellt (manuell über tns platform add) und das Anwendungspaket wird gebaut (manuell über tns build). Die generierten Artefakte liegen anschließend im Ordner platforms. Neben jeder TypeScript-Quelldatei findet sich nun auch jeweils ein Gegenstück mit ausführbarem JavaScript, das in das Anwendungspaket eingefügt wurde. Beim ersten Start fällt direkt der standardmäßige Startbildschirm mit dem NativeScript-Logo auf, der nach Belieben angepasst werden kann und dazu dient, die Zeit zu überbrücken, bis die NativeScript-Laufzeitumgebung bereit ist. Das kann schon einmal einige Sekunden dauern, lässt sich aber über verschiedene Maßnahmen optimieren [8]. Lange Ladezeiten beim Anwendungsstart sind für viele Nutzer ein echtes K.-o.-Kriterium – aktuell noch ein Schwachpunkt bei NativeScript.

Im Entwicklungsprozess selbst kann NativeScript jedoch mit Geschwindigkeit punkten: Solange nur Änderungen an den NativeScript-Quelldateien vorgenommen werden, wird kein erneutes Bauen des Anwendungspakets nötig. Stattdessen werden lediglich die entsprechenden Ressourcen innerhalb des installierten Anwendungspakets ausgetauscht. Ändert der Entwickler nun etwas am Quellcode, wird die App automatisch aktualisiert.

Native Listen und Styling – Beispielanwendung

Am Ende soll die Beispielanwendung aber nicht nur einen simplen Text ausgeben, sondern in der Lage sein, Notizen zu verwalten. Der Nutzer soll dafür eine Übersicht in Form einer Liste präsentiert bekommen. Mit TypeScript ist der Entwickler in der Lage, zunächst ein Datenmodell für Notizen zu definieren. Dann wird die bereits bestehende Komponente zur NotesComponent umgewandelt, die statisch Notizen zur Anzeige bereitstellt (Listing 3). Die zuvor durchgeführte Registrierung der HelloComponent kann übernommen werden.

import { Component } from "@angular/core";
import { Note } from "./note.model";
@Component({
moduleId: module.id,
selector: "nn-notes",
templateUrl: "notes.component.xml",
styleUrls: ["notes.component.css"] // verknüpfte Stylesheets
})
export class NotesComponent {
public notes: Array<Note> = [
new Note("NativeScript ausprobieren",
"Vielleicht erstmal im NativeScript Playground"),
new Note("Geschenk für Mutti", "Dieses Jahr früher Gedanken machen!")
];
}

Listing 3: „notes.component.ts“

Um eine Liste performant zu rendern, ist es bei größeren Datenmengen nicht mit einer simplen Schleife getan. Das ListView-Modul nimmt dem Entwickler dieses Problem ab, indem es die jeweiligen Implementierungen der mobilen Plattformen kapselt. Die gekapselten Implementierungen verhelfen bei der Ausführung schließlich zu einer reibungslosen Darstellung mithilfe der nativen Listenkomponenten von iOS bzw. Android. Im Markup lässt sich das Array für die Notizen an das ListView-Element anfügen und so dynamisch darstellen.

<ListView [items]="notes" class="list-group">
<ng-template let-note="item">
<StackLayout class="list-group-item">
<Label [text]="note.title" class="note-title"></Label>
<Label [text]="note.content"></Label>
</StackLayout>
</ng-template>
</ListView>

Für jede Notiz wird die Schablone innerhalb des ng-template-Elements instanziiert und ergibt so die Listenelemente. Eine Notiz wird darin jeweils durch dessen Titel und Inhalt in Form von zwei Labels dargestellt. Zur Anordnung von Elementen stehen dem Entwickler verschiedene Optionen zur Verfügung. Das hier verwendete StackLayout ordnet Elemente vertikal untereinander an. Das Label für die Ausgabe des Titels einer Notiz hat außerdem eine eigene CSS-Klasse bekommen. In der Komponente wurde bereits ein Stylesheet verknüpft, in dem nun komponentenspezifische Anpassungen gemacht werden können. So soll der Titel einer Notiz fett dargestellt werden, was über den folgenden Eintrag in notes.component.css erreicht wird:

.note-title {
font-weight: bold;
}

Die Möglichkeiten von CSS innerhalb von NativeScript sind im Gegensatz zum Web etwas limitiert, zum Beispiel lassen sich keine Schatten für Elemente definieren [9]. Trotz solcher Einschränkungen kann das Styling von nativen Oberflächenelementen über eine standardisierte Sprache als echte Innovation betrachtet werden.

In Dialog treten

Der Nutzer soll neue Notizen über einen Dialog hinzuzufügen können. Der Aufruf hierzu lässt sich in mobilen Oberflächen beispielsweise in der Aktionsleiste unterbringen. Dort steht gerade noch standardmäßig nur der Name der Anwendung. Indem oberhalb des Markups für die Notizliste ein ActionBar-Element angelegt wird, kann die Aktionsleiste wie folgt angepasst werden:

<ActionBar title="Notizen">
<ActionItem text="Neu" (tap)="addNote()" android.systemIcon="ic_menu_add"
ios.systemIcon="4"></ActionItem>
</ActionBar>

Der Titel für die Ansicht wird über das title-Attribut geändert. Das eingebettete ActionItem definiert den Startpunkt für unsere Aktion. Verschiedene User Experiences lassen sich nicht immer gut über einen Kamm scheren, daher sollten hier für eine ansprechende plattformspezifische Anzeige jeweils Icons spezifiziert werden. Über ios.systemIcon bzw. android.systemIcon lässt sich auf die von den Systemen bereitgestellten Ressourcen zugreifen. In diesem Fall kommt für beide Plattformen je ein kleines Plussymbol heraus. Um beim Drücken auf das Item nun eine Logik ablaufen zu lassen, bindet man das tap-Attribut an eine Methode der Komponentenklasse – diesmal über runde Klammern (Kasten „Data-Binding-Spickzettel für Angular“).

Data-Binding-Spickzettel für Angular

Von Ansichtsmaske zur Komponentenklasse (einseitig):

[text]="classProperty"

text="{{classProperty}}"

Von Komponentenklasse zur Ansichtsmaske (einseitig):

(tap)="classMethod()"

In beide Richtungen (beidseitig):

[(ngModel)]="classProperty"

Die referenzierte Prozedur muss jetzt natürlich noch in der Komponentenklasse angelegt werden. Listing 4 nutzt für die Implementierung das Dialogmodul von NativeScript, um den Nutzer mithilfe einer ihm vertrauten Maske eine neue Notiz anlegen zu lassen (Abb. 2).

Abb. 2: Darstellung des Eingabedialogs auf Android (links) und iOS (rechts)

Mithilfe der prompt-Methode kann ein parametrisierter Eingabedialog angezeigt werden. Das Ergebnis des Dialogs kann der Entwickler asynchron verarbeiten lassen, wodurch schließlich ein neuer Eintrag zum Array der Notizen hinzugefügt wird. Anschließend wird die erstellte Notiz auch direkt in der Liste angezeigt.

import * as Dialogs from "ui/dialogs";
...
public addNote() {
const options = {
title: "Titel eingeben",
inputType: Dialogs.inputType.text,
okButtonText: "Erstellen",
cancelButtonText: "Abbrechen"
}
Dialogs.prompt(options).then((promptResult: Dialogs.PromptResult) => {
if (!promptResult.result) return;
const title = promptResult.text.trim();
if (title.length > 0) {
this.notes.push(new Note(title, ""))
}
})
}

Listing 4: „addNote“-Prozedur in „notes. component.ts“

Um die Vorteile von Angular weiter auszuspielen, bietet es sich an, die Verwaltung der Notizen in einen Service auszulagern. Hierdurch lässt sich Darstellungslogik von Geschäftslogik trennen – ein etabliertes Vorgehen, um Anwendungen wartungsfähig zu gestalten. Der Entwickler legt also eine entsprechende Klasse in note.service.ts an und annotiert sie als Injectable. In app.module.ts muss die Klasse dann noch als Provider definiert werden, anschließend ist Angular in der Lage, den Service als Singleton in Konstruktoren zu injizieren.

Der Service sollte die Notizen anhand von Indizes verwalten und Methoden zum Abrufen und Erstellen anbieten. Die Darstellungskomponente kann nun davon Gebrauch machen, indem sie den Service als Parameter im Konstruktor angibt. Alle Anweisungen zum Verwalten von Notizen lassen sich anschließend durch entsprechende Aufrufe an den Service ersetzen.

Perspektivenwechsel

Eine Detailansicht soll es dem Nutzer ermöglichen, eine Notiz genauer zu betrachten und deren Inhalt anzupassen. Eine neue Ansicht bedeutet neue Komponenten. Dazu wird analog zur NotesComponent eine NoteDetailComponent angelegt und im Modul registriert. In der Routenkonfiguration sollte der Pfad für die Komponente diesmal notes/:id lauten. Über den Doppelpunkt signalisiert der Entwickler Angular, dass die Route einen Pfadparameter erwartet. Die Detailkomponente kann diesen Parameter auslesen, indem sie sich zunächst im Konstruktor eine Instanz der Klasse ActivatedRoute aus @angular/router injizieren lässt. Außerdem sollte die Komponentenklasse das Interface OnInit und somit die Einschubmethode zur Instanziierung der Angular-Komponente implementieren.

ngOnInit() {
const noteId = this.activatedRoute.snapshot.params["id"];
this.note = this.noteService.getOne(noteId);
}

Abgesehen davon, dass es generell ratsam ist, möglichst wenig Logik in den Konstruktor zu packen, ist innerhalb der ngOnInit-Methode sichergestellt, dass alle Eingabeparameter seitens Angular gesetzt sind [10]. Über eine Momentaufnahme der aktuellen Route lässt sich dann zuverlässig der gesuchte Pfadparameter auslesen. Der Notizservice stellt eine Methode bereit, die die passende Notiz liefert. Fehlt nur noch die Definition der eigentlichen Detailansicht (Listing 5).

<ActionBar [title]="note.title"></ActionBar>
<StackLayout>
<TextView hint="Beschreibung eingeben..." [(ngModel)]="note.content"
class="input" textWrap="true"></TextView>
</StackLayout>

Listing 5: „note-detail.component.xml“

Die Aktionsleiste trägt hier dynamisch den Titel der Notiz. Das TextView-Modul stellt plattformunabhängig ein mehrzeiliges Textfeld dar. Mithilfe des ngModel-Attributs kann der Entwickler den Inhalt der Notiz beidseitig mit dem Wert des Textfelds verknüpfen. In der Ansicht der NotesComponent muss dann noch ein Link zur Detailansicht eingefügt werden. Das lässt sich bewerkstelligen, indem das Attribut [nsRouterLink]="['/ notes', note.id]" dem dortigen StackLayout angefügt wird. Damit kann nun jede Notiz einzeln betrachtet und ihr Inhalt angepasst werden.

Bebilderung

Bisher kam die Beispielanwendung gut mit den Grundfunktionen von NativeScript aus. Werden die Anforderungen anspruchsvoller, können Plug-ins benötigte Zusatzfunktionalitäten bereitstellen. Im offiziellen Marktplatz gibt es für viele Probleme schon die entsprechende Lösung. Hier findet sich auch das hauseigene NativeScript-Image-Picker-Plug-in [11], das sich dem Projekt mithilfe des CLI-Befehls tns plugin add nativescript-imagepicker hinzufügen lässt. So soll es dem Nutzer ermöglicht werden, ein Bild für die Detailansicht einer Notiz auszuwählen. Ein Button könnte hierzu beispielsweise die Komponentenmethode in Listing 6 aufrufen.

import * as imagepicker from "nativescript-imagepicker";
...
public addImage() {
const picker = imagepicker.create({mode: "single"});
picker.authorize()
.then(() => picker.present())
.then((selection: imagepicker.SelectedAsset[]) => {
if(selection.length > 0) {
this.note.imageSource = selection[0].fileUri;
}
});
}

Listing 6: addImage-Prozedur in note-detail.component.ts

Eine Instanz des Image Pickers im single-Modus erlaubt die Auswahl eines einzelnen Bilds über die systemspezifische Bildergalerie. Der Aufruf der authorize-Methode sichert der Anwendung dynamisch die Rechte zum Zugriff auf Mediendateien des Systems. Sind die Rechte eingeholt, wird die Bildauswahl asynchron mithilfe der present-Methode gestartet. Deren Ergebnis lässt sich wiederum asynchron verarbeiten, um schließlich den Pfad für das ausgewählte Bild an die aktuelle Notiz anzuhängen. Anschließend kann die Detailansicht um ein Element zur Darstellung des Bilds ergänzt werden. Oberhalb des Textfelds wird dazu mit <Image [src]="note.imageSource"></Image> ein Image-Tag mit dem Dateipfad verknüpft.

Die Abbildungen 3 und 4 zeigen, wie die Beispielanwendung auf den jeweiligen Plattformen nach kleineren visuellen Anpassungen (einsehbar im Quellcode) letztendlich aussieht.

Abb. 3: Beispielanwendung auf Android

Abb. 4: Beispielanwendung auf iOS

Fazit und Ausblick

Das einfache Beispielprojekt zeigt, wie ohne große Kenntnisse von mobiler Entwicklung nicht nur eine, sondern gleich beide großen Plattformen bedient werden können. Mit NativeScript lassen sich Erfahrungen aus der Webentwicklung wiederverwenden, um native Nutzererlebnisse zu schaffen [12]. Sind anspruchsvollere Vorhaben geplant, empfiehlt es sich aber, vor der Entscheidung für NativeScript zunächst einen Prototyp umzusetzen. Die gesamte mobile Entwicklung zu abstrahieren, ist nämlich kein einfaches Vorhaben, und so stößt man manchmal schon früher auf Probleme als man denkt. Mit dem neu eingeführten NativeScript Playground ist es ohne Vorbereitung möglich, Prototypen im Browser zu entwickeln und auf echten Geräten zu testen – eine wirklich praktische Möglichkeit zum Ausprobieren.

Durch die Fähigkeit, einfache Anforderungen schnell und solide abbilden zu können, eignet sich NativeScript besonders für Line-of-Business-Apps sehr gut. Im Verbund mit Angular ergibt sich außerdem noch eine wesentlich verbesserte Wartbarkeit. Ein weiterhin erwähnenswerter Punkt: Die Struktur von Angular erlaubt es, aus einer Webanwendung schnell eine native App zu machen und umgekehrt. Somit ist es sicherlich empfehlenswert, diese innovative Frameworkkombination für das nächste mobile Projekt in Erwägung zu ziehen, wenn die entsprechenden Voraussetzungen gegeben sind.

Nils Mehlhorn ist Software Engineer bei der adesso AG und studiert im Master Software and Network Engineering in Essen. Als Fan von genialen Lösungen im Softwarehandwerk begeistert er sich für neue Technologien und deren realistische Möglichkeiten für den Produktivbetrieb. Twitter: n_mehlhorn

Links & Literatur

[1] Atanasov, Georgi: „NativeScript 3.0 Available Today“: https://www.nativescript.org/blog/nativescript-3.0-available-today/

[2] Mehlhorn, Nils: „Modern Cross-Platform Development for Mobile Applications“. 2017: http://fhdd.opus.hbz-nrw.de/volltexte/2017/1127/

[3] Looper, Jen: „Dial ‘N’ for NativeScript and Android N!“: https://www.nativescript.org/blog/dial-n-for-nativescript-and-android-n/

[4] Green, Brad; VanToll, TJ:„Building Mobile Apps with Angular 2 and NativeScript“: http://angularjs.blogspot.de/2015/12/building-mobile-apps-with-angular-2-and.html

[5] Looper, Jen: „A new Vue for NativeScript“: https://www.nativescript.org/blog/a-new-vue-for-nativescript/

[6] NativeScript Installation: https://docs.nativescript.org/start/quick-setup

[7] VanToll, TJ: „Allow templateUrl files to be XML files“: https://github.com/NativeScript/nativescript-angular/issues/54/

[8] How to Build NativeScript Apps That Start Up Fast: https://docs.nativescript.org/best-practices/startup-times/

[9] NativeScript Documentation – Styling: https://docs.nativescript.org/ui/styling/

[10] Hevery, Miško: „Flaw: Constructor does Real Work“: http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/

[11] Image Picker: https://market.nativescript.org/plugins/nativescript-imagepicker/

[12] Mehlhorn, Nils: „Mobile Cross-Platform Development from a Progressive Perspective“: http://fhdd.opus.hbz-nrw.de/volltexte/2017/1128/

Mit Arduino und Bluetooth LE in die smarte Welt einsteigen

Android meets Arduino
von Dr. Matthias Berning, Dirk Dorsch und Christian Haß

IoT, E-Health, Smart Home oder Industrie 4.0 – ihnen allen sagt man nach, das nächste technologische oder ökonomische Big Thing zu sein. Zentrales Element der Systeme bilden die gemessenen Werte einzelner Sensoren, nachbearbeitet in entsprechenden Big-Data-Strukturen. Kafka, Spark, Flink und Co. sind hier bekannte Vertreter aus dem Open-Source-Bereich. Wie aber sieht es am anderen Ende des Systems aus? Wo kommen die Daten her? Auch diese Seite der Medaille ist weniger komplex als vielleicht vermutet, wie das Beispiel einer Android-Applikation zur Ansteuerung von Temperatursensor und LED-Beleuchtung zeigt.

Die Anbindung einer Android-App an ein ausgefeiltes Backend (natürlich as a Service) gehört zum Handwerkszeug des passionierten Entwicklers. Bei IoT- und Smart-Home-Anwendungen steht aber nicht das Backend im Mittelpunkt, sondern Sensoren und Aktoren. Neben Alexa/Echo, Apples Homekit oder Google Home gehört heute wohl zu jedem gut ausgestatteten Smart Home eine kabellose Wetterstation, um sich die aktuelle Außen- und Raumtemperatur auf dem Smartphone anzeigen zu lassen. Lampen werden heute über eine App geschaltet, gedimmt und farblich auf den Gemütszustand abgestimmt. Obwohl oft eine Peer-to-Peer-Verbindung ausreichen würde, werden auch hier schon häufig Gateways und Cloud-Infrastrukturen eingesetzt. Nicht unbedingt zwingend notwendig, wenn man auf der Couch wissen will, wie es um die Raumtemperatur bestellt ist. In grundlegenden Anwendungsfällen genügt häufig eine direkte Verbindung von Smartphone oder Tablet zum jeweiligen Sensoren- beziehungsweise Aktorendevice. Bei anspruchsvolleren Anwendungen wie E-Health-Applikationen ist eine gesicherte Peer-to-Peer-Verbindung oftmals die einzige Option. Auch wenn es zu den meisten Anwendungen bereits fertige und günstige Lösungen auf dem Markt gibt, macht es durchaus Laune, einen Blick über den Softwaretellerrand zu werfen und etwas Physisches zu schaffen, das sich selbstverständlich mit einer App steuern lässt.

Rapid-Prototyping-Plattformen versetzen den Hobbyelektroniker in die Lage, ein solches System zu bauen. Sensoren für Temperatur, Luftfeuchte, Luftdruck oder auch Atemalkohol sind Euroartikel und ermöglichen es, eine Vielzahl an Applikationen selbst zu entwickeln – zumindest prototypisch und für den Heimgebrauch ausreichend. Um eine Android-App mit einem Temperatursensor und ein paar LEDs zu verbinden, bedarf es vielleicht noch der Bereitschaft, ein wenig zu löten, der C-Skills aus der Vergangenheit und eines Überblicks im Dschungel der Protokolle, Standards und Elektronikplattformen. So einfach teilweise die Handhabung, so vielfältig sind allerdings die Alternativen der Plattformen und Protokollstandards.

Welches Schweinderl hättens denn gern?

Grundlage eines Device ist der Mikrocontroller beziehungsweise die Plattform, die diese MCU einbindet. Hardwarehersteller bieten eigene Entwicklungsboards zur Integration und Erprobung angeschlossener Sensoren und Aktoren an. Diese richten sich aber oft an professionelle Anwender und sind nur bedingt zum dauerhaften Einsatz geeignet. In Sachen Entwicklungsumgebung sind häufig proprietäre Tools mit entsprechendem Preis oder steiler Lernkurve nötig. Prototyping-Plattformen abstrahieren hier über die eingesetzte MCU und verfügen über eine einfache Programmierumgebung. C und C++ sind dabei längst nicht mehr die einzige Sprachoption. Neben der häufig anzutreffenden Skriptsprache Lua kommen auch aus anderen Bereichen bekannte und etablierte Leichtgewichte wie JavaScript oder Python (Micropython) zum Einsatz.

Die nach wie vor am weitesten verbreitete Plattform ist Arduino, mit einer breiten Palette an Entwicklungsboards. SDK, einfache IDE und eine vollständig integrierte Toolchain bringen das eigene Programm ohne weitere Hardware auf die MCU. Neben den Originalboards sind durch den Open-Source-Ansatz zahlreiche kompatible Nachbauten für wenige Euro erhältlich. Das gesamte Ökosystem ist wohl dokumentiert und wird in zahlreichen Foren und Communities aktiv unterstützt. Faktisch lässt sich für fast jedes Problem eine Lösung mit Schaltplänen und Codebeispielen finden.

Ran an die Hardware

Der altbewährte Klassiker ist der Arduino UNO, bestückt mit einem ATmega328P: einer 8-Bit-CPU mit 32 kB Flash und 2 kB RAM bei 16 MHz Takt. Der Formfaktor bietet einfachen Anschluss für Sensorik und erweiterte Funktionen, standardisiert in Form sogenannter Shields erhältlich. Kompakter und trotzdem leistungsfähiger ist das Teensy Development Board. Durch den verbauten ARM Cortex-M4 stehen 32-Bit-Instruktionen, eine Floating Point Unit, 1 MB Flash-Speicher und 256 kB RAM bei einer Taktung von 180 MHz zur Verfügung. Mit etwa einem Zehntel der Größe und einer integrierten SD-Karte ist der Teensy also durchaus beachtlich, vor allem, wenn es um Applikationen mit grafischem Display oder die Verarbeitung von Sound geht.

Die US-Firmen SparkFun und Adafruit bieten eine große Auswahl an Sensoren, Aktoren und weiteren Bauteilen im prototypfreundlichen Format. Adafruit stellt mit der Feather-Plattform sehr kompakte Boards zur Verfügung, die eigenständig, aber auch kombiniert mit unterschiedlichen Schnittstellen wie SD-Karte oder WLAN ausgestattet sind und einfach über steckbare Erweiterungen mit GPS, LED-Display, Servomotorsteuerung oder TFT-Bildschirm ergänzt werden können. Mit dem integrierten Lademodul für LiPo-Akkus bilden sie einen guten Grundstein für batteriebetriebene integrierte Anwendungen. Ebenfalls von Adafruit sind die Flora- und Gemma-Plattformen für Wearables vergleichbar und softwareseitig kompatibel mit dem LilyPad von Arduino.

Liegt das Einsatzszenario klar im Bereich des IoT, spielen Optionen mit drahtloser Konnektivität eine übergeordnete Rolle. Insbesondere für WLAN umfasst der Markt eine breite Auswahl an Boards. Das MKR1000 aus der Arduino-Familie integriert nicht nur ein WLAN-Modul, sondern bildet mit einem im Vergleich zum UNO deutlich leistungsfähigeren Prozessor sowie integriertem Batteriemanagement eine gute Entwicklungsbasis. Der Particle Photon ergänzt das Development-Board um eine vollständige Cloud-Plattform inklusive Devicemanagement und Over-the-Air-Firmware-Updates. Preislich schwer zu unterbieten sind die auf dem ESP8266 des chinesischen Herstellers Espressif basierenden Boards. Der Chip kombiniert WLAN und MCU und ist auch in Form von Breakout-Boards für das Prototyping erhältlich. Auch namhafte Hersteller verkaufen ESP8266-basierte, Arduino-Plattform-kompatible Boards. Mit dem Adafruit HUZZAH ESP8266 Breakout oder dem SparkFun ESP8266 Thing lassen sich Applikation zwischen 10 und 15 Euro entwickeln. Der Nachfolger ESP32 bietet mehr Leistung sowie Bluetooth LE bei moderaten Mehrkosten. Sofern man auf die Arduino-Kompatibilität verzichten kann oder will, ist die auf dem ESP basierende NodeMCU-IoT-Plattform eine interessante Option. Je nach Bezugsquelle gibt es Breakout-Boards für unter 10 Euro. Die Skriptsprache Lua ersetzt hier C/C++.

Neben NodeMCU haben auch andere Plattformen erkannt, dass C/C++ nicht die einzige und auch vielleicht nicht immer die beliebteste Möglichkeit sind, embedded zu programmieren. MicroPython ist eine schlanke und effiziente Implementierung von Python 3 und lässt sich beispielsweise auf dem PyBoard mit einer STM32 MCU mit einer 168 MHz ARM Cortex-M4 CPU und 192 kB RAM durchaus für größere Applikationen einsetzen. Nicht nur Webentwickler finden mit Espruino eine Plattform, die die Firmwareentwicklung mit JavaScript ermöglicht. Neben dem Espruino und dem kleinen Espruino Pico ist der Interpreter auch auf dem ESP8266-basierten Espruino WiFi und dem Puck.js zu finden. Letzter integriert bereits ein paar grundlegende Sensoren wie z. B. Thermometer und Barometer, und dient als vollwertiger Bluetooth Beacon. Möglich wird dies durch den Einsatz des System on a Chip (SoC) nRF52 von Nordic. Der nRF52 SoC bietet neben Bluetooth LE und NFC eine 64 MHz ARM Cortex-M4 CPU mit 64 kB RAM. Aufgrund der Leistung und des dennoch geringen Stromverbrauchs ist der nRF52 auch Basis weiterer Boards, wie das SparkFun nRF52832 Breakout, die Adafruit Feather nRF52 Bluefruit LE oder der RedBear BLE Nano v2. 2016 hat Arduino mit dem Primo/Primo Core in Kooperation mit Nordic ein nRF52832-basiertes Board herausgebracht. Die Produktion ist allerdings eingestellt, Exemplare sind nur noch schwer zu beschaffen.

Do it yourself: Thermometer und dimmbares Licht

Da Dokumentation und Beispiele einen leichten Einstieg ermöglichen, ist ein Arduino UNO oder eine baugleiche, günstigere Alternative für die ersten Projekte eine gute Wahl. Für eine Wetterstation mit steuerbarem Licht braucht es nur wenige Komponenten und wenige Zeilen Code. Die benötigten Hardwarekomponenten sind in den Starterkits unterschiedlicher Anbieter meist vollständig enthalten. Neben den Hardwarekomponenten muss die Entwicklungsumgebung aufgesetzt werden. Da viele Tutorials die Arduino IDE [1] referenzieren, kommt sie auch hier zum Einsatz. Sie ist allerdings eher für kleinere Projekte geeignet. Soll das Projekt softwareseitig langlebiger sein oder etwas größer werden, ist die Eclipse-basierte Sloeber IDE [2] die bessere Wahl. Für den Einstieg reicht aber der eher scriptende Ansatz der Arduino IDE aus. Unser Beispiel setzt grundlegende Kenntnisse der Arduino IDE voraus, die sich mithilfe der ersten Arduino Tutorials [3] schnell erarbeiten lassen. Der Sourcecode der Turorials ist in der IDE integriert, lässt sich schnell nachvollziehen und direkt auf den Arduino laden.

Bevor es an die Implementierung der eingebetteten Firmware gehen kann, müssen die einzelnen Komponenten verdrahtet werden. Für das leuchtende Thermometer braucht es die folgenden Komponenten:

- Temperatursensor Dallas DS18B20 und ein 4.7-kOhm-Widerstand

- Drei LEDs (je eine rote, grüne und blaue), wahlweise eine RGB-LED

- 3 220-Ohm-Widerstände als Vorwiderstand (um LEDs davor zu bewahren, durchzubrennen)

- Arduino UNO (oder kompatibler Nachbau)

- Breadboard und ein paar Steckverbinder, um ohne Löten auszukommen

Die Komponenten werden angeschlossen wie in Abbildung 1 dargestellt. Die LEDs haben je ein langes (Anode bzw. Pluspol) und ein kurzes (Kathode bzw. Minuspol) Bein. Der jeweilige Pluspol der LEDs wird über den 220-Ohm-Vorwiderstand mit den Pins 9, 10 und 11 am Arduino verbunden. Das kurze Beinchen wird bei allen direkt mit der Masse (GND) verschaltet. Der Temperatursensor ist mit dem mittleren Kontakt an Pin 2 angelegt, die linke Leitung geht auf Masse, die rechte wird mit 5 V Spannung vom Arduino versorgt. Die Datenleitung wird über den 4.7-kOhm-Widerstand auf 5 V gezogen, um einen definierten Ausgangspegel zu haben. Die Schaltung steht somit, und die Implementierung der Firmware kann beginnen.

Abb. 1: Anschlussdiagramm am Arduino UNO

Insbesondere bei komplexeren Sensoren zeichnen sich die Arduino-Plattformen durch die Vielzahl an Bibliotheken für verschiedenste Bauteile aus. Der Temperatursensor ist über das 1-Wire-Protokoll angebunden und liefert lediglich Rohdaten, aus denen sich dann die Temperatur ermitteln lässt. Um hier nicht in die Tiefe gehen zu müssen, werden die beiden Bibliotheken OneWire und DallasTemperature in das Skript inkludiert [4]. Die in Listing 1 gezeigte Rumpfapplikation kann nun probeweise auf dem Arduino deployt und anschließend um die benötigte Logik erweitert werden.

#include "Arduino.h"
#include <OneWire.h>
#include <DallasTemperature.h>
// Definition der LED und Sensor-Pins
#define RED 11
#define GREEN 10
#define BLUE 9
#define SENSOR_WIRE 2
// OneWire / DallasTemperature-Objekte instanziieren
OneWire oneWire( SENSOR_WIRE );
DallasTemperature sensors(&oneWire);
DeviceAddress sensorAddress;
// siehe Listing 2
void setupTemperatureSensor() {
Serial.println("Setting Up Temperature Sensor");
}
void setup() {
// Serielle Kommunikation mit der IDE ermöglichen
Serial.begin(115200);
setupTemperatureSensor();
}
void printTemperature() {
Serial.println("not implemented yet");
}
void fadeInLEDs() {
Serial.println("It’s getting brighter");
}
void fadeOutLEDs() {
Serial.println("It’s getting darker");
}
void loop() {
printTemperature();
fadeInLEDs();
fadeOutLEDs();
// eine Sekunde nichts tun
delay(1000);
}

Listing 1: Skelett des Arduino-Skripts

Um die Temperatur auslesen zu können, muss zunächst setupTemperatureSensor sinnvoll implementiert werden. Listing 2 zeigt darüber hinaus in der printTemperature-Funktion das Auslesen der Temperaturwerte und deren Ausgabe auf der Konsole. Beide Methoden lassen sich durch die eingesetzten Bibliotheken ohne tiefere Kenntnisse der Sensorik realisieren.

void setupTemperatureSensor() {
// Initialisierung
sensors.begin();
Serial.print("Found ");
Serial.print(sensors.getDeviceCount(), DEC);
Serial.println(" devices.");
// Zuweisen von Addresse zum Index (über OneWire könnten beliebig viele
// Sensoren an GPIO 2 hängen
if (!sensors.getAddress(sensorAddress, 0))
Serial.println("Unable to find address for Device 0");
// Genauigkeit versus Geschwindigkeit; 12 bit liefert die höchste Genauigkeit,
// 9 bit benötigt für die Conversion am kürzesten
sensors.setResolution(sensorAddress, 12);
}
void printTemperature() {
sensors.requestTemperatures();
float celsius = sensors.getTempC( sensorAddress );
Serial.print("Temperature: ");
Serial.print( celsius );
Serial.println(" Celsius");
}

Listing 2: Verwendung des DS18B20

Für das Dimmen der LEDs kommt mit der Pulsweitenmodulation (PWM) ein eher fortgeschrittenes Konzept zum Einsatz. Die LEDs werden an PWM-fähige Pins (beim UNO 3, 5, 6, 9, 10, 11) angeschlossen. Vereinfacht dargestellt ermöglicht PWM eine Steuerung der ausgegebenen Spannung, indem mit hoher Frequenz zwischen den Zuständen „an“ und „aus“ gewechselt wird. Das Auge nimmt so eine entsprechend gedimmte Helligkeit wahr, die dem Verhältnis der „An“-Zeit zur „Aus“-Zeit entspricht, dem Duty Cycle. Arduino ermöglicht es mit dem Aufruf digitalWrite(PIN, LOW), beziehungsweise digitalWrite(PIN, HIGH), keine oder die volle Spannung am Pin auszugeben. Eine LED ist also wahlweise an oder aus. Würde man versuchen, mit einer kruden PWM-Simulation im loop() zwischen LOW/HIGH zu wechseln, könnte die benötigte Frequenz nicht erreicht werden und die LED flackert. Mit Hardware-PWM an den entsprechenden Pins kann über analogWrite(PIN, duty_cycle) das Dimmen der LEDs eleganter realisiert werden. Hierbei entsprechen analogWrite(PIN, 0) und analogWrite(PIN, 255) den Funktionen digitalWrite(PIN, Low) beziehungsweise digitalWrite(PIN, HIGH). Über den Wertebereich 0-255 kann so eine prozentuale Helligkeit implementiert werden. In Listing 3 wird dieses Prinzip für ein Ein- und Aus-Faden der LEDs genutzt.

void leds(int intensity) {
analogWrite(RED, intensity);
analogWrite(GREEN, intensity);
analogWrite(BLUE, intensity);
}
void fadeInleds() {
for(int i = 0; i <= 255; i++) {
leds(i);
delay(50);
}
}
void fadeOutleds() {
for(int i = 255; i >= 0; i--) {
leds(i);
delay(50);
}
}

Listing 3: Dimmen mit Pulsweitenmodulation

Die Grundlagen einer funktionierenden Applikation sind gelegt. Im loop() wird dauerhaft die Temperatur ausgelesen und das Licht ein- und aus-gefadet. Beides ist wenig sinnvoll, da man zum Ablesen der Temperatur einen Laptop anschließen müsste und die Lichtsteuerung nur durch erneutes Programmieren möglich ist. Wir könnten ein kleines Display anbinden, das erhöht jedoch die Komplexität, außerdem müssten wir ja dann zum Ablesen vom Sofa aufstehen. Anzeige und Steuerung müssen runter von Laptop und Device und elegant über eine Android-App laufen.

Kabel ab

Im SmartHome- oder IoT-Bereich gibt es eine Vielzahl von Protokollen und Kommunikationsstandards. Einzelne Protokolle haben in den unterschiedlichen Anwendungsfällen ebenso ihre Stärken und Schwächen wie die Kommunikationstechnologien. Mit Fokus auf eine direkte M2M-Kommunikation hat die Internet Engineering Task Force (IETF) mit dem Constrained Application Protocol (CoAP) [5] ein Web-Transfer-Protokoll entwickelt, das konzeptionell das REST-Architekturmodell implementiert. Der wesentliche Unterschied zu seiner REST-over-HTTP-Schwester ist, dass CoAP auf UDP und nicht auf TCP aufsetzt und binäre Datenformate für den Austausch nutzt. CoAP ist eher für kleinere Systeme konzipiert. Besteht ein System aus vielen unter Umständen miteinander in Abhängigkeit stehenden Komponenten, ist das über die Organization for the Advancement of Structured Information Standards (OASIS) als IoT-Protokoll standardisiertes Message-Queuing-Telemetry-Transport-(MQTT-)Protokoll [6] die bessere Wahl. MQTT implementiert ein auf ressourcenbeschränkte Geräte ausgelegtes Publish/Subscribe-Konzept und bietet neben Übermittlungsgarantie in unterschiedlichen Quality-of-Service-Stufen Verschlüsselung über TLS. Systeme können entkoppelt voneinander aufgebaut, unkompliziert erweitert werden und über ein nahezu overheadfreies Protokoll kommunizieren. Beide Protokolle setzen i. d. R. auf einen IP-basierten Netzwerkstack. Bei einem direkten Peer-to-peer-Datentransfer sind sie nicht notwendig und lassen sich durch vorhandene Mechanismen der Kommunikationstechnologie ersetzen.

Wollen wir die Daten beispielsweise direkt an ein Backend zur Verarbeitung übertragen, bietet sich der Einsatz eines zellulären Netzwerks mit flächendeckend verteilten Basisstationen an. Ein klassisches Beispiel dafür ist ein GSM/LTE-Modem, das es auch als Modul für das eigene Device gibt. Der klassische Mobilfunk ist jedoch hauptsächlich für Sprachübertragung und hohen Durchsatz optimiert. Das treibt den Overhead bei kleinen Nachrichten mit Sensordaten in die Höhe. In diese Lücke springt Narrowband IoT (NB-IoT), das aktuell in den LTE-Netzen ausgebaut wird. Die Datenpakete werden verkleinert und dafür werden die Zahl der Teilnehmer und die Sendereichweite erhöht. Neben der Hardware ist hier auch immer ein laufender Vertrag mit dem Provider notwendig. Die Verfügbarkeit kann je nach Region stark variieren. In diesem Fall kann ein Low Power Wide Area Network (LPWAN) wie LoRaWAN oder Sigfox eine mögliche Alternative sein, da sich hier mehrere Kilometer überbrücken lassen. Während es sich bei Sigfox um ein Protokoll des gleichnamigen französischen Herstellers handelt, der seine eigene Infrastruktur betreibt, ist LoRaWAN ein offener Standard der LoRa Alliance. Daher lassen sich eigene Basisstationen und damit private Netzwerke betreiben, die sich auf das Einsatzgebiet abstimmen lassen. Beiden gemein ist, dass sie in lizenzfreien Frequenzbändern unterhalb 1 GHz funken und dadurch entsprechenden Reglementierungen unterliegen. In der Regel darf ein Teilnehmer den Kanal nur zu einem Prozent auslasten, was ungefähr einem Paket pro Minute entspricht.

Ebenfalls aus dem Low-Power-Bereich kommen die ausgereiften Technologien XBee und ZigBee, die auf dem IEEE-Standard 802.15.4 aufbauen und noch energieeffizienter arbeiten. Je nach Antenne und Frequenzband sind auch hier Funkverbindungen über mehrere Kilometer möglich, die beiden Technologien sind aber nicht grundsätzlich darauf ausgelegt. Insbesondere in urbanen Einsatzgebieten ohne Sichtverbindung mit vielen Teilnehmern und Störquellen sinkt die zuverlässige Reichweite auf wenige hundert Meter. ZigBee findet sich zusammen mit Protokollen wie Z-Wave und Enocean vor allem im Smart-Home-Bereich. Deren Stärken sind die direkte Kommunikation untereinander ohne Basisstation und die Möglichkeit, ein Mesh-Netzwerk zu bilden, bei dem Nachrichten über mehrere Teilnehmer weitergeben werden. Ein zusätzlicher Vorteil liegt in der Interoperabilität auf Applikationsebene, die durch aufwendige Zertifizierungsprogramme sichergestellt wird. Die smarte Leuchte von Hersteller A lässt sich auch über den Schalter von Hersteller B dimmen. Allerdings verläuft die Einführung von neuen Produkten und Anwendungen entsprechend träge. Ein Nachteil ist das stets benötigte Gateway, um mit der bestehenden IT des Anwenders zu kommunizieren.

Was funkt

Die bessere Ausgangslage haben Standards, die schon länger im Consumermarkt vertreten sind und sich auch auf Geräten wie dem Smartphone des Endanwenders finden. WLAN hat hier eine Vormachtstellung bei der schnellen Datenübertragung. Je nach Konfiguration lassen sich auch Netze über mehr als hundert Meter spannen. Allerdings konnten sich die Erweiterungen zum Betrieb von stark ressourcenbeschränkten Geräten bisher noch nicht durchsetzen. Devices müssen sich entweder dauerhaft im Netz melden oder vor jeder Datenübertragung aufwendig neu einbuchen. Weniger bekannt ist das proprietäre ANT+, das vor allem im Fitness- und Health-Bereich zu finden ist. In anderen Geräten konnte es sich noch nicht durchsetzen und wird längst nicht von allen Mobiltelefonen unterstützt. Eine bessere Ausgangslage hat Bluetooth, seit Kurzem mit der Low-Energy-(LE-)Erweiterung auch für Sensordevices interessant geworden, die über Jahre mit der Energie einer Knopfzelle auskommen müssen. Zusätzlich definiert die Spezifikation ein Protokoll für den bidirektionalen Datentransfer mit Katalogen für bekannte Anwendungsfälle, sodass die Kommunikationsschicht der Anwendung auf einem wohldefinierten Standard aufsetzen kann. Eine zentrale Zertifizierung fällt bei Bluetooth LE weg, bei einer kommerziellen Vermarktung werden allerdings Lizenzgebühren fällig.

Die richtige Wahl der Kommunikationstechnologie hängt stark von der Applikation ab: Wie hoch darf der Stromverbrauch sein, was sind die Mindestanforderungen an die Reichweite, muss eine bestimmte Kostengrenze eingehalten werden, und vieles mehr. Besonders wenn es darum geht, Smartphone oder Tablet mit Sensordevices zu verbinden, eignet sich Bluetooth LE. Es kombiniert ressourcenschonenden Betrieb mit hoher Verbreitung.

BLE-Architektur: Peripherals, Centrals, Services und Characteristics

Wer an Bluetooth denkt, wie es in Freisprecheinrichtungen und Computerperipherie genutzt wird, muss bei Bluetooth LE (BLE) umdenken. Letzteres wurde bei der gemeinsamen Spezifikation 4.0 als Alternative zum bisherigen Stack aufgenommen und zunächst als energiesparendes Bluetooth Smart vermarktet. Geräte, die beides können, wie die meisten modernen Smartphones, tragen die Bezeichnung Dual Mode. Kürzlich ist noch die Spezifikation Bluetooth Mesh für redundant verbundene Netze hinzugekommen, die parallel zu den anderen Modi läuft. Obwohl Frequenz und Datenrate auf dem Kanal identisch sind, gibt es nur wenige Gemeinsamkeiten in den höher liegenden Layern. Die grundlegend beteiligten Komponenten einer BLE-Verbindung sind das Peripheral und das Central Device. Das Peripheral ist ein kleines, ressourcenbeschränktes Gerät mit Fokus auf die Minimierung des Stromverbrauchs. Mit ihm können sich Centrals, weniger ressourcenlimitierte Geräte wie Smartphones und Tablets, verbinden. Im Sinne des Datenflusses ist meist das Peripheral der Server und das Central der Client. Die Energieeinsparungen ergeben sich durch einen beschleunigten Verbindungsaufbau und eine klar strukturierte Datenübertragung mit kleinen Paketen.

Das Generic Access Profile (GAP) definiert Konnektivität und Teilnehmer der Kommunikation. Das Peripheral bietet seine Dienste über das Advertising an. Hierbei broadcastet das Peripheral in der Advertising Data im Allgemeinen sich selbst, also alle notwendigen Informationen, um einem Central die Verbindung zu ermöglichen. Nutzdaten lassen sich auch direkt per Broadcast verteilen. Dies instrumentiert beispielsweise Apples iBeacon oder Googles Eddystone, um ohne eine bestehende Verbindung Daten an beliebig viele Devices zu liefern. Das Advertising Interval legt fest, wie oft diese Daten gesendet werden, und liegt zwischen einer Zehntelsekunde und mehreren Sekunden. Die Konfiguration des Intervalls hat nicht nur maßgeblichen Einfluss auf den Stromverbrauch, sondern auch auf die Dauer des Verbindungsaufbaus. Nur direkt nach einem Advertisement kann das Central diesen Prozess starten oder mehr Daten in der Scan Response beantragen. In Letzterer können weitere Daten übermittelt werden, etwa die angebotenen Services. Beide Pakete sind auf je 31 Byte begrenzt, wobei es hier im aktuellen Bluetooth 5 einige Erweiterungen gibt. Der Verbindungsaufbau erfolgt standardmäßig ohne das von Bluetooth bekannte Pairing. Nach erfolgreichem Abschluss stoppt das Advertising, und das Peripheral reagiert nur noch auf Anfragen vom Central im definierten Connection Interval.

Der Datenaustausch ist über den Generic Attributes Layer (GATT) geregelt. Das Peripheral wird in diesem Kontext zum Server, das Central zum Client. In seiner Funktion als Server definiert das Peripheral das GATT Profile. Das Profile besteht aus einem oder mehreren Services, die wiederum beliebig viele Characteristics beinhalten. Characteristics enthalten je einen Wert, der vom Client gelesen oder geschrieben werden kann. Zusätzlich können die Characteristics weitere Attributes enthalten, die neben Beschreibung, Lese- und Schreibrechten entscheiden, ob eine Verschlüsselung oder Authentifizierung für den Zugriff erforderlich ist. Services und Characteristics werden über UUIDs identifiziert. Generell gibt es keine Beschränkung, wie viele Daten in einer Characteristic übermittelt werden. Ratsam ist aber, eine Größe von 20 Byte nicht zu überschreiten, um Fragmentierung und Protokolloverhead zu vermeiden. Für viele typischen (Sensor-)Werte und Anwendungen sind bereits Characteristics [7] beziehungsweise die gruppierenden Services [8] definiert. Dies beinhaltet auch einige Pflichtfelder zu Name, Version und Hersteller des Device. Anstatt Kataloge zu wälzen, können für die ersten Experimente aber getrost eigene UUIDs generiert werden.

Do it yourself: Verbindung schaffen

Abhängig von der Hardware stellen Hersteller oder die Community Bibliotheken zur Verfügung, um die Integration von Bluetooth Smart Connectivity zu vereinfachen. Der Code aus den Listings 4 bis 6 setzt mit Arduino BLEPeripheral [9] eine Bibliothek ein, die auch von kommerziellen Herstellern geforkt als Basis für eigene Implementierungen verwendet wird. Die Bibliothek setzt das beschriebene GATT Profile um und wird, nachdem sie in der IDE installiert wurde, über #include <BLEPeripheral.h> integriert. Zunächst werden die Script-globalen Variablen für die benötigten Characteristics und unser Device, das BLEPeripheral, definiert. Um eine individuelle Ansteuerung zu ermöglichen, wird jede LED über eine eigene Characteristic exponiert. Da für die Pulsbreitenmodulationen Werte von 0 bis 255 zum Einsatz kommen, kann eine BLEUnsignedShortCharacteristic als Implementierung gewählt werden. Über BLEWrite | BLERead werden die Characteristics als les- und schreibbar festgelegt. Um sie mit generischen BLE-Scanner-Apps einfach auslesen zu können, wird die Temperatur als String in einer Default-BLECharacteristic propagiert. BLENotify ermöglicht dem Client (Central), sich für diesen Wert zu subscriben und regelmäßig die Werte gepusht zu bekommen.

Die setupBLE()-Methode baut das Profile des Peripherals zusammen. Das BLEPeripheral bekommt einen BLEService zugewiesen, über den alle vier Characteristics exponiert werden. Neben dem BLEDescriptor lassen sich über die Bibliothek weitere Properties setzen. Die verwendeten UUIDs sind willkürlich mit ffe1 - ffe4 definiert und werden von der Bibliothek auf das übliche 8-4-4-4-12-Format von 16-Bit UUIDs aufgefüllt.

BLEPeripheral blePeripheral = BLEPeripheral();
BLEUnsignedShortCharacteristic redLedCharacteristic = BLEUnsignedShortCharacteristic("ffe1", BLEWrite | BLERead);
BLEUnsignedShortCharacteristic greenLedCharacteristic = BLEUnsignedShortCharacteristic("ffe2", BLEWrite | BLERead);
BLEUnsignedShortCharacteristic blueLedCharacteristic = BLEUnsignedShortCharacteristic("ffe3", BLEWrite | BLERead);
// String Characteristic für bessere Lesbarkeit
BLECharacteristic temperatureCharacteristic = BLECharacteristic("ffe4", BLERead | BLENotify, 20);
// Buffer für String Transformation
char temperatureBuffer[20];
void setupBle() {
const char * localName = "DasUnding";
BLEService bleService = BLEService("ffe0");
blePeripheral.setDeviceName(localName);
blePeripheral.setLocalName(localName);
// Registrierung des BLE-Services
blePeripheral.setAdvertisedServiceUuid(bleService.uuid());
blePeripheral.addAttribute(bleService);
// Characteristic je LED
blePeripheral.addAttribute(redLedCharacteristic);
blePeripheral.addAttribute(greenLedCharacteristic);
blePeripheral.addAttribute(blueLedCharacteristic);
// Temperatur-Characteristic
blePeripheral.addAttribute(temperatureCharacteristic);
BLEDescriptor bleDescriptor = BLEDescriptor("Senseless Thing", "jm.droid.duino.sample");
blePeripheral.addAttribute(bleDescriptor);
// Initialisierung starten
blePeripheral.begin();
}

Listing 4: BLE-Set-up

Die setupBLE()-Methode wird aus der setup()-Methode des Skripts heraus aufgerufen. Um die Temperatur an ein verbundenes Gerät zu schicken, ist nicht mehr viel nötig. Listing 5 sorgt im loop() über blePeripheral.poll() für das Advertisen seiner selbst. Sobald ein Central verbunden ist, wird der entsprechende Wert in der temperatureCharacteristic gesetzt.

void sendTemperature() {
sensors.requestTemperatures();
// "String conversion"
sprintf(temperatureBuffer, "%f", sensors.getTempC(insideThermometer));
temperatureCharacteristic.setValue( temperatureBuffer );
}
bool isConnected() {
return blePeripheral.central() && blePeripheral.central().connected();
}
void loop() {
blePeripheral.poll();
if( isConnected() ) {
sendTemperature();
}
delay(500);
}

Listing 5: BLE Connectivity

Für die Lichtsteuerung macht sich der Einsatz der BLE-Bibliothek erneut bezahlt. Anstatt im loop() permanent über ledCharacteristic.getValue() eine Änderung des Werts abzufragen, ermöglicht sie die Registrierung eines Listeners über ledCharacteristic.setEventHandler(<EVENT-TYPE>, CALLBACK-FUNCTION). Listing 6 registriert neben den Event Handlers an den Characteristics je einen Handler für die Ereignisse BLEConnected und BLEDisconnected.

void setupBle() {
// siehe oben
blePeripheral.setEventHandler(BLEConnected, bleConnectHandler);
blePeripheral.setEventHandler(BLEDisconnected, bleDisconnectHandler);
redLedCharacteristic.setEventHandler(BLEWritten, redLedCharacteristicWritten);
greenLedCharacteristic.setEventHandler(BLEWritten, greenLedCharacteristicWritten);
blueLedCharacteristic.setEventHandler(BLEWritten, blueLedCharacteristicWritten);
blePeripheral.begin();
}
// Event Handler
void bleConnectHandler(BLECentral& central) {
fadeInLeds();
fadeOutLeds();
}
void bleDisconnectHandler(BLECentral& central) {
fadeOutLeds();
}
// Das nRF-Board nimmt 255 als LOW und 0 als HIGH...
uint8_t normalizeLightIntensity(uint8_t value) {
if(value > 255) value = 255;
if(value < 0) value = 0;
return 255 - value;
}
void redLedCharacteristicWritten(BLECentral& central, BLECharacteristic& characteristic) {
analogWrite(RED, normalizeLightIntensity( redLedCharacteristic.value() ));
}
void greenLedCharacteristicWritten(BLECentral& central, BLECharacteristic& characteristic) {
analogWrite(GREEN, normalizeLightIntensity(greenLedCharacteristic.value()));
}
void blueLedCharacteristicWritten(BLECentral& central, BleCharacteristic& characteristic) {
analogWrite(BLUE, normalizeLightIntensity( blueLedCharacteristic.value() ));
}

Listing 6: Event Handler

Die Software ist ab sofort lauffähig, allerdings fehlt dem Arduino UNO die BLE-Fähigkeit. Eine Option, die mit dem gelisteten Code funktioniert, ist das BLE Shield von ReadBear Labs. In diesem Fall muss die LED an Pin 9 umgesetzt werden, etwa auf Pin 5. Andere Erweiterungen emulieren nur eine serielle Verbindung, was viele Vorteile von GATT wieder aushebelt. Um das System günstig und einfach zu halten, ist es in Abbildung 2 auf einem BLE SoC Breakout aufgebaut, das Prozessor und Funkmodul kombiniert. Die Abbildung zeigt das fertige Device auf Basis des SparkFun nRF52832 Breakout. Das Board besitzt keine Pins, sie müssen separat bestellt und selbst verlötet werden. Der Anschluss zum Programmieren erfolgt über einen externen FTDI USB-to-Serial-Converter, alternativ lässt sich auch ein FTDI-Kabel nutzen. Wichtig ist, dass die Eingangsspannung bei 3,3 V und nicht bei 5 V liegt. Eine zu hohe Spannung führt in sehr kurzer Zeit zu einem Überhitzen des SoCs. Bei FTDI-Adaptern muss unbedingt vorher die Position des Jumpers geprüft werden, bei FTDI-Kabeln sollten wir bereits beim Kauf auf die Spannung achten. Um mit dem nRF52832 Breakout zu arbeiten, muss zunächst die Boarddefinition in der IDE installiert und anschließend das Board im Boardmanager ausgewählt werden. SparkFun bietet eine ausführliche Dokumentation [10].

Das Board belegt einige der Pins für die LEDs oder weist ihnen andere Funktionen zu. Der Einfachheit halber sollte die Pin-Belegung im Vergleich zum Arduino-Beispiel angepasst werden. Der Temperatursensor ist an der 15, die LEDs sind an 12, 13 und 14 angeschlossen. Dies lässt sich im Sourcecode einfach in den Defines aus Listing 1 anpassen. Eine weitere Besonderheit ist, dass für PWM der Wertebereich umgedreht werden muss: 255 entspricht „aus“ und 0 „an“. In Listing 6 ist dies in der normalizeLightIntensity() implementiert. Die LEDs()-Methode aus Listing 3 muss entsprechend geändert werden. Äußerst gewöhnungsbedürftig ist der Upload des Sketches. Hierzu wird das Board erst in den Bootloader-Modus versetzt, indem erst der Reset-Button (Abb. 2, der obere) und dann der zweite Button gedrückt und beide kurz gehalten werden. Anschließend zunächst Reset loslassen, dann den zweiten so lange halten, bis die blaue LED auf dem Board beginnt, immer schneller zu blinken [10].

Abb. 2: nRF52832-Breakout-basiertes batteriebetriebenes Ding

Smarte Kontrolle mit Android

Für die Kontrolle des Device genügt eine einfache Android-Applikation (Abb. 3). Sobald die Verbindung hergestellt ist, wird die Temperaturanzeige gemäß des Sendeintervalls des Device automatisch aktualisiert. Die LEDs lassen sich über die jeweiligen SeekBars steuern. Um die Verbindung zu einem BLE-Device herzustellen, wird die Umgebung nach Devices gescannt. Wie in Listing 7 am Beispiel des Devicenamens gezeigt, lassen sich ScanFilter setzen, um den Prozess zu beschleunigen. Im übergebenen ScanCallback aus Listing 8 kann auf das Ergebnis des Scans reagiert werden. Im Erfolgsfall wird hier die Verbindung zum GATT-Server des Device hergestellt.

Abb. 3: Android Arduino Control

private void startScan() {
if (this.hasPermissions() == false) {
return;
}
if (mIsScanning == true) {
return;
}
// Filter um auf bekannten Devicenamen einzuschränken
ScanFilter scanFilter = new ScanFilter.Builder()
.setDeviceName("DasUnding").build();
List<ScanFilter> filters = new ArrayList<>();
filters.add(scanFilter);

ScanSettings settings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.build();
mBluetoothScanner.startScan(filters, settings, mBluetoothScanCallback);
mBluetoothTimeoutHandler = new Handler();
mBluetoothTimeoutHandler.postDelayed(mBluetoothTimeoutRunnable, BLUETOOTH_SCAN_TIMEOUT);
mIsScanning = true;
}

Listing 7: Bluetoothscan

private ScanCallback mBluetoothScanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
BluetoothDevice device = result.getDevice();
if (device != null && device.getName() != null
&& device.getName().equals(BLUETOOTH_DEVICE_NAME)) {
connectToDevice(device);
mBluetoothScanner.stopScan(mBluetoothScanCallback);
mIsScanning = false;
}
}
@Override
public void onScanFailed(int errorCode) {
Log.e(TAG_NAME, "Bluetooth LE Scan did fail with errorCode: " + errorCode);
}
};
private void connectToDevice(BluetoothDevice device) {
mGatt = device.connectGatt(this, false, mBluetoothGattCallback);
}

Listing 8: Connect im „ScanCallback“

Das Android SDK bildet die BLE-Architektur im Paket android.bluetooth ab (Listing 9). Bei erfolgreichem Scan wird die Verbindung zum Bluetooth Profile, repräsentiert in der Klasse BluetoothGatt, hergestellt. Der Architektur folgend, ermöglicht das Profil über die discoverServices()-Methode das Auslesen der vom Device propagierten Services. BluetoothServices sowie deren BluetoothCharacteristics werden über die oben definierten UUIDs ausgelesen. Die vollständigen 16-Bit UUIDs lassen sich beispielsweise über die nRF-App [11] auslesen. Der am BluetoothGatt-Profil registrierte BluetoothGattCallback behandelt die asynchronen Bluetooth-Ereignisse. Sobald die Verbindung aufgebaut ist, startet die Service Discovery (onConnectionStateChange). In onServicesDiscovered werden die einzelnen BluetoothChracteristics des Service verbunden und entsprechend den Bedürfnissen konfiguriert. Für die Temperaturcharakteristik wird die App am BluetoothGatt für die Notification registriert, um im Sendeintervall des Device die aktualisierten Temperaturwerte über den onCharacteristicChanged-Handler zu erhalten. Schließlich zeigt Listing 10 die Steuerung der LEDs. In der onStopTrackingTouch-Methode der SeekBar werden die aktualisierten Werte in die jeweilige Charakteristik geschrieben. Beim Schreiben der Werte ist es wichtig, die Datentypen der Charakteristik einzuhalten und zu setzen, hier BluetoothGattCharacteristic.FORMAT_UINT8. Beim eigenen Experimentieren mit den BLE Services sollten wir beachten, dass das GATT-Profil in der Regel gecacht wird. Bei Änderungen am Device sollte der Stack auf dem Android-Gerät durch kurzes Beenden und Reaktivieren von Bluetooth und der App neu initialisiert werden.

private BluetoothGattCallback mBluetoothGattCallback =
new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == BluetoothGatt.STATE_CONNECTED) {
gatt.discoverServices();
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
showViewConnected();
mBluetoothService = gatt.getService(UUID_SERVICE);
if (mBluetoothService == null) {
return;
}
BluetoothGattCharacteristic tempCharacteristic = mBluetoothService.getCharacteristic(UUID_CHARACTERISTIC_TEMPERATURE);
if (tempCharacteristic == null) {
return;
}
if (gatt.setCharacteristicNotification(tempCharacteristic, true)) {
BluetoothGattDescriptor descriptor = tempCharacteristic
.getDescriptor(UUID_CHARACTERISTIC_TEMPERATURE_DESCRIPTOR);
if (descriptor != null) {
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(descriptor);
}
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
final String temperature = new String(characteristic.getValue(), StandardCharsets.UTF_8);
runOnUiThread(new Runnable() {
@Override
public void run() {
mTemperatureTextView.setText(temperature);
}
}
);
}
};

Listing 9: Abbildung der BLE-Architektur

@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// after touch release, set new lamp dimming
if (mBluetoothArduinoService != null) {
BluetoothGattCharacteristic characteristic = mBluetoothArduinoService.getCharacteristic(characteristicUUID);
if (characteristic != null) {
Log.d(TAG_NAME, "" + seekBar.getProgress());
characteristic.setValue(seekBar.getProgress(),
BluetoothGattCharacteristic.FORMAT_UINT8, 0);
mGatt.writeCharacteristic(characteristic);
}
}
}

Listing 10: LED-Steuerung

Fazit

Dank Arduino und Arduino-kompatiblen Boards geht der Einstieg schnell. Eine breite Community und zahlreiche Bibliotheken ermöglichen es, schnell eine Firmware in der Qualität einer echten Software zu entwickeln, ohne auf die untersten Abstraktionsschichten hinab zu müssen. Der Arduino Bootloader lässt es zu, eine Firmware einfach zu skripten, für größere Applikationen mit mehreren Sensoren und Aktoren. Mit C++ lässt sich auch eine dauerhaft wartbare Software schreiben. In diesem Fall empfiehlt es sich allerdings, von Beginn an die Eclipse-basierte Sloeber-IDE einzusetzen. Sofern die Grundlagen anhand von Arduino verstanden sind, können wir auf eine für den Anwendungsfall optimierte Plattform wechseln. Für kleinere Basteleien zu Hause ist das nicht zwingend nötig. Das Arduino-Umfeld bietet eine Vielzahl von Modulen, auf deren Basis Laien erstaunlich viele smarte Devices bauen können. Das gilt nicht nur für Sensoren, auch für Displays wie E-Paper. Die meisten der genannten Funkprotokolle lassen sich schnell in eine fertige Applikation integrieren – ohne tiefere Kenntnisse der Elektrotechnik und mit nur geringen Löt-Skills. Dank Espruino, Lua und MicroPython lassen sich die Devices auch mit moderneren Sprachen als C/C++ programmieren.

Dirk Dorsch ist leidenschaftlicher Java-Entwickler und befasst sich seit zehn Jahren mit Java-basierten Web- und Backend-Technologien. Neben der Enterprise-Entwicklung befasst er sich in innovativen IoT-Projekten mit gängigen IoT-(Cloud-)Plattformen und dem Rapid Prototyping der Hardware.

Dr. Matthias Berning hat langjährige Erfahrung in der Integration energieeffizienter Funktechnologien in IoT-Devices. Mit seinem Hintergrund in Elektrotechnik und Informatik arbeitet er stets an der Schnittstelle zwischen physischer und digitaler Welt.

Christian Haß ist seit sieben Jahren passionierter mobile Developer für iOS und Android. In den letzten Jahren hat er seinen Fokus neben klassischen B2B- und B2C-Apps verstärkt auf Connected-Apps im IoT- und E-Health-Bereich gelegt.

Links & Literatur

[1] Arduino-Sourcecode: https://www.arduino.cc/en/main/software

[2] Eclipse: http://eclipse.baeyens.it

[3] Arduino-Tutorial: https://www.arduino.cc/en/Tutorial/HomePage

[4] Bibliotheken: https://www.arduino.cc/en/Guide/Libraries

[5] CoAP: http://coap.technology/

[6] MQTT: http://mqtt.org/

[7] GATT: https://www.bluetooth.com/specifications/gatt/characteristics

[8] GATT-Spezifikation: https://www.bluetooth.com/specifications/gatt/services

[9] BLE: https://github.com/sandeepmistry/arduino-BLEPeripheral

[10] nRF52832 Breakout Board Hookup Guide: https://learn.sparkfun.com/tutorials/nrf52832-breakout-board-hookup-guide

[11] nRF Connect for Mobile: https://play.google.com/store/apps/details?id=no.nordicsemi.android.mcp

Teil 1: Einstieg in das Entwicklerkit für Android Things

Softwarebaukasten fürs Internet der Dinge
von Tam Hanna

In Zeiten von Industrie 4.0, Losgröße Eins und anderen Nettigkeiten ist man als Entwickler von Hardwareprojekten darauf angewiesen, seine Lösungen so schnell wie möglich auf den Markt zu bringen. Hohe Entwicklungskosten sind dabei ein großes Hindernis. Google greift mit Android Things auf der Softwareseite an. Anstatt wie bisher mit C zu programmieren und mit dem wenig spektakulären GUI-Stack von Unix herumzuschustern, können Nutzer von Android Things stattdessen auf das von Android gewohnte Java-API samt Googles Unterstützungsinfrastruktur zurückgreifen.

Artikelserie

Teil 1: Einstieg in das Entwicklerkit für Android Things

Teil 2: Sensorik mit Android Things steuern

Teil 3: Hardware mit Android Things steuern

Heutige Achtkernprozessoren stellen immense Ansprüche an ihre Umgebung. Wer in der Vergangenheit Boards für Mikrochips oder Atmels Achtbitter entwickelt hat, ist auf keinen Fall für die Arbeit mit diesen Systemen gerüstet. Stichworte wie Transmission Lines und Signallaufzeiten sollten ausreichen, um reichlich Schweißperlen auf die Stirn des potenziellen Designers zu zaubern. Google begegnet diesem Problem durch Entwicklerkits. Dabei handelt es sich um am Raspberry-Pi-orientierte Halbzeuge, die sich mit geringem Aufwand in eigene Schaltungen einbinden lassen. Manche bringen sogar Pfostenstecker mit, mit denen man in einer kleinen Serie durchaus produktiv arbeiten kann.

Momentan unterstützt Google sowohl x86- als auch ARM-Prozessoren. Da Intel das Internet of Things mittlerweile aufgegeben hat, werden Edison und Curie von Google nicht mehr mit Updates versorgt. Da die von Freescale – beziehungsweise jetzt NXP – gefertigten Prozessoren der Entwicklerkits nicht jedermanns Sache sind, greifen wir auf einen Raspberry Pi 3 zurück.

Konnte man die Images dafür bisher unbürokratisch herunterladen, setzt Google seit einiger Zeit auf die sogenannte Partners Console. Öffnen Sie den URL https://partner.android.com/things/console/. Klicken Sie auf den grünen Knopf Create your Product, um den Assistenten zum Anlegen einer neuen Hardwarelösung zu starten. Als Produktname wollen wir SUS1 auswählen, im Feld SOM Type wählen wir den Raspberry Pi 3. Die Option für die Google Play Services lassen wir aktiviert. Das Entfernen spart lediglich einige Megabyte Speicherplatz. Für die Größe der OEM-Partition wählen wir 32 MB aus und klicken danach auf Create. Achtung: Der SOM-Typ, die Integration von Google Play und die Größe der für Entwickler-APKs vorgesehenen OEM-Partition lassen sich nach dem Erstellen nicht mehr verändern.

Die Android-Things-Konsole führt Sie danach zu den Produkteinstellungen. Klicken Sie dort auf Factory Images, um mit dem Erstellen eines auf die SD-Karte brennbaren Image zu beginnen. In der Rubrik Bundles wird die Option Empty Bundle ausgewählt, während Sie sich in der Rubrik zur Android-Things-Version für 0.50 oder eine spätere Version entscheiden. Klicken Sie danach auf Download Build, um die Kompilierung des Image anzustoßen. Achten Sie darauf, dass der Downloadlink mitunter eine Fassade ist. Wer ihn zum ersten Mal anklickt, muss eine oder mehrere Minuten warten, während Googles Server ein für die Applikation passendes Image kompiliert. Es wird aber immerhin ein Fortschrittsbalken eingeblendet, der über den aktuellen Zustand der Arbeiten informiert.

Nach getaner Arbeit bietet das Backend das Herunterladen einer ZIP-Datei an. Die Konfiguration des Autors hörte auf den Namen SUS1_Raspberry Pi3_0_OIR1.170720.015_userdebug_build.zip und ist 291,7 MB groß. Entpacken Sie das Archiv sodann, um eine 4,6 GB große IMG-Datei freizulegen. Diese sollte auf eine SD-Karte gebrannt werden, um für den Prozessrechner zur Verfügung zu stehen.

Verbinden Sie daraufhin den Prozessrechner mit Maus, Tastatur, Bildschirm und Internet, bevor Sie das Android-Betriebssystem anwerfen. Wundern Sie sich nicht darüber, dass der Bildschirm anfangs einige Zeit schwarz bleibt. Das Betriebssystem nutzt diese Zeit, um die Partitionstabelle auf den maximalen Speicherbereich auszudehnen. Dieser Prozess kann je nach Kartengeschwindigkeit schon mal eine halbe Stunde dauern. Wenn das geschehen ist, erscheint der Startbildschirm von Android Things. Er zeigt unter anderem die IP-Adresse an, unter der der Prozessrechner auf Eingaben wartet.

Der wichtigste Unterschied zwischen einem gewöhnlichen Android und der IoT-Version ist, dass Google Android Things eine als Things Support Library bezeichnete Erweiterung spendiert. Es handelt sich dabei um ein Softwaremodul, das für die Kommunikation mit verschiedenen Hardwarestandards vorgesehen ist. Google unterstützt zurzeit GPIO, PWM, UART, I2C und SPI. Wem hier das Fehlen des OneWire-Standards auffällt, liegt nicht falsch: Google hat das insbesondere für Temperatursensoren populäre Protokoll bisher nicht implementiert. Viel interessanter ist in diesem Zusammenhang die in Abbildung 2 gezeigte Änderung, die den User Space des Android-Betriebssystems als Ganzes betrifft. Android-Things-Geräte werden in der Praxis nämlich gerne mit Sensoren verbunden, deren Informationen auch für das Betriebssystem interessant sind. So könnten per GPS angelieferte Koordinateninformationen beispielsweise in Google Maps wandern, während Tastaturknöpfe den GUI-Stack mit den von Android TV bekannten Navigatorereignissen parametrieren. Das ermöglicht eine Gruppe von als User Drivers bezeichneten Klassen.

Abb. 1: Android Things ist im Grunde genommen Android

Abb. 2: User Driver ermöglichen die Interaktion zwischen Sensoren und Betriebssystem

Als Entwicklungsumgebung dient – wie sollte es auch anders sein – Android Studio. Die folgenden Schritte entstanden unter Android Studio 2.3.3, als Hostbetriebssystem kam Ubuntu 14.04 zum Einsatz. Für die aktuellste Version von Android Things ist momentan die Version 25.0.3 des SDK-Tools erforderlich. Als API-Level muss 26 oder Android 8.0 ausgewählt sein.

Während Android Studio 3.0 Entwicklern die Möglichkeit bieten wird, Android-Things-Projekte aus dem GUI heraus anzulegen, ist unter Android Studio 2.x ein kleiner Umweg erforderlich. Google bietet in einem Repository unter [1] ein Beispielprojekt an, das Sie durch Anklicken des Downloadbuttons herunterladen können. Legen Sie das Archiv sodann an einem bequem zugänglichen Ort ab und laden Sie es über die Option Open an existing Android Studio Project in Ihre Installation von Android Studio.

Ordnung im Tempel

Da das in GitHub bereitliegende Projektskelett für eine etwas ältere Version von Android vorgesehen ist, wird Gradle einige Male zum Herunterladen zusätzlicher Komponenten auffordern. Geben Sie diesen Anträgen statt und wundern Sie sich nicht, dass mehrere Durchläufe erforderlich sind. Das Build-System kann in jedem Durchlauf nur eine fehlende Komponente monieren. Öffnen Sie im ersten Schritt die zur Applikation gehörende build.gradle-Datei, und passen Sie den Dependencies-Block folgendermaßen an:

dependencies {
provided 'com.google.android.things:androidthings:0.5-devpreview'
}

Hier können Sie auch die Version der verwendeten Build-Tools aktualisieren:

android {
compileSdkVersion 26
buildToolsVersion '25.0.3'

Vergessen Sie dabei nicht, die Target-Attribute anzupassen. Im nächsten Schritt können wir zur Manifestdatei wechseln, die einige Android-Things-spezifische Elemente aufweist:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.androidthings.myproject">
<application
android:allowBackup="true"
android:icon="@android:drawable/sym_def_app_icon"
android:label="@string/app_name">
<uses-library android:name="com.google.android.things"/>

Die erste interessante Änderung ist die Einbindung der Bibliothek com.google.android.things. Sie ist erforderlich, um auf die Programmierschnittstellen zugreifen zu können (Abb. 1 und 2). Besonderheit Nummer zwei ist die Deklaration der MainActivity: Ein Android-Things-Gerät ist in seiner Eigenschaft als Fachidiot nämlich nicht mit einem Programmstarter ausgestattet, sondern wirft stattdessen einfach die Activity an, die die zu erledigenden Mess-, Steuer- und Regelungsaufgaben beackert. Aus diesem Grund muss diese Activity mit zusätzlichen Informationen ausgestattet werden:

<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Launch activity automatically on boot -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.IOT_LAUNCHER"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
</application>
</manifest>

Als letzten Akt der Vorbereitung müssen wir unseren Prozessrechner für Android Studio ansprechbar machen. Das Debuggen von Programmen erfolgt unter Nutzung der Android Debug Bridge (ADB). Da wir aber einen Netzwerk-Deploy durchführen, müssen Sie im ersten Schritt eine Konsole öffnen, in das ADB-Verzeichnis wechseln und dort einen expliziten Verbindungsbefehl abgeben:

tamhan@TAMHAN14:~/Android/Sdk/platform-tools$ ./adb connect 10.42.0.44:5555

Nach dem erfolgreichen Verbindungsaufbau erscheint der Prozessrechner in der Ausgabe von adb devices (Abb. 3). Achten Sie darauf, dass diese Verbindung nach jedem Ruhezustand, jedem Herunterfahren oder Stand-by-Modus von Prozessrechner oder Workstation neu aufgebaut werden muss.

Abb. 3: Dieser Prozessrechner ist einsatzbereit

Debugging per Funk: Der Raspberry Pi 3 unterstützt in der Theorie das Debugging per WLAN. Ich rate davon ab. Die Latenz sorgt dafür, dass der Debugger wesentlich langsamer arbeitet.

Interaktion mit der Hardware

Interessant wird es, wenn wir den in Abbildung 4 schematisch gezeigten 40-Pin-Connector zur Kommunikation mit externer Hardware einsetzen. Im Großen und Ganzen verhält sich der Raspberry Pi dabei wie gewohnt: Beachten Sie, dass die Pins BCM14 und BCM15 bei eingeschaltetem Bluetoothmodus nicht zur Verfügung stehen [2]. Als erste Aufgabe wollen wir die in Abbildung 5 gezeigte Schaltung nachbauen: Sie besteht aus insgesamt fünf Knöpfen, die mit dem Prozessrechner verbunden sind. Die Entprellung soll aus Gründen der Bequemlichkeit per Software erfolgen.

Abb. 4: Die Pinbelegung von Android Things weist verdächtige Ähnlichkeiten zu normalem Unix auf

Abb. 5: Vorwiderstände, ein paar Taster – und der Ersatznavigator für den Raspberry Pi ist einsatzbereit

Die Frage nach der effizientesten Korrelationsmöglichkeit zwischen Hardwareressourcen und Programmcode ist per se nichts Neues. Embedded-Entwickler ärgern sich darüber seit Jahren. Google geht mit Android Things insofern einen neuen Weg, als jedes Element – also sogar jeder GPIO-Pin – durch einen systemweit einzigartigen String beschrieben ist. Beim Portieren von Hardware auf neue Architekturen und beim Umstieg auf Android Things im Allgemeinen kann es interessant sein, eine Liste aller Peripheriegeräte auszugeben. Im Fall des GPIO-Subsystems erfolgt das über folgenden Code, den sie idealerweise in der onCreate-Methode von MainActivity platzieren:

PeripheralManagerService manager = new PeripheralManagerService();
List<String> portList = manager.getGpioList();
if (portList.isEmpty()) {
Log.i(TAG, "No GPIO port available on this device.");
} else {
Log.i(TAG, "List of available ports: " + portList);
}

Führen Sie das Programm sodann aus, um sich am Programmverhalten zu erfreuen.

Für den produktiven Einsatz des GPIO-Pins müssen wir diesen in Form einer Wrapper-Klasse ansprechbar machen. Dies erfolgt über eine Gruppe von Instanzen der Klasse Gpio, die wir im Körper der Activity als Member anlegen:

public class MainActivity extends Activity {
private Gpio myGpioLeft, myGpioRight, myGpioUp, myGpioDown, myGpioCenter;

Im nächsten Schritt können wir die Strings nutzen, um die Objekte zu beleben. Die Interaktion mit Hardware erfolgt in der Android-Things-Bibliothek prinzipiell über ein als PeripheralManagerService bezeichnetes Hilfsobjekt. Es bekommt einen String übergeben und liefert daraufhin die passende Objektinstanz zurück. Google zeigt sich an dieser Stelle insofern kooperativ, als dass die Firmware das Festlegen des High- und des Low-Zustands erlaubt. Hierzu eine Überlegung: Mit unserer Schaltung ziehen Sie die Eingänge des Raspberry Pi aufgrund ihrer hohen Eingangsimpedanz auf High, wenn die Schalter geöffnet sind. Das Drücken des Schalters zieht den Pegel auf Low. Dank der in Android Things erfolgenden Inversion müssen wir uns darüber allerdings nicht weiter aufregen.

Unsere nächste Amtshandlung besteht darin, alle 50 ms die Zustände der Pins einzulesen. Das erledigen wir idealerweise in einem Thread, der folgendermaßen aussieht:

public class SniffThread extends Thread{
...
@Override
public void run() {
while(1==1) {
try {
if (myL.getValue()) Log.d("SUS", "Left!");
if (myR.getValue()) Log.d("SUS", "Right!");
if (myU.getValue()) Log.d("SUS", "Up!");
if (myD.getValue()) Log.d("SUS", "Down!");
if (myC.getValue()) Log.d("SUS", "Center!");
Thread.sleep(50);//20ms
} catch (Exception e) {/*STFU*/}
}
}
}

Ich gehe einfach davon aus, dass Sie wissen, wie Sie den Thread zum Laufen bringen. Drücken Sie danach auf die Taster, um sich an der Ausgabe von Meldungen in der Debugger-Konsole zu erfreuen.

Intelligente Interaktion

Auch wenn Android in den ersten Prototypen auch für Geräte mit Tastenbedienung vorgesehen war: Seit vielen Jahren haben alle Android-Smartphones einen Touchscreen. Dass der GUI-Stack von Googles Betriebssystem auch für die Arbeit mit touchscreenloser Hardware vorbereitet ist, verdanken Entwickler einem mittlerweile in Vergessenheit geratenem Gerät. Google hatte einst versucht, mit Android TV im Smart-TV-Markt Fuß zu fassen. Hierzu wurde der GUI-Stack um eine Gruppe von Features erweitert, die die Interaktion mit Steuerelementen über einen Fünf-Wege-Navigator ermöglichen.

Rein zufällig enthält unsere Schaltung auch fünf Taster. Wir wollen die von ihnen angelieferten Ereignisse fürs Betriebssystem nutzbar machen. Dazu sind im ersten Schritt allerdings einige Steuerelemente erforderlich, die man idealerweise in einer Ressourcendatei anlegt.

Ärgerlicherweise muss unser Android-Things-Beispielprojekt ohne eine derartige Datei auskommen. Man hat im Hause Google die Implementierung schlicht vergessen. Der schnellste Weg zur Lösung dieses Problems ist das Rechtsklicken des App-Nodes im Projektfenster: Wählen Sie daraufhin die Option New | Activity | Empty Activity. Lassen Sie die Einstellungen des Assistenten unverändert. Die daraufhin erstellte Datei Main2Activity.java ist für uns nicht von Relevanz. Pedantische Naturen entfernen Sie aus der Manifestdatei.

Im Unterordner /layout/ gibt es nun eine Datei namens activity_main2.xml, die sich wie gewohnt im WYSIWYG-Editor von Android Studio bearbeiten lässt. Wir schreiben eine aus neun Widgets bestehende Matrix. Im ersten Akt ist eine Gruppe von ineinander verschachtelten Layouts erforderlich. Viel interessanter ist in diesem Zusammenhang der Korpus der einzelnen Widgets, hier beispielsweise der Code für das Zahlenfeld Numero 5:

<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:nextFocusDown="@id/button8"
android:nextFocusLeft="@id/button4"
android:nextFocusRight="@id/button6"
android:nextFocusUp="@id/button2"
android:text="Button 5" />

Die nextFocusX-Attribute erlauben dem Entwickler das Beschreiben der Bewegungsrichtung durch die Steuerelemente. Die IDs werden vom Android-GUI-Stack automatisch aktiviert, wenn die betreffenden Ereignisse von Tastatur oder 5-Way-Navigator eingehen. Für das eigentliche Bereitstellen der im Mark-up enthaltenen Steuerelemente ist ein Aufruf der Methode setContentView erforderlich. Diese Methode dürfte nur den wenigsten Android-Entwicklern bekannt sein, weil sie so gut wie immer von Android Studio generiert wird:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
...

Diese unter Eclipse problemlos kompilierbare Ressourcendatei wird von Android Studios strengerer Verifikation mit dem Fehler Error:(55, 40) No resource found that matches the given name (at 'nextFocusDown' with value '@id/button8') abgelehnt. Das liegt daran, dass AAPT eine strengere Prüfung des Mark-ups vornimmt und feststellt, dass die IDs an dieser Stelle noch nicht mit Widgets verdrahtet sind. Zur Lösung des Problems reicht es aus, monierte Ressourcen mit einem + zu markieren:

android:nextFocusDown="@+id/button8"
android:nextFocusLeft="@id/button4"
android:nextFocusRight="@+id/button6"

Damit ist das Programm zur Ausführung bereit. Ärgerlicherweise erscheint das aber noch nicht am Bildschirm. Über die Ursachen dieses Problems wollen wir in der nächsten Ausgabe sinnieren, und auch darüber, wie es anschließend weitergeht.

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 für Fragen, Trainings und Vorträge gern zur Verfügung. Mail: tamhan@tamoggemon.com

Links & Literatur

[1] Repository für das Beispielprojekt: https://github.com/androidthings/new-project-template

[2] Raspberry Pi 3 Function Mode Matrix: https://developer.android.com/things/hardware/raspberrypi-mode-matrix.html

Table of Contents

- Impressum

- Nichts anbrennen lassen. Mit Googles Firebase Apps auf Herz und Nieren testen (von Tam Hanna)

- Native Apps mit Angular. Mobile Cross-Plattform-Entwicklung mit NativeScript und Angular (von Nils Mehlhorn)

- Android meets Arduino. Mit Arduino und Bluetooth LE in die smarte Welt einsteigen (von Dr. Matthias Berning, Dirk Dorsch und Christian Haß)

- Softwarebaukasten fürs Internet der Dinge. Teil 1: Einstieg in das Entwicklerkit für Android Things (von Tam Hanna)