WLAN-Umweltlogger
Inhaltsverzeichnis
1. Einleitung Inhalt
seit gut eineinhalb Jahren hat die Firma Expressif mit ihrem ESP8266 und insbesondere den verfügbaren ESP-*-Modulen (z.B. ESP-12F) die Landschaft des Bastler-Internet-of-Things gehörig umgekrempelt.
Ursprünglich nur als WLAN-Seriell-Brücke gedacht, existiert inzwischen ein C++-Framework für die Arduino-IDE mit den entsprechenden Vorteilen. Eine Verwendung von NodeMCU ist nicht mehr erforderlich und bringt auch keinen wirklichen Vorteil.
In diesem Artikel geht es um die Beschreibung eines schnell aufzubauenden Umweltloggers, der in meinem Falle für die Langzeitdatenaufzeichnung von Feuchtigkeitsdaten in einer Tiefgarage dient. Weil die Garage sehr groß ist, und auch eine Lüftungsanlage besitzt, brauchen wir mehr als einen Sensorknoten.
2. Beschreibung Sensorknoten Inhalt
Wichtigstes Kriterium ist, daß der Knoten sehr schnell aufgebaut werden können soll. Keine starre Platine, keine Layouterei, keine unnötigen Kosten. Gleich danach kommt die Anforderung, Meßwerte in Echtzeit möglichst per WLAN weiterzugeben – was wir haben, haben wir.
Kommerzielle Lösungen waren mit 300 EUR pro Knoten viel zu teuer, es geht auch besser. Die jetzt beschriebene Lösung wird aus 2 18650 LiPo-Zellen versorgt, verbraucht im Sleep-Modus nur 0.05mA und wacht alle 5 Minuten zur Erfassung und Weiterleitung von Meßwerten auf.
2.1 Knoten-Elektronik Inhalt
Wie eingangs erwählt ist das Herzstück des Sensorknotens der Expressif ESP8266-SoC im Modul ESP-12F. Er kommt mit 4MByte Flash, 80k RAM, eingebautem WiFi, serieller Schnittstelle zum Programmieren und besitzt sogar einen ADC-Kanal.
Bei der Verdrahtung ergeben sich keine Besonderheiten. Ich hab einfach alles auf 400-er Steckbrettern zusammengebaut. Beachtet, daß der Kondensator in der Größe ~10µF notwendig ist; der ESP hat manchmal 350mA Stromspitzen, die abgefangen werden müssen.
Um die Batteriespannung zu messen, nutze ich ein 5k Trimmpoti. Ein Ende an +4,2V, Schleifer an ESP-ADC, anderes Ende GPIO. Zum Messsen also GPIO->0, messen, GPIO->1. Damit fließt über den Spannungsteiler nicht ununterbrochen Strom.
- An PIN 12 und 13 hängt der BME280 Umweltsensor (siehe unten)
- PIN 14 ist der Minuspol des Spannungsteilers zur Batteriespannungsmessung
- PIN16 muß mit Reset verbunden sein wegen Deepsleep
Eine Besonderheit noch zur Stromversorgung: Trotz der angegebenen Spannungsversorgung von 3,3V-3,6V versorge ich den Chip direkt aus den bis zu 4,20V der LiPo-Akkus. Funktioniert super, nur der Stromverbrauch ist eben etwas höher, wenn die Akkus noch die volle Ladung haben. Clamping Dioden heizen eben doch ein bißchen.
2.2 Sensorelement Inhalt
Die Aufzeichnung soll möglichst präzise sein. Ich hab mich daher für den sehr teuren BOSCH Sensortec BME280 (PDF) entschieden, der in einem Vergleich (englisch) aus sechs verschiedenen Sensoren am Besten abgeschnitten hat. Insbesondere waren die Reaktionszeit auf eine Änderung der Eingangsgröße sowie die absolute Genauigkeit und die Langzeitdrift hier am besten.
Der Sensor mißt die folgenden Größen:
- Temperatur in °C
- Relative Luftfeuchte in %rH
- Luftdruck in hPa
Er ist über I2C an den ESP8266 angebunden (Pin 12 und 13) und wird bei jedem Zyklus des ESP, also alle 5 Minuten, abgefragt. Die Bibliothek liest auch die Kompensationskonstanten jedesmal neu aus.
2.3 Windmesser Inhalt
Ein weiterer Sensor wurde notwendig, um den Zustand der Lüftung zu erfassen. Sie hat zum Glück nur die beiden Zustände EIN/AUS. Wir haben drei Möglichkeiten geprüft:
- PC-Lüfter mit Tachosignal: Am dritten Pin (neben GND und +Versorgung) steht per Standard das Tachosignal zur Verfügung. In der Regel werden pro Umdrehung zwei Pulse (Open-Drain) gegen GND generiert. Leider werden die Signale bei unseren Testlüftern nur erzeugt, wenn auch 12V Versorgungsspannung anliegen. Für einen Batteriesensorknoten also ungeeignet.
- PC-Lüfter mit Reedkontakt: Wir haben dann versucht, mit einem Reedkontakt zu arbeiten. Ein am Rotor befestigter Magnet würden bei eingeschalteter Lüftung durch die Umdrehung in Bewegung versetzt und würde den Reedkontakt entsprechend oft betätigen. Leider war die Umdrehungsgeschwindigkeit des Lüfters so groß, daß der Reedkontakt nicht mehr zuverlässig gearbeitet hat und stets angezogen blieb. Das Zählen der Impulse war also auch unpraktisch.
Die dritte Lösung erforderte wieder unseren 3D-Drucker. Wie rechts abgebildet, besteht das Konstrukt (OpenSCAD-Datei) aus zwei Teilen.
- Einem Windkanal mit zwei Befestigungslöchern. Mit einem 1,5mm2 Draht wird dieser am Lüftungsgitter befestigt.
- Eine Klappe, die ebenfalls mit einem geraden Stück 1,5mm2 Draht im Windkanal montiert wird.
Beachtet, daß die beiden Versionen in unterschiedlicher Richtung angebracht werden können. So könnt ihr Zuluft und Abluftbetrieb getrennt erfassen. An der Klappe ist unten ein Magnet zu befestigten, z.B. ein Ringmagnet. Ich habe dafür Heißkleber verwendet.
Der Reedkontakt selbst wird an geeigneter Stelle am Gehäuse des Windkanals verklebt. Da es von solchen Schaltern und Magneten unterschiedlichste Bauformen gibt, sind hier keine Befestigungen vorgesehen. Heiß- oder Sekundenkleber sind eure Freunde.
2.4 Software Inhalt
Glücklicherweise haben sich schon einige Leute Gedanken zum Auslesen des BME280-Sensors gemacht und die Bibliothek von Bosch überarbeitet: bme280.zip
Meine eigene Software ist deswegen sehr schlank und sollte selbsterklärend sein für Leuchte mit Arduino-Kenntnissen:
#include <stdint.h> #include <ESP8266WiFi.h> #include <Wire.h> #include "BME280.h" // PROTOTYPES void i2cWrite(uint8_t i2c_address, uint8_t *p_data, uint8_t data_size, uint8_t repeated_start); void i2cRead(uint8_t i2c_address, uint8_t *p_data, uint8_t data_size); void spiWrite(uint8_t *p_data, uint8_t data_size); void spiRead(uint8_t *p_data, uint8_t data_size); // GLOBAL VARIABLES char system_id[10]; int system_voltage; int system_fanState; BME280 bme280; void setup() { // ********** SETUP SERIAL sprintf(system_id, "%06x", ESP.getChipId()); Serial.begin(115200); Serial.println(String("\nLOGGER [")+ system_id +"]: wake up..."); // ********** READ VOLTAGE system_voltage=0; pinMode(14, OUTPUT); digitalWrite(14, LOW); for (int i=0;i<20;i++) { delay(1); system_voltage += analogRead(A0); } system_voltage /= 20; digitalWrite(14, HIGH); Serial.println(String("LOGGER [") + system_id +"]: vcc="+system_voltage); // ********** READ FAN STATUS pinMode(5, INPUT_PULLUP); system_fanState=0; for (int i=0;i<10;i++) { delay(1); system_fanState += digitalRead(5); } pinMode(5, INPUT); if (system_fanState>4) { system_fanState=1; } else { system_fanState = 0; } Serial.println(String("LOGGER [") + system_id +"]: fan="+system_fanState); Wire.begin(13,12); Serial.print(String("LOGGER [")+ system_id +"]: BME280 - result of .begin(): "); if (bme280.begin(BME280_I2C_ADDRESS1)!=0) { Serial.println("ERROR"); } else { bme280.writeConfigRegister(BME280_STANDBY_500_US,BME280_FILTER_OFF,0); bme280.writeControlRegisters(BME280_OVERSAMPLING_1X,BME280_OVERSAMPLING_1X,BME280_OVERSAMPLING_1X,BME280_MODE_FORCED); Serial.println("OK"); } Serial.println(String("LOGGER [")+ system_id +"]: wifi connect..."); WiFi.persistent(false); WiFi.mode(WIFI_STA); WiFi.begin("TG_LOGGER", "TG_LOGGER"); int result = WiFi.waitForConnectResult(); if (result != WL_CONNECTED) { Serial.println(String("LOGGER [")+ system_id +"]: wifi failed... sleeping."); ESP.deepSleep(60*1000000, WAKE_RF_DEFAULT); } Serial.print(String("LOGGER [")+ system_id +"]: wifi connected, I am "); Serial.println(WiFi.localIP()); } void loop() { bme280.read(); float system_temp = bme280.temperature(); float system_humidity = bme280.humidity(); float system_pressure = bme280.pressure()/100.0; Serial.println(String("LOGGER [") + system_id +"]: t="+system_temp+"degC, h="+system_humidity+"%RH, p="+ system_pressure +"hPa"); String url = String("/tg_logger/index.php?id=")+ system_id +"&v="+system_voltage+"&t="+ system_temp +"&h="+ system_humidity +"&p="+ system_pressure +"&f="+ system_fanState; Serial.print(String("LOGGER [") + system_id +"]: requesting url "); Serial.println(url); WiFiClient client; if (client.connect("scmd.dynvpn.de", 80)) { client.print(String("GET ") + url + " HTTP/1.1\r\nHost: scmd.dynvpn.de\r\nConnection: close\r\n\r\n"); unsigned long timeout = millis(); while (client.available() == 0) { if (millis() - timeout > 5000) { Serial.println(">>> Client Timeout !"); client.stop(); break; } } } Serial.println(String("LOGGER [") + system_id +"]: sleeping"); ESP.deepSleep(300*1000000, WAKE_RF_DEFAULT); } void i2cWrite(uint8_t i2c_address, uint8_t *p_data, uint8_t data_size, uint8_t repeated_start) { Wire.beginTransmission(i2c_address); Wire.write(p_data,data_size); Wire.endTransmission(repeated_start!=0?false:true); } void i2cRead(uint8_t i2c_address, uint8_t *p_data, uint8_t data_size) { uint8_t i = 0; Wire.requestFrom(i2c_address,data_size); while (Wire.available() && i<data_size) { p_data[i] = Wire.read(); i += 1; } } void spiWrite(uint8_t *p_data, uint8_t data_size) {}; void spiRead(uint8_t *p_data, uint8_t data_size) {};
Es hat nur semantische Gründe, weswegen ich nicht alles in die setup()-Routine packe: Die loop()-Funktion sollte nach meinem Dafürhalten die eigentliche Funktionslogik enthalten, auch wenn sie in diesem Falle nur einmal läuft, und der Chip dann wieder schlafengeht. Übrigens kümmert sich der DeepSleep-Befehl auch um das Dissassoziieren vom Accesspoint.
Die Logik ist sehr einfach:
- Aufwachen
- BME konfigurieren
- Mit WLAN verbinden (bei Fehler eine Minute schlafen)
- BME auslesen
- Meßwerte absenden
- 5 Minuten schlafen
2.5 Gehäuse Inhalt
Man könnte zwar ein Gehäuse kaufen, aber a) sind die teuer und b) langweilt sich dann der 3D-Drucker. Also, schnell in OpenSCAD ein Gehäuse geschustert (bzw. meine universelle Arbeit angepaßt), als STL exportiert und viermal auf den Drucker geschickt. Dann ist die Elektrik zumindest ein bißchen geschützt. Die einzelnen Entwurfsschritte sind hier abgebildet…
2.6 Stromversorgung Inhalt
Der Knoten soll zum einen möglichst lange halten. Keiner will alle 2 Tage Batterien wechseln gehen. Zum anderen muß die Versorgung aber auch den äußerst wählerischen ESP versorgen, der sich zu ganz kurzen Spitzen auch einmal 500mA genehmigt. Wenn er die nicht bekommt, friert der Chip ein und wird auch vom Watchdog nicht zurückgesetzt.
Ich hab das mit 2×18650 LiIon-Akkus gelöst, die parallel auf eine Kapazität von rechnerisch 7400mAh kommen. Bei einem durchschnittlichen Verbrauch von 0.5mA würde so ein Knoten theoretisch also über ein Jahr lang laufen. Nun, wir werden sehen, auf alle Fälle ist eine Spannungsmessung über einen Spannungsteiler eingebaut.
Man möchte natürlich darauf achten, daß beide Zellen gleich voll geladen sind, wenn man sie einsetzt. Das SCAD-Modell des Batteriehalters ist übrigens aus diesem Thingiverse-Beitrag.
3. Beschreibung WLAN-Netzwerk Inhalt
Leider ist in unserer Tiefgarage gar kein WLAN auffindbar. Wir haben uns daher ein wenig beholfen und einem WR1043-ND Router OpenWRT spendiert. Einer der drei Antennenanschlüsse wurde mit einer 14 dB(i) Richtantenne bestückt, die jetzt aus einem Lüftungsschacht guckt. Damit hat der Router Empfang und bucht sich als DHCP-Client in mein WLAN ein. Gleichzeitig spannt er seinerseits ein Logger-WLAN auf, das in der TG mit zwei 9 dB(i) Antennen verteilt wird.
Einen Repeatermodus gibt es in der Konstellation leider nicht, nachdem aber die Knoten die Verbindung initiieren, ist das auch kein Problem. DNS sollte ja in OpenWRT automatisch konfiguriert werden.
4. Beschreibung Serverseite Inhalt
Die einzelnen Knoten rufen ja einen URL der Form http://server/script?id=… auf. Der Phantasie, was man damit dann anstellt, sind natürlich keine Grenzen gesetzt. In unserem Falle werden die Rohdaten mit SQLite und CSV geloggt, und auch gleichzeitig mit jQuery und Flot-Charts (jQuery Plugin) in eine ansprechende grafische Form gebracht.
Eine Auswertung ist dann im nächsten Jahr im Sommer geplant. Wahrscheinlich werden wir dafür dann fachmännische Hilfe benötigen und eventuell die Lüftung neu einstellen.
Bislang keine Kommentare vorhanden.
Einen Kommentar hinterlassen