Archiv des Autors: glaskugelsehen

OTA, ESP8266 und Arduino IDE – ein starkes Team

gks_ota0

Einleitung

In letzter Zeit habe ich mich viel mit dem ESP8266 beschäftigt und in einigen Blogbeiträgen bereits darüber berichtet. Jetzt bin ich auf ein neues Thema gestoßen, dass ich sofort weiter teilen muss. OTA over-the-air, also Upload eines Programmes über die WiFi Schnittstelle ist vor allem da hilfreich und sinnvoll, wo ich die Schaltung nicht mehr im Zugriff habe oder wo ich keinen USB-Seriell-Wandler anschließen möchte, weil die Schaltung mit Netzspannung betrieben wird (z.B. SONOFF). Meine Begeisterung für OTA möchte ich in diesem BlogPost ausdrücken.

Obwohl ich heute meine Programme hauptsächlich auf dem WeMos D1 Mini entwickel, weil er mit seinem USB-Seriell Wandler einfach zu handhaben ist und sehr kompakt baut (Bezug z.B. über: http://stores.ebay.de/modulink) werde ich in diesem Post beschreiben, wie OTA auf einem ESP-12E funktioniert, der ja von Hause aus keinen USB-Seriell-Wandler mit sich bringt und deshalb ist OTA hier besonders interessant. Leider funktioniert OTA nicht mit dem ESP-01. 512k Speicher sind für OTA einfach zu wenig. Für OTA muss der Chip mindestens doppelt so viel Flash bereits stellen, wie das kompilierte Programm groß ist. Der OTA Loader für sich braucht schon ca. 240k.

Der OTA Loader

Als ersten Schritt muss man auf den ESP ein Programm laden, dass die Schnittstelle für OTA bereit stellt. Für diesem Schritt benötigt man natürlich einen USB-Seriell-Wandler, der wie hier beschrieben angeschlossen wird. Das von mir präferierte OTA-Basis Programm ist in einer Library enthalten, die über den Library Manager geladen wird. Dazu gehen wir in der Arduino IDE auf Sketch->Bibliothek einbinden->Bibliotheken verwalten und geben als Suchbegriff OTA ein. Die ArduinoOTA Library sollte dann als oberstes Suchergebnis erscheinen.

gks_ota1

Diese Library installieren. Unter Datei->Beispiele->ArduinoOTA->BasicOTA laden wir jetzt das Programm. Dieses Programm muss nur an zwei Stellen angepasst werden. Der ESP verbindet sich mit einem WLAN. Dafür muss im Programm die SSID und das WLAN Passwort eingetragen werden.

gks_ota2

Das Programm unter neuem Namen abspeichern. Es wird später dann als Basic Programm immer wieder als Grundgerüst verwendet.Vor dem Upload des Programms wird noch unter Werkzeuge->Board das ESP Board auswählen – in meinem Fall für den ESP-12E das NodeMCU-1.0(ESP-12E Module).

gks_ota6

Unter Werkzeuge->Port muss noch der richtige Port eingestellt werden unter dem der USB-Seriell-Wandler sichtbar ist. Im nächsten Schritt erfolgt der Upload des Programms dann wie immer mit der Abfolge
– ESP in den Programmiermodus versetzen, dazu GPIO0 auf LOW ziehen und den ESP aus- und wieder einschalten oder resetten. (Bei anderen ESP Varianten entfällt dieser Schritt ggf)
– Upload des Programms

Nach dem Upload des Programms zeigt der serielle Monitor das Einbuchen des ESP ins WLAN. Der ESP macht jetzt nichts weiter als auf ein neues Programm zu warten.

gks_ota4Nach einem Neustart der Arduino IDE sieht man jetzt einen neuen Port, der jetzt für den OTA Upload ausgewählt wird.

gks_ota5 Der Rechner, von dem die Programmierung und der Upload erfolgt, muss sich natürlich im gleichen Netz befinden.

zusätzlich benötigt – Python 2.7

Bevor es mit dem OTA Upload losgehen kann, muss noch folgendes geprüft werden. Für den Upload over-the-air nutzt die IDE ein Python Script. Dafür muss auf dem Rechner Python in der Version 2.7 installiert sein (die Version 3.x funktioniert hier nicht). Windows User können die Version 2.7 hier downloaden. Auf Linux und OSx Systemen sollte Python bereits vorinstalliert sein. Auch hier auf die Version achten. Anleitungen zur Installation von Python gibt es zahlreiche im Netz, deshalb gehe ich hier nicht ins Detail. Für Windows Nutzer ist noch wichtig, dass der Installationspfad von Python der Path Variablen hinzugefügt wird. Das kann bei der Installation als zweiter Schritt beim Setup ausgeführt werden. Unter Customize Python Add Python to path auswählen.

Erster Upload

Bis zu diesem Punkt ist der ESP mit dem BasicOTA Programm geflasht, dass momentan nichts anderes macht, als auf ein neues Programm zu warten. Das eigene Programm wird dem BasicOTA Programm hinzugefügt. Wichtig ist jetzt der Befehl  ArduinoOTA.handle();, der ganz oben in der loop() steht, dieser Befehl muss zyklisch immer wieder aufgerufen werden, weil hier geprüft wird, ob ein OTA Update ansteht. Das USB Kabel könnte jetzt prinzipiell entfernt werden, es ist aber ganz interessant beim ersten OTA Upload die Ausgabe mal anzusehen. Allerdings muss dafür eine zweite Instanz von  der Arduino IDE oder ein anderes Terminalprogramm benutzt werde. Ich nutze dafür unter Linux CuteCom, unter Windows könnte man Putty nutzen.

Als Beispiel habe ich das BasicOTA Programm um eine Ausgabe in der loop() erweitert, alle 5s wird über die serielle Schnittstelle ein Status ausgegeben. In der loop() sollte kein delay() benutzt werden, weil damit der Test, ob ein OTA Update ansteht (s.o.) für diese Zeit nicht ausgeführt würde und die IDE einen Fehler melden würde, da sie keine Antwort vom ESP bekommt.

 void loop() {
ArduinoOTA.handle();
if (millis() - lastmillis >  5000) {
Serial.print("still running since ");
Serial.print(millis()1000);
Serial.println(" seconds");
lastmillis = millis();
}
} ... 

Das Programm wird jetzt wie gewohnt über den Upload Button auf den ESP geladen. Es ist nicht notwendig, den ESP in den Programmiermodus zu versetzen oder zu resetten. Ein sehr erfreulicher Nebeneffekt ist beim Upload zu sehen, der Upload geht sehr viel schneller als über die serielle Schnittstelle. Wenn ein Terminal offen ist, wird der Upload Fortschritt auch hier angezeigt. Sobald der Upload abgeschlossen ist, resettet sich der ESP und startet das neue Programm.

gks_ota7

Anmerkungen zur Sicherheit

Solange man sich im eigenen Netz befindet und man sich sicher fühlt, kann man das BasicOTA so nutzen, wie es bereitgestellt wird. Es wäre aber prinzipiell möglich für jemanden, der sich im selben Netz befindet, einen SW Upload zu starten. Es gibt 2 Möglichkeiten, das zu erschweren. Im BasicOTA Code kann der IP-Port, unter dem der ESP gefunden wird, verändert werden, standardmäßig ist hier Port 8266 gesetzt. Zusätzlich kann man ein Passwort setzen, dass dann in der IDE vor dem Upload abgefragt wird.
Darüber hinaus muss man sich genau überlegen, was das laufende Programm möglicherweise gerade macht, wenn der Upload beginnt. Hat der ESP gerade das Wasserventil geöffnet, um den Garten zu bewässern, muss man sichergehen, dass das Ventil geschlossen wird, bevor der Upload beginnt.
Im BasicOTA Code sind deshalb Programm-Module vorgesehen, die beim Upload zu einem bestimmten Status aufgerufen werden.

   ArduinoOTA.onStart([]() {
Serial.println("Start");
});
ArduinoOTA.onEnd([]() {
Serial.println("\nEnd");
});

Hier kann man eigenen Code einbauen, der einen definierten Zustand herstellt, also die o.g. Ventile schließt oder einen Status ins EEPROM schreibt. Da nach dem Upload der ESP resettet wird, wird eh das gesamte setup() durchlaufen.

Ausblick

Das Verfahren des Programm Uploads over-the-air in der beschriebenen Form ist für den Entwicklungsprozess aus der IDE vorgesehen. Für Schaltungen, die sich in anderen Netzen befinden gibt es die Möglichkeit des Uploads über Webserver (von einem Rechner aus dem gleichen Netz) z.B. von jemandem, der mit der Arduino IDE nicht vertraut ist oder über einen Server in einem anderen Netz, wo sich der ESP selbstständig eine neue Software abholt. Diese Varianten habe ich aber noch nicht erprobt, deshalb kann ich nicht näher darauf eingehen.

 

 

Advertisements

Was hat ein kühles Bier mit IoT zu tun

adafruit3Seit einiger Zeit beschäftige ich mich intensiv mit dem Internet-of-Things (IoT) und frage mich dabei immer wieder: Wozu braucht man denn so was. Aber zugegebenermaßen – in Verbindung mit Mikrocontrollern wie dem Arduino ist es natürlich sehr reizvoll vom Smartphone Steckdosen zu schalten oder die Temperatur und Feuchte seines Kellers zu messen und vom Smartphone aus zu kontrolieren, und das egal wo ich mich gerade befinde.

Ein aktuelles Problem aus dem wahren Leben brachte mich diesem Thema gerade jetzt näher und ich möchte meine Erfahrungen mit euch teilen.

Die Aufgabe

Für ein kleines Fest sollten natürlich ausreichend kalte Getränke, insbesonders leckeres Kölsch zur Verfügung stehen. Der Kühlschrank in der Küche war schon mit leckerem Essen gefüllt und die Temperatur im Keller lag bei ca. 21°C während die Außentemperatur auf knapp 30°C tagsüber anstieg.

Die Idee

Was also tun? Im Keller steht seit einiger Zeit ein ungenutzter Gefrierschrank. Den kann man doch hervorragend als Kühlschrank nutzen, wenn man verhindert, dass er die Getränke einfriert. Also einfach, so die Idee, die Kühlung bei einer Temperatur von ca 5°C ausschalten und bei 6°C wieder einschalten.

Die Realisierung

Alles, was ich dazu brauche habe ich in meiner Bastelkiste:
a) zum Messen der Temperatur
– einen Arduino
– einen DS18B20 Temperatursensor
– einen 4k7 Widerstand
b) zum Schalten der Steckdose
– einen 433MHz Sender- eine Funksteckdose
c) zusätzlich
– ein ProtoshieldDS18B20

