Archiv der Kategorie: Tutorial

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 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.

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

Arduino WiFi für unter 3 Euro – der ESP8266

IMG_20150116_090344

In den letzten Monaten ist ein regelrechter Hype ausgebrochen um den ESP8266, ein Chip aus chinesischer Entwicklung, der auf einer winzigen Fläche sowohl Access Point als auch WiFi Client sein kann. Inzwischen exisitieren verschiedene Breakout Boards, die zunächst eine UART-WiFi Schnittstelle bereitstellen und auf eBay für unter 3 Euro gehandelt werden. Da der Chip einen 32bit Prozessor enthält kann er aber auch mit eigener Firmware bestückt werden. Ich möchte mich hier nicht näher mit Details beschäftigen, da es bereits viele Webseiten zu diesem Thema gibt. Eine kurze und knappe Zusammenfassung auf deutsch mit Verlinkung zu weiteren Seiten hat das FabLab Nürnberg erstellt.

Inzwischen konnte ich einige Erfahrungen mit diesem Modul machen und solange man kein Highspeed WiFi braucht ist das Modul eine gute Wahl und macht für kleines Geld IoT möglich. Die ersten Versuche mit diesem Modul habe ich in Verbindung mit einem Arduino MEGA gemacht. Der MEGA hat mehrere in Hardware implementierte serielle Schnittstellen, so kann man sowohl die Verbindung zum Computer als auch die Verbindung zum ESP8266 mit 115200 Baud laufen lassen. In den neueren Firmwares des ESP ist die Baudrate aber auch einstellbar, so dass ein Betrieb an einer SoftSerial Schnittstelle bei z.B. 9600 Baud, damit an einem UNO, auch möglich ist.  Die Verbindung des ESP an den MEGA muss über Pegelwandler 5V->3.3V erfolgen, die Schnittstelle ist hier nicht tolerant. Ich habe die Verbindung von TX (MEGA) zu RX (ESP) über einen Spannungsteile aus 1k und 2.2k herunter geteilt. Der ESP braucht einen eigenen 3.3V Spannungswandler, der 3.3V Ausgang des Arduino liefert zu wenig Strom für den ESP (max 200mA).

Für erste Experimente, bei denen die AT Befehle „per Hand“ eingegeben werden, eignet sich das folgende kurze Programm.

void setup() {
  Serial.begin(115200);
  Serial1.begin(115200);
  Serial.println("Program started");
}

void loop() {
  if(Serial1.available()) {   
    char ser = Serial1.read();
    /*if (ser==10) {         // kann genutzt werden, um ein Gefühl für die Antwortzeiten zu bekommen
      Serial.print("  ");
      Serial.print(millis());
    }*/
    Serial.print(ser);
  }
  if(Serial.available()) {   
    char ser = Serial.read();
    Serial1.print(ser);
  }
}

Die meisten Einstellungen des Chip bleiben über ein Power on Reset bestehen. Es ist also nicht notwendig, im Programm die Einstellungen jedes mal neu zu setzen. Ich habe mir zu diesem Zweck ein Programm erstellt mit den wichtigsten Einstellungen (Access Point, SSID, PASS, Client usw.). Mit diesem Programm mache ich die Grundkonfiguration des Chip.

// ESP_Minimal.ino - sketch to quickly view and change some of ESP8266 parameters
// most of the ESP parameters are stored in eeprom, so also outlive the power on reset
// special thanks to http://rayshobby.net/?p=9734
// sketch is written for ArduinoMega, ESP connected with voltage devider to Serial1
// This code is in the public domain.

#define SET // for just viewing parameters, set this as comment

#define BUFFER_SIZE 512
#define esp Serial1
#define dbg Serial //For debugging purposes

char buffer[BUFFER_SIZE];
char OKrn[] = "OK\r\n";
int mode;

boolean wait_for_esp_response(int timeout, char* term=OKrn) {
 unsigned long t=millis();
 bool found=false;
 int i=0;
 int len=strlen(term);
 // wait for at most timeout milliseconds
 // or if OK\r\n is found
 while(millis()<t+timeout) {
 if(esp.available()) {
 buffer[i++]=esp.read();
 if(i>=len) {
 if(strncmp(buffer+i-len, term, len)==0) {
 found=true;
 break;
 }
 }
 }
 }
 buffer[i]=0;
 dbg.print(buffer);
 return found;
}

