Minimalist

Wetterstation für Minimalisten

Die Bauanleitung für Minimalisten ersetzt den ESP32 mit einem leistungsschwächeren ESP8266 Microcontroller und verzichtet auf einige Extras. Dazu gehören der redundante Umweltsensor, die Ladelogik für einen Akku und die Unterstützung von Solarzellen.

Im Gegenzug ist der gesamte Aufbau nicht nur kostengünstiger und einfacher umzusetzen, sondern auch energieeffizienter. Auf Batteriebetrieb sollte die Wetterstation zwei oder drei Monate durchhalten.

Um Deine Wetterstation trotz der fehlenden Komponenten umweltfreundlich zu betreiben, kannst Du die Stromversorgung mit Hilfe von Akkus im AA-Format realisieren, die Du dann eben mit einem entsprechenden Ladegerät regelmäßig aufladen musst.

Benötigte Komponenten:

  • 1 x Microcontroller (ESP8266 D1 mini) für die Steuerung
  • 1 x Sensor (BME280 oder BMP280) für die Erfassung der Umgebungswerte
  • 2 x Widerstand (1M Ohm, 100k Ohm) zur Spannungsmessung
  • 1 x Dip-Schalter zur Aktivierung des Tiefschlafmodus
  • 1 x Taster zum Start des Konfigurationsmodus
  • 1 x Steckplatine oder Lochplatine für den Aufbau
  • 1 x Batteriefach mit 4 AA Batterien (Reihenschaltung) für die Stromversorgung

Stromversorgung

Der ESP8266 benötigt eine Eingangsspannung von mindestens 3.3V. Das Batteriefach liefert bei einer Reihenschaltung die Summe der Einzelspannungen der 4 Batterien. In der Regel sind das ca. 1.2-1.5 Volt pro Batterie. Die genaue Spannung hängt von den eingesetzten Materialien und dem Zustand der Batterie ab. Damit liefert das Batteriefach also Spannungen zwischen 4.8 und 6.0 Volt.

Um die 3.3V für den ESP8266 zu erzeugen, nutzen wir den integrierten Abwärtswandler (engl. step down bzw. buck converter).

Wie auf dem Schaltplan oben links abgebildet ist der Anschluss relativ einfach. Der GND-Anschluss und 5V Anschluss wird mit dem Minus- und Pluspol des Batteriefachs verbunden.

Beachte jedoch, dass Du die Batterien immer entnehmen solltest, bevor Du den ESP8266 über den integrierten USB-Anschluss an Deinen Rechner anschließt. Ansonsten könnte Dein Rechner möglicherweise beschädigt werden.

Schaltung der Wetterstation

Spannungsmessung

Damit Du den Zustand der Batterien jederzeit im Blick hast und weißt, wann es Zeit wird Deine Wetterstation zu laden, bietet es sich an, die Batteriespannung zu messen. Um das zu bewerkstelligen, nutzen wir einen Analogeingang am ESP8266. Tatsächlich hat der ES8266 genau einen Eingang mit Analog-Digital-Wandler (A0). Der Eingang unterstützt jedoch nur Spannungen zwischen 0 und 1V. Legt man eine höhere Eingangsspannung an riskiert man Beschädigungen.

Um die Spannungen unserer Batterien messen zu können, brauchen wir also einen Spannungsteiler, der die Eingangsspannung reduziert. Ein Spannungsteiler lässt sich einfach mit 2 Widerständen realisieren, deren Wert in einem geeigneten Verhältnis steht. Auf dem Schaltplan findest Du die Anordnung der Widerstände im oberen rechten Bereich. Mit dem vorgeschlagenen 1M Ohm und 100K Ohm Widerstand ergibt sich eine Spannungsreduktion um Faktor 11 (100K / (100K + 1000K)). Damit kannst Du also Spannungen zwischen 0 und 11V messen.

Aufgrund der hohen Werte der Widerstände fließt durch den Spannungsteiler nur wenig Strom. Daher verbinden wir ihn direkt mit der Stromversorgung.

Aufbau der Schaltung auf einer Steckplatine

Bei niedrigeren Widerständen könnte man den Spannungsteiler auch über einen PIN am ESP8266 bei Bedarf einschalten. Dazu würde man die Verbindung zum Minuspol über einen PIN am ESP8266 realisieren, den man als Ausgang konfiguriert und bei Bedarf auf den Wert LOW setzt.