Der DS18B20 wurde an ein Flachbandkabel gelötet und mit Acryldichtmasse wasserdicht gekapselt. Das Flachbandkabel wurde durch die Türdichtung des Gefrierschranks geführt und der Sensor wurde mit Klebeband im Innern befestigt. Das Programm wurde schnell aus den im Netz vorhandenen Bausteinen zusammengesetzt. Zum Messen der Temperatur wurde die OneWire Lib genutzt, wobei das Programm auf die bekannte Adresse des genutzten Sensors reduziert wurde. Jedem, der mit Netzspannung in seinen Projekten arbeitet, kann ich nur raten, diese immer mit einer Funksteckdose zu schalten. Netzspannung ist lebensgefährlich und mit Nutzung einer Funksteckdose kann fast nichts mehr passieren. Für die Ansteuerung der Funksteckdose benutze ich die RCSwitch Lib. Ich habe den Vorteil Steckdosen vom TypA mit einem Hauscode zu besitzen, damit ist das Schalten sehr simpel.

#include <OneWire.h>
OneWire  ds(6);  // on pin x (a 4.7K pullup resistor is necessary)
#include <RCSwitch.h>
RCSwitch mySwitch = RCSwitch();

boolean refri_stat;
float fptemp = 5.5;  // initial set
float delta = 0.5;  // adjustment range

/*************************** Sketch Code ************************************/

void setup(void) {
  Serial.begin(115200);
  Serial.println(F("Program started ..."));
  // Transmitter is connected to Arduino Pin #xx
  mySwitch.enableTransmit(7);
  // Switch off refrigerator
  mySwitch.switchOff("01111", "01000");
  refri_stat = false;
}

void loop(void) {
  float temp_c = (measure_t());    // call the subroutine to measure
  if (!refri_stat && temp_c > fptemp + delta) {    // decide to switch on/off with hysteresis
    mySwitch.switchOn("01111", "01000");
    refri_stat = true;
  }
  if (refri_stat && temp_c < fptemp - delta) {
    mySwitch.switchOff("01111", "01000");
    refri_stat = false;
  }
  Serial.print(" on/off state = ");
  Serial.println(refri_stat);
}

float measure_t() {
  byte i;
  byte present = 0;
  byte type_s;
  byte data[12];
  byte addr[8] = { 0x28, 0x62, 0x21, 0x80, 0x04, 0x00, 0x00, 0xC1 };    // adress of the sensor
  ds.reset();
  ds.select(addr);
  ds.write(0x44, 1);        // start conversion, with parasite power on at the end
  delay(1000);     // maybe 750ms is enough, maybe not
  // we might do a ds.depower() here, but the reset will take care of it.
  present = ds.reset();
  ds.select(addr);
  ds.write(0xBE);         // Read Scratchpad
  for ( i = 0; i < 9; i++) {           // we need 9 bytes
    data[i] = ds.read();
  }
  int16_t raw = (data[1] << 8) | data[0];
  float celsius = (float)raw / 16.0;
  Serial.print("Sensor present = ");
  Serial.print(present, HEX);
  Serial.print("  Temperature[C] = ");
  Serial.print(celsius);
  Serial.print(" ");
  return celsius;
}

Die Ausgabe sieht dann so aus, gemessen wird ca. im Sekundenrhythmus, hier noch bei Raumtemperatur:

Program started ... 
Sensor present = 1  Temperature[C] = 24.94  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.00  on/off state = 1 
Sensor present = 1  Temperature[C] = 24.94  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.00  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.00  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.00  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.00  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.00  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.00  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.00  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.00  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.00  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.06  on/off state = 1 
Sensor present = 1  Temperature[C] = 25.06  on/off state = 1 

…. und nun zum Thema IoT

