Mit dem Empfangsmodul RX868-SH von ELV und einem Arduino lässt sich mit wenig Aufwand ein Wetterdatenempfänger bauen. Er empfängt die auf 868 MHz ausgestrahlten Daten verschiedener Sensoren, wie des Temperatur/Feuchtesensors ASH 2200 und des Kombisensors KS 300, dekodiert sie und gibt die Messwerte als Text über die USB/Seriell Schnittstelle des Arduino aus.
Das RX868-SH Modul besteht aus einem Superhet-Empfänger für 868,35 MHz. Er verfügt über die nötige Großsignalfestigkeit und Trennschärfe, um gute Empfangsergebnisse auch in einer mit WLAN und LTE belasteten Umgebung zu zeigen. Das Modul verfügt über vier Anschlüsse: Masse, Betriebsspannung von 2,3 bis 5,5 V, Datenausgang DATA und Steuereingang EN zum An- und Abschalten des Moduls. Der Anschluss an einen Arduino erfordert keine weiteren Bauteile. Die Spannungsversorgung kann mit 5 oder 3,3 Volt erfolgen. Der Steuereingang ist direkt mit der Betriebsspannung verbunden, damit der Empfänger immer aktiv ist. Der DATA Ausgang ist an einen Interrupt-fähigen I/O Pin des Arduino angeschlossen, da die Software die Dekodierung des Signals mittels Interrupts realisiert. Es sollte eigentlich jeder gängige Arduino mit einem ATmega Prozessor funktionieren. Für meine Versuche habe ich einen Arduino Nano verwendet, da dieser auch gleich einen USB-Seriell Wandler aufgelötet hat, mit dem sich Programmierung und Datenübertragung realisieren lassen.
Die Verbindung von Arduino und RX868-SH erfolgt nach dem Schema:
Arduino RX868 D3 ___________ DATA GND __________ GND 5V/3V3 _______ +UB |_ EN
Es ist auf möglichst kurze Leitungslängen zu achten! Die Verwendung von Breadboards oder ähnlichen Experimentieraufbauten ist nicht zu empfehlen. Sie funktionieren zwar im Prinzip, jedoch lässt die Empfangsleistung stark nach, da es zu Interferenzen zwischen der Antenne des RX868 und einer großflächigen Verdrahtung kommt.
Der RX868 liefert an seinem Datenausgang DATA ein digitales Signal, wobei ein HIGH Pegel das Vorhandensein eines aktiven HF-Trägersignals auf 868,35 MHz anzeigt. Das Signal gelangt auf den digitalen Eingang 3 des Arduino. Der Sketch konfiguriert in seiner setup() Methode diesen Eingang so, dass jede Impulsflanke des Signals einen Interrupt auslöst, den dann die Methode recv() abhandelt:
#define DATA_PIN 3 volatile unsigned long hilo = 0L; volatile unsigned long lohi = 0L; volatile bool hasNewValue = false; Decoder decoder(1100, 1300); void setup() { // initialize receiver pinMode(DATA_PIN, INPUT); // initialize interrupt attachInterrupt(digitalPinToInterrupt(DATA_PIN), recv, CHANGE); // ... } /* * Interrupt routine, * Called on each slope of the signal from the receiver. */ void recv() { int x = digitalRead(DATA_PIN); if (0 == x) { // slope hi->lo hilo = micros(); } else { // slope lo->hi unsigned long now = micros(); hasNewValue |= decoder.pulse(now - lohi, now - hilo); lohi = now; } }
Das Programm merkt sich den Zeitpunkt jeder Signalflanke. Nachdem ein komplettes Symbol bestehend aus einer High-Low und einer Low-High Flanke eingetroffen ist, reicht recv die Zeitpunkte an die Methode pulse der Klasse Decoder weiter, die sich um die Filterung und Dekodierung des Signals kümmert.
Wenn der Decoder ein komplettes Datenpaket erfolgreich dekodiert hat, dann setzt die Interruptroutine die globale Variable hasNewValue auf true. Die loop Methode des Sketches tut nichts weiter, als permanent den Wert von hasNewValue abzufragen. Signalisiert er das Vorliegen eines dekodierten Pakets, dann erfolgt die unmittelbare Weiterverarbeitung - im einfachsten Fall die Ausgabe der Messwerte in einem für Menschen lesbaren Format über die serielle (USB) Schnittstelle.
void loop() { if (hasNewValue) { DecoderOutput data = decoder.getDecoderOutput(); printDecoderOutput(data); hasNewValue = false; } }
Die Verlagerung der Signalverarbeitung in eine Interrupt-Routine entkoppelt diesen zeitkritischen Vorgang von der unter Umständen langsamen Weiterverarbeitung und Ausgabe. Denn während die Ausgabe über die serielle Schnittstelle mit einer festen Baudrate noch läuft, kann ja schon wieder der nächste Signalwechsel am Dateneingang stattfinden.
Das verwendete Übertragungsprotokoll kodiert ein Bit in einem Symbol von 1220 µs Länge. Die Kodierung von logisch 1 und 0 wird über das Puls-Pausen Verhältnis vorgenommen: 366 µs Puls (HIGH) und 854 µs Pause (LOW) repräsentieren ein Bit mit dem Wert 1, 854 µs Puls und 366 µs Pause ein 0 Bit.
Sensoren und Empfänger haben das SRD Frequenzband um 868 MHz nicht exklusiv zur Verfügung. Liest man den DATA Ausgang des RX868 fortlaufend aus, so sieht man eine auf den ersten Blick chaotisch anmutende Abfolge von Impulsen, die von allen möglichen Geräten aus der näheren Umgebung stammen. Die Software muss nun in der Lage sein, daraus das Signal nur der uns interessierenden Sensoren zu selektieren und zu dekodieren. Der Schlüssel dazu ist die bekannte, konstante Symbollänge von 1220 µs. Der Decoder lässt dazu in der ersten Verarbeitungsstufe nur solche Impulse passieren, die eine Periodenlänge zwischen 1100 und 1300 µs haben (diese Werte kann man bei der Initialisierung des Decoder spezifizieren). Danach trifft er aufgrund des Verhältnisses der Pulsdauer von HIGH zu LOW die Entscheidung, ob es sich um eine logische 0 oder 1 handelt.
Ein komplettes Datenpaket beginnt mit einem Synchronisationsblock von zehn Bits mit dem Wert 0. Dann folgen ein Startbit mit dem Wert 1 und je nach Sensortyp bis zu 80 Bits mit der kodierten Information Temperatur, Luftfeuchte, Windgeschwindigkeit und Regenmenge. Den Abschluss bilden mehrere Prüfsummen zur Erkennung von Übertragungsfehlern (Dokumentation des Datenprotokolls).
Die komplette Dekodierung ist in der Klasse Decoder (Sourcecode: Decoder.h, Decoder.cpp gekapselt. Nach der Initialisierung befindet sich der Decoder im Status WAIT. Wenn das erste Synchronisationsbit 0 eingeht, wechselt er in den Status SYNC, in dem er verbleibt, bis eine ausreichende Anzahl Synchronisationsbits sowie das Startbit 1 eingegangen sind. Danach ist der Decoder im Status DATA, in welchem er die Nutzdaten empfängt und in einen Puffer schreibt. Das Übertragungsprotokoll definiert leider kein explizites Stoppsymbol, so dass die Software das Ende eines Datenpakets daran erkennt, dass ein ungültiger Impuls eintrifft. Daraufhin erfolgt die Dekodierung der im Puffer stehenden Bits und die Verifizierung anhand der Prüfsummen. Ist alles in Ordnung, dann gibt Decoder.pulse() den Wert true zurück und der Aufrufende kann sich per getDecoderOutput() die dekodierten Daten als Variable vom Typ DecoderOutput abholen.
Wie oben in der Definition der loop() Funktion gezeigt, besteht die einfachste Möglichkeit der Weiterverarbeitung der dekodieren Daten in ihrer direkten Ausgabe über die serielle Schnittstelle des Arduino. Dazu kann man die im Sourcecode enthaltene Funktion printDecoderOutput() verwenden oder sich eine neue Methode nach eigenen Wünschen schreiben.
Eine Alternative ist die Verwendung der Funktion handleLogViewOutput(). Sie emuliert das Verhalten des Wetterdatenempfängers USB-WDE1 (ebenfalls von ELV) und gibt die Wetterdaten in einem zu LogView kompatiblen Format aus (Dokumentation des Ausgabeformats). Ziel ist die einfache maschinelle Weiterverarbeitung der Wetterdaten auf einem per USB angeschlossenen Host, zum Beispiel einem Raspberry Pi.
Die Funktion handleLogViewOutput() speichert die empfangenen Daten für zehn Minuten in einem Cache zwischen. Dadurch enthält eine Ausgabezeile die Daten möglichst vieler Sensoren und vereinfacht so die Auswertung auf dem Host.
Auf diese Art und Weise ist es möglich, einen USB-WDE1 zu ersetzen, falls ELV sich eines Tages dazu entschließt, ihn nicht mehr herzustellen. Die passenden Sensoren sind ja schon einige Zeit nicht mehr im Angebot, jedoch gibt es auch dafür Abhilfe mittels eines Eigenbaus.
Die Klasse Decoder lässt sich nicht nur für einen Arduino, sondern auch für andere Architekturen, wie Intel oder ARM kompilieren.
Zum Beispiel kann man den Decoder auf einem Raspberry Pi laufen lassen und den Empfänger RX868 direkt an seine GPIO-Leiste anschließen. Ein entsprechendes Beispielprogramm findet sich im Unterverzeichnis raspi-wiringPi des Sketches. Es arbeitet allerdings nicht Interrupt-basiert, sondern mit einer festen Abtastrate. Die Entkopplung zwischen zeitkritischer Dekodierung und Ausgabe erfolgt durch Multithreading. Details dazu erläutert mein Blog 868 MHz Wetterdatenempfänger mit RX868 und Raspberry Pi.
Während der Entwicklung der Software ist es sehr hilfreich, den Decoder-Algorithmus mit verschiedenen Daten schnell in der Entwicklungsumgebung testen zu können, ohne erst ein kompliziertes Hardware-Setup vornehmen zu müssen. Das Unterverzeichnis test enthält dazu ein Testprogramm für die Klasse Decoder, das auf dem Entwicklungsrechner mit Intel-Prozessor läuft.
Falls dann eines Tages auch das RX868 Empfangsmodul nicht mehr verfügbar ist, kann der Einsatz eines einfachen Software Defined Radios weiterhelfen, wie ich es in Empfang von Wettersensordaten mit RTL-SDR beschrieben habe.
Der Sourcecode des vollständigen Projektes steht als Arduino-Library im Github Repository WeatherRX868 zur Verfügung. Man kann entweder das als ZIP gepackte Release herunterladen und in die Arduino Entwicklungsumgebung integrieren oder das Git Repository direkt in das Verzeichnis libraries seines Sketchbooks klonen. Nach dem Neustart der Arduino IDE findet man dann unter File - Examples - WeatherRX868 den vollständigen Sketch. Ihn kann man direkt in einen angeschlossenen Arduino flashen und als Ausgangspunkt für eigene Experimente verwenden. Die Software steht unter der GNU General Public License v3.0.
Mit den in meinen Blogs beschriebenen Konzepten ist es möglich, sukzessive die meisten Komponenten einer ELV Wetterstation, wie den Sensor S 300 TH und den Empfänger USB-WDE1 zu ersetzen. Ich kann so eine seit vielen Jahren bestehende Wetterstation weiterbetreiben und erweitern. Ein Umstieg auf ein neues System, das dann vielleicht auch nur ein oder zwei Jahre produziert wird, ist nicht notwendig. Auch das ist ein Beitrag zur sinnvollen Verwendung von Ressourcen und der Vermeidung von Abfall.