Tutorial: Website parsen mit dem ESP8266 – Teil 3 – Projekt Hühnerstall – Step-by-step

WordPress1Einleitung

Wie bereits im letzten Teil dieses Tutorials angekündigt, möchte ich heute ein konkretes Projekt vorstellen, dass – zugegebenermaßen – meiner Fantasie entspringt, sich aber auch mit wenigen Änderungen auf andere Anwendungen anpassen lässt.

Dieser Teil ist deutlich größer geworden, als ursprünglich geplant, ein Lob und Dank also jetzt schon an alle, die sich jetzt hier durcharbeiten werden. Aber es wird sich lohnen. Ziel ist es, eine Steuerung der Ein- und Ausgangsklappe eines Hühnerstalls in Abhängigkeit von Sonnenauf- und -untergang zu bauen oder mit anderen Worten: wenn die Sonne weg ist soll die Klappe zu sein. Die Information für die Zeiten des Sonnauf- und -untergangs hole ich mir – ihr könnt es euch denken – von einer Webseite. Einwände, dass man die Zeiten auch berechnen kann, möchte ich mir an dieser Stelle gar nicht anhören, vielleicht soll die Zahl der Hühner ja im nächsten Schritt getwittert werden, dann braucht man eh den Internetzugang.

In diesem Tutorial wird nicht nur das Titelthema „parsen“ behandelt, sondern auch der Umgang mit der Time Library und der Sleep Mode für den ESP. Bis zum endgültigen Projekt Code gibt es viele Beispiele, die man zum besseren Verständnis einzel durchspielen kann.

Auf geht’s

Das Vorgehen ist wie im letzten Beitrag bereits beschrieben

  1. Webseite aussuchen: Ich habe mich für die Seite http://www.netzwolf.info/astronomie/ephemeriden/sonne entschieden, da sie die gewünschte Information ohne viel unnötige zusätzliche Daten bereitstellt.
  2. Quelltext der Seite untersuchen und die Suchbegriffe finden, glücklicherweise stellt die Webseite uns auch die aktuelle Zeit als UTC zur Verfügung und die Information, ob es Winterzeit (W) oder Sommerzeit (S) ist.
 
.... 
Weltzeit (UTC) : <b>14:57</b> 13.02.2016
....
Sonnenaufgang : <b>07:51</b> 13.02.2016 (W)
Kulmination 26.0°: <b>12:48</b> 13.02.2016
Sonnenuntergang : <b>17:45</b> 13.02.2016 (W)
....

Wie man sieht, ist die Information sehr leicht zu parsen. Suche nach „Sonnenaufgang : <b>“, nachfolgende 5 Zeichen sind die gesuchte Zeit, genauso für den Sonnenuntergang. Wir benötigen aber auch das Datum, da immer der nächste Sonnenauf- oder -untergang angegeben wird. Zusätzlich sollte man immer den Quelltext daraufhin untersuchen, welche Zeilenumbrüche genutzt werden, damit der korrekte bzw. ein sinnvoller Delimiter für den einzulesenden String bestimmt wird, in diesem Fall ist das 0x0A, also das ASCII Zeichen für LF oder „\n“.

3. in den Code (aus dem Teil 1) einbauen

Im Header Bereich des Codes zusätzliche Variablen Definitionen einbauen und Server und die URL eintragen

 
...
String SonnenA, SonnenU, WeltZ;
boolean sommerzeit;
... 
const char* host = "www.netzwolf.info";
... 
  String url = "/astronomie/ephemeriden/sonne";
... 