Ein gewisses ungutes Gefühl blieb bestehen. Was ist mit der Temperatur? Schaltet der Arduino zuverlässig? Stürzt das Programm nicht ab? Ich hatte verständlicherweise keine Lust ständig in den Keller zu laufen, also musste eine andere Lösung her. Die Daten müssen in die Cloud! Während ich in meinem letzten Blogbeitrag den Service von data.sparkfun.com  genutzt habe, werde ich hier den Service von Adafruit nutzen. Adafruit bietet nicht nur die Speicherung der Daten, sondern auch ein Dashboard, um seine Daten zu sehen und Eingaben zu tätigen. Die Basis der Kommunikation setzt hier auf das MQTT Protokoll auf. Das einzige zusätzliche Material, was zum Einsatz kam, war ein Ethernet Shield. Über ein Powerline Netzwerk wurde vom Router das Ethernet bis in den Keller verlängert.

Mit Hilfe der Tutorials auf Adafruit war mein Account schnell erstellt. Auf meinem Dashboard wollte ich nicht nur die Temperatur und den Zustand der Funksteckdose anzeigen lassen, sondern ich wollte auch den Sollwert der Temperatur verändern können. Ich brauchte also 3 Feeds:
1. die Temperatur – schreibend (publish) vom Arduino auf den Broker (so bezeichnet man den Server in einer MQTT Kommunikation)
2. den Zustand der Funksteckdose (an/aus) und damit die Funktion des Gefrierschranks (kühlen) – schreibend vom Arduino auf den Broker
3. die Einstellung des Soll-Temperaturwertes: am Dashboard einzustellen, der Arduino greift lesend (subscribe) auf den Broker zu

Hier die Sicht auf das Dashboard knapp 3 Stunden nach dem Einschalten. Die Temperatur schwingt zunächst noch über den Zielwert um sich dann später zwischen 13°C und 14°C einzupendeln. Der Status ist momentan Kühlung = aus. Die Einschaltdauer liegt jeweils bei ca. 1 min, aus bei ca. 30 min.

adafruit2

Das oben gelistete Programm wurde durch die notwendigen Befehle der Adafruit IO Library ergänzt. Ich habe versucht das durch zahlreiche Kommentare verständlich zu machen.

#include <OneWire.h>
OneWire  ds(6);  // on pin x (a 4.7K pullup resistor is necessary)
#include <RCSwitch.h>
RCSwitch mySwitch = RCSwitch();
boolean refri_stat;

/***************************************************
  Adafruit MQTT Library Ethernet Example

  Adafruit invests time and resources providing this open source code,
  please support Adafruit and open-source hardware by purchasing
  products from Adafruit!

  Written by Alec Moore
  Derived from the code written by Limor Fried/Ladyada for Adafruit Industries.
  MIT license, all text above must be included in any redistribution
 ****************************************************/
#include <SPI.h>
#include "Adafruit_MQTT.h"
#include "Adafruit_MQTT_Client.h"

#include <Ethernet.h>
#include <EthernetClient.h>
#include <Dns.h>
#include <Dhcp.h>

/************************* Ethernet Client Setup *****************************/
byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};

//Uncomment the following, and set to a valid ip if you don't have dhcp available.
IPAddress iotIP (192, 168, 2, 99);
//Uncomment the following, and set to your preference if you don't have automatic dns.
//IPAddress dnsIP (8, 8, 8, 8);
//If you uncommented either of the above lines, make sure to change "Ethernet.begin(mac)" to "Ethernet.begin(mac, iotIP)" or "Ethernet.begin(mac, iotIP, dnsIP)"


/************************* Adafruit.io Setup *********************************/

#define AIO_SERVER      "io.adafruit.com"
#define AIO_SERVERPORT  1883
#define AIO_USERNAME    "<your username here>"
#define AIO_KEY         "<your AIO key here>"


/************ Global State (you don't need to change this!) ******************/

//Set up the ethernet client
EthernetClient client;

// Store the MQTT server, client ID, username, and password in flash memory.
// This is required for using the Adafruit MQTT library.
const char MQTT_SERVER[] PROGMEM    = AIO_SERVER;
// Set a unique MQTT client ID using the AIO key + the date and time the sketch
// was compiled (so this should be unique across multiple devices for a user,
// alternatively you can manually set this to a GUID or other random value).
const char MQTT_CLIENTID[] PROGMEM  = __TIME__ AIO_USERNAME;
const char MQTT_USERNAME[] PROGMEM  = AIO_USERNAME;
const char MQTT_PASSWORD[] PROGMEM  = AIO_KEY;

Adafruit_MQTT_Client mqtt(&client, MQTT_SERVER, AIO_SERVERPORT, MQTT_CLIENTID, MQTT_USERNAME, MQTT_PASSWORD);

/****************************** Feeds ***************************************/

// Setup a feed called 'biertemp' for publishing.
// Notice MQTT paths for AIO follow the form: <username>/feeds/<feedname>
const char BIERMON_FEED[] PROGMEM = AIO_USERNAME "/feeds/biertemp";
Adafruit_MQTT_Publish biermon = Adafruit_MQTT_Publish(&mqtt, BIERMON_FEED);

const char ONOFF_FEED[] PROGMEM = AIO_USERNAME "/feeds/onoff";
Adafruit_MQTT_Publish onoffbutton = Adafruit_MQTT_Publish(&mqtt, ONOFF_FEED);

const char SETTEMP_FEED[] PROGMEM = AIO_USERNAME "/feeds/settemp";              // definition of feedname
Adafruit_MQTT_Subscribe settemp = Adafruit_MQTT_Subscribe(&mqtt, SETTEMP_FEED);   // subscription
 
float stat = 0;
float fptemp = 25;  // initial set
float delta = 0.1;  // adjustment range
unsigned long lastmillis;

/*************************** Sketch Code ************************************/

void setup(void) {
  Serial.begin(115200);
  Serial.println(F("Program started ..."));
  // Transmitter is connected to Arduino Pin #xx
  mySwitch.enableTransmit(7);
  // Switch off refrigerator
  mySwitch.switchOff("01111", "01000");
  refri_stat = false;
  Ethernet.begin(mac, iotIP);
  delay(1000); //give the ethernet a second to initialize
  Serial.print("My IP address: ");
  Serial.println(Ethernet.localIP());
  mqtt.subscribe(&settemp);      // activate subscription to feed settemp
}

void loop(void) {
  MQTT_connect();
  Adafruit_MQTT_Subscribe *subscription;   // check subscription
  while ((subscription = mqtt.readSubscription(500))) {
    // Check if its the settemp feed
    if (subscription == &settemp) {
      Serial.print(F("Got new Temp: "));
      Serial.println((char *)settemp.lastread);
      fptemp = atof((char *)settemp.lastread);  // set to new temperature
      Serial.println(fptemp);
    }
    else {
      Serial.print(F("nothing to read "));
    }
  }
  float temp_c = (measure_t());
  if (!refri_stat && temp_c > fptemp + delta) {
    mySwitch.switchOn("01111", "01000");
    refri_stat = true;
  }
  if (refri_stat && temp_c < fptemp - delta) {
    mySwitch.switchOff("01111", "01000");
    refri_stat = false;
  }
  Serial.print(" on/off state = ");
  Serial.println(refri_stat);
  // Now we can publish stuff!
  if (stat != float(refri_stat)) {    // only when changed
    Serial.print(F("\nSending changed on/off state "));
    Serial.print(refri_stat);
    Serial.println("...");
    stat = float(refri_stat);     // published value has to be float (don't know why)
    if (! onoffbutton.publish(stat)) {
      Serial.println(F("Failed"));
    } else {
      Serial.println(F("OK!"));
    }
  }
  if (millis() > lastmillis + 15000) {  // read / write broker every xx milliseconds
    MQTT_connect();
    // Now we can publish stuff!
    Serial.print(F("\nSending temp val "));
    Serial.print(temp_c);
    Serial.println("...");
    if (! biermon.publish(temp_c)) {
      Serial.println(F("Failed"));
    } else {
      Serial.println(F("OK!"));
    }
    // ping the server to keep the mqtt connection alive
    if (! mqtt.ping()) {
      mqtt.disconnect();
    }
    lastmillis = millis();
  }

}