Umweltmessung

Damit Deine Wetterstation auch den Umweltzustand messen kann, nutzen wir einen BME280 Umweltsensor. Falls Du auf die Erfassung der relativen Luftfeuchtigkeit verzichten kannst, kannst Du alternativ auch einen BMP280 Sensor nehmen. Dieser ist kostengünstiger und hatte in den letzten Monaten deutlich geringere Lieferzeiten.

Der Sensor kommuniziert mit dem ESP8266 über einen sog. I2C-Bus. Neben einer Stromversorgung über VIN und GND müssen wir also noch eine Leitung für das Taktsignal SCL und eine Leitung für das Datensignal SDA verbinden. Unser ESP8266 unterstütz den Betrieb von I2C-fähigen Komponenten über die Anschlüsse D1 und D2, wobei D1 den Takt und D2 die Daten überträgt.

Der BME280 Sensor ist sehr sparsam und hat nur einen geringen Ruhestrom. Entsprechend können wir auch den Sensor permanent mit der Stromquelle verbinden. Alternativ könnte man auch für den Sensor eine Schaltung über einen PIN des ESP8266 realisieren. Wenn Du Komponenten mit hohem Strombedarf ein- und ausschalten möchtest, solltest Du jedoch die Grenzwerte auf dem Datenblatt des ESP8266 beachten. Für Komponenten deren Strombedarf über den Grenzwerten liegt, solltest Du das Ein- und Ausschalten indirekt über einen Transistor oder ein Relais bewerkstelligen, damit der ESP8266 nicht beschädigt wird.

Konfigurationsmodus

Damit Du die Wetterstation später mit Hilfe der mobilen App konfigurieren kannst, brauchen wir eine Möglichkeit, die Wetterstation gezielt in den Konfigurationsmodus zu bringen. Zu diesem Zweck schließen wir einen Taster (SETUP) an den Pin D5 an und verbinden ihn mit der Masse, d.h. dem Minuspol der Batterien. Im Microcontroller nutzen wir später einen sogenannten Pull-Up-Widerstand, um die Spannung am Pin bei geöffnetem Taster auf HIGH „hochzuziehen“. Wir der Taster gedrückt, fällt die Spannung ab und eine Messung am Pin wird folglich LOW ergeben. Beim Einschalten der Wetterstation kannst Du später einen gedrückten Taster durch Auslesen des Werts von D5 erkennen.

Wenn Du dir den Schaltplan genau anschaust, wirst Du sehen, dass wir dort den Schalter SETUP nicht mit GND, sondern mit D7 verbunden haben.  Der Grund hierfür liegt jedoch nicht daran, dass wir Energie sparen möchten, sondern daran, dass der von uns verwendete Taster einen Pin-Abstand von 2 hat. D.h. es handelt sich lediglich um eine Layoutoptimierung für unsere konkreten Komponente, mit denen wir uns das Anlöten eines Kabels ersparen. Im Gegenzug müssen wir dann einfach D7 entsprechend konfigurieren (wieder OUTPUT und LOW).

Konfiguration der Wetterstation mit der MoWeSta App

Tiefschlafmodus

Damit Du die Wetterstation nicht so viel Energie benötigt, versetzen wir sie zwischen den Messungen regelmäßig über längere Zeit in den Tiefschlafmodus (engl. deep sleep). Damit der Microcontroller nach Ablauf der Zeit wieder automatisch aufwachen kann, muss der Eingang für das Reset-Signal (RST) mit dem Anschluss D0 verbunden werden. Die Verbindung dieser Pins hat jedoch einen unerwünschten Nebeneffekt. Sobald die Verbindung hergestellt ist, lässt sich der Microcontroller nicht mehr über den USB-Anschluss programmieren.

Damit wir einfach zwischen Unterstützung für Tiefschlaf und Programmierung wechseln können, stellen wir die Verbindung über einen Dip-Schalter her, den Du dann bei Bedarf einfach umschalten kannst. D.h. wenn Du die Software ändern möchtest, unterbrichst Du die Verbindung über den Dip-Schalter und wenn Du die Software installiert hast und das Gerät betrieben möchtest, stellst Du die Verbindung her.

Platine

