Blog-Archive
Tutorial: Website parsen mit dem ESP8266 – Teil 1
Einleitung
In den letzten Tagen habe ich mich verstärkt mit der Abfrage verschiedener Websites beschäftigt und der Nutzung der Informationen im eigenen Programm. Das Finden und Extrahieren von Informationen aus einem Text, einem String oder einer Seite nennt man parsen. Da ich dabei viel gelernt habe, möchte ich dieses Wissen gerne weitergeben. Wichtig ist mir dabei wie immer, dass ihr das versteht und selbstständig nachvollziehen könnt. Deshalb werden meine Erklärungen vielleicht etwas ausführlicher und richten sich an die Programmieranfänger unter euch. Da ich das Tutorial für den ESP schreibe, werde ich auch auch auf die grundsätzliche Abfrage von Websites mit dem ESP eingehen.
Der ESP als Client
In den Libraries des ESP ist ein Beispiel enthalten, wie mit den ESP als Client Informationen von einem Webserver abgerufen werden können. In meinem Tutorial über die Speicherung von MySQL Daten habe ich ja bereits ein Beispiel zur Abfrage eines Webservers gezeigt. Das folgende Beispiel zeigt die Anwendung des ESP als Client mit Nutzung der neuesten Libraries. Das Beispiel ruft google.de auf und schreibt die Ausgabe auf die serielle Schnittstelle.
/* * Basic ESP Client example, based on the ESP libraries examples */ #include <ESP8266WiFi.h> const char* ssid = "yourSSID"; const char* password = "yourPASSPHRASE"; const char* host = "google.de"; void setup() { Serial.begin(115200); delay(10); // We start by connecting to a WiFi network Serial.println(); Serial.println(); Serial.print("Connecting to "); Serial.println(ssid); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(""); Serial.println("WiFi connected"); Serial.println("IP address: "); Serial.println(WiFi.localIP()); Serial.println(); } void loop() { Serial.print("connecting to "); Serial.println(host); // Use WiFiClient class to create TCP connections WiFiClient client; const int httpPort = 80; if (!client.connect(host, httpPort)) { Serial.println("connection failed"); return; } // We now create a URI for the request String url = "/"; Serial.print("Requesting URL: "); Serial.println(url); // This will send the request to the server client.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n"); // start waiting for the response unsigned long lasttime = millis(); while (!client.available() && millis() - lasttime < 1000) {delay(1);} // wait max 1s for data // Read all the lines of the reply from server and print them to Serial while(client.available()){ char readchar = client.read(); Serial.print(readchar); } Serial.println(); Serial.println("closing connection"); delay(5000); }
Auf einige Teile dieses Codes möchte ich nochmal tiefer eingehen.
// We start by connecting to a WiFi network Serial.println(); Serial.println(); Serial.print("Connecting to "); Serial.println(ssid); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
Im setup wird die Verbindung zum lokalen WLAN aufgebaut, dazu müssen im Parameterblock oben im Programm SSID und Passwort eingetragen werden.
Das Programm versucht nun (hier endlos) eine Verbindung zum WLAN aufzubauen. In meinem Fall wird die Verbindung nach ca. 5-8 Sekunden aufgebaut, wenn es bis dahin keine Verbindung gibt, muss man ein Reset machen und manchmal auch den Code neu flashen.
WiFiClient client; const int httpPort = 80; if (!client.connect(host, httpPort)) { Serial.println("connection failed"); return; }
Im loop wird dann die Verbindung zum Host, hier google.de aufgebaut
// We now create a URI for the request String url = "/"; Serial.print("Requesting URL: "); Serial.println(url); // This will send the request to the server client.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n");
Zunächst wird die URL für die Abfrage zusammengesetzt. Als erstes wird der Teil der nach dem Host in der URL kommt in einen String geschrieben. In diesem Fall kommt nach google.de nichts mehr, deshalb steht hier nur ein „/“. Im nchsten Schritt werden die für die Abfrage notwendigen Parameter zum String hinzugefügt. ( die \r\n stehen für CR und LF und müssen an diesen Stellen stehen, am Ende der Abfrage sogar zweimal). Die client.print() Anweisung schickt die Abfrage zum Server.
// start waiting for the response unsigned long lasttime = millis(); while (!client.available() && millis() - lasttime < 1000) {delay(1);} // wait max 1s for data
Jetzt wartet der Client auf eine Antwort des Servers. Manche Server lassen sich damit Zeit, deshalb wartet der Client entweder bis er Daten bekommt (client-available()=true) oder die Zeit abgelaufen ist (hier 1000ms).
while(client.available()){ char readchar = client.read(); Serial.print(readchar); }
Solange Daten empfangen werden, gibt der ESP diese Buchstabe für Buchstabe auf der seriellen Schnittstelle aus. Im Client der Library ist hier alternativ eine Methode vorgeschlagen, die Daten zeilenweise (bis zum CR) zu lesen und auszugeben. Diese Methode werden wir für die nächsten Beispiele verwenden, also jetzt schon mal in das Beispiel einbauen. Manchmal ist es sinnvoller, statt nach einem CR ein LF zu suchen, dann sind die Strings möglicherweise kürzer, doch dazu später mehr.
// Read all the lines of the reply from server and print them to Serial while(client.available()){ String line = client.readStringUntil('\r'); Serial.print(line); }
Die Aussgabe auf der seriellen Schnittstelle sieht dann so aus
Connecting to DD-WRT ............. WiFi connected IP address: 192.168.2.109 connecting to google.de Requesting URL: / HTTP/1.1 301 Moved Permanently Location: http://www.google.de/ Content-Type: text/html; charset=UTF-8 Date: Sat, 30 Jan 2016 22:25:54 GMT Expires: Mon, 29 Feb 2016 22:25:54 GMT Cache-Control: public, max-age=2592000 Server: gws Content-Length: 218 X-XSS-Protection: 1; mode=block X-Frame-Options: SAMEORIGIN Connection: close <HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8"> <TITLE>301 Moved</TITLE></HEAD><BODY> <H1>301 Moved</H1> The document has moved <A HREF="http://www.google.de/">here</A>. </BODY></HTML> closing connection
Am Anfang der Ausgabe kommen Informationen über den Server, die eigentliche Ausgabe der Webseite beginnt ab <HTML>. Die Webseite bietet nicht viel an nutzbarer Information, allerdings könnte man aus den Header Daten die Uhrzeit parsen, anstatt den NTP Client einzubinden. Das möchte ich als abschließendes erstes Beispiel für das parsen zeigen. Wichtig beim parsen ist, dass man ein sich eindeutige Suchbegriffe sucht, über die man die Zeile in der die Information steht und als zweites der Platz in der Zeile eindeutig bestimmen kann. Für die Zeitabfrage ist das die Zeile
Date: Sat, 30 Jan 2016 22:25:54 GMT
Als erstes suchen wir also im eingelesenen String nach dem Suchwort „Date“ und wissen dann, dass wir in der richtigen Zeile sind. Das Schöne an diesem Beispiel ist, dass die Zeit immer genau an der selben Stelle im String steht.
Die beiden wichtigsten Befehle beim parsen sind:
String.indexOf(„Suchstring“) -> gibt als int die Position im String aus, wo der Suchstring gefunden wird
String.substring(von, bis) -> liefert String, der bei „von“ beginnt und vor „bis“ endet.
Da wir wissen, dass das Suchwort „Date“ immer an Position 1 im String zu finden ist (auf Position 0 steht ein LF), können wir einfach die Position im String abzählen, wo der gesuchte Teil steht. Doch zuvor testen wir, ob der Suchstring im gelesenen String vorkommt.
_________1_________2_________3_________4
1234567890123456789012345678901234567890
Date: Sat, 30 Jan 2016 22:25:54 GMT
Der entsprechende Code-Block sieht dann so aus:
while (client.available()) { String line = client.readStringUntil('\r'); if (line.indexOf("Date:")>=0) { // Searchstring exists? Serial.println(); Serial.print("Heute ist der "); Serial.print(line.substring(12, 23)); Serial.print(", es ist "); Serial.println(line.substring(24, 32)); } Serial.print(line); }
Die Uhrzeit wird in GMT ausgegeben, zur korrekten Darstellung muss (in der Winterzeit) 1 Stunde addiert werden. Dafür müssen Teile des Strings zunächst mit dem Befehl atoi() in einen Integer umgewandelt werden. Dieser Befehl kann nicht direkt auf den String ausgeführt werden. Dafür wird zunächste der Teilstring extrahiert und an die Funktion atoi() der Pointer auf den neuen String übergeben. Dafür setzen wir die Funktion String.c_str() ein.
while (client.available()) { String line = client.readStringUntil('\r'); if (line.indexOf("Date:")>=1) { // Searchstring exists? Serial.println(); Serial.print("Heute ist der "); Serial.print(line.substring(12, 23)); String stundeSubString = line.substring(24,26); int stunde = atoi(stundeSubString.c_str()); if (stunde==23) stunde=0; else stunde++; Serial.print(", es ist "); if (stunde<10) Serial.print("0"); Serial.print(stunde); Serial.print(line.substring(26, 32)); if (stunde==0) Serial.print(" + 1 Tag"); else Serial.println(); // Tagsprung ?? }
Natürlich müsste bei einem Tagsprung, also bei 0 Uhr bis 0:59 auch der Tag geändert werden. Das ist eine nette Übung für euch. Man sieht, mit der Umwandlung der Ausgabe in rechenbares Format, kann der Code schnell umfangreicher werden.
Die Ausgabe sieht jetzt so aus:
Connecting to DD-WRT ............. WiFi connected IP address: 192.168.2.109 connecting to google.de Requesting URL: / HTTP/1.1 301 Moved Permanently Location: http://www.google.de/ Content-Type: text/html; charset=UTF-8 Heute ist der 31 Jan 2016, es ist 08:33:24 Date: Sun, 31 Jan 2016 07:33:24 GMT Expires: Tue, 01 Mar 2016 07:33:24 GMT Cache-Control: public, max-age=2592000 Server: gws Content-Length: 218 X-XSS-Protection: 1; mode=block X-Frame-Options: SAMEORIGIN Connection: close <HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8"> <TITLE>301 Moved</TITLE></HEAD><BODY> <H1>301 Moved</H1> The document has moved <A HREF="http://www.google.de/">here</A>. </BODY></HTML> closing connection
Die war der erste Teil meines Tutorials zum parsen von Webseiten. Ich hoffe, ihr konntet meinen Erklärungen folgen und die Beispiele funktionierten auch bei euch. Wie immer, freue ich mich auf eure Rückmeldungen. Im zweiten Teil werde ich euch weitere Tips geben, wie eine Webseite „auseinander genommen“ werden kann.
Anmerkung: google.de gibt dem ESP nur eine sehr reduzierte Information zurück. Offensichtlich erkennt google, dass hier kein Browser anfragt.
Arduino goes Wireless
Die Experimente zu meinem Tutorial „Speicherung von Arduino Messdaten auf Webserver und deren Darstellung“ setzten alle auf einen Arduino mit Ethernet Shield zur Verbindung mit dem Internet auf. Nun gibt es Situationen, wo kein Ethernet Anschluss in Reichweite ist, trotzdem aber die Verbindung ins Internet benötigt gewollt ist. Oder es ist Mobilität gefordert, während Daten übertragen werden. Das Arduino Wifi Shield wäre eine Lösung, ist aber mit fast 70€ eine sehr kostspielige und zusätzlich muss das Programm daraufhin angepasst werden. Eine wesentlich kostengünstigere und ohne Anpassung des Programms einsetzbare Lösung ist ein Wireless Client. Ich habe mich für den TP-Link TL-WR702N entschieden, der schon inklusive Versand für weniger als 20€ zu bekommen ist.
Der TL-WR702N ist im Auslieferungszustand ein Access Point, für unsere Anwendung muss er aber als Client oder als Bridge konfiguriert werden. Der TL-WR702N stellt als Client über WLAN die Verbindung zum Access Point her, der Arduino mit Ethernet Shield wird mit dem TL-WR702N über die Ethernet Schnittstelle verbunden und verhält sich genauso, wie bei direkter Verbindung nur über Ethernet.
Die Konfiguration als Client ist im Handbuch ausreichend beschrieben, deshalb möchte ich hier nicht näher darauf eingehen. Einen Sonderfall möchte ich an dieser Stelle näher beschreiben, nämlich, wenn ich keinen Rechner zur Verfügung habe, den TL-WR702N zu konfigurieren. Eine Konfiguration über Wireless, z.B. mit einem Smartphone oder Tablet ist im Handbuch nicht beschrieben.
Nachfolgend gehe ich davon aus, dass der TL-WR702N im Auslieferungszustand vorliegt. Dieser Zustand kann jederzeit hergestellt werden, indem der Reset Taster mit einem spitzen Gegenstand z.B. einer aufgebogenen Büroklammer, für ca. 7 Sekunden gedrückt wird, bis die Betriebs LED mehrmals kurz blinkt. Die nachfolgenden Schritte habe ich mit meinem Nexus 7 durchgespielt, mit anderen Geräten kann das leicht variieren.
Zunächst verbinde ich mich mit dem Tablet auf den Access Point des TL-WR702N. Die SSID, nach der ich suche, steht auf dem Typenschild des TL-WR702N, ebenso die zugehörige Passphrase.
Wenn ich die Verbindung hergestellt habe, wird mein Tablet eine IP aus dem 192.168.0.x Netz erhalten haben. Im Browser des Tablet gebe ich 192.168.0.254 ein, die IP des TL-WR702N zur Konfiguration. In der Abfragemaske wird für User: admin und Passwort: admin eingegeben. Wir kommen auf die Konfiguration des Routers und Erstellen eine Konfiguration für eine Bridge.
Der Vorteil der Bridge Konfiguration ist, dass ich später noch über WLAN auf die Konfigurationsschnittstelle komme, um Änderungen zu machen. Das ist im Client Modus nur über Ethernet möglich. Wichtig ist jetzt, dass über Quick Setup gegangen wird, sonst verliert man nach Reboot die Konfigurationsseite. Also Quick Setup, Next, Bridge, Next.
Unter Survey sehe ich die zur Verfügung stehenden WLANs und kann den gewünschten Access Point direkt mit connect auswählen. Nach Eingabe des Password für dieses WLAN gehe ich weiter mit Next. Wenn die Meldung erscheint „Your AP channel is not the same as the Bridged AP’s channel, do you want to change your channel to the Bridged AP’s channel?“ kann ich das mit OK bestätigen. Im nächsten Fenster wird kann die bestehende Verschlüsselung bestätigt werden oder eine neue Passphrase vergeben werden. Mit Next kommt man auf das letzte Fenster zum Reboot des Routers.
Beim Reboot verliere ich die Verbindung zur Konfiguration des Routers. Mein Tablet verliert möglicherweise auch die Anbindung zu Access Point oder verbindet sich zu einem anderen bekannten Access Point. Eigentlich ist jetzt alles fein und ich kann mein Ethernet Shield mit dem Router verbinden und loslegen. Wenn ich die Konfiguration mit dem Tablet bearbeiten will, muss ich die IP des Routers herausfinden. Wenn ich nicht die Möglichkeit habe, die IP auf meinem Router nachzusehen, kann ich folgenden Trick anwenden. Die Android App Ping-Tools hilft mir dabei. Ich stelle über das Menü links oben die Funktion UPnP scanner ein und schon sehe ich meinen TP-Link mit seiner IP-Adresse.
Über diese IP in der Browserzeile komme ich jetzt wieder auf die Konfigurationsschnittstelle, falls ich noch etwas ändern möchte. Sinnvoll wäre z.B. die Änderung des Standard Passworts für den Admin Zugang.
Der Stromverbrauch des TL-WR702N liegt übrigens nur bei ca, 150mA. Bei einer mobilen Anwendung kann der Router also ohne weiteres aus der 5V Schnittstelle des Arduino mit versorgt werden oder Arduino und der TL-WR702N werden über eine Powerbank mit USB Schnittstelle versorgt.
Als nächstes steht jetzt ein Test mit dem TP-Link TL-MR3020 an, der für unter 30€ mit gleichem Verfahren einen Arduino ins 3G/4G Mobilfunknetz und damit ins Internet bringt. Zusätzlich ist nur ein USB Stick mit Mobilfunkkarte notwendig.
Arduino (ATmega) mit Bewegungsmelder aufwecken
Vor kurzem hatte ich die Idee, eine Schaltung mit einem ATmega aufzubauen, die immer nur dann etwas macht, wenn jemand in der Nähe ist. Da die Schaltung mit Batterie betrieben werden sollte und eine möglichst lange Laufzeit aufweisen sollte, sollte der ATmega solange schlafen liegen, bis jemand in die Nähe kommt.
Ideen mit IR-Sensor oder Ultraschall Sensor wurden schnell wieder verworfen, das diese selbst zuviel Strom brauchen. Mechanische Schalter wollte ich auch nicht einsetzen und Induktionsspulen erschienen mir zu kompliziert und zu unempfindlich. Die Lösung brachte ein Pyroelektrischer Bewegungssensor. Bei eBay (Suchbgriff: PIR Motion Sensor) sind solche Sensoren für wenig Geld erhältlich. Vor allem der Standby Stromverbrauch von ca. 60uA (Datenblatt) fand ich fast schon nicht zu glauben. Aber schlauer ist man erst, wenn man es ausprobiert. Und heute war es dann soweit. Als erstes habe ich den Stromverbrauch gemessen, im Ruhemodus waren das ca. 70uA, die im aktiven Modus (Ausgang HIGH) dann auf ca. 170uA ansteigen.
Für den ersten Aufbau musste ein Versuchsaufbau auf dem Steckbrett ausreichen. Der Arduino ist für Experimente im Sleep-Modus ungeeignet, da zwar der ATmega schlafen geht, aber andere Bauteile, wie der USB Wandler, der Spannungswandler und die LEDs weiterhin Strom verbrauchen.
Der PIR Sensor wird wie der ATmega mit 5V betrieben und gibt einen HIGH Pegel ab, wenn er Bewegung erkennt. Am ATmega hängt neben der Reset Beschaltung und dem Quarz nebst Kondensatoren ein LCD Display. Die Programmierung erfolgte über einen mySmartUSB light ISP.
Da der PIR-Sensor einen HIGH Pegel abgibt, der ATmega aber mit einem LOW Pegel geweckt werden will, wurde der Pegel mit einem BS170 invertiert. Zum Aufwecken wurde der INT0 am Eingang D2 genutzt, der mit einem externen Pullup auf HIGH gezogen wird. Die Programmierung erfolgte unter der Arduino IDE. Das LCD wird über D8 mit Strom versorgt und wird im Schlafmodus ebenfalls deaktiviert. Im LCD wird während der Aktivphase die Laufzeit in Sekunden und die Anzahl der Aufweckvorgänge angezeigt. Der Stromverbrauch liegt im Sleep-Modus bei ca. 450uA und im Aktiv-Modus bei 18mA.
#include <avr/sleep.h> #include <LiquidCrystal.h> #define LCD_POW 8 #define INT2 2 int toggle=0; // initialize the library with the numbers of the interface pins LiquidCrystal lcd(12, 11, 5, 4, 3, 6); void wakeUpNow(){ // here the interrupt is handled after wakeup detachInterrupt(0); toggle = toggle++; } void setup() { pinMode(INT2, INPUT); pinMode(LCD_POW, OUTPUT); digitalWrite(LCD_POW, HIGH); // switch on power fpr LCD delay(100); // wait for power to stabilize // set up the LCD's number of columns and rows: lcd.begin(16, 2); // Print a message to the LCD. lcd.print("hello, world!"); } void sleepNow(){ pinMode(LCD_POW,INPUT); // Ausgänge für Schlafen vorbereiten pinMode(12,INPUT); // Ausgänge für Schlafen vorbereiten pinMode(11,INPUT); // Ausgänge für Schlafen vorbereiten pinMode(5,INPUT); // Ausgänge für Schlafen vorbereiten pinMode(4,INPUT); // Ausgänge für Schlafen vorbereiten pinMode(3,INPUT); // Ausgänge für Schlafen vorbereiten pinMode(6,INPUT); // Ausgänge für Schlafen vorbereiten set_sleep_mode(SLEEP_MODE_PWR_DOWN); sleep_enable(); attachInterrupt(0,wakeUpNow, LOW); sleep_mode(); // wakes up here sleep_disable(); detachInterrupt(0); pinMode(12,OUTPUT); // Ausgänge für Schlafen vorbereiten pinMode(11,OUTPUT); // Ausgänge für Schlafen vorbereiten pinMode(5,OUTPUT); // Ausgänge für Schlafen vorbereiten pinMode(4,OUTPUT); // Ausgänge für Schlafen vorbereiten pinMode(3,OUTPUT); // Ausgänge für Schlafen vorbereiten pinMode(6,OUTPUT); // Ausgänge für Schlafen vorbereiten pinMode(LCD_POW, OUTPUT); digitalWrite(LCD_POW, HIGH); // switch on power fpr LCD delay(100); // wait for power to stabilize lcd.begin(16, 2); } void loop() { // set the cursor to column 0, line 1 // (note: line 1 is the second row, since counting begins with 0): lcd.setCursor(0, 1); // print the number of seconds since reset: lcd.print(millis()/1000); lcd.print(" "); lcd.print(toggle); if (digitalRead(INT2)) sleepNow(); // only if HIGH again }