Für einen ersten Test den Suchstring einsetzen (Achtung: die Zahl der Leerzeichen im Suchstring)

  while (client.available()) {
    String line = client.readStringUntil('\n');
    if (line.indexOf("Sonnenaufgang    : <b>") >= 0) { // Searchstring exists?
      Serial.println();
      Serial.println(line);
      SonnenA=line.substring(22, 27) + line.substring(32, 43);
      Serial.println(SonnenA);
      if (line.indexOf("(S)")>=0) {
        sommerzeit=true;
      }
      else if (line.indexOf("(W)")>=0) {
        sommerzeit=false;
      }
      if (sommerzeit) Serial.println("Es ist Sommerzeit"); else Serial.println("Es ist Winterzeit");
    }
    if (line.indexOf("Sonnenuntergang  : <b>") >= 0) { // Searchstring exists?
      Serial.println();
      Serial.println(line);
      SonnenU=line.substring(22, 27) + line.substring(32, 43);
      Serial.println(SonnenU);
    }
    if (line.indexOf("Weltzeit (UTC)   : <b>") >= 0) { // Searchstring exists?
      Serial.println();
      Serial.println(line);
      WeltZ=line.substring(22, 27) + line.substring(32, 43);
      Serial.println(WeltZ);
    }
  }

und die Ausgabe prüfen.

Connecting to TP-LINK_360E60 
........... 
WiFi connected 
IP address:  
192.168.0.100 
 
connecting to www.netzwolf.info 
Requesting URL: /astronomie/ephemeriden/sonne 
 
Weltzeit (UTC)   : <b>20:31</b>  13.02.2016 
20:31 13.02.2016 
 
Sonnenaufgang    : <b>07:49</b>  14.02.2016 (W) 
07:49 14.02.2016 
Es ist Winterzeit 
 
Sonnenuntergang  : <b>17:47</b>  14.02.2016 (W) 
17:47 14.02.2016 
 
closing connection 

Soweit so gut, die Suche funktioniert, jetzt geht es daran, die Zeiten verarbeitbar zu machen.

Um mit den mit den Uhrzeiten bequemer umgehen und rechnen zu können, setze ich die neueste Time Library ein, die ich mit dem Library Manager aktualisiere.

OpenWeather2

Ein Ausflug in die Grundlagen von time.h

Die Funktionen der Time Library sind nicht ganz einfach zu verstehen, deshalb möchte ich an dieser Stelle einen kleinen Ausflug in die Grundlagen machen und zeigen wie die Berechnung funktioniert. Ich habe mal ein Programm erstellt, dass ich zum besseren Verständnis schrittweise durchgehen werde. Das Programm berechnet die Sekunden bis zum Sonnauf- oder -untergang. Die Zeiten sind dabei als Stringkonstanten genauso im Programm hinterlegt, wie wir sie gerade aus der Webseite erzeugt haben. Das Programm kann auch auf einem UNO ausprobiert werden.

Das Programm macht aus den Strings jeweils für die Time Library verarbeitbare Zeitwerte. Als erstes wird die „Uhr“ der Time Library gesetzt, dann die Zeitwerte für Sonnenauf- und -untergang in die korrekte Form (struct) der Lib gebracht. Ausgegeben wird schlussendlich die Zeit bis zum nächsten Sonnenauf- bzw -untergang.

#include <TimeLib.h> 
tmElements_t SunA; 
tmElements_t SunU; 
//                0123456789012345 
String WeltZ =   "16:24 13.02.2016";
String SonnenA = "07:49 14.02.2016";    
String SonnenU = "17:47 14.02.2016";
boolean sommerzeit=false;
int timezone=1;
unsigned int Hour, Minute, Second, Day, Month, Year;

In diesem Code Block wird die Time Library included und die Variablen definiert. Eine besondere Bedeutung habe dabei die struct  Definitionen (hier in der Theorie nachzulesen). Die struct Definition selbst ist in der Time Library enthalten, hier wird diese struct unseren Zeitvariablen zugewiesen. Zum besseren Verständnis hier ein Auszug aus der Library selbst mit der Definition der struct.

typedef struct { 
 uint8_t Second; 
 uint8_t Minute; 
 uint8_t Hour; 
 uint8_t Wday; // day of week, sunday is day 1
 uint8_t Day;
 uint8_t Month; 
 uint8_t Year; // offset from 1970; 
} tmElements_t, TimeElements, *tmElementsPtr_t;

