Kapitel 6: Bastelstunde
Das Hauptgewicht dieses Buchs lag bisher ja auf der Software-Seite der Hausautomation. Nun gibt es leider nicht immer für alles eine Fertigkomponente. Oder der Preis für eine solche Fertigkomponente ist in einer für Privatleute inakzeptablen Höhe. Andererseits gibt es heute alles, was zum selber bauen (fast) beliebiger Komponenten nötig ist, zu durchaus akzeptablen Preisen und in riesiger Auswahl.
Natürlich wird ein selbstgebautes Gerät nie so kompakt und elegant sein können, wie ein industriell gefertigtes Produkt. Für viele Zwecke wird es aber dennoch gut genug sein.
Grundätzliches und was man zum Basteln braucht
Das “Internet der Dinge” besteht aus unabhängigen Einzelkomponenten, die miteinander und mit übergeordneten Steuerungen vernetzt sind, und gemeinsam ein “intelligentes” System bilden. In diesem Kapitel werden zunächst die grundsätzlichen Bestandteile besprechen und dann auch selber kreativ tätig sein.
Jargon
Natürlich hat sich im IoT-Bereich ein eigener Jargon entwickelt. Ich denke, es genügt zunächst, wenn Sie mit folgenden Begriffen etwas anfangen können
- IoT (Internet of Things, Internet der Dinge) - Ein Sammelbegriff für alle autonomen “Dinge” die sich via Internet vernetzen können.
- GPIO (general purpose input ouput) - Universalanschlüsse, die entweder etwas ausgeben oder etwas einlesen können. Was welcher Anschluss tut, wird softwareseitig bestimmt.
- ADC (analog digital converter) - Ein Pin, der eine Spannung am Eingang in einen proportionalen Zahlenwert umwandeln kann.
- PWM (pulse width modulation) - Ein Pin, der “pseudo-analoge” Ausgangssignale erzeugen kann, indem er schnelle Ein/Ausschaltvorgänge produziert. Je nach auszugebendem Wert ist das Verhältnis von Ein- zu Ausschaltzeit anders. So kann beispielsweise ein Licht gedimmt werden, indem man es nur in 50% statt in 100% der Zeit einschaltet, diese Schaltvorgänge aber so schnell nacheinander ausübt, dass das Auge davon getäuscht wird. In gleicher Weise kann man auch die Leistung von Motoren variieren.
- Output Current (Ausgangsstrom) - Die mögliche Ausgangsleistung. Wenn man mehr Leistung verlangt, als diese Zahl angibt, dann wird es nicht einfach “nicht gehen”, sondern es wird möglicherweise etwas durchbrennen. Und zwar etwas im Inneren des Chips, was man nicht reparieren kann. Daher: Wenn Sie nicht genau wissen, was Sie tun (und wie man es berechnet), betrachten sie alle Ausgangspins als praktisch “leistungslos” und hängen sie niemals etwas Stärkeres als etwa eine LED an. Alles, was mehr Leistung braucht, braucht einen Verstärker.
- Input Voltage (Eingangsspannung) - Manche der gebräuchlichen Chips wollen 5 Volt, manche 3.3 Volt. Wenn Sie 3.3 Volt statt 5 Volt liefern, wird es oft klappen. Wenn Sie aber 5 Volt statt 3.3 Volt anschliessen, wird meist etwas kaputt gehen. Also schließen Sie, wenn es sein muss, 5 Volt Peripherie an 3.3 Volt Ausgänge an, aber niemals umgekehrt. Oder nur über einen Spannungswandler.
- serial Port/serielle Schnittstelle - ein altehrwürdiger asynchroner Kommunikationsstandard, der ursprünglich als RS 232 mit 25 Leitungen definiert wurde, später als 9-poliger Anschluss am PC Einzug hielt, und bei Mikrokontrollern auf 3 Leitungen (Rx,Tx, Gnd) reduziert wurde. Da bei einer asynchronen Kommunikation kein gemeinsamer Takt vorgegeben wird, müssen sich die beteiligen Partner im Voraus auf bestimmte Eckwerte einigen (Baudrate, Zahl der Start- Daten- und Stopbits, Parität), sonst kommt es zu keiner Verständigung. Diese Daten sind im Standard nicht fest integriert, sondern müssen für jede Verbindung individuell festgelegt werden. Oft ist man mit 9600,8,n,1 auf der sicheren Seite, aber im Zweifel muss man das Datenblatt studieren. Es geht aber nichts kaputt, wenn man falsche Parameter einsetzt, sondern es kommt lediglich kein Kontakt zustande. Man darf also ruhig ausprobieren.
- I2C (I square C: Inter-integrated circuit) - Ein synchroner Kommunikationsstandard, der mit zwei Drähten auskommt (Daher wird es manchmal auch TWI - Two Wire Interface genannt). An diesen zwei Drähten können bis zu 1024 Peripheriegeräte hängen. Es wird häufig verwendet, um Peripheriebausteine mit Steuergeräten zu verbinden. (Synchron bedeutet: Der Master gibt an einem Draht (SCL) den Takt vor, der für alle Geräte gilt). Es gibt daher bei diesen Bus viel weniger Raum für Missverständnisse, als bei RS232&Co.
- SPI (Serial Peripheral Interface) - Ein weiterer synchroner Kommunikationsstandard, der allerdings drei gemeinsame Drähte und einen weiteren Draht je Peripheriegerät benötigt.
- 1 Wire - Ein asynchroner Kommunikationsstandard, der sogar mit nur einem Draht (und Masse) auskommt. Sogar die Stromversorgung erfolgt dabei über den Signaldraht. Das Peripheriegerät braucht daher keine eigene Energiequelle, muss aber für die Dauer, in denen das Steuergerät das Signal auf “Low” zieht, ausreichend Energie speichern können (z.B. mit einem Kondensator).
- Breakout-Board: Damit ist eine Platine gemeint, die einen für Hobbyisten kaum handhabbaren winzigen SMB-Chip mit Anschlüssen versieht, die der durchschnittlich begabte Bastler mit seinen Projekten verlöten kann, oder die man mit entsprechenden Stiftleisten in eine Platine oder ein Breadboard stecken kann. Ausserdem enthält ein Breakout oft auch vom Chip benötigte Peripherie wie Widerstände und Kondensatoren, manchmal auch Schutzschaltungen gegen Überspannung etc.
- Firmware: Eine Software-Schicht, die zwischen Hardware und Anwendungsprogrammen vermittelt, und oft fest in einen nichtflüchtigen Speicher “gebrannt” wird. Bei PCs wird die Firmware meist BIOS genannt, und als Anwender hat man selten damit zu tun. Bei Mikrokontrollern dagegen muss man je nach gewünschter Entwicklungs- und Ablaufumgebung unterschiedliche Firmwares verwenden. Den Prozess, Firmware zum Controller zu übertragen, nennt man meist “flashen” oder “brennen”, und er erfolgt fast immer über eine serielle Schnittstelle, die bei “besseren” Boards in einen USB Anschluss konvertiert wird.
- SOC (System on a Chip) - Ein nicht ganz scharfer Begriff, der Geräte meint, die im Prinzip aus einem einzigen Chip bestehen, welcher dann eben Mikroprozessor, Speicher und Steuerlogik beinhaltet. in der Realität brauchen solche SOCs trotzdem noch eine gewisse periphere Beschaltung mit geregelter Stromversorgung, Pufferschaltungen etc., aber diese ist vergleichsweise einfach und preisgünstig machbar.
Endkomponenten
Sensoren oder steuerbare Schalter, wie zum Beispiel Licht-, Wärme-, Druck-, Tast-Sensoren, LEDs, LCD-Anzeigen, Lautsprecher, Relais, Optokoppler usw. Es gibt eine Reihe von online-Shops, bei denen man solche Dinge beziehen kann. Nebst alteingesessenen Betrieben wie Conrad oder ELV gibt es auch eine Menge kleinerer Firmen, die zum Teil günstigere Preise und gerade in diesem Bereich auch eine grössere Auswahl bieten. Als Beispiele seien Mikroshop (https://www.mikroshop.ch), Funduino (https://www.funduinoshop.com/) oder DIY-Shop (https://www.diy-shop.ch/de/) genannt Googlen Sie am besten einfach nach dem Namen bzw. der Produktnummer der Komponente die Sie benötigen, und Sie werden fündig werden.
Viele der ersten Suchtreffer werden von Aliexpress sein. Das ist eine Sammelfirma für tausende von winzigen bis riesigen chinesischen Herstellern. Wenn Sie einige Wochen warten können, und auch damit umgehen können, wenn die Produkte manchmal etwas anders sind, als erwartet und ohne jede Dokumentation kommen, dann kann das ein gute Option sein. Die Produkte kosten oft weniger als ein Fünftel der hiesigen Preise.
In der Regel ist es aber meines Erachtens vernünftig, hiesige Geschäfte zu berücksichtigen: Die Lieferung erfolgt schneller, bei Problemen bekommt man rascher Hilfe, die Garantiebedingungen sind klar und durchsetzbar, und man ist auch nie in Gefahr, unwissentlich gegen irgendwelche Zoll- oder Einfuhrbestimmungen zu verstoßen.
Steuergeräte
Natürlich benötigen wir irgendein “intelligentes” Gerät, das die Endkomponente ansteuern kann. Dieses Gerät ist der Vermittler zwischen dem Endgerät und unserer Heimautomations-Zentrale. Hier gibt es zwei grundsätzlich verschiedene Herangehensweisen: Das Programm kann direkt auf diesem Gerät laufen, Steueraufgaben mit einer gewissen eigenen Intelligenz erledigen, oder aber es beschränkt sich darauf, Sensordaten weiterzuleiten und Steuerbefehle entgegenzunehmen und den Aktor entsprechend anzusteuern.
Welchen Weg man wählt, hängt von der Aufgabe, von den zur Verfügung stehenden Mikrocontrollern/SOCs und natürlich den individuellen Präferenzen ab. Auf einem Raspberry wird man auch komplexere Programme direkt ablaufen lassen können, während man auf einem Arduino nur vergleichsweise einfache Aufgaben direkt erledigen kann. Für komplexere Aufgaben wird man ihn eher fernsteuern - zum Beispiel mit einem Raspberry.
Raspberry Pi
Zur Steuerung von Komponenten kann man den uns schon bestens bekannten Raspberry Pi einsetzen. Auch wenn wir sie bisher noch nicht benutzt haben, sind Ihnen die GPIO-Pins mit Sicherheit schon aufgefallen. Diese Anschlüsse kann man zum Lesen oder Schreiben von Schaltimpulsen verwenden. Da Steueraufgaben nicht viel Rechenleistung brauchen, ist es im Grunde egal, welchen Raspberry Sie verwenden. Für kleine Peripheriegeräte empfiehlt sich meines Erachtens der Zero W, den es für knapp 10 Euro gibt, und der bereits per WLAN angebunden werden kann. Oder vielleicht haben Sie von Ihren ersten Raspberry-Gehversuchen noch einen alten, langsamen Model B im Keller. Auch der tut es, braucht allerdings ein Netzwerkkabel oder einen Wifi-Stick für die Einbindung ins Netz.
Die Pin-Nummerierung der Raspberries ist leider reichlich verwirrend: es gibt verschiedene Systeme (Broadcom-Pin, Header-Nummer, GPIO, Wiring-Pi usw.), die bezüglich Nummernfolge keiner Logik zu folgen scheinen. Leider sind auf dem Pi selbst auch keine Pin-Nummern aufgedruckt. Schauen Sie im Zweifelsfall auf einem entsprechenden Schaubild, z.B. http://pi4j.com/pins/model-3b-rev1.html oder http://pinout.xyz wie es bei Ihrem Pi ist. Prüfen Sie zweimal, ob Sie jeden Draht richtig gesteckt haben, bevor Sie einschalten. Wenn Sie falsche Verbindungen ziehen, und dann Strom darauf geben, geht höchstwahrscheinlich etwas kaputt. Oft leider der Pi.
Raspberries können über I2C, SPI und die serielle Schnittstelle mit Peripheriegeräten kommunizieren. Man muss dabei aber beachten, dass der Raspi keine Spannungen über 3.3 Volt verträgt, viele andere Geräte aber hemmungslos mit 5 Volt kommunizieren.
Arduino
Der Arduino ist klar der Platzhirsch unter den Mikrocontrollern im Hobbybereich. Er steht hier nur deshalb erst an zweiter Stelle, weil in diesem Buch der Raspberry schon in Verwendung und darum beim Leser vermutlich schon vorhanden ist. Arduino ist eine ursprünglich für italienische Schulen entwickelte OpenSource-Plattform2. Wegen Lizenzquerelen entstanden verschiedene Markennamen (Arduino, Genuino), und es gibt inzwischen eine unübersehbare Zahl von Varianten und Nachahmerprodukten. Dazu sollte man noch wissen, dass es bei vielen Clones, die einen billigeren Chip zur USB Anbindung verwenden, manchmal knifflig sein kann, sie mit dem Computer zu verbinden. Anfängern würde ich in jedem Fall zu Original Arduino/Genuino Modellen raten. Im Folgenden meine ich jeweils den Arduino/Genuino Uno R3, aber alle Varianten sind ähnlich genug, dass das keine grosse Rolle spielt.
Arduinos sind vergleichsweise schwachbrüstig: Der Uno hat ganze 32 kB Speicher und läuft mit 16 Mhz. Ihr Handy hat vermutlich mehr als hunderttausendmal so viel Speicher und die hundertfache Taktfrequenz. Das darf aber keinen falschen Eindruck erwecken: Ein Mikrocontroller muss keine Videos abspielen, sondern er soll nur Mess- und Schaltaufgaben erledigen. Dafür ist die Leistung absolut ausreichend. Da er kein Betriebssystem laden muss, beginnt ein Arduino (bzw. dessen Firmware) sofort nach dem Einschalten, sein Programm abzuarbeiten. Man muss ihn am Ende auch nicht irgendwie “herunterfahren” sondern kann einfach den Stecker ziehen. Programme für den Arduino kann man komfortabel auf dem PC in der “Arduino IDE” (https://www.arduino.cc/en/main/software) entwickeln und dann über den USB Anschluss auf den Arduino übertragen. Die Programmiersprache ist im Prinzip C++ mit einigen Einschränkungen und dafür einigen systemspezifischen Erweiterungen, etwa Befehle, um GPIOs zu schalten. Für viele Standardaufgaben existiert ein riesiger Fundus an vorgefertigten Libraries, so dass das Programmieren sich oft darauf beschränkt, die richtige Library zu finden und diese mit ein paar wenigen Programmzeilen anzubinden.
Das Programm wird in einem nichtflüchtigen Speicher gehalten und startet bei jedem Einschalten sofort, bis es von einem anderen Programm überschrieben wird. Beim Arduino sind nie mehrere Programme gleichzeitig im Speicher. Arduinos brauchen relativ wenig Strom und können daher, speziell in der “Arduino Nano”-Variante auch gut mit Batterien betrieben werden.
Der Arduino Uno ist einsteigerfreundlich, denn er kommt mit verschiedenen Stromquellen zwischen 5 und 15 Volt zurecht und verzeiht auch schonmal eine falsche Verschaltung ohne kapitalen Schaden.
Eine weitergehende Einführung würde den Rahmen dieses Buches sprengen; bei Interesse empfehle ich, Google mit “Arduino” zu füttern oder direkt auf der Seite http://arduino.cc vorbeizuschauen.
Für uns interessant: Auch Arduinos können via I2C, SPI und serielle Schnittstelle kommunizieren.
Ein großer Vorteil für Bastler gegenüber dem Raspberry: Die Pins sind beschriftet, die Verwechslungsgefahr daher deutlich geringer.
Zur Anschauung hier ein kurzes Arduino-Programm, welches eine LED an GPIO 3 blinken lässt:
#define LED 3
void setup() {
pinMode(LED, OUTPUT);
}
void loop() {
digitalWrite(LED, HIGH);
delay(500);
digitalWrite(LED, LOW);
delay(1000);
}
ESP8266 und ESP32
Diese Chips des chinesischen Herstellers Espressif sind vielleicht wegen des etwas sperrigeren Namens im Hobby-Bereich weniger bekannt, als die Arduinos, sind aber gleichwohl in den letzten Jahren mit über 100 Millionen verkauften Exemplaren inzwischen zu den meistverkauften IoT-Geräten geworden. Viele WiFi-fähige IoT-Geräte haben einen dieser Chips an Bord.
Das ist an sich kein Wunder: Ein ESP8266-Board kostet weniger als ein Arduino, hat aber einen mit bis zu 160 Mhz deutlich schneller getakteten Prozessor und je nach Version auch mehr Speicher und ebenso frei programmierbare GPIOs. Und als wichtigstes Merkmal haben diese Chips integriertes WLAN und, im Fall des ESP32, auch Bluetooth an Bord. Beides braucht beim Arduino relativ teure Zusatzhardware.
Der ESP8266 kam 2014 auf den Markt, der leistungsfähigere ESP32 folgte 2016. Für den älteren, aber für die meisten Zwecke ausreichenden ESP8266 findet man generell mehr Unterstützung im Internet und mehr kompatible Software, so dass ich mich im Folgenden auf diesen beschränke.
Man kann den ESP8266 (ebenso den ESP32) entweder direkt als “nackten” Chip oder in einer minimalen Breakout-Package kaufen. Beides ist nicht empfehlenswert. Stattdessen sollte man sich als Amateur eher für ein “Development Board” entscheiden. Ein solches Board ist immer noch recht klein, enthält aber alles, was man braucht, um den ESP8266 direkt an den USB Port eines Computers anzuschliessen und dort zu programmieren. Auch bei den Development Boards hat man allerdings die Qual der Wahl zwischen einer Vielzahl von Herstellern. Als besonders empfehlenswert würde ich derzeit die NodeMCU, die Wemos D1 Mini und die Adafruit Feather-Boards hervorheben. Diese Geräte kosten bei europäischen Händlern um 5-15€, aus China 1-2€, sind also für Smarthome Projekte ohne Weiteres erschwinglich. Der Markt entwickelt sich allerdings so schnell, dass Sie besser eigene Recherchen machen, als sich auf meine möglicherweise bereits veralteten Empfehlungen hier abzustützen. Das, was ich Ihnen im Weiteren zeigen möchte, sollte mit jedem beliebigen ESP8266-Board möglich sein, das Sie erfolgreich mit Ihrem Computer verbinden können.
Hier als Beispiel eine NodeMCU:
Zum Programmieren kann man eine Vielzahl von Tools und IDEs verwenden, die meisten Programmiersprachen von JavaScript über C/C++ bis Python haben ihren Platz. NodeMCU etwa ist nicht nur eine Platine, sondern auch ein ganzes Ökosystem aus Firmware und Programmierphilosophie, basierend auf der Skriptsprache Lua. Viele Hobbyisten nutzen jedoch die Tatsache, dass man die ESP-Familie auch ganz einfach in die Arduino IDE einbinden und Arduino-Programme ohne grosse Änderungen verwenden kann. Das macht nicht nur die Lernkurve flacher, sondern hat auch den Vorteil, dass man die existierende riesige Zahl von Bibliotheken für den Arduino mit allenfalls minimalen Änderungen verwenden kann, und somit das Rad nicht immer wieder neu erfinden muss.
Wenn das Verbinden des NodeMCU mit dem Computer nicht klappt, müssen Sie eventuell einen USB-Treiber laden: https://www.silabs.com/products/development-tools/software/usb-to-uart-bridge-vcp-drivers
Wenn Sie sich allerdings bisher noch nicht an die Arduino IDE “gewöhnt” haben, sollten Sie sich vielleicht zunächst auch andere SDKs anschauen, an vorderster Front für den NodeMCU natürlich dessen eigene firmware und SDK: https://github.com/nodemcu/nodemcu-firmware, erwähnenswert aber auch PlatformIO https://platformio.org und MongooseOS https://mongoose-os.com/software.html
Und, last but not least, eine sehr interessante Alternative ist ESPEasy (https://www.letscontrolit.com/wiki/index.php/ESPEasy). Hier enthält die Firmware einen kompletten Webserver, der sich bequem per Web-Interface programmieren lässt. Zu diesem Zweck sind eine ganze Reihe von Sensoren vorinstalliert. Ich werde etwas später ein Projekt auf einem NodeMCU via ESPEasy erstellen.
Breadboards und Jumperkabel
Fast nie lötet man ein Projekt sofort zusammen. Stattdessen baut man es zunächst auf einem Experimentierbrett auf und prüft, ob es sich tatsächlich so verhält, wie es soll. Als Standard hat sich ein bestimmtes Layout ebenso wie der Name Breadboard3 etabliert.
Meist (nicht immer) befinden sich auf beiden Längsseiten Kontaktreihen für die Stromversorgung, wobei alle Anschlüsse derselben Seite und derselben Polung miteinander verbunden sind. In der Mitte des Bretts finden sich zwei oder mehrere Reihen von Fünferblöcken, die jeweils miteinander verbunden sind. Zur besseren Beschreibbarkeit eines Versuchsaufbaus sind die Reihen und Spalten oft auch beschriftet, wie hier. Hier sind also beispielsweise 3 a-e und 3 f-j jeweils miteinander verbunden, nicht aber 3e und 3f oder 2e und 3e.
Die Löcher sind in einem bestimmten Rastermass (2.54 mm bzw. 1/10 Zoll) angeordnet, das nicht ganz zufällig exakt zu den Beinchen vieler Elektronik-Komponenten passt. Zur Verdrahtung verwendet man am besten vorgefertigte “Jumper Wires”, die es in verschiedenen Längen zu kaufen gibt. Zur Überbrückung kurzer Distanzen kann man sich auch mit Heftklammern behelfen.
Platinen etc. für definitiven Aufbau
Wenn das selbst gebaute IoT Gerät tut, was es soll, sollte man es allerdings nicht als Steckbrett-Aufbau in den harten Alltag entlassen. Die Kontakte sind etwas wacklig, das Ganze ist nicht stabil, und die Lieblings-Katze könnte in einer destruktiven Phase die Arbeit von vielen Tagen zerstören.
Es hilft nichts: Für den definitiven Aufbau sollten Sie sich an den Gedanken gewöhnen, einen Lötkolben in die Hand zu nehmen. Das ist nicht so schwierig, wie man es sich als Einsteiger oft vorstellt. Am besten schauen Sie sich dazu ein Lehrvideo an, wie zum Beispiel dieses: https://www.youtube.com/watch?v=4DWUZp1t7Ls. Am Lötkolben würde ich nicht unbedingt sparen - mit einem 10-Euro-Gerät werden Sie vermutlich bald ein Mehrfaches der Einsparung für kaputte Bauteile und Magentabletten ausgeben. Der Lötkolben soll gut in der Hand liegen und in nützlicher Frist 300-350°C erreichen können.
Als Träger für die Elektronik bieten sich Lochrasterplatinen an (wenn Sie nicht gleich Leiterplatten selber ätzen wollen). Da gibt es verschiedene Varianten mit Lötstreifen, Lötpunkten, Lötdoppelpunkten etc. und verschiedene Materialien von Glasfaser bis Hartpappe, getränkt mi Epoxydharz, Melamin oder Polyester. Ich bevorzuge nach einigem Experimentieren simple Lötpunkte auf Hartpappe. Letzteres, weil es sich einfacher auf Mass schneiden lässt. Auch die Hartpappe ist übrigens mit Kunststoff getränkt und ausgehärtet, so dass sie wesentlich beständiger ist, als der Begriff suggeriert. Aber sie lässt sich trotzdem leichter bearbeiten, als eine Glasfaser-Basis.
Bonus: 3D Drucker fürs Gehäuse
Um die Katze noch weiter zu desavouieren, sollte man seinen Elektronikbauwerken ein schützendes Gehäuse spendieren. Gehäuse gibt es in allen möglichen Formen und Grössen zu kaufen, aber natürlich ist just gerade diese spezielle Form mit dieser speziellen Befestigungsart, die wir jetzt gerade brauchen, entweder nicht vorrätig oder überhaupt nicht zu bekommen. Dann hilft nur selber machen. 3D Drucker sind absolut erschwinglich geworden. Wenn Sie nicht gerade sehr grosse Stücke bauen wollen, und mit einer Genauigkeit im Fünftelmillimeterbereich leben können, kommen Sie mit 200 bis 300 Euro zu einem brauchbaren Gerät, das Ihnen robuste und formschöne Gehäuse, Verankerungen, Schienen und Klammern herstellen kann. Falls Sie Inspiration brauchen, können sie sich zum Beispiel auf Thingiverse umsehen. Hier einige Beispiele für NodeMCU Gehäuse: https://www.thingiverse.com/search?q=nodemcu. Diese Entwürfe können Sie direkt ausdrucken, ausgedruckt kaufen, verändern, oder einfach nur als Anregung für eigene Projekte verwenden.
Der Entwurf solcher 3D-Stücke braucht entweder ein gewisses gestalterisches Talent, oder mathematisches Verständnis. Wenn Sie eher zu Ersterem tendieren, hilft Ihnen ein visuelles 3D Programm, wie zum Beispiel das sehr einsteigerfreundliche kostenlose Tinkercad https://www.tinkercad.com/. Für fortgeschrittene Designer gibt es eine Vielzahl von Alternativen, aber fortgeschrittene Designer haben bestimmt schon ihr Lieblingsprogramm gefunden, und brauchen hier keine Hinweise. Wenn Sie hingegen eher mathematisches, als künstlerisches Geschick haben, sei Ihnen OpenScad (http://www.openscad.org) ans Herz gelegt. Mit OpenScad erstellen Sie dreidimensionale Objekte mit einer Programmiersprache.
Beispielsweise konstruiert das hier:
translate([10,0,0])
cube([10,10,10]);
Einen Würfel mit 10 Millimetern Kantenlänge an der Position 10,0,0.
Und dieses Programm:
// Diameter of the fan
blade_length=60; // [20:100]
// Width of a blade
blade_width=7; //[5:10]
// Diameter of the axe
axe_diameter=2; // [2:10]
// Diameter of the center hub
hub_diameter=18; // [10:30]
// Angle of the blades
angle=45; // [30:60]
// Height of the center hub
height=blade_width*2.5*cos(angle);
// compensate shrink for axe hole
shrink=1.1;
propeller();
module propeller(){
difference(){
cylinder(d=hub_diameter,h=height,$fn=100);
for(i=[0:60:360])
rotate([0,0,i])
blade();
cylinder(d=axe_diameter*shrink,h= 8,$fn=40);
}
}
module blade(){
translate([0,0,height/2])
rotate([90,angle,0])
linear_extrude(height=blade_length/2)
scale([0.2,2.5])
circle(d=blade_width,$fn=50);
}
erzeugt das hier:
Sie können sich vorstellen, dass so ein “technisches” Objekt mit Freihandzeichnen, künstlerische Begabung hin oder her, eher schwierig herzustellen und zu variieren wäre. In diesem Programm dagegen ist es ein Klacks, die Rotorblätter etwas länger und in einem anderen Anstellwinkel zu fertigen. Also wählen Sie Ihr Werkzeug je nach Talent und Anforderung, und probieren Sie anfangs auch unterschiedliche Werkzeuge aus.
Egal, mit welchem Programm Sie das Objekt erstellt haben, im Anschluss müssen Sie es als *.stl-Datei (oder ein anderes Format, das der Slicer versteht) exportieren, und ein weiteres Programm, eben diesen Slicer bemühen. Ein Slicer schneidet, wie der Name sagt, das 3D Objekt in hauchdünne horizontale Scheiben (Wobei “hauchdünn” je nach Druckermodell und Einstellungen etwas wie 0.2 oder 0.4 Millimeter bedeutet.) Bekannte Slicer-Programme sind etwa “cura” oder “slic3r”, wobei letzteres mein Favorit ist.
Diese Scheiben werden dann in eine Reihe von Aktionsbefehlen für den Drucker in einer Sprache namens “Gcode” umgesetzt. Keine Angst, das geht vollautomatisch. Diese .gcode-Datei wiederum kann man dann dem 3D-Drucker zum Frass vorwerfen, und dieser wird, falls er mit ausreichend Filament versehen ist, in etlichen Minuten bis Stunden Druckzeit Scheibe um Scheibe das Objekt aufbauen. Und dieses wird eine verblüffend hohe Stabilität haben. Ein echtes Kunststoff-Objekt, allerdings mit dem Vorteil, dass dieser Kunststoff (Wenn Sie PLA verwenden) aus nachwachsenden Rohstoffen hergestellt und biologisch abbaubar ist.
Es wird eine Weile dauern, bis Sie brauchbare Resultate bekommen, und noch länger, bis Sie richtig gute Resultate bekommen. Man braucht eine gewisse Erfahrung, um sofort zu erkennen, welche Strukturen druckbar sind, und welche nicht (z.B. Überhänge nur bis zu einem gewissen Winkel, Brücken nur bis zu einer gewissen Länge). Und zu viele Parameter kann und muss man beim 3D-Druck variieren und zu viel kann schief gehen. Aber es macht auch viel Freude, “echte” Objekte herzustellen, die einzigartige Unikate sind (zumindest so lange, bis Sie sie ein zweites Mal ausdrucken oder auf Thingiverse publizieren).
Gehäuse für einige der in diesem Kapitel diskutierten Basteleien finden Sie im Quellcode Verzeichnis, das Sie wie im Anhang gezeigt clonen können, und dann:
git checkout -f origin/master
git clean -f
Und zwar im Unterverzeichnis “Bastelstunde”, jeweils als *.stl, *.scad und *.png Datei.
Genug der Theorie, jetzt bauen wir ein richtiges IoT-Gerät, und das mit einem Aufwand von wenigen Euro.
Barometer
Einleitung
Ein Heimautomationssystem braucht zweifellos auch ein Monitoring des Luftdrucks. Schließlich wollen wir den Boiler und das Auto nur dann mit Nachtstrom laden, wenn die Wetteraussichten für den nächsten Tag wenig Sonne versprechen, und die Kaffeemaschine für extra starken Kaffee vorbereiten, wenn die Wetteraussichten trübe sind. Sicher, es gibt Wettervorhersage-Apps, aber wir erhalten genauere Prognosen, wenn wir deren Angaben mit eigenen Luftdruck- und Temperaturmessungen kombinieren.
Für den Luftdruck fand ich keine Homematic Komponente. Wir müssen uns da also selbst behelfen. Von Bosch gibt es Barometer-Thermometer-Chips für kleines Geld, die sich für derartige Zwecke ausgezeichnet eignen. Die Chips tragen je nach Version die Bezeichnungen BMP-180, BMP-280 oder BME-280 (Mit Hygrometer). Es handelt sich allerdings um winzige Dinger, die man als Hobbyist unmöglich direkt irgendwo einsetzen kann. Die Lötstellen sind mikroskopisch klein, und es ist periphere Beschaltung notwendig. Glücklicherweise gibt es von verschiedenen Herstellern Breakout Boards, die die periphere Beschaltung erledigen und nur die interessierenden Anschlüsse in Hobbylöt-freundlicher Grösse nach Aussen führen. Von Adafruit und Grove gibt es derartige Boards für unter 20 Euro; wenn man ein paar Wochen warten und damit leben kann, dass man im Voraus nicht ganz sicher weiss, welche Chip-Variante dann tatsächlich verbaut ist, kann man BMP280/BME280/BMP180-Breakouts auch bei AliExpress für unter 4 Euro bekommen.
Ich habe diesen hier verwendet:
Wie Sie sehen, muss man die Pins selber anlöten. Aber das sollte Sie ja nun nicht mehr schrecken. Eine solche Platine ist nicht so empfindlich, wie man vielleicht glaubt, Man kann den Lötkolben durchaus einige Sekunden lang an die Pins halten, ohne dass etwas durchbrennt. Der eigentliche Chip ist ja auch ein ganzes Stück weit von den Lötösen entfernt.
Machen Sie aber bitte nicht denselben Fehler wie ich beim ersten Mal, die Platine “richtig herum”, also mit dem Chip nach oben, zu löten. Dann sehen Sie nämlich die Beschriftung der Anschlüsse nicht mehr, wenn sie im Breadboard sitzt… Setzen Sie die Header stattdessen so ein, wie hier gezeigt:
Dem Chip ist es völlig egal, ob er auf dem Kopf steht, und so sehen Sie jederzeit, welcher Pin wofür ist. Das gilt natürlich nur für den Versuchsaufbau. Im definitiven Aufbau später sollten Sie der Messgenauigkeit wegen darauf achten, dass der Chip möglichst frei und möglichst weit von anderen Komponenten entfernt platziert wird.
Wie liest man die Messwerte aus, die ein solches Bauteil liefert? Dafür gibt es, wie nicht anders zu erwarten, mehrere Standards, die ich in der Einleitung kurz angesprochen hatte. Bosch hat diesem Chip gleich zwei davon spendiert: I2C und SPI. Die Implementationsdetails brauchen uns jetzt nicht zu interessieren und würden auch zu weit führen, denn wir werden ohnehin eine Library benutzen, die uns das mühsame Einsammeln der Bits abnimmt. Wir müssen nur genug wissen, damit wir für Gerät und Software den richtigen Standard auswählen können.
Zunächst steht die Entscheidung an, wie wir die Daten überhaupt ins Netz holen. Da wir ohnehin einen Raspberry Pi als Heimserver verwenden, bietet es sich an, diesem gleich noch den Nebenjob aufzuhalsen, den BMP-280 auszulesen und dessen Messwerte im ioBroker bereitzustellen. Und wie der glückliche Zufall so spielt, hat der Raspberry schon alles an Bord, was es braucht, um mit einem Peripheriegerät über I2C und SPI zu kommunizieren. Es fehlt nur noch die Software. Weil wir uns inzwischen mit JavaScript und dem NodeJS Ökosystem recht gut auskennen, suchen wir eine NodeJS-Lösung.
Auftritt Johnny-Five (http://johnny-five.io). Fragen Sie mich nicht, warum das so heisst. Johnny-Five ist viel mehr, als nur eine Möglichkeit, den BMP 280 auszulesen. Es ist ein universelles Konzept, Hardware mit verschiedensten Geräten zu steuern. Der Raspberry Pi als Host ist eigentlich nur zweite Wahl. Sein eigentliches Milieu hat Johnny im Land der Arduinos, Espruinos und wie sie alle heissen: Kleinen Mikrocontrollern mit vielen Ein- und Ausgängen. Man kann ein Johnny-Five Programm mit minimalen Änderungen (Im Wesentlichen die Nummern der Pins) für jede der unterstützen Plattformen verwenden. Wenn Sie den Barometer später an einen ferngesteuerten Arduino hängen wollen, kein Problem!
Aber damit wir den BMP 280 nicht gleich durch falsches Anschließen grillen, geht es zunächst ans Datenblatt: https://ae-bst.resource.bosch.com/media/_tech/media/datasheets/BST-BMP280-DS001-19.pdf. Ich habe es bereits für Sie studiert, und das Wesentliche ist:
- Johnny-Five kommuniziert via I2C mit einem BMP 280.
- Der BMP 280 wird mit 3.3 Volt betrieben, was just exakt die Spannung unserer Raspberry-I2C Pins ist.
- Wenn wir wollen, dass der BMP 280 den I2C Bus benutzt, müssen wir vor oder spätestes mit Anlegen der Betriebsspannung dafür sorgen, dass der CSB Pin auf HIGH ist. Wenn CSB irgendwann nach dem Anlegen der Spannung nicht mehr HIGH ist, wird der Chip sich für I2C tot stellen. Wir fixieren diesen Pin also auf 3.3 Volt.
- Der BMP 280 kann zwei I2C Adressen benutzen (das ist nützlich, wenn man zwei BMP280 am selben Bus hängen hat). Welche Adresse das ist, entscheidet der Zustand des Pins SDO. Zieht man ihn auf LOW, ist die Adresse 0x76, zieht man ihn auf HIGH, hört er auf 0x77 (und das erwartet Johnny-Five standardmässig). Wenn man SDO gar nicht beschaltet, ist die Adresse undefiniert, was dazu führt, dass der Chip mal erreichbar ist und mal nicht. Kein erwünschtes Verhalten, darum verbinden wir auch SDO fix mit 3.3 Volt. Es gibt allerdings auch Breakout-Boards, die die Adresse bereits fix eingestellt haben, und den SDO-Pin gar nicht nach aussen führen.
Mit diesem Vorwissen geht es weiter auf dem Raspi. Ich würde vorschlagen, zunächst zur Übung, und zum schauen, ob überhaupt alles funktioniert, zunächst ein kleines Testprogramm zu erstellen. Gehen Sie per SSH auf den Raspberry (oder schliessen Sie ihn an Tastatur und Bildschirm an) und erstellen Sie ein Verzeichnis “barometer”. Geben Sie dann Folgendes ein:
cd barometer
npm init -y
npm install --save johnny-five raspi-io
Dies wird eine Weile dauern. Eventuell (je nach bereits vorhandener Software) meckert der Installer, dass pigpio noch nicht installiert sei, und wird versuchen, es zu installieren, was mangels Admin-Rechten scheitern wird. Brechen Sie in diesem Fall ab und geben Sie zunächst ein:
sudo npm install pigpio
Und starten Sie dann erneut die Installation von johnny-five und raspi-io
Danach müssen Sie den Raspberry neu starten, damit der I2C Bus aktiviert wird. Ich würde vorschlagen, Sie fahren ihn zunächst herunter, stecken ihn dann aus, erledigen die Verkabelung zum BMP280 und stecken ihn dann wieder ein. Auf diese Weise ist die Gefahr versehentlicher Beschädigungen am geringsten.
Da der Raspi je nach Version nur einen oder zwei 3.3 Volt Anschlüsse hat, wir aber drei Pins (CSB, SDO und VCC) mit 3.3 Volt verbinden wollen, müssen Sie z.B. drei Kabel zusammenlöten, mit einem Verbinder zusammenfassen oder über ein Steckbrett auf drei Pins verteilen. Achtung: Hier ist Sorgfalt gefragt, Wenn Sie den Sensor versehentlich mit 5 Volt verbinden, können Sie ihn anschließend höchstwahrscheinlich wegwerfen (Ausser, Sie haben eines jener Breakout-Boards erwischt, die auch einen Spannungsregler beinhalten).
Wir verbinden also:
- SDA mit Pin 3/GPIO 8 des Raspberry (Das ist, wenn Sie den Raspberry so vor sich halten, dass er mit der Schmalseite mit den USB-Anschlüssen zu Ihnen zeigt und die GPIO Stiftleise rechts ist, der zweite Pin von oben der linken Stiftleiste).
- SCL mit Pin 5/GPIO 9 (das ist der dritte Pin links von oben)
- GND mit PIN 6 / GROUND (das ist der dritte Pin rechts, gegenüber von SCL)
- CSB,SDO und VCC mit Pin 1 (das ist der oberste Pin links).
Kontrollieren Sie noch einmal, ob alle Kabel richtig verbunden sind und starten Sie dann denn Raspi neu. Öffnen Sie dann wieder die ssh-Konsole und geben Sie ein:
cd barometer
nano bmp280.js
Dann geben Sie bitte folgendes Progrämmchen ein:
const five=require('johnny-five')
const Raspi=require('raspi-io');
const board=new five.Board({
io: new Raspi()
})
board.on('ready',()=>{
const bmp280=new five.Multi({
controller: 'BMP280', // Bei Verbindungsproblemen versuchen Sie 'BME280' oder '\
BMP180'
freq: 1000 // Abfragehäufigkeit in ms.
})
bmp280.on('data',()=>{
let temperature=bmp280.thermometer.celsius.toFixed(1);
let pressure=bmp280.barometer.pressure.toFixed(1);
let humidity=bmp280.hygrometer.relativeHumidity
console.log(`${temperature}°C, ${pressure} kPa, ${humidity}%rH`);
})
})
Beenden Sie den Editor mit CTRL-O und CTRL-X und starten Sie das Programm mit sudo node bmp280.js.
Vermutlich werden noch ein paar Fehlermeldungen wegen Tippfehlern kommen, aber dann werden Sie mit einer Ausgabe wie dieser belohnt:
27.7°C, 96.5 kPa, 0%rH
27.7°C, 96.5 kPa, 0%rH
27.7°C, 96.5 kPa, 0%rH
27.7°C, 96.5 kPa, 0%rH
27.7°C, 96.5 kPa, 0%rH
27.7°C, 96.5 kPa, 0%rH
(Da verschiedene Chipversionen im Umlauf sind, die nicht alle dieselben Messwerte liefern, habe ich hier Code für alles eingegeben. Wenn Sie z.B. wie hier für humidity unsinnige Werte erhalten, bedeutet das, dass der eingesetzte Chip die Luftfeuchtemessung nicht unterstützt. Alle Varianten unterstützen aber pressure und temperature.)
Da unterschiedliche Chips im Handel sind, kann es auch sein, dass ein als BMP-280 verkauftes Board einen BME-280 enthält und daher im Skript nur als “BME280” korrekt angesprochen werden kann. Sie müssen eventuell ein wenig experimentieren. Wenn es mit keinem der ähnlichen Chips funktioniert, oder wenn es nur instabil läuft, ist vielleicht die Baudrate des I2C Bus für Ihren vielbeschäftigten Raspi zu schnell eingestellt. Das können Sie direkt auf dem Raspi so ändern:
sudo nano /boot/config.txt
Suchen Sie dort die Zeile dtparam=i2c_arm_baudrate=100000 und ändern Sie den Wert in 10000. Speichern Sie config.txt mit CTRL-O und verlassen Sie den Editor mit CTRL-X. Starten Sie dann denn Raspi mit sudo reboot neu. Dann sollte es klappen.
Wenn Sie an korrektem Wert für die Temperatur interessiert sind, sollten Sie den Chip weit genug vom Raspberry weg montieren, sonst messen Sie (wie hier) eher dessen Temperatur, als die der Umgebung.
An dieser Stelle möchte ich darauf hinweisen, dass man ein auf dem RaspberryPi laufendes NodeJS Programm auch vom Arbeitscomputer aus debuggen kann, was bei komplizierteren Programmen oft einfacher ist, als einem Fehler mit verstreuten console.log() Anweisungen auf die Spur zu kommen. Sie finden die Anleitung zum Remote-Debugging im Anhang.
Anbindung an ioBroker
Nachdem unser Barometer grundsätzlich funktioniert, muss es an ioBroker angebunden werden. Die erste Idee ist: Wir benötigen einen ioBroker Adapter. Eine schnelle Websuche liefert allerdings keine Ergebnisse für BMP/BME-280, wir müssten somit selber einen schreiben.
Das kann man natürlich tun; wie es geht, habe ich ja im Kapitel 5 gezeigt. Allerdings gibt es einen viel einfacheren Weg, und den möchte ich Ihnen hier vorstellen.
Dazu hole ich erst mal ganz weit aus und benutze einen NodeMCU zum Anbinden des BMP280. Sie erinnern sich, NodeMCU ist ein Development Board rund um den ESP8266, einen Mikrocontroller mit WLAN.
Wenn Sie eine NodeMCU zur Hand haben, können Sie das Folgende gleich mitmachen, wenn nicht, überfliegen Sie Sie diesen Abschnitt nur; ich werde später das Vorgehen mit dem Raspberry Pi zeigen.
- Besorgen Sie zunächst die Arduino IDE von der Originalquelle https://www.arduino.cc/en/Main/Software.
- Installieren Sie dann die ESP8266 Tools in die Arduino IDE wie hier beschrieben: http://blog.opendatalab.de/codeforbuga/2016/07/02/arduino-ide-mit-nodemcu-esp8266. (Falls Sie die NodeMCU bisher noch nie an den Computer angeschlossen hatten, beachten Sie bitte, dass Sie meist zunächst den Treiber von SiliconLabs (https://www.silabs.com/products/development-tools/software/usb-to-uart-bridge-vcp-drivers) installieren müssen, damit das Board erkannt wird.)
- Besorgen Sie sich dann die ESPEasy Firmware von der Originalseite: https://www.letscontrolit.com/wiki/index.php/ESPEasy. Folgen Sie den Installationsanweisungen für Ihr System.
Das Einbinden des NodeMCU mit ESPEasy ist ein zwei-Schritt-Prozess, ähnlich dem, den Sie vielleicht von der Einrichtung der Osram Lightify Bridge in Erinnerung haben: Zunächst spannt der Chip einen Accesspoint auf, mit dem man sich verbinden kann, um die Zugangsdaten zum “Echten” Netzwerk einzugeben. Danach kann man ihn über dieses Netzwerk erreichen. Also:
- Nach erfolgreichem Hochladen der Firmware drücken Sie auf den “RST” Knopf des NodeMCU und suchen mit Ihrem Computer nach dem Wifi AccessPoint ESP_Easy_0 (oder so ähnlich) und verbinden Sie sich mit dem Password configesp mit diesem WLAN. Sie gelangen auf eine Seite, auf der Sie die Zugangsdaten für Ihr richtiges WLAN eintragen können.
- Verbinden Sie Ihren Computer wieder mit dem richtigen WLAN und rufen Sie mit dem Browser die Adresse auf, die Ihnen beim Einrichten gezeigt wurde. Wenn alles geklappt hat, sollten Sie jetzt von der Steuerseite des ESPEasy begrüsst werden. Ziehen Sie jetzt vor dem Verkabeln bitte wieder den Stecker (Herunterfahren ist bei Mikrocontrollern nicht nötig), damit wir das Gerät nicht kaputt machen.
- Nehmen Sie das BMP280 Board und Verbinden Sie:
- VCC, SDO und CSB mit 3.3 Volt
- GND mit GND
- SCL mit D1
- SDA mit D2
Die fliegende Verkabelung könnte etwa so aussehen:
Da die NodeMCU mehr 3.3 Volt-Anschlüsse hat, als der Raspberry, brauchen wir hier nichts zu basteln, um VCC, SDO und CSB auf “HIGH” Level zu legen.
Stecken Sie dann die NodeMCU wieder an ein USB Ladegerät oder den USB Port des Computers (direkte Verbindung ist jetzt nicht mehr zwingend nötig, da wir von nun an nur noch per WiFi mit der NodeMCU kommunizieren). Gehen Sie mit dem Webbrowser wieder auf dieselbe Seite wie vorhin und gehen Sie zum Tab “Hardware”. Vergewissern Sie sich, dass dort bei I2C Interface dieselben Pins angegeben sind, wo wir SLC und SDA eingesteckt hatten.
Auf der Seite “Devices” erstellen Sie ein neues Device und suchen den Eintrag für BMP280 in der Liste:
)
Geben Sie die korrekten Daten ein, wie hier gezeigt.
Wenn alles stimmt, werden Sie sofort von den ersten Messwerten belohnt:
Wenn das der Fall ist, wollen wir nun diese Messwerte an unseren ioBroker übertragen. Installieren Sie zunächst auf der Adapter-Seite http://homeview.local:8081/#adapters den “MQTT Broker/Client”. MQTT hatte ich im Theorieteil schon kurz beschrieben. Als “Typ” bei der Konfiguration geben Sie bitte ein “Server/Broker”, Port belassen Sie auf 1883. Den Rest lassen Sie am besten auf den Vorgaben. Nach dem Speichern wird der MQTT Broker starten, aber “gelb” bleiben. Das ist eine etwas unglückliche Art, anzuzeigen, dass er noch keine Clients hat. Lassen Sie sich davon also nicht irritieren. Gehen Sie stattdessen wieder auf die Kontroll-Seite des ESPEasy, und wählen Sie den Reiter “Controllers”. Klicken Sie bei 1 auf “Edit”, dort als Protokoll: OpenHAB MQTT. Die restlichen Angaben wie hier gezeigt:
Beenden Sie mit “Submit” und schauen Sie wieder auf die ioBroker Seite. Unter “Instanzen” sollte mqtt.0 jetzt “grün” geworden sein oder bald werden. Wenn Sie unter “Objekte” den mqtt.0 Eintrag öffnen, sollten Sie jetzt ungefähr das hier sehen:
Dieses mqtt Objekt in ioBroker können Sie genau so auslesen, wie alle anderen Objekte auch. Sie können auch irgendeinen MQTT Client, zum Beispiel MQTT Dash auf Ihrem Android Handy, oder Mqtt Buddy auf dem iPad oder iPhone installieren, das entsprechende Topic abonnieren und die Werte sehen. (Sofern das Smartphone im selben Netz wie der ioBroker eingebucht ist, natürlich).
Fazit: Wir haben mit minimalem Aufwand ein “intelligentes” Barometer an unsere Heimautomation angeschlossen. Dies war so einfach, weil ESPEasy von Haus aus Unterstützung sowohl für den BMP280, als auch für MQTT mitbringt. Können wir ähnlich einfach die Daten auch nach ioBroker bringen, wenn der Sensor direkt am Raspberry angeschlossen ist?
Ja, können wir. Allerdings ist ein wenig mehr “Handarbeit” nötig, daher wollte ich mit obigem Beispiel zunächst das Prinzip zeigen.
Anbindung Raspberry mit MQTT an ioBroker
Kehren wir also wieder zurück zu unserem Johnny-five Programm im Raspberry Pi. Um die Messwerte des BMP280 via MQTT zu veröffentlichen, brauchen wir eine MQTT Client library. Davon gibt es (natürlich) einige auf npmjs.org. Wir wählen die MQTT client library von Matteo Collina. Zunächst müssen wir die Library in unser Projekt einbinden. Gehen Sie ins Verzeichnis des Johnny-Five Programs mit dem BMP280 Adapter und geben Sie ein4:
npm install --save mqtt
Die Verwendung im Programm selbst ist dann geradezu trivial:
const five=require('johnny-five')
const Raspi=require('raspi-io');
// MQTT einbinden und mit dem MQTT Broker auf dem Raspberry verbinden.
const mqtt=require('mqtt').connect('mqtt://homeview.local')
const MQTT_TOPIC="/Wetter/Wohnzimmer/"
const board=new five.Board({
io: new Raspi(),
repl: false
})
board.on('ready',()=>{
const bmp280=new five.Multi({
controller: 'BME280',
})
bmp280.on('data',()=>{
let temperature=bmp280.thermometer.celsius.toFixed(1);
let pressure=bmp280.barometer.pressure.toFixed(1);
// Statt Loggen auf die Konsole schicken wir die Resultate nun zum MQTT Broke\
r.
mqtt.publish(MQTT_TOPIC+"Temperatur",temperature)
mqtt.publish(MQTT_TOPIC+"Luftdruck",pressure*10)
})
})
Da MQTT keine bestimmte Form der Nachrichten vorschreibt, hätten wir auch Temperatur und Druck in eine einzelne Nachricht packen und versenden können. Da wir die REPL nicht mehr brauchen, initialisieren wir das Board mit repl: false.
Kaum zu glauben, aber das ist (fast) alles! Sie können das Programm im “homeview” Verzeichnis auf dem Raspberry mit sudo node bmp280.js laufen lassen, und werden die Messwerte im ioBroker oder irgendeinem anderen MQTT Client sehen können.
Okay, aber wie bringen wir unseren Raspi nun dazu, das Barometer zusammen mit dem Homeview-Server laufen zu lassen? Einen separaten node-Prozess für dieses Progrämmchen zu starten, wäre ja overkill.
Glücklicherweise ist das sehr einfach:
Editieren Sie app.js (im homeview-Verzeichnis auf dem Raspi) mit nano app.js. Geben Sie ganz am Anfang ein: require('./bmp280') und verlassen Sie den Editor wieder mit CTRL-O,EINGABETASTE,CTRL-X.
Starten Sie dann den Server neu mit sudo service homeview restart.
Jetzt haben Sie das Barometer so eingebunden, dass es gleichzeitig mit dem Homeview-Server startet. In der ioBroker-Admin-Oberfläche (http://homeview.local:8081) finden Sie im Reiter “Objekte” nun einträge für “mqtt.0.Wetter.Wohnzimmer.Temperatur” und “mqtt.0.Wetter.Wohnzimmer.Luftdruck”. Falls Sie nicht auf Meereshöhe leben, werden Sie allerdings höchstwahrscheinlich feststellen, dass der Luftdruck nicht stimmt. Das liegt nicht daran. dass der BMP280 kaputt wäre, sondern daran, dass er den tatsächlichen Luftdruck liefert. Wir sind vom Wetterbericht aber gewöhnt, den normalisierten, also auf Meereshöhe umgerechneten, Luftdruck angezeigt zu bekommen.
Man kann den normalisierten Luftdruck vereinfacht so errechnen:
korrigiert = gemessen / ((1-hoehe_ueber_meer)/44330)^5.255)
Oder, in JavaScript formuliert:
korrigiert = gemessen / Math.pow(1 - hoehe_ueber_meer/44330, 5.255)
Falls Sie noch genauere Werte benötigen, können Sie auf die Dokumentation des Herstellers zum Kalibrieren und Auslesen des Sensors zurückgreifen. Bosch stellt auch einen einen Treiber zur Verfügung, der alle Möglichkeiten des Chips zeigt und nutzt.
Eine einfacher verständliche Erläuterung sehen Sie hier: http://www.netzmafia.de/skripten/hardware/RasPi/Projekt-BMP280/index.html
Die endgültige Version des Programms sieht dann so aus:
const five=require('johnny-five')
const Raspi=require('raspi-io');
const altitude = 440; // unsere Höhe in MüM.
// MQTT einbinden und mit dem MQTT Broker auf dem Raspberry verbinden.
const mqtt=require('mqtt').connect('mqtt://localhost',{
'clientId': 'wohnzimmer-barometer',
'will': {
'topic': 'info/connection/barometer',
'payload': 'getrennt'
}
})
mqtt.on("connect",()=>{
mqtt.publish("info/connection/barometer","verbunden");
});
const MQTT_TOPIC="/Wetter/Wohnzimmer/"
const board=new five.Board({
io: new Raspi(),
repl: false
})
// Korrektur auf aktuelle Höhe über Meeresspiegel, in mbar
const corr=(raw)=>{
const corrected=raw/Math.pow((1-altitude/44330),5.255)
return (10*corrected).toFixed(1)
}
board.on('ready',()=>{
const bmp280=new five.Multi({
controller: 'BME280',
freq: 30000
})
bmp280.on('data',()=>{
let temperature=bmp280.thermometer.celsius.toFixed(1);
let pressure=corr(bmp280.barometer.pressure);
mqtt.publish(MQTT_TOPIC+"Temperatur",temperature)
mqtt.publish(MQTT_TOPIC+"Luftdruck",pressure)
})
})
Nicht viel Neues: Bei der Initialisierung haben wir mit dem Attribut “will” ein “Testament” übergeben, also eine letzte Nachricht, die der Broker bei einem Verbindungsabbruch noch übermitteln soll. In corr() rechnen wir lokale Messwerte in kPa in normalisierte Werte in mbar um. Wir fragen alle 30 Sekunden die Werte ab und publizieren diese via MQTT.
Jetzt haben wir alles zusammen, um auch den Luftdruck in unserer WebApp anzuzeigen. Die nötigen Ergänzungen sollten Sie nun nicht mehr vor unlösbare Probleme stellen:
// In aurelia_project/environments/dev.ts und aurelia_project/environments/prod.t\
s:
// ...
devices:{
barometer: "mqtt.0.Wetter.Wohnzimmer.Luftdruck",
aussen_temp: "hm-rpc.0.OEQ0088064.1.TEMPERATURE",
// ...
}
// in config.ts:
// ...
barometer_cfg:{
devices: env.devices.barometer,
size: gauge_size,
min:970,
max:1045,
message: "luftdruck",
bands: [{"from":970,"to":995,color: "#42d4f4"},
{from:995,to:1020,color:"green"},
{from:1020,to:1045,color:"#42d4f4"}]
}
// ...
// in app.ts:
// ...
private gauges = [configs.wohnzimmertemp_cfg, configs.aussentemp_cfg,
configs.bad_oben_cfg,configs.dusche_cfg, configs.dachstock_cfg,
configs.barometer_cfg,configs.powermeter_cfg]
// ...
Und in app.html:
<!-- ... -->
<div class="gauge">
<circular-gauge cfg.bind="conf.barometer_cfg"></circular-gauge>
</div>
<!-- ... -->
Danach sollte es ungefähr so aussehen:
Motor
Natürlich kann man nicht nur Sensoren auslesen, sondern wir können mit IoT Geräten auch etwas tun. Zur Demonstration hier eine Schrittmotorsteuerung. Wir werden etwas Ähnliches später zur Regulierung des Boilers benötigen. Hier aber erst mal nur das Grundprinzip.
Zunächst der fliegend verdrahtete Versuchsaufbau:
Für erste Experimente verwende ich immer den Arduino, weil er schnell und einfach aufzusetzen ist, und weil er Fehler eher verzeiht, als Raspberry oder ESP8266. Wenn der Aufbau auf dem Arduino funktioniert, kann man ihn leicht auf eines der anderen Geräte portieren.
Oben rechts im Bild sehen Sie zwei Drähte aus einer Lüsterklemme kommen, das ist die externe Stromversorgung für den Motor. Der Arduino (und erst recht der Raspberry) ist zu schwach, um den Motor direkt anzutreiben. Auf dem Breadboard sitzt ein ULN2083 IC, welches folgende Anchlussbelegung hat:
Wie es innerlich aussieht, braucht uns nicht zu interessieren (es ist ein 8-fach Darlington Array, falls Sie es doch wissen wollen). Wichtiger ist, was es tut: Es macht einen schwachen Eingangsstrom zu einem viel stärkeren Ausgangsstrom. Und das an 8 unabhängigen Leitungen. Die Beschaltung ist sehr einfach: Der Eingang 1B wird auf den Ausgang 1C umgesetzt und so weiter. bei GND steckt man den gemeinsamen Minuspol ein (und der muss auch zum Arduino geleitet werden), bei COM kommt der Pluspol für die Leistungsseite, das kann je nach Motor eine Spannung von 3 bis 20 Volt sein und sollte die vom Motor benötigte Leistung liefern können, also mindestens 500mA. Man kann dieses IC durchaus nicht nur für Motorsteuerungen verwenden, sondern immer dann, wenn man einen Stromverstärker braucht. Allerdings muss man beachten, dass die Darlington Schaltung bei positivem Eingangssignal eine Emitter-Schaltung öffnet. Oder andersherum ausgedrückt: Wenn man “1” an den Eingang anlegt, dann kann man den Ausgang als Minuspol des geschalteten Stroms benutzen, nicht als Pluspol. Das passt hier glücklicherweise ganz gut, wie Sie gleich sehen werden.
Als Motor verwenden wir einen der meist gebauten (und darum billigen) Schrittmotoren überhaupt, den 28BYJ-48. Den gibt es in 5 Volt und 12 Volt Ausführungen. Ich habe hier 5 Volt genommen. Dieser Motor ist in einer Vielzahl von Geräten verbaut. Vielleicht müssen Sie ihn gar nicht kaufen, sondern können ihn aus einem alten Drucker, Scanner, CD-Laufwerk, Klimaanlage, Lüfter etc. ausbauen. Wenn Sie ihn kaufen, wird er auch kein allzu tiefes Loch in die Haushaltskasse reißen, mit 5-10 Euro sind Sie dabei. Das Verstärker IC wird noch rund 20-40 Cent zusätzlich kosten.
Der 28BYJ-48 ist ein sogenannter unipolarer Schrittmotor. Das bedeutet, dass der Strom immer in dieselbe Richtung fließt. Die Drehrichtung legt man nicht durch Umpolen, sondern durch geeignete Reihenfolge der Aktivierung der Spulen fest. Daher braucht man auch keine spezielle Motorsteuerung (H-Bridge), sondern ein simples Universal-Verstärker-IC genügt.
Ein Schrittmotor unterscheidet sich von einen “gewöhnlichen” Elektromotor dadurch, dass er nicht einfach ein- oder ausgeschaltet wird, sondern dass man ihn schrittweise (sic!) um einen bestimmten Betrag drehen kann. Das erreicht man, indem man die Spulen in geeigneter Weise und Reihenfolge mit Strom versorgt. Der 28BYJ-48 hat 5 Drähte. Einer davon (idR der Rote) ist der gemeinsame Pluspol, der ins Zentrum jeder der beiden Spulen geht, die anderen 4 sind Minuspole für jedes Ende der Spulen. Sie sehen, das passt wunderbar zu unserem Darlington-IC, das diese Minuspole durchschalten oder sperren kann.
Bewaffnet mit diesem Wissen können wir den Motor nun verschalten: Der rote Draht kommt zum Pluspol der externen Stromversorgung, die auch zu Pin 10 des IC geht.
Den orangen Draht verbinden wir mit Pin 18 des IC, und dessen Steuerleitung auf Pin 1 des IC geht zu GPIO 3 des Arduino. In analoger Weise verbinden wir den gelben Draht mit GPIO 4, den pinken mit GPIO 5 und den blauen mit GPIO 5. Zu guter Letzt verbinden wir noch einen der GND Pins des Arduino mit Pin 9 des IC, den wir auch mit dem Minuspol der externen Stromversorgung verbinden. Fertig.
Nun zum Programm für den Arduino. Wählen Sie in der Arduino IDE den richtigen Chip (bei mir Arduino/Genuino Uno) und den richtigen Port aus, und geben Sie das Programm ein:
/**
* Steuerung eines 28BYJ-48 Stepper Motors mit einem 2803APG Darlington Array.
* Die rote Leitung ist gemeinsamer Pluspol und kommt an (externe) 5V Leitung.
* Die anderen Pins sind nach der Farbe der Anschlüsse des Motors benannt.
*/
#define orange 2
#define yellow 3
#define pink 4
#define blue 5
// Wieviele Schritte für eine ganze Umdrehung (Nicht bei allen Modellen gleich)
const int steps360=512;
// Dauer der Pause zwischen zwei Schritten in Mikrosekunden. Nicht zu klein wähle\
n.
const int pause = 10000;
// Reihenfolge der Spulen. Wir wählen hier das 8-Schritt Verfahren.
String steps[] = {"0111", "0011", "1011", "1001", "1101", "1100", "1110", "0110"}\
;
void setup() {
pinMode(orange, OUTPUT);
pinMode(yellow, OUTPUT);
pinMode(pink, OUTPUT);
pinMode(blue, OUTPUT);
}
/**
* Drehung im Gegenuhrzeigersinn (Counterclockwise) um "count" Schritte.
*/
void ccw(int count) {
for (int i = 0; i < count; i++) {
for (int j = 0; j < 8; j++) {
setPins(steps[j]);
delayMicroseconds(pause);
}
}
}
/**
* Drehung im Uhrzeigersinn (clockwise) um "count" Schritte.
*/
void cw(int count) {
for (int i = 0; i < count; i++) {
for (int j = 7; j >= 0; j--) {
setPins(steps[j]);
delayMicroseconds(pause);
}
}
}
void setPins(String step) {
digitalWrite(orange, step[0] == '1' ? HIGH : LOW);
digitalWrite(yellow, step[1] == '1' ? HIGH : LOW);
digitalWrite(pink, step[2] == '1' ? HIGH : LOW);
digitalWrite(blue, step[3] == '1' ? HIGH : LOW);
}
void loop() {
cw(steps360);
delay(100);
ccw(steps360);
delay(1000);
}
Das Programm sollte selbsterklärend sein. Die Pause (delayMicroseconds()) dient dazu, der Achse des Motors Zeit zu geben, in die eingestellte Position zu kommen. Wenn diese Zeit zu klein gewählt ist, wird der Motor einzelne Schritte “verlieren” oder sich gar nicht mehr bewegen. Je weniger Strom die Spannungsquelle liefert, desto länger muss diese Zeit sein. Für ein Standard-USB Netzteil sind Werte um die 1000 Mikrosekunden okay. Es spricht aber natürlich nichts dagegen, den Motor z.B. mit 20000 langsamer und dafür sicherer zu bewegen.
Dieses Programm macht für jeden Schritt 8 Teilschritte. Das Array steps[] gibt für jeden Teilschritt an, durch welche Spulen Strom fliesst (“1”), und welche nicht (“0”). Etwas weiter hinten werde ich noch ein Verfahren mit 4 Schritten vorstellen.
Es scheint verschiedene Getriebevarianten bei diesem Motor zu geben; jedenfalls ist die Zahl der Schritte für eine Umdrehung nicht bei jedem gleich. Bei meinem sind es 512(*8) Schritte, was in der Konstanten steps360 festgehalten wird.
Wenn Sie dieses Programm hochladen und die Stromversorgung anschliessen, sollte der Motor immer eine Umdrehung im Uhrzeigersinn und dann wieder eine Umdrehung im Gegenuhrzeigersinn machen (von der Achse aus gesehen).
Wenn das klappt, können Sie als Fingerübung den Motor an die NodeMCU anschliessen.
Wir machen jetzt aber weiter mit einer mechanischen Steuerung für den Boiler:
Elektroboiler mechanisch steuern
Es gibt eine eiserne Grundregel für Hobby-Maker: Finger weg vom Stromnetz. Wenn wir am Raspberry ein Kabel falsch stecken, kostet es schlimmstenfalls den Raspberry. Wenn wir an der Hauselektrik etwas falsch machen, gibt es schlimmstenfalls ein Feuer oder einen lebensgefährlichen Stromschlag. Arbeiten an Spannungen von mehr als 50 Volt bei eine Stärke von mehr als 500mA sind dem Elektriker vorbehalten.
Aber wir wollen trotzdem den Elektroboiler steuern. Warmwasser ist ein hervorragender und vergleichsweise billiger Energiespeicher. Überschlägig gilt folgende Rechnung: Um ein Gramm Wasser um ein Grad aufzuheizen, braucht man 1 Kalorie. Oder andersherum ausgedrückt: Warmwasser enthält pro Liter und Grad eine Kilokalorie an Energie. Wenn wir unseren 400-Liter-Boiler mit Sonnenstrom von 50 auf 70 Grad aufheizen, dann stecken wir 8000 Kilokalorien hinein, das sind fast 10 kWh. Diese 10 kWh sparen wir an Netzstrom, wenn wir Heisswasser entnehmen, bis der Boiler wieder abgekühlt ist. Und diese gespeicherte Energie steht auch nachts zur Verfügung, wenn die Sonne nicht scheint, Ganz ohne teuren Akku.
Wir wollen also den Boiler je nach Sonnenkraft unterschiedlich weit aufheizen. In einer realen Anwendung sollten Sie aber darauf achten, dass er mindestens einmal pro Woche mindestens 65°C erreicht, um Legionellen abzutöten, die sonst sehr ernste Erkrankungen auslösen könnten! Und natürlich sollten Sie ihn nicht zu nah an 100° bringen, um keinen Wärmeverlust oder gar Gefahr durch Überdruck zu provozieren. Last but not least gilt auch: Je höher die Wassertemperatur, desto aggressiver die Korrosion. Am besten sehen Sie zu, dass die Temperatur sich zwischen 45 und 75°C bewegt. Natürlich kann man eine solche Steuerung automatisieren. Wir haben längst das Rüstzeug, um das zu programmieren.
Schwieriger ist die Hardware-Seite: Wenn wir die Stromanschlüsse nicht berühren dürfen, wie können wir den Boiler dann regeln?
- Wir können einen modernen Boiler kaufen, der von Haus aus Smarthome-fähig ist. Ich finde es aber schade, einen funktionierenden Boiler zu entsorgen. Zumal, seien wir ehrlich, die Investition sich erst nach mehreren Jahren rentieren würde. Strom ist einfach noch zu billig.
- Wir können einen Elektriker beauftragen, uns ein fernsteuerbares Relais in die Stromzuleitung zum Boiler einzubauen. Hier gilt Ähnliches: Ein Elektriker müsste herkommen und die Installation machen. Das würde leicht mehrere hundert Euro kosten.
- Wir können den Boiler pseudo-manuell schalten. Also quasi eine Hand simulieren. Spätestens seit der Useless box ist das Bedienen von Schaltern durch Robot-Bauteile ins allgemeine Bewusstsein gelangt. Und da dies ein Buch übers Selbermachen ist, wählen wir natürlich diesen Weg.
Der Boiler hat einen Knopf zum Temperaturregeln:
Wenn man den abzieht, kommt die Achse eines Potentiometers zum Vorschein:
Dort können wir eine mit dem 3D-Drucker auf Mass angefertigte Halterung anbringen:
Und an dieser wiederum unseren Schrittmotor aus dem letzten Kapitel montieren.
Auf diese Weise berühren wir keine stromführenden Teile und sind stets auf der sicheren Seite.
Der Rest ist Software. Die eigentliche Steuerung haben wir ja schon im vorherigen Kapitel abgehandelt. Die Frage, auf welchem Gerät die Steuersoftware laufen soll, ist eigentlich nur eine Frage der örtlichen Begebenheiten und der persönlichen Präferenzen. Bei uns sollte die Anbindung per WLAN erfolgen, daher fiel der Arduino weg. Der ESP8266 hatte vom Keller aus Probleme, sich mit dem WLAN zu verbinden, daher entschied ich mich für einen Pi Zero W als Boilersteuerung. Damit kehren wir zurück zu Johnny-Five.
/*********************************************************************
* Johnny Five driver for unipolar stepper motor (e.g. 28BYJ-48)
*********************************************************************/
const five = require('johnny-five')
// Dismal nur 4 Schritte
const seq = ["0011", "0110", "1100", "1001"]
/**
* Constructor arguments: 4 Pins for motor coils, delay in milliseconds to wait a\
fter
* each step (allow the motor to reach new position).
*/
class Stepper {
constructor(orange, yellow, pink, blue, motor_power, delay) {
this.in1 = new five.Pin(orange, { mode: five.Pin.OUTPUT });
this.in2 = new five.Pin(yellow, { mode: five.Pin.OUTPUT });
this.in3 = new five.Pin(pink, { mode: five.Pin.OUTPUT });
this.in4 = new five.Pin(blue, { mode: five.Pin.OUTPUT });
this.speed = delay;
this.coils=[this.in1,this.in2,this.in3,this.in4]
}
set_step(step, dir) {
let lcoils=dir ? this.coils : this.coils.reverse()
for(let i=0;i<step.length;i++){
lcoils[i].write(parseInt(step[i]))
}
}
/**
* rotate clockwise
*/
async cw(steps) {
for (let i = 0; i < steps; i++) {
for (let j = seq.length - 1; j >= 0; j--) {
let step = seq[j]
this.set_step(step,false);
await this.sleep(this.speed);
}
}
}
/**
* rotate counterclockwise
*/
async ccw(steps) {
for (let i = 0; i < steps; i++) {
for (let j = 0; j < seq.length; j++) {
let step = seq[j]
this.set_step(step,true);
await this.sleep(this.speed);
}
}
}
sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms)
})
}
}
exports.Stepper = Stepper
Man sieht sofort, dass dieses Node-Modul sehr ähnlich ist, wie das Arduino-Programm im vorherigen Kapitel. Nur habe ich diesmal nur vier Teilschritte pro Step definiert, die dafür etwas weiter sind.
Es wird recht simpel in ein Node-Programm eingebunden:
const Raspi = require('raspi-io')
const five = require('johnny-five')
const Stepper=require('./stepper').Stepper
const board = five.Board({
io: new Raspi()
}
);
const orange = "GPIO26"
const yellow = "GPIO19"
const pink = "GPIO13"
const blue = "GPIO6"
let speed = 6;
let stepper;
board.on('ready', async () => {
stepper = new Stepper(orange, yellow, pink, blue, speed)
await stepper.forward(50);
await stepper.backward(50);
board.repl.inject({
stp:stepper
})
function setSpeed(sp) {
stepper.speed = sp;
}
})
Die Pin-Nummern entsprechen dem Setup in meiner experimentellen Pi-Zero Verbidung:
Nach dem Start mit sudo node index.jswird der Motor zuerst 50 Schritte im Uhrzeigersinn, dann 50 Schritte im Gegenuhrzeigersinn drehen, und dann auf Befehle warten.
Die Bedeutung des board.repl.inject Ausdrucks ist, den Stepper für interaktive Bedienung in der REPL freizugeben. Sie können nach dem Start des Programms in der Kommandozeile zum Beispiel eingeben: stp.cw(100), um den Motor “manuell” 100 Schritte im Uhrzeigersinn drehen zu lassen. Wenn Sie das nicht mehr benötigen, können Sie den Programmteil einfach auskommentieren und das Board mit repl: false initialiseren, wie wir es schon heim Barometer gemacht haben.
Doch wie geben wir dem Raspi Kommandos übers Netz? Wir werden ihn ja nicht immer an der Konsole haben. Da NodeJS ohnehin schon läuft, könnten wir einen REST-Service mit Express aufbauen, um Kommandos entgegenzunehmen und Rückmeldungen zu liefern. Ich habe mich aber für eine einfachere und flexiblere Lösung entschieden: Wir werden den Raspi über MQTT steuern, da wir ohnehin schon eine MQTT Infrastruktur fürs Barometer aufgebaut haben. Wie wir dort gesehen haben, ist MQTT sehr einfach einzurichten, und MQTT wird unseren schwachen Pi Zero auch weniger ans Limit bringen, als ein REST Server.
Spätestens an diesem Punkt müssen wir uns allerdings noch einmal ernsthafte Gedanken über die Absicherung unserer Hausautomation machen. Wenn jemand unerlaubt ein paar Lichter ein- und ausschaltet, ist das eine Sache. Wenn jemand mit dem Boiler herumspielt, ist das schon etwas ganz anderes. Das Heimnetz soll nicht von aussen erreichbar sein; eine Firewall ist Pflicht. Aber auch Gäste oder Freunde des Nachwuchses, die ganz legal ins Gäste-WLAN dürfen, sollen nicht an der Boilersteuerung herumspielen können. Der Zugang zum MQTT Broker muss somit mit einem Passwort gesichert sein, und der Datenverkehr muss verschlüsselt erfolgen. Für beides hat der MQTT-Adapter des ioBroker glücklicherweise schon Vorkehrungen; sie müssen es auf der Einstellungsseite der MQTT Instanz nur noch aktivieren.
Folgende MQTT Topics sollen unterstützt werden:
- Boiler/setTemp - Boiler auf x Grad einstellen
- Boiler/calibrate - Motor Kalibrieren
- Boiler/temp -Temperatur auslesen
Komponente zur Temperaturregelung
Als erstes implementieren wir eine Komponente, mit der wir manuell von unserer Web-Oberfläche aus die Temperatur einstellen können.
Das Programm dazu wird Ihnen inzwischen trivial vorkommen; dank der schon geleisteten Vorarbeiten sind nur recht wenige Codezeilen nötig:
import {sliderHorizontal} from 'd3-simple-slider'
import { bindable, noView, autoinject } from 'aurelia-framework'
import {Helper, Component, eaMessage} from './helper'
@autoinject
@noView
export class Setpoint implements Component{
@bindable cfg
body: any;
component_name="Setpoint";
private slider
constructor(private hlp:Helper, public element:Element){}
attached(){
this.hlp.initialize(this, {
minValue: 0,
maxValue: 100,
step: 1,
width: 250,
height: 50,
displayValue: true,
message: "setpoint"
})
}
configure() {}
render() {
let dim=this.hlp.defaultFrame(this)
let debounced=Util.debounce((val)=>{ // (1)
this.hlp.ea.publish(this.cfg.message,val)
},500,this)
this.slider=sliderHorizontal() // (2)
.min(this.cfg.minValue)
.max(this.cfg.maxValue)
.step(this.cfg.step)
.width(dim.w-2*Helper.BORDER-2*this.cfg.offset)
.displayValue(this.cfg.displayValue)
.on('onchange',debounced) // (3)
this.body.append("g") // (4)
.attr("width",dim.w)
.attr("height",this.cfg.height)
.attr("transform",`translate(${dim.x+Helper.BORDER+this.cfg.offset},${dim.y\
+Helper.BORDER+this.cfg.offset})`)
.call(this.slider)
}
update(value:eaMessage){
this.slider.value(value.data)
}
}
Wir verwenden eine Fertigkomponente (d3-simple-slider von John Walley), die wir in unsere Standard-Komponentenhülle einzubauen. Bei (2) konfigurieren wir den Slider, bei (4) wird er eingebunden. Neu ist vielleicht die Funktion bei (1). Hier lassen wir uns von der Util - Komponente eine Funktion erstellen, die eine “entprellte” Version unserer gewohnten ea.publish-Routine darstellt. Diese Funktion übergeben wir bei (3) dem onChange-Event des Sliders. Die Bedeutung dieses etwas umständlich erscheinenden Vorgehens ist: Wenn der Anwender am Schieber zieht, dann liefert dieser für jede kleine Änderung einen “onchange” Event. Wir würden also in kürzester Zeit von Hunderten von Events überschüttet. Unser “debounce” führt dazu, dass this.hlp.ea.publish erst dann stattfindet, wenn für mindestens 500 Millisekunden kein neuer Event eintraf. Dann wird der letzte Wert (und nur dieser) weitergeleitet.
Der dazugehörige Eintrag in config.ts ist:
"boiler_cfg":{
"minValue": 30,
"maxValue": 80,
"step": 5,
"width": gauge_size,
"height": switch_size ,
"caption": "Boilertemperatur",
"offset": 5
}
und in app.html:
<require from="components/setpoint"></require>
<!-- ... -->
<div class="gauge">
<setpoint cfg.bind="conf.boiler_cfg"></setpoint>
</div>
Das genügt, um den Slider anzuzeigen und bedienbar zu machen. Allerdings tut er noch nichts. Wie üblich haben wir die Aurelia-Komponente so allgemein formuliert, dass sie keine spezifische Aufgabe übernehmen kann. Dadurch können wir diesen Slider bei Befarf auch für andere Aufgaben ensetzen. Den Boiler-spezifischen Code könnten wir wie bei den Thermometern und den Schaltern in app.ts unterbringen, allerdings ist diese Klasse mittlerweile recht groß und damit unübersichtlich geworden. Jede Klasse sollte eine einzige Aufgabe erledigen, und diese Regel haben wir hier verletzt - was in der Experimentierphase verzeihlich ist. Aber die endgültige Anwendung sollte robuster und leichter wartbar sein. Es kann gut sein, dass man nach einem Jahr etwas verändern muss, und glauben Sie mir: Nach einem Jahr werden Sie sich in Spaghetticode nicht mehr zurechtfinden.
Zeit für ein Refactoring.
Sie erhalten diesen Stand des Projekts mit
git checkout -f origin/teil_20
git clean -f
npm install
Unser Projekt geht in eine neue Versionsstufe, das sollten wir auch an der Versionsnummer deutlich machen. Geben Sie ein:
npm version minor
Damit setzt npm die Versionsnummer von 0.1.0 auf 0.2.0. npm version major würde die erste Ziffer hochzählen und npm version rev die letzte.
Beim Aufbau des bisherigen Codes haben wir ja gesehen, dass es drei verschiedene Typen von Instrumenten gibt:
- “Gauges”, die Werte anzeigen können
- “Switches”, die Zustände anzeigen und modifizieren können
- “Charts”, die Charts anzeigen können.
Der Plan ist nun, die Steuerung jedes Typs in eine eigene Klasse auszulagern und in app.js nur noch diese Klassen einzubinden.
Erstellen Sie also ein neues Unterverzeichnis devices in src.
Dort beginnen wir mit der Steuerklasse für die Gauges:
import { autoinject } from "aurelia-framework";
import { FetchService } from "../services/fetchservice";
import { EventAggregator } from "aurelia-event-aggregator";
export type ScaleDef = {
from: number,
to: number,
color: string
}
export interface GaugeDef {
devices: Array<string>
size: number
upper: [ScaleDef]
lower: [ScaleDef]
message: [string]
caption?: string
visible?: boolean
interval?: number
}
@autoinject
export class Gauges {
private gauges: Array<GaugeDef> = []
constructor(private fetcher: FetchService, private ea: EventAggregator) { }
run(gaugelist: Array<any>) {
this.gauges = gaugelist
this.gauges.forEach(gauge => {
this.getValue(gauge)
const interval = Math.round((gauge.interval || 1000) + 5000 * Math.random())
setInterval(() => { this.getValue(gauge) }, interval)
})
}
getValue(gauge: GaugeDef) {
gauge.devices.forEach(async (device, index) => {
try {
const result = await this.fetcher.getIobrokerValue(device)
this.ea.publish(gauge.message[index], parseFloat(result))
} catch (err) {
console.log("An error occured: " + err)
this.ea.publish(gauge.message[index], "?")
}
})
}
}
Neu ist hier zuerst mal die Definition eines “types” namens ScaleDef. Types sind gewissermassen das, was TypeScript ausmacht: Definierbare Typen. Hier sagen wir, dass ein Objekt, dass sich “ScaleDef” nennen will genau die Variablen from, to und color haben muss, wobei from und to Zahlen sein müssen, und color eine Zeichenkette. Der Compiler wird sich beschweren, wenn wir irgendwo etwas übergeben, wo ein ScaleDef erwartet wird, das diese Kriterien nicht erfüllt.
Gleich darunter folgt ein “interface”, das ist eine weitere Möglichkeit, definierbare Typen darzustellen, die Sie vielleicht schon aus anderen Programmiersprachen kennen. Interfaces sind ein wenig mächtiger, als types, funktionieren aber ganz ähnlich. Sie sehen, dass das Interface GaugeDef unter anderem auch auf unsere vorhin definierte ScaleDef zurückgreift. Ein ? am Ende des Attributnamens erklärt dieses Attribut für optional.
Diese beiden Definitionen sind streng genommen nicht notwendig. Sie werden bei Übersetzen in JavaScript sowieso wegtranspiliert. Aber sie bewahren uns vor einer ganzen Klasse von Programmierfehlern: Vergessene Attribute oder Tippfehler in Attributnamen. TypeScript kann bereits beim Transpilieren testen, ob die Typen korrekt sind, so dass nicht erst beim Programmlauf eine Exception geworfen wird, wie das bei reinem JavaScript bei solchen Fehlern der Fall wäre.
Dann folgt die eigentliche Klassendefinition, die wohl nichts Überraschendes enthält: Es geschieht ungefähr dasselbe, wie in der entsprechenden Funktion in app.ts. Nur eines habe ich noch geändert: Man kann das Intervall zwischen zwei Updates optional in der Konfiguration jedes Instruments separat einstellen. Es ist ja nicht bei allen Anzeigen gleich sinnvoll, jede Sekunde einen neuen Wert abzulesen. Es wird also für jedes Instrument ein eigener setInterval() Aufruf abgesetzt. Mit Random() sorgen wir dafür, dass die Aufrufe ein wenig über die Zeit verteilt werden, auch wenn das Interval auf denselben Wert gesetzt ist.
Vielleicht wundern Sie sich über den Aufruf von this.getValue(gauge) in run(), kurz bevor this.getValue() sowieso im setInterval aufgerufen wird. Das mache ich deswegen weil setValue sonst erst nach erstem Ablauf des Inervalls aufgerufen würde. So lange würde man nur einen Null-Wert sehen. Auf diese Weise erhält die Anzeige gleich von Anfang an einen korrekten Wert.
Damit verwandt die Frage: Wieso setzt man die Abfrage für den Startwert nicht gleich im constructor ab, sondern macht dafür eine spezielle run() Funktion? Nun, zum Zeitpunkt des Konstruktors existiert die visuelle Komponente noch nicht. Der Versuch, den Zeiger zu verstellen, würde darum zu diesem Zeitpunkt ins Leere laufen und mit einer Fehlermeldung enden. Die run() Funktion rufen wir im Rahmen von attached() in app.ts auf (s. etwas weiter unten), und das findet ja nach der Konstruktion des DOM statt.
In gleicher Weise habe ich Klassen für Switches und Charts erstellt und im devices-Ordner abgelegt. Ich spare mir jetzt hier das Listing, Sie sehen die Klassen ja im Quellcode-Archiv.
Jetzt wird app.ts sehr viel übersichtlicher:
1 import { autoinject } from 'aurelia-framework'
2 import configs from './config'
3 import { Boiler } from "./routes/boiler";
4 import { Gauges } from './devices/gauges';
5 import { Switches } from './devices/switches';
6 import { Charts } from './devices/charts';
7
8 @autoinject
9 export class App {
10 conf = configs
11
12 constructor(
13 private gauges: Gauges, private switches: Switches,
14 private charts: Charts) { }
15
16 attached() {
17 this.gauges.run([configs.wohnzimmertemp_cfg, configs.aussentemp_cfg,
18 configs.bad_oben_cfg, configs.dusche_cfg, configs.dachstock_cfg,
19 configs.barometer_cfg, configs.powermeter_cfg])
20
21 this.switches.run([configs.fernsehlicht_cfg, configs.carloader_cfg,
22 configs.mediacenter_cfg, configs.extender_cfg, configs.e14_cfg,
23 configs.aussenlicht_cfg])
24
25 this.charts.run([configs.aussen_chart_cfg, configs.barometer_chart_cfg])
26
27 }
28
29 switchGauge(hide, show) {
30 this.conf[hide].visible = false
31 this.conf[show].visible = true
32 }
33 }
Im constructor() werden die Klassen instanziiert (Falls Ihnen das nicht mehr klar ist, lesen Sie bitte noch einmal im Kapitel über das Aurelia Modulkonzept nach), und in attached() wird die jeweilge run() Funktion aufgerufen, die die Intervalle erstellt und die Aufrufe des EventAggregators regelt. Mehr benötigen wir nicht mehr in app.ts.
Nun bauen wir auch für den Boiler eine eigene Steuerklasse in “devices”. Hier gibt es nur einen.
import { FetchService } from '../services/fetchservice'
import { autoinject } from 'aurelia-framework'
import { EventAggregator } from 'aurelia-event-aggregator'
import configs from '../config'
@autoinject
export class Boiler {
constructor(private fetcher: FetchService,
private ea: EventAggregator) {
this.ea.subscribe(configs.boiler_cfg.message, (msg => {
this.fetcher.setIoBrokerValue("mqtt.0.Boiler.setTemp", msg)
}))
}
run() {
this.getValue()
setInterval(() => this.getValue(), 10000)
}
getValue() {
this.fetcher.getIobrokerValue("mqtt.0.Boiler.setTemp", undefined)
.then(temp => {
this.ea.publish(configs.boiler_cfg.message, temp)
})
}
}
Sie sehen, dass auch das nicht mehr schwierig ist: Ich setze eine EventAggregator-Subscription auf die message des Boilers und mache daraus einen Schreibzugriff auf den entsprechenden state des mqtt Brokers, der wiederum daraus eine MQTT Nachricht macht. Allerdings müssen wir noch dafür sorgen, dass dieses Teilprogramm zusammen mit dem Hauptprogramm gestartet wird. Wir ergänzen also den constructor von app.ts:
constructor(
private gauges: Gauges, private switches: Switches,
private charts: Charts private boiler:Boiler) { }
Durch das Injizieren des Boilers wird dieser instanziiert. Jetzt können Sie das Programm starten, und es sollte funktionieren.
Wenn Sie einen MQTT Cient, zum Beispiel MQTT Dash auf dem Android-Handy, oder MQTT Tool auf iOS Geräten, auf Ihren ioBroker richten und “/Boiler/setTemp” abonnieren, werden Sie jede Änderung am Schieber sofort auch auf den Handy sehen. Falls der MQTT Server auf dem ioBroker noch keinen entsprechenden State hat, genügt es, von irgendeinem MQTT Client, der mit dem ioBroker verbunden ist, eine entsprechende Nachricht abzusetzen. Der Broker akzeptiert ja alle topics und erstellt für jedes neue Topic umgehend einen ioBroker-State, den wir wiederum wie gewohnt programatisch oder auch über das Admin-UI unter “Objekte” lesen und verändern können.
Die andere Richtung funktioniert allerdings noch nicht: Wenn von woanders her eine Nachricht an /Boiler/setTemp geschickt wird, lässt das den Slider kalt. Wir müssen ihn noch lauschen lassen.
Ergänzen Sie Boiler.ts so:
// constructor...{}
run() {
this.getValue()
setInterval(() => this.getValue(), 10000)
}
getValue() {
this.fetcher.getIobrokerValue("mqtt.0.Boiler.setTemp", undefined)
.then(temp => {
this.ea.publish(configs.boiler_cfg.message, temp)
})
}
und app.ts so:
attached() {
this.gauges.run([configs.wohnzimmertemp_cfg, configs.aussentemp_cfg,
configs.powermeter_cfg])
this.switches.run([configs.fernsehlicht_cfg, configs.carloader_cfg,
configs.aussenlicht_cfg])
this.charts.run([configs.aussen_chart_cfg, configs.barometer_chart_cfg])
this.boiler.run()
}
Wir starten also auch in boiler.ts eine run() Funktion. Diese liest alle 10 Sekunden den aktuellen Wert des ioBroker States für das MQTT Topic /Boiler/setTemp aus und sendet eine entsprechende Nachricht an die setpoint - Komponente (oder an wen auch immer, der die entsprechende Nachricht abonniert hat). Wenn Sie das Programm erneut starten, werden Sie sehen, dass der Slider sofort auf den aktuellen Wert geht. Wenn Sie den Slider verstellen, können Sie am Handy den neuen Wert sehen. Wenn Sie am Handy einen neuen Wert eingeben, wird der Slider nach einigen Sekunden ebenfalls auf diesen neuen Wert gehen. Wir haben jetzt also einen bidirektionalen Informationsfluss. Da die Boilerregelung keine Komponente ist, bei der es auf Geschwindigkeit ankommt, ist die kleine Verzögerung nicht schlimm.
Motorsteuerung
Bisher haben wir virtuelle Messwerte vom PC über den ioBroker auf ein Handy geschickt und zurück, aber noch keine wirkliche Funktionalität. Wir müssen die Software mit unserem Motor verbinden. Wir hatten dort ja schon eine Motorsteuerung mit Johnny-Five gebaut, die auf Kommando im Uhrzeiger- oder Gegenuhrzeigersinn drehen kann. Alles was noch fehlt, ist ein MQTT Client in diesem Programm, und eine Möglichkeit, als °C formulierte MQTT Nachrichten in Steps für den Motor umzusetzen.
Dazu bohren wir stepper.js ein wenig auf:
class Stepper{
// ...
setScale(steps, start, end, domainFrom, domainTo, setOrigin) { // (1)
this.steps360 = steps;
this.position = start;
this.calc = val => {
return five.Fn.map(val, domainFrom, domainTo, start, end)
}
this.actPos = 0
if (setOrigin) {
this.ccw(steps)
}
}
async goto(pos) { // (2)
if (!this.running) {
this.running = true
let actPos = this.actPos
let newPos = this.calc(pos) // (3)
this.actPos = newPos
this.set_step("1001",true) // (4)
tis.sleep(10)
this.run(newPos - actPos).then(() => { // (5)
this.sleep(10)
this.set_step("0000",true) // (6)
this.running = false
})
}
}
async run(steps) { // (7)
if (steps < 0) {
return this.ccw(steps * -1)
} else {
return this.cw(steps)
}
}
// ...
}
Bei (1) tun wir etwas, was wir so ähnlich schon bei unseren allerersten Anzeigeinstrumenten getan haben: Wir setzen einen Wertebereich (domain) auf einen Drehwinkel (range) um. Nur benutzen wir diesmal für die Rechenarbeit keine d3js-Scale (wir haben hier auf dem Boiler-Controller ja kein D3js installiert und wollen das auch nicht unbedingt), sondern die Funktion Fn.map fon Johnny-Five, die ungefähr dasselbe tut. Unsere Domain sind die Boilertemperaturen von 30-100°C, die Range ist der Drehwinkel des Potentiometers von 0-240°. Zu jeder Boilertemperatur gehört ein bestimmter Winkel, und den können wir errechnen, wenn wir wissen, wieviele Schritte für eine 360° Drehung nötig sind.
Bei (2) wenden wir das Wissen an: Zunächst schauen wir nach, ob der Motor gerade am Drehen ist. Wenn ja, akzeptieren wir keine neuen Kommandos. Dann setzen wir das Flag, welches anzeigt, dass er nun beschäftigt ist. Bei (3) errechnen wir die neue Winkelposition. Dann kommt etwas Seltsames bei (4), auf das ich in wenigen Sekunden zurückkommen werde, bei (5) drehen wir den Motor um die Differenz zwischen alter und neuer Position. Danach warten wir einige Millisekunden, um die Drehachse und die Zahnräder zur Ruhe kommen zu lassen, dann schalten wir bei (6) alle Spulen aus. Der Motor hat nämlich ein recht hohes Haltemoment und saugt rund 300mA, nur um seine momentane Position zu halten. Das ist hier aber gar nicht nötig (Das Potentiometer wird sich sowieso nicht von allein bewegen), und es führt zu einer starken Erwärmung des Motors und zu erhöhtem Stromverbrauch. Daher schalten wir am Ende jeder Einstellungsbedwegung alle Spulen aus. Sie werden feststellen, dass der Motor jetzt völlig kühl bleibt. Allerdings sind die Spulen jetzt nicht mehr in dem Zustand, in dem sie am End des Schritts sein sollten. Daher setzen wir sie am Anfang der Drehfunktion bei (4) auf die Konstellation, die sie natürlicherweise am Ende des vorherigen Schrittes hatten.
(7) ist lediglich eine gewisse Vereinfachung. Wenn die Differenz aus neuer und alter Temperatur positiv ist, wird im Uhrzeigersinn gedreht, sonst im Gegenuhrzeigersinn.
Auch index.js des Motortreibers wurde ein wenig geändert und mit MQTT verbunden:
const Raspi = require('raspi-io')
const five = require('johnny-five')
const Stepper=require('./stepper').Stepper
const mqtt=require('mqtt').connect('mqtt://homepi.local',{
clientId: "Boiler-Motor",
will:{
topic: "/Boiler/ackn",
payload: "motor disconnected"
}
})
const board = five.Board({
io: new Raspi(),
repl: false
}
);
const range=240
const stepsPer360=512
const orange = "GPIO26"
const yellow = "GPIO19"
const pink = "GPIO13"
const blue = "GPIO6"
let speed = 15;
let stepper;
mqtt.on("connect",()=>{
mqtt.subscribe("/Boiler/setTemp")
mqtt.publish("/Boiler/ackn","motor ready")
})
mqtt.on('message',(topic,message)=>{ // (1)
if(topic==="/Boiler/setTemp"){
stepper.goto(parseInt(message)).then(()=>{
})
}
})
board.on('ready', async () => { // (2)
stepper = new Stepper(orange, yellow, pink, blue, speed)
stepper.setScale(stepsPer360,0,240,20,100,true)
function setSpeed(sp) {
stepper.speed = sp;
}
})
Neu ist bei (1), dass wir “setTemp”-Nachrichten von MQTT abfangen, un diese an die eben diskutierte goto-Funktion des Steppers weiterleiten. Bei (2) initialisieren wir den Controller, indem wir die Verbindungen deklarieren und die Scale setzen. Der letzte Parameter “true” bewirkt, wenn Sie nochmal kurz bei stepper.js nachsehen, das der Motor um eine volle Drehung im Gegenuhrzeigersinn bewegt wird. Das heisst, er wird ziemlich sicher am linken Anschlag “anstossen”. Nun, das schadet einem Schrittmotor nichts. Er dreht einfach nicht mehr weiter, wenn er am Anschlag ist. Aber auf diese Weise haben wir einen definierten Ausgangspunkt: Wir wissen jetzt mit Sicherheit, dass das Potentiometer ganz nach links gedreht ist.