float measure_t() {
  byte i;
  byte present = 0;
  byte type_s;
  byte data[12];
  byte addr[8] = { 0x28, 0x62, 0x21, 0x80, 0x04, 0x00, 0x00, 0xC1 };
  //byte addr[8] = { 0x28, 0x42, 0xCC, 0x7F, 0x04, 0x00, 0x00, 0xAA };
  ds.reset();
  ds.select(addr);
  ds.write(0x44, 1);        // start conversion, with parasite power on at the end

  delay(1000);     // maybe 750ms is enough, maybe not
  // we might do a ds.depower() here, but the reset will take care of it.

  present = ds.reset();
  ds.select(addr);
  ds.write(0xBE);         // Read Scratchpad
  for ( i = 0; i < 9; i++) {           // we need 9 bytes
    data[i] = ds.read();
  }
  int16_t raw = (data[1] << 8) | data[0];
  float celsius = (float)raw / 16.0;
  Serial.print("Sensor present = ");
  Serial.print(present, HEX);
  Serial.print("  Temperature[C] = ");
  Serial.print(celsius);
  Serial.print(" ");
  return celsius;
}

// Function to connect and reconnect as necessary to the MQTT server.
// Should be called in the loop function and it will take care if connecting.
void MQTT_connect() {
  int8_t ret;

  // Stop if already connected.
  if (mqtt.connected()) {
    return;
  }

  Serial.print("Connecting to MQTT... ");

  while ((ret = mqtt.connect()) != 0) { // connect will return 0 for connected
    Serial.println(mqtt.connectErrorString(ret));
    Serial.println("Retrying MQTT connection in 5 seconds...");
    mqtt.disconnect();
    delay(5000);  // wait 5 seconds
  }
  Serial.println("MQTT Connected!");
}

Video

Video (Downloadlink):

Daten in der Cloud – Visualisierung auf dem Bildschirm

screenshot1Einleitung

In den letzten Wochen habe ich mich intensiv mit dem Thema IoT beschäftigt. Auf der Suche nach Diensten zur Speicherung von Daten in der Cloud bin ich auf data.sparkfun.com aufmerksam geworden. Dieser Dienst eignet sich gerade für Programmieranfänger hervorragend Daten strukturiert in der Cloud abzulegen.

In diesem Blogpost beschreibe ich eine einfache Methode auf data.sparkfun abgelegte numerische Daten grafisch zu präsentieren. Dabei bediene ich mich wieder des Javascript Moduls dygraphs, das ich schon in früheren Blogposts genutzt habe, um Daten grafisch darzustellen.

Der Dienst data.sparkfun

Die Konfiguration einer Datentabelle auf data.sparkfun ist denkbar einfach, so dass ich an dieser Stelle auf eine Anleitung verzichten möchte.

stream-create

Mit nur wenigen Klicks hat man eine Datentabelle (data stream) angelegt. Auf der Webseite sind nur wenige Eingaben zu machen. Der Dienst ist kostenlos und anonym, allerdings ist die Größe des Streams auf 50MB beschränkt, die ältesten Daten werden bei Überschreitung gelöscht. Die Daten sind öffentlich, jeder, der die url kennt, kann die Daten abrufen. Eine Sicherheit für die Verfügbarkeit der Daten wird nicht gegeben. Nichtsdestotrotz ist es ein hervorragender Dienst, um mit dem IoT erste Erfahrungen zu machen.

Die Daten und deren Darstellung

screenshot2Über die Webseite data.sparkfun.com kann man seine Daten tabellarisch ansehen und als Datei im csv oder json Format herunterladen. Eine grafische Darstellung ist hier nicht vorhanden. Anleitungen zur grafischen Darstellung mit Google Charts oder als eigene Platform (analog.io) existieren, sind mir aber entweder zu unflexibel oder zu google-lastig. Mein Favorit ist nach wie vor DyGraphs, zur Nutzung mit data.sparkfun gab es aber bisher keine Anleitung, so dass ich selbst kreativ werden musste.  Dabei habe ich wieder so einiges über JavaScript lernen müssen / dürfen und meine Erfahrungen gebe ich wie immer gerne weiter.

JavaScript eignet sich für diese Aufgabe im Gegensatz zu PHP besonders deshalb, weil kein separater Server zum Aufruf dieser Webseite benötigt wird. Die html-Datei mit der Formatierung der Webseite kann lokal auf dem eigenen Rechner liegen, die Verarbeitung findet komplett im Browser statt.

Die hier genutzte html-Beispieldatei greift auf einen aktiven Stream auf data.sparkfun zu und zeigt im Screenshot oben die Entladung und Ladung eines Li-Ionen Akkus, über den der Arduino mit Ethernetshield, der die Daten auf data.sparkfun pushed, gleichzeitig mit Strom  versorgt wird.

Die Struktur der html-Datei erkläre ich hier, damit sollte die Anpassung auf einen eigenen Stream recht einfach sein.

screenshot3Zuoberst findet man die üblichen HTML Tags im Kopfbereich einer html-Datei und die Angabe der Quelle für 2 JavaScript-Module, hier als hosted Version, die auskommentierten Zeilen nutzt man, wenn die Dateien lokal im gleichen Verzeichnis wie die html-Datei liegt. Es folgt eine einfache farbliche Formatierung der Webseite.

screenshot4Die nachfolgenden Zeilen sind in eine onload() Funktion eingebettet und definieren die Größe der späteren Grafik, die über die id=graphdiv2 später befüllt wird.

screenshot5Dies ist die Funktion zum Download der Daten von data.sparkfun. An dieser Stelle muss der public Key der eigenen Daten eingetragen werden. Über eine jquery Funktion werden die Daten von Sparkfun geladen. An dieser Stelle kann ein Filter definiert werden, entweder man lädt die Daten page-weise ( data: {page: 1};  ) oder nutzt einen der hier beschriebenen möglichen Filter zur Einschränkung der Zahl der Daten.  Der hier benutzte Filter „today“ für den timestamp ist nirgendwo dokumentiert und habe ich zufällig herausgefunden, also nicht wundern, wenn mit meinem Script eine leere Grafik erscheint. Für einen ersten Test kann man die Zeile auskommentieren.

screenshot6Die json-Daten müssen in ein Array überführt werden, dabei werte ich aus, wie groß der zeitliche Abstand der Daten ist und entscheide darüber, ob die Datenpunkte mit Linien verbunden werden (hier 250.000 Sekunden, also etwas über 4 Minuten) .  Hier müssen die Namen der Datenfelder eingesetzt werden , in meinem Fall ubatt und ucc.

screenshot7Damit die zeitliche Darstellung der Daten korrekt ist, wird das gesamte Array in der Reihenfolge umgekehrt. Das Array wird dann an Dygraph übergeben mit einigen Parametern zur Darstellung und den Labeln für die Daten.