Weiter gehts im Code

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  Serial.print("WeltZ (UTC) = ");
  Serial.println(WeltZ);
  Serial.print("SonnenA = ");
  Serial.println(SonnenA);
  Serial.print("SonnenU = ");
  Serial.println(SonnenU);
  
  // set time to actual time 
  Hour=atoi(WeltZ.substring(0,2).c_str());
  Minute=atoi(WeltZ.substring(3,5).c_str());
  Day=atoi(WeltZ.substring(6,8).c_str());
  Month=atoi(WeltZ.substring(9,11).c_str());
  Year=atoi(WeltZ.substring(13,16).c_str());
  setTime(Hour, Minute, 0, Day, Month, Year);    // time is set to UTC
  Serial.print("Arduino is set to UTC ");
  digitalClockDisplay();
  Serial.print("eqivalent UNIX time is ");
  Serial.println(now());

Im setup() werden die Strings zunächst noch einmal seriell ausgegeben. Im zweiten Teil wird der String für die Weltzeit (die aktuelle Zeit) auseinandergenommen und zunächst den Integern Hour, Minute, Second, usw zugewiesen. Dabei benutze ich neben dem Befehl String.substring() den wir bereits in den ersten Teilen des Tutorials kennengelernt haben, den Befehl atoi(). Da atoi() einen char typ erwartet muss mit .c_str() der Pointer auf den String übergeben werden. Mit setTime() wird die Uhrzeit dann „gesetzt“, d.h. ab jetzt tickt die interne Uhr. Mit der helper Funktion  digitalClockDisplay() wird die Uhrzeit in lesbarer Form ausgegeben. Da alle weitren Berechnungen in Sekunden laufen werden alle weiteren Berechnungen mit der Unixtime durchgeführt.

  // make SonnenA struct
  // make SonnenA struct
  SunA.Hour = atoi(SonnenA.substring(0, 2).c_str());
  SunA.Minute = atoi(SonnenA.substring(3, 5).c_str());
  SunA.Second = 0;
  SunA.Day = atoi(SonnenA.substring(6, 8).c_str());
  SunA.Month = atoi(SonnenA.substring(9, 11).c_str());
  SunA.Year = y2kYearToTm(atoi(SonnenA.substring(13, 16).c_str()));  // offset from 1970
  unsigned long SunA_ux = makeTime(SunA);     // unixtime
  SunA_ux = sommerzeit ? SunA_ux - 2 * timezone * 3600 : SunA_ux - timezone * 3600; // Timezone und summer/wintertime
  if (now() >= SunA_ux) {
    Serial.print("letzter Sonnenaufgang war ");
    Serial.println(SunA_ux);
    Serial.print("vor ");
    Serial.print(now() - SunA_ux);
    Serial.println(" Sekunden");
  }
  else {
    Serial.print("nächster Sonnenaufgang ");
    Serial.println(SunA_ux);
    Serial.print("in ");
    Serial.print(SunA_ux - now());
    Serial.println(" Sekunden");
  }

In diesem Codeblock wird die struct mit der Zeit für den Sonnenaufgang belegt, die Umwandlung des Strings erfolgt analog der Weltzeit mit dem kleinen Unetrschied, dass ein Offset für das Jahr eingebaut werden muss. Dafür benutze ich ein Makro,y2kYearToTm() ,dass in der Time Library enthalten ist. In den nachfolgenden Zeilen wird die Unixtime für den Sonnenaufgang berechnet, der Befehl makeTime() berechnet diese aus der struct. In der nächsten Zeile wird dann die Umberechnung auf UTC gemäß Zeitzone und Sommer-/Winterzeit durchgeführt. Dieser Block wird analog für den Sonnenuntergang wiederholt.

Die loop() bleibt leer, die Berechnungen brauchen wir hier als Lehrbeispiel nur ein einziges Mal. Die beiden Funktionen am Schluß des Codes werden auch in den Beispielen der Time Lib zur fomatierten Ausgabe von Zeit und Datum benutzt.