void setup() {
 esp.begin(115200); //esp baud rate
 dbg.begin(115200); //debugger baud rate
 dbg.println("start of program ...");
 esp.println("AT"); // check for existence of ESP8266
 if (!wait_for_esp_response(1000)) {
 dbg.println("no ESP found");
 while(true); // hang forever
 }
 // some important checks of operation modes
 esp.println("AT+CWMODE?"); // check mode 1=Client, 2=AP, 3=both
 wait_for_esp_response(1000);
 char *result = strstr(buffer, "+CWMODE:");
 if (result!=0) {
 mode=atoi(result+8);
 if (mode!=1) {
 esp.println("AT+CWSAP?"); // actual AP configuration
 }
 if (mode!=3) {
 esp.println("AT+CWJAP?"); // actual connected WiFi
 wait_for_esp_response(1000);
 }
 }

 esp.println("AT+CIPMUX?"); // check connection mode 0=single, 1=multiple
 wait_for_esp_response(1000);
 esp.println("AT+CIPMODE?"); // check data mode 0=normal, 1=transparent
 wait_for_esp_response(1000);
 esp.println("AT+CIFSR"); // actual IP adress
 wait_for_esp_response(1000);

#ifdef SET
 esp.println("AT+CWMODE=3"); // set mode 1=Client, 2=AP, 3=both
 wait_for_esp_response(1000);
 // set AP or Client
 esp.println("AT+CWMODE?"); // check mode 1=Client, 2=AP, 3=both
 wait_for_esp_response(1000);
 result = strstr(buffer, "+CWMODE:");
 if (result!=0) {
 if (mode!=1) { // AP settings
 esp.println("AT+CWSAP=\"ESP_9F2556\",\"blablabla\",1,3"); // set AP: SSID, PASS, channel, encryption
 wait_for_esp_response(1000); // 0=open, 1=WEP, 2=WPA_PSK, 3=WPA2_PSK, 4=WPA_WPA2_PSK
 }
 if (mode!=2) { // Client settings
 esp.println("AT+CWJAP=\"yourSSID\",\"passphrase\""); // connect client to AP
 wait_for_esp_response(5000); // was really that long!
 }
 }

#endif

 esp.println("AT+RST"); // RESET, necessary for some changes of settings
 wait_for_esp_response(3000, "ready");
}

void loop() {

}

Die zu ändernden Parameter werden direkt im Code verändert. Wenn die erste Zeile #define SET auskommentiert wird, werden die wichtigsten Parameter nur angezeigt.

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.

IMG_20150114_181434

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.

Screenshot_2015-01-14-16-45-17_bea

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.

Screenshot_2015-01-14-16-59-39

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.

Screenshot_2015-01-14-17-08-36

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.

Screenshot_2015-01-14-17-14-33

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.

Screenshot_2015-01-14-17-22-38

Ü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.

 

 

Tutorial: Speicherung von Arduino Messdaten auf Webserver und deren Darstellung – Addendum

In diesem Post fasse ich einige Informationen zusammen, die ich noch loswerden wollte

Addendum1 – Config-file für MySQL Datenbank

Wenn man mit verschiedenen Abfragen einer Datenbank arbeitet und immer wieder die Zugangsdaten eingeben muss, ist das auf Dauer ziemlich nervig. Übersichtlicher ist es dann auch die Zugangsdaten separat in einen File, hier mit dem Namen config.php zuschreiben, der dann in die eigentliche Abfrage included wird.


<?php
$mysql_host = "<hier Name des Server/Host eintragen>";
$mysql_db = "<hier Datenbanknamen eintragen>";
$mysql_user = "<hier MySQL User eintragen>";
$mysql_pw = "<hier MySQL Passwort eintragen>";
$connection = mysql_connect($mysql_host, $mysql_user, $mysql_pw) or die("Verbindung zur Datenbank fehlgeschlagen.");
mysql_select_db($mysql_db, $connection) or die("Datenbank konnte nicht ausgewaehlt werden.");
?>

Die Abfrage sieht dann so aus, hier das Beipiel aus Teil 4 zur Abfrage der letzten 20 Datensätze, Zusätzlich habe ich hier die Tabelle in eine Stringvariable gesetzt. Damit wird der File leichter anpassbar für andere Tabellen.


<?php
require ($_SERVER['DOCUMENT_ROOT']."/config.php");
$mysql_table = "analog_data";
$abfrage = "SELECT datum, analog0, analog1 FROM ".$mysql_table." ORDER BY datum DESC LIMIT 20";
$ergebnis = mysql_query($abfrage);
while($row = mysql_fetch_array($ergebnis))
{
echo $row[datum].",".$row[analog0].",".$row[analog1]."<br>";
}
?>