Damit Du den Aufbau direkt zuhause nachbauen kannst, kannst Du statt einer fertigen Platine eine Experimentierplatine verwenden. Die von uns genutzten Experimentierplatinen haben bereits einzelne Verbindungen, wodurch man bei geschickter Platzierung weniger löten muss.

Die Komponenten werden einfach durch die Platine gesteckt und dann unten verlötet. Wenn die bestehenden Bahnen nicht ausreichen oder Querverbindungen benötigt werden, musst Du dann eben einfach ein paar kurze Kabel anlöten. Damit Du dabei nicht durcheinander kommst bietet es sich an, unterschiedliche Farben zu verwenden.

Wenn Du den Hardwareaufbau erstmal ausprobieren möchtest, kannst Du natürlich auch eine Steckplatine (engl. Breadboard) nehmen und alles mit kleinen Steckkabeln verbinden.

Die Wetterstation auf einer Experimentierplatine

Falls Du lieber eine richtige Platine nutzen möchtest, findest Du in unserem Verzeichnis bei Github ein fertiges Design. Mit den Dateien kannst Du die Platine z.B. bei Aisler fertigen lassen.

Für die Bestückung brauchst Du zwei Jumper, die den Dip-Switch und den Taster ersetzen. Um den Setupmodus zu aktivieren setzt Du die Brücke auf den Setup Jumper und drückst den Resetknopf am ESP8266.

Sobald die Station im Setupmodus ist, kannst Du sie mit MoWeSta für Android oder MoWeSta für iOS konfigurieren. Für den Betrieb entfernst Du die Brücke vom Setup Jumper und platzierst sie auf den Sleep Jumper.

Die Wetterstation auf der Platine in unserem Verzeichnis bei Github

Software

Nach Fertigstellung der Hardware kannst Du die Software für die Station umsetzen oder die Software nehmen, die wir für diesen Aufbau auf Github veröffentlich haben. Bei der Software handelt es sich um Code für die Arduino Plattform. Die erforderliche Entwicklungsplattform und die von uns genutzten Bibliotheken sind allesamt kostenlos erhältlich.

Nach der Installation der Arduino Entwicklungsumgebung, musst Du zunächst die Boardverwalter URL http://arduino.esp8266.com/stable/package_esp8266com_index.json für den ESP8266 hinzufügen. Danach kannst Du im Werkzeugmenü den ESP8266 D1 Mini auswählen und die Datei öffnen.

Anhand der #include-Anweisungen siehst Du welche Bibliotheken benutzt werden. Falls Du sie noch nicht installiert hast, kannst Du das über den Eintrag „Bibliotheken verwalten“ im Werkzeugmenü nachholen. Für den BME280 Sensor brauchst Du die Adafruit BME280 Bibliothek. Falls Du einen BMP280 nutzt, brauchst Du die Adafruit BMP280 Bibliothek und für die Gerätekonfiguration und das Hochladen von Messungen brauchst du ArduinoJson.

Softwarebibliotheken in der Arduino IDE installieren

Nach den #include-Anweisungen folgt eine Reihe von #define-Anweisungen, die verschiedene Konstanten definieren. Dazu gehören klarere Bezeichner für die verschiedenen Ports, die Einstellungen für das WLAN Netzwerk, das zur Gerätekonfiguration bereitgestellt wird, die URL über die Messungen hochgeladen werden und Konstanten für verschiedene Zeitangaben, wie z.B. die Tiefschlafzeit zwischen Messungen oder die maximale Wartezeit für den Aufbau zum WLAN Netz.

#define ARDUINOJSON_USE_LONG_LONG 1

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
#include <WiFiClientSecure.h>
#include <EEPROM.h>
#include <Adafruit_BMP280.h>
#include "ArduinoJson.h"

// Hardware setup 
#define VOLTAGE_PIN A0
#define VOLTAGE_MULTIPLIER 11
#define VOLTAGE_DIVIDER 255.0
#define EEPROM_SIZE 512
#define BUTTON_PIN D5
#define BUTTON_OUT D7

//Credentials for the Access Point
#define WLAN_AP_SSID "MoWeSta - WiFi"
#define WLAN_AP_PASSWORD "mowesta123456wifi"

// API-URL where measurements will be uploaded to
#define MOWESTA_UPLOAD_URL "https://www.mowesta.com/api/devices/current/measurements/now"
//#define MOWESTA_UPLOAD_URL "https://beta.mowesta.com/api/devices/current/measurements/now"
#define MOWESTA_SOFTWARE_VERSION 2