Die vollständige html-Datei liegt auf meinem Git zusammen mit dem Arduino Programm zum pushen der Daten.

Ausblick

Ich habe inzwischen den phant Server auch auf einem RaspberryPi laufen, funktioniert hervorragend. Leider habe ich den mqtt-output noch nicht ans laufen gebracht. Dieser Service ist bei Sparkfun leider sehr unzuverlässig, so dass ich davon – zumindest bei der hosted Version – erst mal abraten würde.
Speziell für den Arduino Uno werde ich einen Stream erstellen, wo alle analogen Eingänge als Integer und die digitalen Eingänge als Boolean 1/0 gepushed werden können. Auf einer Webseite soll man dann wählen können, welche Daten grafisch aufbereitet werden sollen. Auch das zeitliche filtern der Daten will ich auf der Webseite eingebbar machen.

 

 

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.

 

 

 

Tutorial: Website parsen mit dem ESP8266 – Teil 2

Einleitung

Im ersten Teil dieses Tutorials habe ich gezeigt, wie man Informationen nach einem Suchschema aus einer Website extrahiert. In diesem Teil des Tutorial möchte ich zunächst auch darauf eingehen, was man beachten sollte, wenn man eine Website auswählt, aus der Daten extrahiert werden sollen. Später werden wir dann ein zweites Beispiel genauer betrachten.

Der Inhalt einer Website

Die heutigen Websites werden immer umfangreicher und bieten eine Fülle von Informationen, was leider auch eine Menge von Daten zur Folge hat. Zusätzlich werden Animationen, Grafiken oder Werbung mit übertragen, was die Datenmenge zusätzlich erhöht. Je mehr Daten empfangen werden, umso mehr muss der ESP analysieren, um die gesuchte Information zu finden.

Nehmen wir als Beipiel an, wir wollen die aktuelle Temperatur an unserem Wohnort aus einer Website auslesen. Es gibt eine große Zahl von Websites, die diese Information bereit stellen. Um zu bewerten, ob eine Website geeignet ist, diese Information für den ESP verträglich zu liefern, sollte man sich den Quelltext der Website mal ansehen. Ich habe als Beispiel mal eine Website ausgewählt, die die aktuelle Temperatur für sehr viele Orte liefern kann. Auf der Seite http://www.wunderground.com/ kann man sich eine Messstation in der Nähe seines Wohnortes suchen. Die url für die Messstation am Kölner Flughafen wäre dann http://www.wunderground.com/global/stations/10513.html.

Eine durchaus auch sinnvolle Alternative wäre es, eine Website auszuwählen, die weniger Daten transferiert. Eine gute Adresse sind in einem solchen Fall z.B. private Websites, die Informationen ihrer Wetterstationen online stellen.

Auf jeden Fall sollte man sich jetzt mal den Quelltext dieser Website ansehen, am einfachsten geht das z.B. im Firefox über den Menuepunkt Entwicklerwerkzeuge->Seitenquelltext anzeigen oder die Kurztaste Strg+u. Man muss kein HTML verstehen, um zu erkennen, dass hier Massen an Daten übertragen werden.

OpenWeather1

In diesem Fall von wunderground.com hat man, was die aktuelle Temperatur angeht Glück, die steht nämlich im Quelltext ganz oben als Meta-Information.

    <meta property="og:title" content="Koeln, Germany | 3.6&deg; | Mostly Cloudy" /> 

Um jetzt einen eindeutigen Suchbegriff in der Zeile zu finden, testen wir, ob bestimmte Buchstabenketten nur in dieser Zeile des Quelltextes vorkommen. Ich entscheide mich für den Suchstring „og:title„. Das hat den Vorteil, dass ich ohne den Suchstring zu ändern auch andere Städte suchen könnte.

Für einen ersten Test sollte es reichen den folgenden Codeblock im Code aus dem ersten Teil des Tutorials einzusetzen

  
while (client.available()) {
  String line = client.readStringUntil('\n');
  if (line.indexOf("og:title") >= 0) { // Searchstring exists?
    Serial.println();
    Serial.print(line);
  }
}

Natürlich muss der Server und die URL ebenfalls eingetragen werden

 
... 
const char* host = "www.wunderground.com"; 
... 
String url = "/global/stations/10513.html?MR=1";
... 

Die Ausgabe sieht jetzt so aus:


Connecting to TP-LINK_360E60
............
WiFi connected
IP address:
192.168.0.100

connecting to www.wunderground.com
Requesting URL: /global/stations/10513.html?MR=1

         <meta property="og:title" content="Koeln, Germany | 3.5&deg; | Scattered Clouds" />
closing connection

Die richtige Zeile habe ich also schon gefunden, jetzt gehts darum, den Temperaturwert aus der Zeile zu extrahieren. Der Wert steht zwischen den Zeichen „| “ und „&“. Wir suchen also das erste Vorkommen des ersten Suchstrings und extrahieren von da bis zum zweiten Suchstring. Der Code wird also folgendermaßen ergänzt. Als kleines Extra habe ich auch noch die Umwandlung der Temperatur in float eingebaut.


while (client.available()) {
  String line = client.readStringUntil('\n');
  if (line.indexOf("og:title") >= 0) { // Searchstring exists?
    Serial.println();
    Serial.println(line);
    int vonPos = line.indexOf("| "); 
    int bisPos = line.indexOf("&");
    Serial.print("die aktuelle Temperatur ist ");
    Serial.print(line.substring(vonPos+2, bisPos));
    Serial.println(" Grad C");
   // Umandlung in float
   String temp=line.substring(vonPos+2, bisPos);
   char char1[8];
   temp.toCharArray(char1, temp.length()+1);
   float tempWert=atof(char1);
   Serial.println(tempWert);
  }
}

Die Ausgabe sieht jetzt so aus


Connecting to TP-LINK_360E60
........
WiFi connected
IP address:
192.168.0.100

connecting to www.wunderground.com
Requesting URL: /global/stations/10513.html?MR=1

<meta property="og:title" content="Koeln, Germany | 3.6&deg; | Mostly Cloudy" />
die aktuelle Temperatur ist 3.6 Grad C
3.60

closing connection

Als zweiten Parameter hätte ich gerne den Luftdruck, hier wird es schon schwieriger, den im Quelltext zu finden. Ich suche im Quelltext nach „Druck“, das sollte ja in der Nähe des eigentlichen Wertes zu finden sein, wie mir die Website selbst verrät. Der Suchstring „Druck“ kommt 3x vor und ich sehe, dass der gesuchte Messwert des Luftdrucks 2x auf der Website ausgegeben wird. In beiden Fällen steht der Messwert aber nicht in der selben Zeile. Jetzt heißt es also zusätzlich Zeilen abzählen.

Ich suche also zunächst nach „<td><dfn>Druck“ und dann nach „wx-value“. In dieser Zeile findet sich dann mein Luftdruckwert. So dachte ich! Es passiert hier etwas seltsames.

Nachdem ich den Suchstring „Druck“ nicht finde konnte, obwohl er ja im Firefox im Quelltext war, habe ich in der Linux Command Line mit

wget http://www.wunderground.com/global/stations/10513.html?MR=1

den Quelltext in einen File geschrieben. Bei der Untersuchung dieses Files fiel mir dann auf, dass hier „Pressure“ statt „Druck“ verwendet wird. Die Website scheint zu erkennen, dass mein Firefox deutsch spricht, was mein ESP sicher nicht zu erkennen gibt.

Der Code wird jetzt schon etwas umfangreicher, ich habe zusätzliche Flags eingefügt, die mir zeigen, ob ich den ersten Suchstring gefunden habe, dann , ob ich den zweiten Suchstring gefunden habe. Zusätzlich ein Flag, damit ich den selben Suchstring nicht noch ein zweites mal auswerte, da er ja auf der Seite 2x vorkommt.


  // Read all the lines of the reply from server and print them to Serial
  boolean markF = false;
  boolean foundF = false;
  while (client.available()) {
    String line = client.readStringUntil('\n');
    if (line.indexOf("og:title") >= 0) { // Searchstring exists?
      Serial.println();
      Serial.println(line);
      int vonPos = line.indexOf("| ");
      int bisPos = line.indexOf("&");
      Serial.print("die aktuelle Temperatur ist ");
      Serial.print(line.substring(vonPos + 2, bisPos));
      Serial.println(" Grad C");
      // Umandlung in float
      String temp = line.substring(vonPos + 2, bisPos);
      char char1[8];
      temp.toCharArray(char1, temp.length() + 1);
      float tempWert = atof(char1);
      Serial.println(tempWert);
    }
    if (line.indexOf(">Pressure") >= 0 && !foundF) {  
      Serial.println();
      Serial.println(line);
      markF = true; // first time found
    } 
    if (line.indexOf("wx-value") >=0 && markF && !foundF) {   // search only the first 
      Serial.println();
      Serial.println(line);
      int vonPos = line.indexOf(">");
      int bisPos = line.indexOf("<",vonPos+1);
      Serial.print("der aktuelle Luftdruck ist ");
      Serial.print(line.substring(vonPos + 1, bisPos));
      Serial.println(" hPa");
      // Umandlung in float
      String druck = line.substring(vonPos + 1, bisPos);
      char char1[8];
      druck.toCharArray(char1, druck.length() + 1);
      float druckWert = atof(char1);
      Serial.println(druckWert);
      markF=false;
      foundF=true;      // don't search the second 
    }
  }

[/code]

Und zum Schluß noch die Ausgabe

[code]
Connecting to TP-LINK_360E60 
........... 
WiFi connected 
IP address:  
192.168.0.100 
 
connecting to www.wunderground.com 
Requesting URL: /global/stations/10513.html?MR=1 
 
    <meta property="og:title" content="Koeln, Germany | 3.9&deg; | Mostly Cloudy" /> 
die aktuelle Temperatur ist 3.9 Grad C 
3.90 
 
        <td><dfn>Pressure</dfn></td> 
 
        <span class="wx-value">1025</span> 
der aktuelle Luftdruck ist 1025 hPa 
1025.00 
 
closing connection 

Ich hoffe, dass mein Beispiel zeigt, wie einfach es ist, Informationen aus Websites zu parsen. Die Weiterverwendung dieser Information ist eurer Fantasie überlassen. Im nächsten und letzten Teil dieses Tutorials werde ich ein Beispiel für eine Steuerung vorstellen.

Aufgabe ist: Die Klappe eines Hühnerstalls soll bei Sonnenaufgang geöffnet werden und bei Sonnenuntergang wieder geschlossen werden.Die Informationen dazu – ihr könnt es euch denken – hole ich mir von einer Website.

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.

One-Click Abfrage der Deutschen Bahn

Als regelmäßiger Bahnfahrer hatte ich mich schon lange darüber geärgert, dass ich immer eine komplette Reiseabfrage machen muss, wenn ich sehen will, wann mein nächster Zug geht.

Also ganz konkret: ich bin mit meiner Arbeit fertig und will die nächste Verbindung nach Hause wissen

  1. Schritt: Reiseportal der DB aufrufen – von unterwegs nutze ich immer das Portal für mobile Devices http://www.bahn.de/m/view/de/index.shtml
  2. Menuepunkt Live Auskunft wählen
  3. Startbahnhof und Zielbahnhof eingeben
  4. Abfrage starten

oder – ich will wissen, ob der Zug an meinem Bahnhof Verspätung hat

  1. Schritt: Reiseportal der DB aufrufen
  2. Ankunft / Abfahrt wählen
  3. Bahnhof eingeben
  4. Abfrage starten

Zugegebenermaßen kann man Lesezeichen jeweils für Schritt 2 setzen, es bleibt aber immer noch Schritt 3 und 4.

Die Lösung

Es gibt eine Schnittstelle für „parametrisierte Übergabe“ an die Reiseauskunft, deren Beschreibung ich im Wiki des FHEM gefunden habe. DBPlan im FHEM Wiki oder als direkter Link zum pdf.

Die Abfrage der nächstmöglichen Reiseverbindung von Frankfurt-Niederrad nach Köln-Ehrenfeld sieht also jetzt (oneCLick) so aus und lässt sich als Lesezeichen ablegen.

http://mobile.bahn.de/bin/query.exe/dox?S=Frankfurt-Niederrad&Z=K%C3%B6ln-Ehrenfeld&timeSel=depart&start=1

BahnAbfrage1

Man kann die Bahnhöfe meist im Klartext eingeben, nur manchmal, wenn es eine Mehrdeutigkeit gibt, kommt es zu einer weiteren Abfrage (Beispiel: Köln-Chorweiler). Das kann man umgehen, wenn man statt des Bahnhofes im Klartext die Bahnhofkennziffer, die sogenannte IBNR eingibt.  Die IBNR für alle Bahnhöfe findet man über die Suche auf dieser Homepage eines wahrhaft Bahnbegeisterten. Der Link oben sieht dann also so aus:

http://mobile.bahn.de/bin/query.exe/dox?S=8002050&Z=8000208&timeSel=depart&start=1

Die Abfrage der nächsten Abfahrten an meinem Bahnhof fragt man so ab:

http://mobile.bahn.de/bin/mobil/bhftafel.exe/dox?ld=15055&rt=1&time=actual&productsFilter=111110000&start=yes&boardType=dep&input=8000208

BahnAbfrage2

Einen besonderen Augenmerk möchte ich hier noch auf den Parameter productsFilter=111110000 lenken. Damit lässt sich einstellen, ob man alle Verkehrsmittel, also auch Straßenbahnen oder Busse, Schiffe usw oder nur die Schienenfahrzeuge wie hier sehen will. Näheres dazu in der Dokumentation der Parameter im PDF.

Eine letzte Frage bleibt unbeantwortet: Warum findet man diese Dokumentation eigentlich nicht auf den Seiten der Deutschen Bahn AG? An Google gibt die Bahn diese Daten ja auch weiter. Schön wäre jetzt noch, wenn man die Ausgabe als json oder xml zurückbekommen würde, dann wäre der Weg für die persönliche Abfahrtstafel an der Wohnungstüre geebnet.

Sony Camera Remote Control mit ESP8266

Einleitung

Seit kurzem bin ich begeisterter Besitzer einer Sony DSC-HX90V. Für die Fernsteuerung dieser Kamera hat Sony eine WiFi Schnittstelle eingebaut und bietet eine App für Smartphones an, siehe PlayMemories. Leider gibt es keine weitere Schnittstelle (IR, elektrisch, Funk) um die Kamera remote zu bedienen.

Die Idee

Warum also sollte man die Kamera nicht über die WiFi Schnittstelle fernsteuern und damit gleichzeitig eine Schnittstelle per Mikrocontroller schaffen. Eine Realisierung einer WiFi Remote für eine GoPro wurde vor kurzem auf Hackaday vorgestellt. Eine Suche nach entsprechenden Programmen für Sony Kameras war ohne Erfolg. Auch die Sony Dokumentation der API brachte mich nicht weiter, da ich kein Android programmiere.
Über das ESP8266 Forum, fand ich zumindest Interessierte mit der gleichen Idee, aber keine Lösung.

ESP_Sony3

Reverse Engineering zur Lösung

Durch Zufall stieß ich auf die TimeLapse App von Thibaud Michel.
Mit Hilfe eines WiFi PacketAnalysers habe ich mir auf dem Tablett das Protokoll näher angeschaut und dann mit der Sony Doku der APK verglichen.
ESP_Sony1
Nach einiger Recherche fand ich heraus, dass es sich um ein einfaches HTTP POST Verfahren handelt mit der Übergabe der Parameter in einem json Format. Das nachzubilden mit dem ESP8266 war nur noch Fleißarbeit.

Proof of Concept

Die hier vorgestellte Lösung ist ein erster „Proof of Concept“ ohne jegliches Fehlerhandling und soll zunächst nur die Funktion des Schnittstelle darstellen. An einem ESP8266-01 ist lediglich ein Pushbutton zur Auslösung der Kamera angeschlossen der Rest ist Programm. Als Test wurde das Programm auf meinem Prototyp Board entwickelt. Auf diesem Board ist die 3,3V Stromversorgung und der FTDI zur Programmierung und zur Ausgabe von Debug Informationen fest verbunden. Der ESP8266 ist steckbar. Einzige zusätzliche Komponente ist der Pushbutton zur Fernauslösung der Kamera. Eine Beschreibung des Protoboards und die Einbindung der ESP8266 Libraries in die Arduino IDE findet sich hier. Die prinzipielle Verschaltung hier. Der Code ist ausführlich kommentiert und weicht nicht erheblich von den bekannten WiFiClient Codes ab, sollte also leicht verständlich sein. Auf eine Auswertung der json Antworten der Kamera wurde verzichtet.

//----------------------------------------------------------------------------------------------------------------------
// This program is based on: WiFiClient from ESP libraries
//
// Camera handling by Reinhard Nickels https://glaskugelsehen.wordpress.com/
// tested with DSC-HX90V, more about protocol in documentation of CameraRemoteAPI https://developer.sony.com/develop/cameras/
// 
// Licenced under the Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0) licence:
// http://creativecommons.org/licenses/by-sa/3.0/
//
// Requires Arduino IDE with esp8266 core: https://github.com/esp8266/Arduino install by boardmanager
//----------------------------------------------------------------------------------------------------------------------

#include <ESP8266WiFi.h>
#define DEBUG 1
#define BUTTON 2   // pushbutoon on GPIO2
volatile int counter;

const char* ssid     = "DIRECT-GKC2:DSC-HX90V";
const char* password = "5uv2LSwj";     // your WPA2 password

const char* host = "192.168.122.1";   // fixed IP of camera
const int httpPort = 8080;

char JSON_1[] = "{\"version\":\"1.0\",\"id\":1,\"method\":\"getVersions\",\"params\":[]}";
char JSON_2[] = "{\"version\":\"1.0\",\"id\":1,\"method\":\"startRecMode\",\"params\":[]}";
char JSON_3[] = "{\"version\":\"1.0\",\"id\":1,\"method\":\"startLiveview\",\"params\":[]}";
char JSON_4[] = "{\"version\":\"1.0\",\"id\":1,\"method\":\"stopLiveview\",\"params\":[]}";
char JSON_5[] = "{\"version\":\"1.0\",\"id\":1,\"method\":\"actTakePicture\",\"params\":[]}";
// char JSON_6[]="{\"method\":\"getEvent\",\"params\":[true],\"id\":1,\"version\":\"1.0\"}";


unsigned long lastmillis;

WiFiClient client;

void setup() {
  Serial.begin(115200);
  delay(10);

  pinMode(BUTTON, INPUT_PULLUP);
  attachInterrupt(2, pulleddown, FALLING);  // handled by interrupt to debounce

  // We start by connecting to a WiFi network

  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {   // wait for WiFi connection
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  delay(1000);
  httpPost(JSON_1);  // initial connect to camera
  httpPost(JSON_2); // startRecMode
  httpPost(JSON_3);  //startLiveview  - in this mode change camera settings  (skip to speedup operation)
}

void loop() {
  if (buttonpressed()){
    Serial.println("pressed..");
    httpPost(JSON_4); //stopLiveview    (skip to speedup operation)
    httpPost(JSON_5);  //actTakePicture
    httpPost(JSON_3);  //startLiveview    (skip to speedup operation)
    }
}

void httpPost(char* jString) {
  if (DEBUG) {Serial.print("Msg send: ");Serial.println(jString);}
  Serial.print("connecting to ");
  Serial.println(host);
  if (!client.connect(host, httpPort)) {
    Serial.println("connection failed");
    return;
  }
  else {
    Serial.print("connected to ");
    Serial.print(host);
    Serial.print(":");
    Serial.println(httpPort);
  }

  // We now create a URI for the request
  String url = "/sony/camera/";

  Serial.print("Requesting URL: ");
  Serial.println(url);

  // This will send the request to the server
  client.print(String("POST " + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n"));
  client.println("Content-Type: application/json");
  client.print("Content-Length: ");
  client.println(strlen(jString));
  // End of headers
  client.println();
  // Request body
  client.println(jString);
  Serial.println("wait for data");
  lastmillis = millis();
  while (!client.available() && millis() - lastmillis < 8000) {} // wait 8s max for answer

  // 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);
  }
  Serial.println();
  Serial.println("----closing connection----");
  Serial.println();
  client.stop();
}

void pulleddown()  // Interrupt handler
{
  counter++;
}

boolean buttonpressed() {  // function to check if pressed
  if (counter!=0) {  
    counter=0;
    delay(10);     // je nach Schalter 
    if (counter==0 && !digitalRead(BUTTON)) return true;
  }
  return false;
}
 

ESP_Sony2

Bedienung

Für die Funktion muss auf der Kamera die aktuelle Version der Smart-Fernbedienung (das Gegenstück für die PlayMemories App) installiert sein.

Im Programm muss die SSID und das Passwort der benutzten Kamera eingetragen werden. Die Kamera wird nach dem Einschalten über das Menue in den Fernsteuermodus gebracht. Nach dem Einschalten des ESP8266 wird die Verbindung zur Kamera hergestellt und die Kamera wird in einen Modus versetzt, wo Einstellungen (Zoom, Blende, Zeit, …) möglich sind.

Ein Druck auf den Pushbutton löst die Kamera aus und die Kamera kehrt in den vorhergehenden Modus zurück.

Über die serielle Schnittstelle werden Debug Informationen ausgegeben, hier ein Beispiel nach dem Einschalten des ESP bis nach der ersten Fernauslösung der Kamera.

Connecting to DIRECT-GKC2:DSC-HX90V 
....... 
WiFi connected 
IP address:  
192.168.122.115 
Msg send: {"version":"1.0","id":1,"method":"getVersions","params":[]} 
connecting to 192.168.122.1 
connected to 192.168.122.1:8080 
Requesting URL: /sony/camera/ 
wait for data 
HTTP/1.1 200 OK
Connection: close
Content-Length: 51
Content-Type: application/json

{"result":[["1.0","1.1","1.2","1.3","1.4"]],"id":1} 
----closing connection---- 
 
Msg send: {"version":"1.0","id":1,"method":"startRecMode","params":[]} 
connecting to 192.168.122.1 
connected to 192.168.122.1:8080 
Requesting URL: /sony/camera/ 
wait for data 
HTTP/1.1 200 OK
Connection: close
Content-Length: 21
Content-Type: application/json

{"result":[0],"id":1} 
----closing connection---- 
 
Msg send: {"version":"1.0","id":1,"method":"startLiveview","params":[]} 
connecting to 192.168.122.1 
connected to 192.168.122.1:8080 
Requesting URL: /sony/camera/ 
wait for data 
HTTP/1.1 200 OK
Connection: close
Content-Length: 75
Content-Type: application/json

{"result":["http:\/\/192.168.122.1:8080\/liveview\/liveviewstream"],"id":1} 
----closing connection---- 
 
pressed.. 
Msg send: {"version":"1.0","id":1,"method":"stopLiveview","params":[]} 
connecting to 192.168.122.1 
connected to 192.168.122.1:8080 
Requesting URL: /sony/camera/ 
wait for data 
HTTP/1.1 200 OK
Connection: close
Content-Length: 21
Content-Type: application/json

{"result":[0],"id":1} 
----closing connection---- 
 
Msg send: {"version":"1.0","id":1,"method":"actTakePicture","params":[]} 
connecting to 192.168.122.1 
connected to 192.168.122.1:8080 
Requesting URL: /sony/camera/ 
wait for data 
HTTP/1.1 200 OK
Connection: close
Content-Length: 108
Content-Type: application/json

{"result":[["http:\/\/192.168.122.1:8080\/postview\/memory\/DCIM\/100MSDCF\/DSC01272.JPG?size=Scn"]],"id":1} 
----closing connection---- 
 
Msg send: {"version":"1.0","id":1,"method":"startLiveview","params":[]} 
connecting to 192.168.122.1 
connected to 192.168.122.1:8080 
Requesting URL: /sony/camera/ 
wait for data 
HTTP/1.1 200 OK
Connection: close
Content-Length: 75
Content-Type: application/json

{"result":["http:\/\/192.168.122.1:8080\/liveview\/liveviewstream"],"id":1} 
----closing connection---- 
 

Video

Video (Downloadlink):

Fazit und Ausblick

Dieser Blogeintrag beschreibt in einfacher Weise die grundsätzliche Funktion der WiFi Fernsteuerung einer Sony Kamera. Die Funktionsweise lässt sich zumindest auf die Kameras übertragen, die PlayMemories kompatibel sind. Möglicherweise ist dann die Serveradresse der Kamera zu modifizieren. Der Schritt zu komplexeren Lösungen ist reine Programmierarbeit. Ersetzt man den Pushbutton mit einer Zeitschleife kann man sehr einfach die Einzelbilder für TimeLapse Videos (Zeitraffer) aufnehmen. Die Auslösung kann natürlich auch durch Sensoren (Annäherung, Bewegung, …) erfolgen. Prinzipiell ist auch der Start und Stop einer Videoaufnahme (Kamera im Videomodus) möglich. Die entsprechenden json Kommandos sind in der Doku der APK enthalten. Wenn man auf den Modus „Liveview“ verzichtet ist die Auslöseverzögerung nur minimal (siehe Kommentar im Code, diese Zeilen dann auskommentieren), allerdings verliert man dann die Möglichkeit Kameraeinstellungen zu ändern. Für einen Batteriebetrieb ist der ESP8266 bekanntermaßen nicht so gut geeignet, aber für einen einzelnen Shot braucht es nicht viel Zeit. Für Timelapse Projekte wäre es sicher sinnvoll den Sleep Mode des ESP zu nutzen (was bei der Bauform ESP-01 nicht funktioniert ohne Modifikation). Ich würde hier das Modell ESP-12 empfehlen, das außerdem CE und FCC zertifiziert ist.

 

Addendum2 zum Tutorial – Speicherung von MySQL DB Werten mit ESP8266

Nachdem ich mich in meinen letzten Blogs intensiv mit dem ESP8266 auseinandergesetzt habe, steht logischerweise eine Erweiterung meines Tutorials zur Speicherung von Messwerten in eine MySQL DB unter Verwendung des ESP8266 an. Das Programm wurde weitestgehend analog dem bisherigen Programm aufgebaut, so dass es relativ einfach zu verstehen sein sollte.  Im Programm müssen noch die persönlichen Daten für das WLAN und den Host eingetragen werden.

/* 
 Programm zur Speicherung von Messwerten in einer webbasierten MySQL DB
  based on standard programs of the ESP8266Wifi library 
  and examples on sparkfun.com
 */
 
#include <ESP8266WiFi.h>
#define CYCLE 60000

const char* ssid     = "hier deine SSID";
const char* password = "und das Passwort des WLANs";
const char* host = "hier deinen Host eintragen";
unsigned long value = 0;
unsigned int lastcall = CYCLE;
int conn_time;

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(".");
    conn_time++;
    if (conn_time > 20) {
      break;
    }
  }
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("");
    Serial.println("WiFi connected");
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());
  }
  else {
    Serial.println("");
    Serial.println("no WiFi connection");
    ESP.deepSleep(100000);
    Serial.println("gone to sleep");
  }
}

void loop() {
  while (millis() - lastcall < CYCLE) {
    delay(1);
  }
  lastcall = millis();
  ++value;

  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 = "/w2mysql.php";
  url += "?A0=";
  url += millis();
  url += "&A1=";
  url += value;
 
  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");
  delay(1000);

  // 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);
  }

  Serial.println();
  Serial.println("closing connection");
}

Performance Test ESP8266

Der ESP8266 gehört inzwischen zu meinen Standardboards. Die Integration in die Arduino IDE ist erfolgt und die Zahl der Libraries nimmt ständig zu. Um ein Gefühl für die Performance des Boards zu bekommen habe ich das Primzahlenprogramm genutzt, mit dem ich bereits den STM32 Maple gegen den Arduino UNO getestet habe.

Das Ergebnis als Übersicht, alle Zeitangaben in Millisekunden.

Datentyp Suchbereich Arduino Maple ESP8266
uint8_t 255 550 30 83
uint16_t 65535 10760 241 1054
uint32_t 2*65535 84551 583 2919
uint32_t 1000000 abgebrochen 10042 30369

Auf eine Besonderheit möchte ich an dieser Stelle hinweisen:

Ich hatte anfänglich Schwierigkeiten beim ESP mit dem uint32_t Datenformat. Aus zunächst unerklärlichen Gründen gab es immer Abstürze des Programms aufgrund von Watchdog Fehlern. Das hängt wohl damit zusammen, dass der ESP seine komplette WLAN Abarbeitung im Hintergrund erledigt. Wenn aber längere Loops im Programm sind, bekommt er offensichtlich Schwierigkeiten. Um das zu vermeiden habe ich den Befehl yield() in die Loop eingebaut. Das verhindert den Absturz, frisst aber auch ca. 6μs je Aufruf.