Der Code kann hier komplett kopiert werden, jedoch muss in der letzte Funktion ein „<“ korrigiert werden, Damit kommt WordPress im Code-Block nicht klar :-(
Den File gibt es auch auf meinem git als Download.

#include <code><TimeLib.h></code>              <-code tags ------- / entfernen
tmElements_t SunA;
tmElements_t SunU;
//                0123456789012345
String WeltZ =   "01:38 14.02.2016";
String SonnenA = "07:49 14.02.2016";
String SonnenU = "17:47 14.02.2016";
boolean sommerzeit = false;
int timezone = 1;
unsigned int Hour, Minute, Second, Day, Month, Year;

void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
Serial.print("*******************************\nWeltZ (UTC) = ");
Serial.println(WeltZ);
Serial.print("SonnenA = ");
Serial.println(SonnenA);
Serial.print("SonnenU = ");
Serial.println(SonnenU);

// set time to actual time
Hour = atoi(WeltZ.substring(0, 2).c_str());
Minute = atoi(WeltZ.substring(3, 5).c_str());
Day = atoi(WeltZ.substring(6, 8).c_str());
Month = atoi(WeltZ.substring(9, 11).c_str());
Year = atoi(WeltZ.substring(13, 16).c_str());
setTime(Hour, Minute, 0, Day, Month, Year);    // time is set to UTC
Serial.print("Arduino is set to UTC ");
digitalClockDisplay();
Serial.print("eqivalent UNIX time is ");
Serial.println(now());

// make SonnenA struct
SunA.Hour = atoi(SonnenA.substring(0, 2).c_str());
SunA.Minute = atoi(SonnenA.substring(3, 5).c_str());
SunA.Second = 0;
SunA.Day = atoi(SonnenA.substring(6, 8).c_str());
SunA.Month = atoi(SonnenA.substring(9, 11).c_str());
SunA.Year = y2kYearToTm(atoi(SonnenA.substring(13, 16).c_str()));  // offset from 1970
unsigned long SunA_ux = makeTime(SunA);     // unixtime
SunA_ux = sommerzeit ? SunA_ux - 2 * timezone * 3600 : SunA_ux - timezone * 3600; // Timezone und summer/wintertime
if (now() >= SunA_ux) {
Serial.print("letzter Sonnenaufgang war ");
Serial.println(SunA_ux);
Serial.print("vor ");
Serial.print(now() - SunA_ux);
Serial.println(" Sekunden");
}
else {
Serial.print("nächster Sonnenaufgang ");
Serial.println(SunA_ux);
Serial.print("in ");
Serial.print(SunA_ux - now());
Serial.println(" Sekunden");
}

// make SonnenU struct
SunU.Hour = atoi(SonnenU.substring(0, 2).c_str());
SunU.Minute = atoi(SonnenU.substring(3, 5).c_str());
SunU.Second = 0;
SunU.Day = atoi(SonnenU.substring(6, 8).c_str());
SunU.Month = atoi(SonnenU.substring(9, 11).c_str());
SunU.Year = y2kYearToTm(atoi(SonnenU.substring(13, 16).c_str()));  // offset from 1970
unsigned long SunU_ux = makeTime(SunU);
SunU_ux = sommerzeit ? SunU_ux - 2 * timezone * 3600 : SunU_ux - timezone * 3600; // Timezone und summer/wintertime
if (now() >= SunU_ux) {
Serial.print("letzter Sonnenuntergang war ");
Serial.println(SunU_ux);
Serial.print("vor ");
Serial.print(now() - SunU_ux);
Serial.println(" Sekunden");
}
else {
Serial.print("nächster Sonnenuntergang ");
Serial.println(SunU_ux);
Serial.print("in ");
Serial.print(SunU_ux - now());
Serial.println(" Sekunden");
}
}

void loop() {
// put your main code here, to run repeatedly:
}