// memory for json document
#define JSON_BUFFER_SIZE 1000

// deep sleep period between measurements (us = 1e-6)
#define DEEP_SLEEP_PERIOD 600000000
// timeout to wait for the wifi connection in ms
#define TIMEOUT_WIFI 20000
// timeout to wait for the ntp sync in ms
#define TIMEOUT_NTP 20000
// timeout to wait for the setup data in sec
#define TIMEOUT_READ 40

Nach den #define-Anweisungen werden die globalen Variablen definiert. Dazu gehört eine Variable mit dem Wurzelzertifikat, das zur Validierung von HTTPS Verbindungen verwendet wird, die IP-Adressen für das Konfigurationsnetzwerk und die Geräteeinstellungen, die bei der Konfiguration mit Hilfe der App im Flash-Speicher des ESP8266 abgelegt werden.


// Let's encrypt root cert (valid until June 4, 2035)
const char certificate_anchors[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----
)EOF";
X509List certificates(certificate_anchors);

// Access point config
IPAddress ip_address_local(192, 168, 4, 1);
IPAddress ip_address_gateway(192, 168, 4, 1);
IPAddress ip_subnet_mask(255, 255, 255, 0);
WiFiServer wifi_server(8080);

// Variables set to data values received from the app
String* setting_wlan_ssid;
String* setting_wlan_password;
String* setting_device_id;
String* setting_device_token;
String* setting_longitude;
String* setting_latitude;

// Flag to store mode
bool mode_setup = false;

// Store boot time for timeout
long time_boot;

// Sensor to gather measurements
Adafruit_BMP280 sensor_bmp;

// eeprom reading and writing
void eeprom_write_string(int* add, String* data);
String* eeprom_read_string(int* add);

Danach werden die verschiedenen Funktionen deklariert, so dass sie sich gegenseitig referenzieren können. Zuletzt erfolgt die Implementierung der einzelnen Funktionen, beginnend bei der Setup-Funktion, die bei jedem Start ausgeführt wird.

// settings reading, writing, printing and validation
void settings_read();
void settings_write();
void settings_print();
bool settings_valid();

// device setup via mobile app or netcat
bool setup_process_connection(WiFiClient*);
bool setup_process_data(String);

// measurement and upload
void measurement_capture();

Die Setup-Funktion konfiguriert zunächst den seriellen Ausgang und danach die einzelnen Ein- und Ausgänge des Microcontrollers. Nach Abschluss der Konfiguration, werden die Einstellungen vom Flash-Speicher gelesen. Danach wird geprüft, ob der Knopf für den Konfigurationsmodus gerade gedrückt wird. Ist dies der Fall, wird das WLAN des Geräts im Access Point Modus gestartet, so dass sich die mobile Anwendung mit dem Gerät per WLAN verbinden kann. Für den Fall, dass der Knopf nicht gedrückt wird, wird geprüft, ob die aktuellen Einstellungen gültig und vollständig sind. Falls die Einstellungen gültig sind, wird eine Verbindung mit dem konfigurierten WLAN-Netzwerk hergestellt. Ansonsten geht das Gerät zurück in den Tiefschlaf.

void setup() {
  //Open serial port to display data and set baudrate
  Serial.begin(115200); 
  // Wait for serial port to be initialized
  while (!Serial);   
  Serial.println();
  // power the button, voltage divider and sensor
  pinMode(BUTTON_OUT, OUTPUT);
  digitalWrite(BUTTON_OUT, LOW);
  // Configure the button to enter setup
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  // Configure the LED to indicate setup
  pinMode(LED_BUILTIN, OUTPUT);
  // Initialize EEPROM with complete size
  EEPROM.begin(EEPROM_SIZE); 
  // Read the settings from eeprom
  settings_read();
  // Check if button is pressed to enter setup
  mode_setup = (digitalRead(BUTTON_PIN) == LOW);
  time_boot = millis();
  if (mode_setup) {
    digitalWrite(LED_BUILTIN, LOW);    
    Serial.println("Entering setup mode.");
    WiFi.softAPConfig(ip_address_local, ip_address_gateway, ip_subnet_mask);
    WiFi.softAP(WLAN_AP_SSID, WLAN_AP_PASSWORD);  
    IPAddress ip_address = WiFi.softAPIP();
    Serial.print("IP address is: '");
    Serial.print(ip_address);
    Serial.println("'");
    wifi_server.begin();    
  } else if (settings_valid()){
    digitalWrite(LED_BUILTIN, HIGH);    
    Serial.print("Connecting to WiFi network '");
    Serial.print(*setting_wlan_ssid);
    Serial.println("'");
    WiFi.begin(setting_wlan_ssid->c_str(), setting_wlan_password->c_str());
  } else {
    Serial.println("Invalid settings, setup required.");
    settings_print();
  }
}

Im Anschluss an die Setup-Funktion wird die Loop-Funktion aufgerufen. Diese wartet je nach Zustand entweder auf eine eingehende Verbindung der mobilen Anwendung oder auf eine erfolgreiche Verbindung mit dem WLAN-Netzwerk zum Hochladen von Messungen. Für den Fall, dass die Einstellungen unvollständig sind, blinkt die LED kurz auf und das Gerät geht zurück in den Tiefschlafmodus.

void loop() {
  if (mode_setup) {
    // setup mode, keeps handling incoming clients until successful setup
    WiFiClient client = wifi_server.available();
    if (client) {
      digitalWrite(LED_BUILTIN, HIGH);    
      if (setup_process_connection(&client)) {
        WiFi.softAPdisconnect(true);  
        delay(5000);
        ESP.restart();      
      } else {
        digitalWrite(LED_BUILTIN, LOW);            
      }
    }  
  } else if (settings_valid()) {
    // measurement mode, captures and uploads a measurement when wifi is up
    if (WiFi.status() == WL_CONNECTED) {
      if (sensor_bmp.begin(BMP280_ADDRESS_ALT, BMP280_CHIPID)) {  
          measurement_capture();
      } else {
        Serial.println("Failed to activate sensor.");
      }
      WiFi.disconnect();
      delay(1000);
      Serial.end();
      ESP.deepSleep(DEEP_SLEEP_PERIOD);
    } else {
      // check for excessive waiting time
      long time_now = millis();
      long time_delta = (time_now > time_boot) ? (time_now - time_boot) : time_boot - time_now;
      if (time_delta > TIMEOUT_WIFI) {
        Serial.println("Failed to activate wifi.");
        WiFi.disconnect();
        delay(1000);
        Serial.end();
        ESP.deepSleep(DEEP_SLEEP_PERIOD);
      }
      delay(500);
    }
  } else {
    // blink to indicate missing config
    for(int i = 0; i < 10; i++){
      digitalWrite(LED_BUILTIN, LOW);   
      delay(50);                       
      digitalWrite(LED_BUILTIN, HIGH);    
      delay(50);           
    } 
    Serial.end();
    ESP.deepSleep(DEEP_SLEEP_PERIOD);
  }
}

Falls sich das Gerät im Konfigurationsmodus befindet und sich eine mobile Anwendung erfolgreich mit dem Gerät verbunden hat, wird in der Setup_Process_Connection die eingehende Verbindung bearbeitet. Dabei werden die Einstellungen der mobilen Anwendung in Form einer Zeichenkette im JSON Format übertragen. Die Prüfung einer eingehenden Zeichenkette erfolgt in der Setup_Process_Data-Funktion. Entspricht die Zeichenkette den formalen Vorgaben, wird der Inhalt im Flash-Speicher gespeichert und die mobile Anwendung erhält die Antwort „Success“. Für den Fall, dass ein Fehler aufgetreten ist, wir der Wert „Error“ oder „Timeout“ zurückgeliefert, je nachdem ob die Zeichenkette falsch oder nicht rechtzeitig übertragen wurde. Im Fehlerfall bleibt das Gerät weiterhin im Konfigurationsmodus. Bei einer erfolgreichen Übertragung wird zunächst der WLAN Access Point deaktiviert und danach das Gerät neu gestartet. Durch den Neustart wird sofort eine Messung durchgeführt, die dann in der mobilen Anwendung abgerufen werden kann. Dadurch kann man prüfen, dass alles richtig funktioniert hat.


/**
 * Parses the client data and stores the relevant information in flash memory. 
 * It is assumed that the client data is in json format. 
 *
 * @param data The data received from the client.
 * @return True if the data seems ok and has been stored to flash, false otherwise.
 */
bool setup_process_data(String data) {
  StaticJsonDocument<JSON_BUFFER_SIZE> doc;  //Memory pool
  Serial.print("Received settings: ");
  Serial.println(data);
  DeserializationError error = deserializeJson(doc, data);
  if (error) {
    Serial.print("Deserialization failed with code ");
    Serial.println(error.c_str());
    return false;
  } else {
    delete setting_wlan_ssid;
    delete setting_wlan_password;
    delete setting_device_id;
    delete setting_device_token;
    delete setting_longitude;
    delete setting_latitude;
    setting_wlan_ssid = new String(doc["ssid"]);
    setting_wlan_password = new String(doc["password"]);
    setting_device_id = new String(doc["id"]);
    setting_device_token = new String(doc["token"]);
    setting_longitude = new String(doc["longitude"]);
    setting_latitude = new String(doc["latitude"]);
    settings_print();
    if (settings_valid()) {
      Serial.println("Storing valid settings.");
      settings_write();
      return true;
    } else {
      Serial.println("Ignoring invalid settings.");
      return false;
    }    
  }
}


/**
 * Performs the setup data exchange with the app and returns
 * whether the arduino has successfully received a new configuration.
 *
 * @param client The wifi client that represents the connection.
 * @return True if the setup data has been retrieved successfully.
 */
bool setup_process_connection(WiFiClient *client) {
  Serial.println("Client connection established.");           
  String data = "";
  bool didRead = true;
  client->setTimeout(TIMEOUT_READ);                        
  while (client->connected() && didRead) {       
    String chunk = client->readStringUntil('\n');
    didRead = chunk.length() > 0; // if line is empty, break immediately
    data += chunk;
  }
  if (data.length() > 0) {
    if(setup_process_data(data)) {
      client->println("Success\n");   // Notify client
      client->flush();
      // Wait for client to close connection, since we will 
      // be disabling the accesspoint directly afterwards
      while (client->connected()); 
      client->stop();
      Serial.println("Success, client disconnected.");
      return true;
    } else {
      client->println("Error\n");     //Notify client
      client->flush();
      client->stop();
      Serial.println("Error, client disconnected.");
      return false;
    }
  } else {
    client->println("Timeout\n");     //Notify client
    client->flush();
    client->stop();
    Serial.println("Timeout, client disconnected.");
    return false;
  }
}

Damit die übertragenen Daten permanent gespeichert sind, werden sie im EEPROM abgelegt. Dazu enthält das Programm zwei Hilfsfunktionen, mit denen man Daten mit unbekannter Länge sequentiell Speichern und Laden kann.

/**
 * Writes a String in EEPROM memory starting at a defined address.
 *
 * @param add  In/out parameter with the address.
 * @param data The string to write.
 */
void eeprom_write_string(int* add, String* data) {
  int s = data->length();
  if (*add + s + 1 < EEPROM_SIZE) {
    for (int i = 0; i < s; i++) {
      EEPROM.write(*add + i, (*data)[i]);
    }
    EEPROM.write(*add + s, '\0');
  }
  *add += s + 1;
}

/**
 * Reads a String from EEPROM memory starting at a defined address.
 * Reads until a terminating null character.
 *
 * @param add In/out parameter with the address.
 * @return A pointer to the string.
 */
String* eeprom_read_string(int* add) {
  char data[EEPROM_SIZE + 1];
  int limit = EEPROM_SIZE - *add;
  limit = limit < 0 ? 0 : limit;
  int len = 0;
  unsigned char k;
  if (limit > 0) {
    k = EEPROM.read(*add);
    while (k != '\0' && len < limit)
    {
      k = EEPROM.read(*add + len);
      data[len] = k;
      len++;
    }
  }
  data[len] = '\0';
  *add += len;
  return new String(data);
}

Diese Funktionen werden dann zur Realisierung der Funktionen zum Speichern und Laden der Einstellungen genutzt. Dazu werden einfach die WLAN Zugangsdaten, die Zugangsdaten für MoWeSta und der Gerätestandort nacheinander im EEPROM abgelegt.

/**
 * Reads the settings from the eeprom to memory.
 */
