TheThingsNetwork Gateway Status und Node-Red Worldmap

Node-Red entwickelt sich immer mehr zu einem meiner Lieblingstools. Die Umwandlung und Restrukturierung  von Daten gestaltet sich damit so einfach, dass man viele Herausforderungen mit nur wenigen „Programmier“-schritten lösen kann.

Nachdem ich vor ein paar Tagen zum ersten Mal die Worldmap in Node-Red ausprobiert habe, entstanden darüber einige Ideen, die ich verwirklichen möchte.

Inspiriert von Bjoerns Techblog und Refues Traffic Dashboard wollte ich den Status der TTN Gateways unserer Community in Node-Reds Worldmap darstellen.

Bjoern stellt die API zur Abfrage des Gateway Status in seinem Blog ausführlich vor, darauf kann ich an dieser Stelle verzichten. Zur Abfrage der API nutze ich den http request Node. Für die Abfrage der Community Gateways verwende ich die selben GPS Koordinaten und den Radius, wie auf der Community Seite definiert.

Screenshot_20190210_154725

Die vollständige url lautet damit: https://www.thethingsnetwork.org/gateway-data/location?latitude=50.962010&longitude=6.970313&distance=16200

Im http request Node kann man bereits die Ausgabe als „json formatted“ auswählen.
In nachfolgenden function Node wird die Ausgabe so umgeformt, dass für jedes Gateway der Liste sein online Status als Farbe und natürlich seine Geoposition auf der Karte abgebildet wird. Als Trigger für die Abfrage dient die Worldmap selber. Beim Laden oder Refresh der Seite wird der http request Node neu getriggert. Ein Gateway wird grün dargestellt, wenn es in den letzten 15 Minuten „gesehen“ (last_seen) wurde. Wenn es jedoch länger als 15 Minuten nicht gesehen wurde orange und wenn es länger als 60 Minuten stumm war in rot dargestellt. Wenn man ein Gateway auf der Worldmap anklickt, werden weitere Information wie Description und Owner angezeigt.

Der function Node, der den http request triggert ist simple

if (msg.payload.action === "connected") 
return msg;

Ein wenig umfangreicher ist hier der function Node, der die Daten für den worlmap Node aufbereitet

var NumOfGW = Object.keys(msg.payload).length; 
var gwArr=[];
var mapArr=[];
var timeDiff15 = new Date((new Date()) -15*1000*60);
var timeDiff60 = new Date((new Date()) -60*1000*60);
for (var i = 0; i< NumOfGW; i++){
    gwArr.push(Object.keys(msg.payload)[i]); 
}
for (var i = 0; i< NumOfGW; i++){
if (typeof(msg.payload[gwArr[i]].location)!== "undefined"){
    message = {
    lat: msg.payload[gwArr[i]].location.latitude,
    lon: msg.payload[gwArr[i]].location.longitude,
    name: msg.payload[gwArr[i]].id,
    };
    if (new Date(msg.payload[gwArr[i]].last_seen) === "undefined"){
        message.iconColor="green";
    }
    else if (new Date(msg.payload[gwArr[i]].last_seen)<timeDiff60){
        message.iconColor="red";
    }
    else if (new Date(msg.payload[gwArr[i]].last_seen)<timeDiff15){
        message.iconColor="orange";
    }
    else {
        message.iconColor="green";
    }
    message.Description = msg.payload[gwArr[i]].description;
    message.Owner = msg.payload[gwArr[i]].owner;
    message.lastSeen = msg.payload[gwArr[i]].last_seen;
    mapArr.push(message);
   }
}
msg.payload=mapArr;
return msg;

Auch der worldmap Node wird mit den Geodaten der Community konfiguriert. Dadurch wird die Karte im Zentrum von Köln zentriert und mit einem angenehmen Zoomfaktor angezeigt.

Screenshot_20190210_155440

Wer die komplette Funktion auf seinem Node-Red nachbauen möchte muss nur die worldmap Nodes nachinstallieren. Dazu auf die 3 waagerechten Striche oben rechts, dann Manage Palette, install und nach worldmap suchen. Es wird dann node-red-contrib-web-worldmap gefunden, dass muss installiert werden.

Hier zum direkten Einbinden die Node-Red Daten

[{"id":"54aa39db.4c074","type":"worldmap in","z":"142c7153.7b2197","name":"","path":"/worldmap","x":194.88333129882812,"y":258.1000061035156,"wires":[["c8d2dfcd.98075"]]},{"id":"de77cedf.98a7f","type":"http request","z":"142c7153.7b2197","name":"GW Status","method":"GET","ret":"obj","url":"https://www.thethingsnetwork.org/gateway-data/location?latitude=50.962010&amp;amp;amp;amp;amp;longitude=6.970313&amp;amp;amp;amp;amp;distance=16200","tls":"","x":424.949951171875,"y":160.1666259765625,"wires":[["4b276cf0.dc2bf4","633070e6.4339a"]]},{"id":"b9bebb3c.1ba67","type":"inject","z":"142c7153.7b2197","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":183.50003051757812,"y":159.1666259765625,"wires":[["de77cedf.98a7f"]]},{"id":"88ce05cb.6685a8","type":"debug","z":"142c7153.7b2197","name":"","active":true,"console":"false","complete":"false","x":881.9500732421875,"y":223.7332763671875,"wires":[]},{"id":"4b276cf0.dc2bf4","type":"function","z":"142c7153.7b2197","name":"extract Geo&amp;amp;amp;amp;Status","func":"var NumOfGW = Object.keys(msg.payload).length; \nvar gwArr=[];\nvar mapArr=[];\nvar timeDiff15 = new Date((new Date()) -15*1000*60);\nvar timeDiff60 = new Date((new Date()) -60*1000*60);\nfor (var i = 0; i&amp;amp;amp;lt; NumOfGW; i++){\n gwArr.push(Object.keys(msg.payload)[i]); \n}\nfor (var i = 0; i&amp;amp;amp;lt; NumOfGW; i++){\n message = {\n lat: msg.payload[gwArr[i]].location.latitude,\n lon: msg.payload[gwArr[i]].location.longitude,\n name: msg.payload[gwArr[i]].id,\n };\n if (new Date(msg.payload[gwArr[i]].last_seen)=== undefined){\n message.iconColor=\"green\";\n }\n else if (new Date(msg.payload[gwArr[i]].last_seen)&amp;amp;amp;lt;timeDiff60){\n message.iconColor=\"red\";\n }\n else if (new Date(msg.payload[gwArr[i]].last_seen)&amp;amp;amp;lt;timeDiff15){\n message.iconColor=\"orange\";\n }\n else {\n message.iconColor=\"green\";\n }\n message.Description = msg.payload[gwArr[i]].description;\n message.Owner = msg.payload[gwArr[i]].owner;\n message.lastSeen = msg.payload[gwArr[i]].last_seen;\n mapArr.push(message);\n }\nmsg.payload=mapArr;\nreturn msg;\n","outputs":1,"noerr":0,"x":705.9500732421875,"y":142,"wires":[["88ce05cb.6685a8","a02be639.d80688"]]},{"id":"633070e6.4339a","type":"debug","z":"142c7153.7b2197","name":"","active":false,"console":"false","complete":"false","x":644.9500732421875,"y":217.45001220703125,"wires":[]},{"id":"a02be639.d80688","type":"worldmap","z":"142c7153.7b2197","name":"","lat":"50.962010","lon":"6.970313","zoom":"13","layer":"OSM","cluster":"","maxage":"","usermenu":"show","layers":"show","panit":"false","panlock":"false","zoomlock":"false","path":"/worldmap","x":921.5,"y":143.66665649414062,"wires":[]},{"id":"c8d2dfcd.98075","type":"function","z":"142c7153.7b2197","name":"retrigger from map","func":"if (msg.payload.action === \"connected\")\nreturn msg;","outputs":1,"noerr":0,"x":420.9500427246094,"y":258.4833068847656,"wires":[["de77cedf.98a7f"]]}]

Beeindruckend ist auch, wenn man im http request die url für den Abruf aller Gateways in Deutschland nutzt: https://www.thethingsnetwork.org/gateway-data/country/de

Der zweite Screenshot entstand nach der Änderung des Parameters „cluster if zoom level is less than“


			

Logbuch Server einrichten – Teil 2

Im ersten Teil habe ich beschrieben, wie der Server mit nginx und node-red bespielt wurde. In diesem Teil werde ich zeigen, wie InfluxDB und Grafana installiert und konfiguriert werden. Auch in diesem Tutorial sind Eingaben und Ausgaben farblich gekennzeichnet.

Installation und Einrichtung der InfluxDB

Die Installation ist auf der Influx Webseite gut beschrieben, daran habe ich mich orientiert. Dabei jeweils auf die Version der Software achten. Zur reinen Installation sind nur 2 Schritte erforderlich.

curl -sL https://repos.influxdata.com/influxdb.key | sudo apt-key add -
source /etc/lsb-release
echo "deb https://repos.influxdata.com/${DISTRIB_ID,,} ${DISTRIB_CODENAME} stable" | sudo tee /etc/apt/sources.list.d/influxdb.list

deb https://repos.influxdata.com/ubuntu xenial stable 

sudo apt-get update && sudo apt-get install influxdb
Hit:1 http://security.ubuntu.com/ubuntu xenial-security InRelease
Hit:2 https://repos.influxdata.com/ubuntu xenial InRelease 
Hit:3 http://us.archive.ubuntu.com/ubuntu xenial InRelease
Hit:4 http://us.archive.ubuntu.com/ubuntu xenial-updates InRelease
Hit:5 http://us.archive.ubuntu.com/ubuntu xenial-backports InRelease
Reading package lists... Done
Reading package lists... Done
Building dependency tree 
Reading state information... Done
The following NEW packages will be installed:
 influxdb
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
Need to get 23.0 MB of archives.
After this operation, 76.5 MB of additional disk space will be used.
Get:1 https://repos.influxdata.com/ubuntu xenial/stable amd64 influxdb amd64 1.6.0-1 [23.0 MB]
Fetched 23.0 MB in 0s (27.7 MB/s) 
Selecting previously unselected package influxdb.
(Reading database ... 104707 files and directories currently installed.)
Preparing to unpack .../influxdb_1.6.0-1_amd64.deb ...
Unpacking influxdb (1.6.0-1) ...
Processing triggers for man-db (2.7.5-1) ...
Setting up influxdb (1.6.0-1) ...
Created symlink from /etc/systemd/system/influxd.service to /lib/systemd/system/influxdb.service.
Created symlink from /etc/systemd/system/multi-user.target.wants/influxdb.service to /lib/systemd/system/influxdb.service.

Im nächsten Schritt wird der Service influxdb gestartet

sudo service influxdb start

Eine Kontrolle zeigt, dass die Datenbank läuft

ps -ax|grep influx
 5372 ? Ssl 0:00 /usr/bin/influxd -config /etc/influxdb/influxdb.conf

User, Autorisierung und Authentifikation

Im momentanen Zustand ist influx noch vollkommen offen für jeden, deshalb werde ich in den nächsten Schritten User und deren Rechte anlegen. Eine detaillierte Beschreibung findet sich auf den Webseiten von InfluxDB.

Zunächst benötigen wir einen User „admin“, der alle Rechte besitzt. Ich starte influx im ssh-Terminal mit

influx
Connected to http://localhost:8086 version 1.6.0
InfluxDB shell version: 1.6.0
> 

Wir befinden uns jetzt auf der Befehlszeile der InfluxDB, alle weiteren Ein- und Ausgaben kommen von dort. Zunächst wird der User admin mit einem Passwort angelegt.

CREATE USER admin WITH PASSWORD '<password>' WITH ALL PRIVILEGES
>

Ich lege einen weiteren User an, dessen Rechte allerdings dann auf eine bestimmte DB erteilt werden.

CREATE USER <username> WITH PASSWORD '<password>'
>

Als nächstes erstelle ich eine Datenbank und gebe dem zuletzt angelegten User alle Rechte darauf

CREATE DATABASE <datenbank-name>
>

GRANT ALL ON "<datenbank-name>" TO "<username>"
>

Zusätzlich lege ich noch einen Read-only User auf diese DB an

CREATE USER RoUser WITH PASSWORD '<password>'
>

GRANT READ ON "<datenbank-name>" TO "RoUser"
>

Influx wird geschlossen mir Strg-d oder „exit“. Damit die Authentifikation generell wirksam wird, muss der Config-File noch angepasst werden.

sudo nano /etc/influxdb/influxdb.conf

darin im Abschnitt [http]

auth-enabled = true

setzen. Den Service dann restarten.

sudo service influxdb stop
sudo service influxdb start

Beim nächsten Aufruf von influx daran denken, dass sich nun der User admin zunächst mit

auth
username: admin
password: xxx

authentifizieren muss.

…..tbc…..

 

 

 

 

 

Logbuch – Server einrichten

In der letzten Zeit habe ich einige Server neu eingerichtet, hauptsächlich zur Nutzung für IoT Projekte. Empfehlen möchte ich hier die Cloud Server von Hetzner, die sich für diesen Zweck hervorragend eignen. Ich lasse mir die Server mit einer minimal Ubuntu Konfiguration betanken. Dieses Logbuch beschreibt die Installation eines Servers für das Projekt openair.cologne. Auf diesem Server werden die Sensordaten gespeichert und grafisch ausgewertet.

Folgende SW wird benötigt (wird mglw noch vervollständigt)
– Node-Red
– nginx
– Influx
– Grafana

Vorbereitung

Ich gehe davon aus, dass der Server bereits einen ssh Zugang besitzt. Ich nutze Linux aus der Command Line zur Verbindung mit dem Server, Windows Nutzer müssen hier Putty nutzen. Damit die nachfolgende Beschreibung auch auf andere Server angewendet werden kann, wurden die veränderbaren Daten in  <  > Klammern gesetzt und müssen entsprechend ersetzt werden.  Eingaben und Ausgaben des Servers sind farblich gekennzeichnet.

ssh <user>@<servername oder IP>
<user>@<servername> password:   xxx
Welcome to Ubuntu 16.04.4 LTS (GNU/Linux 4.4.0-128-generic x86_64)

* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage

0 packages can be updated.
0 updates are security updates.


*** System restart required ***
Last login: Tue Jul 10 20:38:06 2018 from 2.206.50.224
<user>@<servername>:~$

Als erster Schritt muss immer zuerst ein Update / Upgrade des Systems ausgelöst werden.

sudo apt-get update
sudo apt-get upgrade

Da ich immer gerne einen Filemanager einsetze, installiere ich mir noch den MidnightCommander.

sudo apt-get install mc

Installation node-red

Die Installation von node-red wird sehr gut in den Tutorials von DigitalOcean beschrieben. Daran habe ich mich weitestgehend orientiert.

sudo apt-get install nodejs-legacy

Testen, ob die Installation erfolgreich war

node -v
v4.2.6

Dann weiter

sudo apt-get install npm

und testen

npm -v
3.5.2

und jetzt die Installation von node-red.

sudo npm install -g --unsafe-perm node-red node-red-admin

Node-red wird nicht mit der Installation gestartet, zunächst müssen noch einige Konfigurationen vorgenommen werden. Als erstes lege ich  einen User an und vergebe sein  Passwort, unter dem node-red laufen soll.

sudo adduser node-red-user

Um node-red bei Systemstart automatisch starten zu lassen ist eine Datei zu erstellen

sudo nano /etc/systemd/system/node-red.service

mit folgendem Inhalt (ggf hier den Usernamen  ändern)

[Unit]
Description=Node-RED
After=syslog.target network.target

[Service]
ExecStart=/usr/local/bin/node-red-pi --max-old-space-size=128 -v
Restart=on-failure
KillSignal=SIGINT

# log output to syslog as 'node-red'
SyslogIdentifier=node-red
StandardOutput=syslog

# non-root user to run as
WorkingDirectory=/home/node-red-user/
User=node-red-user
Group=node-red-user

[Install]
WantedBy=multi-user.target

Dann wird der Service enabled

sudo systemctl enable node-red

und gestartet

sudo systemctl start node-red

Node-Red kann jetzt aus dem Browser mit dem Aufruf <servername:1880> gestartet werden. Zukünftig soll node-red nicht unter dem Port 1880 sonder unter <servername>/node-red aufgerufen werden, dazu benötige ich als Reverse Proxy nginx.

Installation nginx

nginx wird hier hauptsächlich als Reverse-Proxy eingesetzt, kann aber auch als Webserver genutzt werden. Weiterführende Tutorials findet ihr auf DigitalOcean.

sudo apt-get install nginx

nginx wird mit der Installation bereits als Service gestartet. Es wird getestet, ob nginx läuft.

systemctl status nginx
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2018-07-09 20:31:55 CEST; 24h ago
Main PID: 23970 (nginx)
Tasks: 2
Memory: 2.0M
CPU: 386ms
CGroup: /system.slice/nginx.service
├─23970 nginx: master process /usr/sbin/nginx -g daemon on; master_process on
└─23971 nginx: worker process

Zusätzlich sollte auch der Webservice erreichbar sein. Dazu aus dem Browser den Server unter seinem Namen oder seiner IP aufrufen. Zu sehen sein sollte eine Begrüßungsseite von nginx.

Um später auf die ursprüngliche Konfiguration zurückgehen zu können, benenne ich den Konfigurationsfile um

sudo mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default.old

und erstelle einen neuen – zunächst leeren File – mit

sudo nano /etc/nginx/sites-available/default

dieser File wird hiermit befüllt, <….> entsprechend anpassen

server {
   listen 80;
   server_name <servername>;
   root /var/www/html;
   index index.html index.htm index.nginx-debian.html;

   location /node-red {
      proxy_pass http://localhost:1880;
      proxy_http_version 1.1;
      proxy_set_header Host $http_host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
   }
}

und der Service wird restartet

sudo systemctl restart nginx

Damit node-red auch richtig funktioniert muss noch eine kleine aber wesentliche Änderung vorgenommen werden, dazu editieren

sudo nano /home/node-red-user/.node-red/settings.js

und diese Zeile anpassen

 httpRoot: '/node-red',

danach den Service neu starten mit

sudo systemctl stop node-red
sudo systemctl start node-red

Danach sollte node-red aus dem Browser mit <servername>/node-red aufrufbar sein.

Node-Red User Authentication

Momentan ist node-red noch vollkommen offen aus dem Internet. Ich lege einen/mehrere User an, die node-red nutzen dürfen. Dazu wird die Datei settings.js editiert. Vorher öffne ich noch ein zweites ssh Fenster auf dem gleichen Server. In diesem Fenster kann ich den Hash erzeugen. Ich öffne die settings.js im ersten Fenster

sudo nano /home/node-red-user/.node-red/settings.js

in dieser Datei findet man einen Bereich unterhalb von // Securing Node-RED mit einem Beispiel, wie die Authentisierung eingefügt werden muss. Hier ein Beispiel, wie der einzufügende Teil für einen User aussehen sollte

adminAuth: {
   type: "credentials",
   users: [{
       username: "admin",
       password: "your hash",
       permissions: "*"
   }]
},

Das Komma am Schluss des Blocks nicht vergessen. Werden mehrere User eingerichtet, muss jeder User in { } gesetzt werden.

adminAuth: {
   type: "credentials",
   users: [
       {
       username: "admin",
       password: "your hash",
       permissions: "*"
       },
       {
       username: "user2",
       password: "user2 hash",
       permissions: "*"
       }
   ]
},

Den Hash erzeuge ich im zweiten Fenster, ich werde nach meinem sudo Passwort gefragt und danach nach dem Passwort for Node-Red.

sudo node-red-admin hash-pw
[sudo] password for <user>:    xxx 
Password:  xxx
$2a$08$refeJAyPqLeeb6EIfNv0Mu.EhZMV6H/wZlJleUdTZWWNplrjfuZ5y

Den ausgegebenen Hash kopiere im anderen Fenster in den File settings.js.