void digitalClockDisplay() {      // helper function to print time/date readable from structure
Serial.print(hour());
printDigits(minute());
printDigits(second());
Serial.print(" ");
Serial.print(day());
Serial.print(".");
Serial.print(month());
Serial.print(".");
Serial.print(year());
Serial.println();
}

void printDigits(int digits) {
// utility function for digital clock display: prints preceding colon and leading 0
Serial.print(":");
<code>if (digits < 10) </code>                 x------ remove <code></code>
Serial.print('0');
Serial.print(digits);
}

Zurück zum Projekt

Die Stringverarbeitung aus dem letzten Programm werden jetzt in den ursprünglichen Code integriert. Der geplante Programmablauf ist folgender:

Bei der ersten Inbetriebnahme weiß der Controller zunächst nicht, ob die Hühnstall Türe offen oder geschlosssen sein soll und wann die nächste Aktion ansteht. Die aktuelle Zeit wird also bestimmt und das nächste Schaltevent bestimmt. Wichtig ist hier, dass der erste Zustand der Türe zunächst undefiniert ist und erst mit dem ersten Event korrekt gesetzt wird. Der Controller weiß jetzt auch, wie lange es bis zur nächsten Aktion noch dauert und legt sich schlafen. Da die Funktion sleep als Input den maximalem Wert einer unsigned long Variable annimmt, kann das modul maximal hexFFFF FFFF = 4.294.967.295 μs schlafen, das sind ca. 1h 12min. Um einfacher zu debuggen, habe ich mich entschieden, den ESP maximal 1h schlafen zu legen. Da ich festgestellt habe, dass eine Stunde schlafen nicht exakt eien Stunde ist, muss man sich an das Event „heranarbeiten“. Auch wenn es nicht so entscheidend ist, will ich den Zeitpunkt des Sonnenauf- bzw -untergangs doch ziemlich genau treffen.

Folgende Logik ist im Programm abgebildet, nachdem zunächst die Zeit bis zum nächsten Schaltevent bestimmt wurde:
Ist die Zeit bis zum nächsten Event < 1 min wird die Zeit mit delay() überbrückt
Ist die Zeit bis zum nächsten Event < 65 min geht der ESP bis 1 min vor dem nächsten Event schlafen, beim nächsten Aufwachen triggert dann die letzte Bedingung
Treffen beide Bedingungen nicht zu, legt der ESP sich für eine Stunde schlafen.

Den ESP schlafen zu legen ist relativ einfach. Außer einem include benötigt man nur 2 Befehle. #include verweist direkt auf die Espressif SDK. Der Zeit Parameter der übergeben wird ist vom Typ unsigned long. Wird „0“ als Wert übergeben, schläft der ESP für immer. Das ist wichtig zu beachten, wenn man mit berechneten Sleepzeiten arbeitet, hier muss man Vorkehrungen treffen, dass der Wert nicht „0“ wird. Mit ist das einmal in der Testphase passiert und ich habe lange nach dem Fehler gesucht.


extern "C" {
#include "user_interface.h"
}

void setup() {
Serial.begin(115200);
Serial.println("\r\nStart...");
}
void loop() {
Serial.println("going to sleep now...");
system_deep_sleep_set_option(0);
system_deep_sleep(10000000);            // deep sleep for 10 seconds
delay(1000);
}

Der endgültige Code ist schon recht umfangreich geworden. Es sind viele Debug Informationen Prints im Code , um diesen besser zu verstehen. Das Ansteuern der Hühnerklappe habe ich mir noch geschenkt, weil ich den Beitrag endlich fertig stellen wollte. Danke allen, die sich bis hier durchgearbeitet haben. Den aktuellen Code könnt ihr von meinem GIT laden. Feedback ist wie immer sehr willkommmen.

 

 

 

Advertisements

Veröffentlicht am 28. Februar 2016, in Arduino, ESP8266, Tutorial. Setze ein Lesezeichen auf den Permalink. Hinterlasse einen Kommentar.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

%d Bloggern gefällt das: