Archiv der Kategorie: node-red
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.
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&amp;lt; NumOfGW; i++){ gwArr.push(Object.keys(msg.payload)[i]); } for (var i = 0; i&amp;lt; 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.
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;longitude=6.970313&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;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;lt; NumOfGW; i++){\n gwArr.push(Object.keys(msg.payload)[i]); \n}\nfor (var i = 0; i&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;lt;timeDiff60){\n message.iconColor=\"red\";\n }\n else if (new Date(msg.payload[gwArr[i]].last_seen)&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
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.
Installation InfluxDB
….. mit der Installation der InfluxDB geht’s weiter im nächsten Blogpost.
Create your own workflow to visualize your TTN coverage
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.
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.