void settings_read() {
  int address = 0;
  setting_wlan_ssid = eeprom_read_string(&address);
  setting_wlan_password  = eeprom_read_string(&address);
  setting_device_id = eeprom_read_string(&address);
  setting_device_token = eeprom_read_string(&address);
  setting_longitude = eeprom_read_string(&address);
  setting_latitude = eeprom_read_string(&address);
}

/**
 * Writes the settings from memory to the eeprom.
 */
void settings_write() {
  int address = 0;
  eeprom_write_string(&address, setting_wlan_ssid);
  eeprom_write_string(&address, setting_wlan_password);
  eeprom_write_string(&address, setting_device_id);
  eeprom_write_string(&address, setting_device_token);
  eeprom_write_string(&address, setting_longitude);
  eeprom_write_string(&address, setting_latitude);
  EEPROM.commit();
}

/**
 * Prints the settings onto the serial console.
 */
void settings_print() {
  Serial.print("WLAN SSID: ");
  Serial.println(*setting_wlan_ssid);
  Serial.print("WLAN Password: ");
  Serial.println(*setting_wlan_password);
  Serial.print("Device ID: ");
  Serial.println(*setting_device_id);
  Serial.print("Device Token: ");
  Serial.println(*setting_device_token);
  Serial.print("Longitude: ");
  Serial.println(*setting_longitude);
  Serial.print("Latitude: ");
  Serial.println(*setting_latitude);
}


/**
 * Determines whether the settings are valid. This is
 * done by making sure that every string has a non-zero length.
 * 
 * @return True if the settings are valid, false otherwise.
 */
bool settings_valid() {
    return setting_wlan_ssid->length() > 0 &&
      setting_wlan_password->length() > 0 &&
      setting_device_id->length() > 0 &&
      setting_device_token->length() > 0 &&
      setting_longitude->length() > 0 &&    
      setting_latitude->length() > 0;
}

Falls sich das Gerät nicht im Konfigurationsmodus befindet und die Verbindung zum WLAN-Netzwerk aufgebaut werden konnte, wird eine Messung aller Sensorwerte durchgeführt und das Ergebnis wird verschlüsselt übertragen. Die gesamte Logik hierzu befindet sich in der measurement_capture-Funktion. Da die Übertragung verschlüsselt erfolgen soll, benötigt das Gerät zunächst Kenntnis der aktuellen Uhrzeit. Hierzu wird zunächst mit Hilfe des Network Time Protocols die Uhrzeit abgerufen. Danach erfolgt der Aufbau der JSON Zeichenkette, mit der die Messungen übertragen werden. Hierbei werden die Messungen durchgeführt und in das entsprechende Format konvertiert.


/**
 * Synchronizes the time with an ntp server, captures a measurement,
 * generates a json document and uploads it to the mowesta backend.
 */
void measurement_capture() {
  WiFiClientSecure wifi;
  HTTPClient http;
  StaticJsonDocument<JSON_BUFFER_SIZE> doc;
  sensors_event_t temperature_event, pressure_event;

  // set trusted root certificates and perform ntp time sync for https
  configTime(0, 0, "time2-1.uni-duisburg-essen.de", "time2-3.uni-duisburg-essen.de", "pool.ntp.org");
  Serial.print("Waiting for NTP time sync: ");
  time_t now = time(nullptr);
  // check that time is after January 1st, 2021
  int max_wait = TIMEOUT_NTP;
  while (now < 1606694400) {
    if (max_wait < 0) {
      Serial.println("Timeout");
      return;
    }
    delay(500);
    max_wait -= 500;
    now = time(nullptr);
  }
  Serial.println(now);
  wifi.setTrustAnchors(&certificates);

  // capture sensor data
  sensor_bmp.getTemperatureSensor()->getEvent(&temperature_event);
  sensor_bmp.getPressureSensor()->getEvent(&pressure_event);
   
  // create json document for upload
  doc["id"] = 0;
  doc["creationTime"] = 0;
  doc["modificationTime"] = 0;
  doc["deleted"] = "false";
  JsonObject coordinate = doc.createNestedObject("coordinate");
  coordinate["latitude"] = setting_latitude->toDouble();
  coordinate["longitude"] = setting_longitude->toDouble();
  doc["time"] = now * 1000;           
  doc["device"] = atol(setting_device_id->c_str());        
  doc["version"] = MOWESTA_SOFTWARE_VERSION;
  doc["temperature"] = (char*)0;
  doc["pressure"] = (char*)0;
  doc["humidity"] = (char*)0;
  JsonObject data = doc.createNestedObject("data");
  data["SENSOR_TEMPERATURE"] = String(temperature_event.temperature);
  data["SENSOR_PRESSURE"] = String(pressure_event.pressure);
  data["BATTERY_VOLTAGE"] = String((analogRead(VOLTAGE_PIN) / VOLTAGE_DIVIDER) * VOLTAGE_MULTIPLIER);
  
  // print measurement
  serializeJsonPretty(doc, Serial); 
  Serial.println();

  // upload measurement
  String body;
  serializeJson(doc, body); 
  http.begin(wifi, MOWESTA_UPLOAD_URL);
  http.addHeader("Accept", "application/json");
  http.addHeader("Authorization", "Bearer " + *setting_device_token);
  http.addHeader("Content-Type", "application/json");
  int code = http.POST(body); 
  if (code / 100 == 2) {
    Serial.println("Upload successful.");    
  } else {
    Serial.print("Upload failed: ");
    Serial.println(code);
  }  
  http.end();
}

Einrichtung und Nutzung

Sobald Du die Software auf dem ESP8266 installiert hast, kannst Du die Wetterstation über MoWeSta für iOS oder MoWeSta für Android einrichten. Dazu aktivierst Du zunächst den Setupmodus der Wetterstation indem Du den Setupknopf gedrückt hältst und den Resetknopf am ESP8266 antippst. Sobald der Setupmodus aktiviert ist, leuchtet die LED am Arduino blau und Du kannst den Setupknopf wieder loslassen.

Danach öffnest Du die Geräte-Seite in der App und tippst auf den gelben Plusknopf. Falls Du die Geräteseite nicht siehst, musst Du Deine MoWeSta App zuerst mit deinem Konto verbinden. Danach gibst Du die GPS Position ein (oder nutzt das GPS an Deinem Gerät um sie automatisch eingeben zu lassen) und gibst die Zugangsdaten für das WLAN Netzwerk an, das der ESP8266 für das Hochladen der Messungen verwenden soll. Zum Schluss kannst Du noch einen Gerätenamen vergeben.

Wenn Du alles eingegeben hast, tippst Du auf den Knopf am unteren Bildschirmrand um die Konfiguration von Deinem iOS oder Android Gerät an den ESP8266 zu übertragen. Wenn Du gefragt wirst, ob sich Dein Gerät mit dem MoWeSta – Wifi verbinden darf, tippe auf die Schaltfläche mit der Aufschrift „Ja“ oder „OK“.

Sobald die Einrichtung abgeschlossen ist, kannst Du Deine Wetterstation in der Liste der Geräte sehen. Falls Du sie nicht findest, lade die Seite neu. Wenn Du eine Messung „manuell“ erstellen möchtest, kannst Du einfach auf den Resetknopf des ESP8266 drücken. Sobald er zurückgesetzt wird, startet das Programm und der ESP8266 verbindet sich mit dem WLAN, macht eine Messung und lädt sie in den MoWeSta Dienst hoch.

Wenn Du dir nicht sicher bist, ob alles klappt, kannst Du Deine Wetterstation mit Deinem Rechner verbinden und den „seriellen Monitor“ in der Arduino IDE öffnen. Stelle die Baudrate des seriellen Monitors auf 115200 und drücke dann den Resetknopf am ESP8266. Danach solltest Du im Monitor eine Ausgabe sehen, die so ähnlich aussieht wie in dem Bild auf der rechten Seite.

Die erste Zeile „Connecting to WiFi network ‚…'“ zeigt den Verbindungsaufbau an. Die zweite Zeile „Waiting for NTP time sync: …“ signalisiert eine erfolgreiche Zeitsynchronisation. Die Zeilen zwischen den geschweiften Klammern sind die Daten, die zum MoWeSta Dienst hochgeladen werden und die letzte Zeile „Upload successful.“ zeigt an, dass die Daten erfolgreich hochgeladen wurden.

Ausgabe im seriellen Monitor der Arduino IDE

Falls etwas nicht klappt oder Du Anmerkungen oder Fragen hast, schreib gerne eine Email an marcus.handte@uni-due.de.

Viel Spaß beim Experimentieren!