password: "$2a$08$refeJAyPqLeeb6EIfNv0Mu.EhZMV6H/wZlJleUdTZWWNplrjfuZ5y",

Nach dem Speichern des Files und einem Neustart von Node-Red

sudo systemctl stop node-red
sudo systemctl start node-red

werde ich nach dem Aufruf der Node-Red Seite im Browser nach meinen Zugangsdaten gefragt.

node-red1

Installation InfluxDB

…..  mit der Installation der InfluxDB geht’s weiter im nächsten Blogpost.

 

 

Create your own workflow to visualize your TTN coverage

ttn-koln2.png

In short

To determine the radio coverage, GPS data is sent via TheThingsNetwork, processed with Node-Red and stored in an InfluxDB. The data is read from the database via JS and displayed graphically on a map.

The detailed version

This blog post will be the first of a series related to TheThingsNetwork. For some months now I have been actively involved in the development of the TheThingsNetwork in the city of Cologne. The TheThingsNetwork initiative aims to build an open and free wireless infrastructure for the Internet of Things. The community provides the gateways for use by everyone and the central infrastructure is provided by various data centers worldwide, for Europe by the Netherlands.

When building a radio infrastructure, the most important information is the range and coverage of the city in the area. The TTN Mapper from JP Meijers works very well for this application. If I had a smartphone that would be suitable for this app, this post would probably never have existed.
For my own purposes, however, I wanted to build a webpage that I could adapt flexibly to my own needs. In addition, it was a special attraction for me to familiarize myself with this topic and to master the task.

The Node

The first task was to build a TTN node equipped with a GPS module that sends the GPS data via the TTN. The construction of this node was similar to the node described by Bjoern in his blog. A GPS module is serially connected to an Arduino Pro Mini and an RFM95W is used to send the data. The circuits are powered by a lithium-ion battery and the voltage is stabilized by a low quiescent current LDO and a low dropout voltage type HX7333.  Even if the structure doesn’t look very professional, it still fulfils its function.

img_20171121_192556.jpg

The antenna used is an old GSM 900MHz window antenna. The only change to Bjoern’s software is that in my case OTAA activation is used.
The software checks the validity of the GPS data and sends latitude and longitude data encoded in 6 bytes. The relevant lines here:

void get_coords () {
  bool newData = false;
  unsigned long chars;
  unsigned short sentences, failed;
  float flat, flon;
  unsigned long age;

  // For one second we parse GPS data and report some key values
  for (unsigned long start = millis(); millis() - start < 1000;) {
    while (SoftS.available()) {
      char c = SoftS.read();
      Serial.write(c); // uncomment this line if you want to see the GPS data flowing
      if (gps.encode(c)) { // Did a new valid sentence come in?
        newData = true;
      }
    }
  }

  if ( newData ) {
    gps.f_get_position(&flat, &flon, &age);
    flat = (flat == TinyGPS::GPS_INVALID_F_ANGLE ) ? 0.0 : flat;
    flon = (flon == TinyGPS::GPS_INVALID_F_ANGLE ) ? 0.0 : flon;
  }

  gps.stats(&chars, &sentences, &failed);

  int32_t lat = flat * 10000;
  int32_t lon = flon * 10000;

  // Pad 2 int32_t to 6 8uint_t, big endian (24 bit each, having 11 meter precision)
  coords[0] = lat;
  coords[1] = lat >> 8;
  coords[2] = lat >> 16;

  coords[3] = lon;
  coords[4] = lon >> 8;
  coords[5] = lon >> 16;
  }
} ... 

The complete program code can be found in my git. Define an application and device in the TheThingsNetwork console and fill in the keys in the program code.
To decode the data in the TTN cloud define this function in the TTN Application Console.

function Decoder(b, port) {
 var lat = (b[0] | b[1]<<8 | b[2]<<16 | (b[2] & 0x80 ? 0xFF<<24 : 0)) / 10000;
 var lng = (b[3] | b[4]<<8 | b[5]<<16 | (b[5] & 0x80 ? 0xFF<<24 : 0)) / 10000;
return {
 location: {
 lat: lat,
 lng: lng
 },
 love: "TTN payload functions"
 };
}

Data Processing

Node-Red handles the further processing of the data from the TTN cloud. For the first attempts an installation on a RaspberryPi is sufficient or you can use an online version on https://fred.sensetecnic.com/

The gps-logger node is connected to the application in the TTN cloud and receives json fomatted data. The function node decodes the data and prepares the data for the influxdb. The influxdb node finally writes the data to the influxdb. For each gateway receiving the data a record is written in the database. Later when visualizing the data on the map only the record with the highest rssi will be drawn to the map. The record will keep data for the gps location of the node, the gps location and ID of the receiving gateway, rssi, snr, the node’s counter of the record and the device ID of the node. Also the distance between the node and the gateway is calculated and written to the database but not used so far. The structure of the record is defined in the multiple-readings function node.

var lgid = msg.metadata.gateways.length;
var array_aussen = [];
var array_innen = [];
var last_time;
for (i = 0; i < lgid; i++) {
 array_innen = [{
 counter: msg.counter,
 rssi: msg.metadata.gateways[i].rssi,
 snr: msg.metadata.gateways[i].snr,
 lat_gw: msg.metadata.gateways[i].latitude,
 lon_gw: msg.metadata.gateways[i].longitude,
 no_gw: lgid,
 lat_sense: msg.payload.location.lat,
 lon_sense: msg.payload.location.lng,
 dist2gw: distance(msg.metadata.gateways[i].latitude,msg.metadata.gateways[i].longitude,msg.payload.location.lat,msg.payload.location.lng),
 time: new Date(msg.metadata.gateways[i].time).getTime() * 1000000 || 0
 },
 {
 dev_id: msg.dev_id,
 gtw_id: msg.metadata.gateways[i].gtw_id
 }
 ];
 if (array_innen[0].time === last_time) {
 array_innen[0].time += 1000;
 }
 array_aussen.push(array_innen);
}
var msg1 = {};
msg1.payload = array_aussen;
var msg2 = {};
msg2.payload = array_innen[0].time;
return [msg1, msg2];
function distance(gw_lat, gw_lon,sense_lat, sense_lon){
 if(sense_lat<1 || sense_lon < 1){
 return -1;
 }
 else {
 return Math.round(Math.sqrt(Math.pow(70.12*(gw_lon-sense_lon),2) + Math.pow(111.3*(gw_lat-sense_lat),2))*1000);
 }}

The Website

The far more difficult part of the task was the creation of the website. My Javascript knowledge was rather rudimentary in the beginning and I had to learn a lot while programming the website. For those who want to deal with this, the source code is located on the git. The website performs the following functions: The data is fetched by ajax from the influxdb, first the data of the receiving gateways, then the gps data of the node. The data is displayed as colored squares with the JS extension LeafletJS on an Openstreetmap. If you move the mouse over the squares, detailed data such as rssi, snr, number of receiving gateways and ID of the gateway with the highest rssi are displayed. The coding of color of the squares for rssi is the same as in the ttnmapper. If you hover over a gateway, the gateway ID is displayed.

The time period can be limited by passing HTTP GET parameters. In this way, you can also select whether the map should be colored or black and white. To overpass the parameters a simple http form is provided. A separately recorded gpx track can be placed under the data via a control element on the page. In this way, it is possible to illustrate very clearly where there is no radio coverage. The following video shows the possibilities that the website offers perhaps best.

VIDEO – coming soon

last but not least

The workflow described here may not be suitable for processing a lot of mapping data. I provide the members of the TTN community in Cologne with access to this server, so the amount of data is limited. The workflow is very simple and you don’t need to have a very high programming knowledge to understand it. Maybe this will be also interesting for other communities. You are welcome to try out the website, here in direct access to all available timeseries data or via the filter page with input option for the time span and selection of the optional b/w representation.

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();
}
} ... 

(edit nach Hinweis von Wolfgang) Zusätzlich wird die globale Variable

unsigned long lastmillis; 

im oberen Teil des Programms definiert.
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.

 

 

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.