Internet of Things/Watson-opdrachten
Inleiding
We geven deze opdrachten "van achter naar voor". We beginnen aan de kant van de acties, en werken terug via de externe data naar de beslissingen.
Voor het testen van de uitwerkingen kun je dankbaar gebruik maken van de mogelijkheden die NodeRed hiervoor biedt, zoals de inject- en debug-nodes.
Van event naar actie
Je kunt de onderstaande opdrachten het beste uittesten met behulp van een inject-node: daarmee genereer je de event en de bijbehorende data. In een later stadium koppel je dan de output-node aan de keten die de eigenlijke event met bijbehorende data genereert.
Verstuur een e-mail (of een twitterbericht)
Je kunt vanuit NodeRed een event laten resulteren in het versturen van een e-mail. Hiervoor gebruik je de E-mail output node. Deze moet je configureren (double click):
- vul de geadresseerde in (To)
- vul de naam van de mail-server (SMTP server in) en de bijbehorende port
- geef je user name en wachtwoord
N.B.: mail servers zoals gmail zijn tegenwoordig redelijk kritisch m.b.t. de toepassingen die daarvan gebruik maken. NodeRed wordt als een minder veilige toepassing gezien. Je kunt deze beveiliging eventueel tijdelijk uitzetten ("allow access for less secure apps"). Als je two-factor authentication gebruikt, kun je een app-specifiek wachtwoord genereren voor NodeRed. Dat wachtwoord kun je dan ook weer gemakkelijk intrekken. (Deze aanpak heeft mijn voorkeur.)
Voor het genereren van de inhoud van de mail gebruiken we een inject-node.
- gebruik als payload: "string"
- en vul als waarde voor deze string je mail-bericht in
- verbindt de inject-node met de mail-output-node
Door het activeren van de inject-node wordt een bericht gestuurd naar de mail-output-node; deze verstuurt de mail met de payload als inhoud.
In een complete flow zul je de inhoud van de mail waarschijnlijk anders willen genereren. Je kunt daarvoor bijvoorbeeld een template-node gebruiken, waarin je bijv. de waarde van een sensor in kunt vullen.
Vervolgstap
Vervolgstap:
- maak een flow van de inject-node, via een template-node, naar de mail-output-node.
- in het template beschrijf je de inhoud van het mail-bericht
- hierbij geef je door middel van
payload
aan waar de inhoud van de inkomende payload ingevuld moet worden. Zie het onderstaande voorbeeld.
- hierbij geef je door middel van
- vul in de inject-node een waarde voor de payload in (als string), om te testen.
Mail vanuit NodeRed: de temperatuursensor heeft de alarmerende waarde van: {{payload}}. Waarschuw zo snel mogelijk de personen in de buurt.
Versturen van een twitterbericht
Dit is analoog aan het versturen van een e-mail. Je hebt hiervoor de gegevens van een twitter-account nodig.
Versturen van een spraak-bericht naar een webtoepassing
In deze stap gebruiken we de BlueMix/Watson-node voor het omzetten van tekst naar spraak. Dit werkt alleen in NodeRed-apps op BlueMix.
Je moet eerst de spraak-service activeren en koppelen aan je NodeRed-app:
- ga naar het BlueMix Dashboard, en klik op "create service"
- selecteer onder "services": Watson -> Speech to text
- creëer deze service; deze moet dan verschijnen in de lijst van actieve services in het dashboard
- verbindt deze service aan je app:
- klik op de service in het dashboard
- selecteer de tab "connections", en maak een connection voor je app.
- je krijgt dan de melding dat "restaging" van je app nodig is.
- daarna is de Watson-node "text to speech" beschikbaar in je app.
Je kunt deze text-to-speech node uittesten met de volgende flow:
[{"id":"66e9d467.2f6ea4","type":"watson-text-to-speech","z":"1cb7579d.55d42","name":"Hello","lang":"en-US","langhidden":"en-US","voice":"en-US_LisaVoice","voicehidden":"en-US_LisaVoice","format":"audio/wav","password":"c8NIlTTJie2z","x":286.5,"y":263,"wires":[["24f4abc0.eada5c"]]},{"id":"e5b9682d.fbe5d8","type":"inject","z":"1cb7579d.55d42","name":"Hello","topic":"","payload":"Hello there!","payloadType":"str","repeat":"","crontab":"","once":false,"x":103.5,"y":263,"wires":[["66e9d467.2f6ea4"]]},{"id":"276b9206.20eefe","type":"http in","z":"1cb7579d.55d42","name":"","url":"/hello","method":"get","swaggerDoc":"","x":92.5,"y":408,"wires":[["7e727858.28dfc8"]]},{"id":"7e727858.28dfc8","type":"template","z":"1cb7579d.55d42","name":"hello-html","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<!DOCTYPE html>\n <html>\n <head>\n <title>IBM Watson - Text To Speech</title>\n <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js\"></script>\n \n <script type=\"text/javascript\">\n var socketaddy = \"wss://\" + window.location.host + \"/ws/audio\";\n $(document).ready(function(){\n var output = document.getElementById('output')\n $('#output').on('playing', function () {\n $('#text').text('Playing audio.')\n \n });\n $('#output').on('ended', function () {\n $('#text').text('Waiting for audio...')\n \n });\n sock = new WebSocket(socketaddy);\n sock.onopen = function(){\n $('#text').text('Waiting for audio...');\n console.log(\"Connected websocket\");\n };\n sock.onerror = function(){ \n console.log(\"Websocket error\"); \n };\n sock.onclose = function () {\n $('#text').text('Not connected. Refresh the page?')\n }\n sock.onmessage = function(evt){\n console.log(\"Websocket message\", evt); \n output.src = window.URL.createObjectURL(evt.data);\n output.play();\n };\n });\n </script>\n \n </head>\n <body style=\"font-size: 56px; font-family: helvetica; text-align: center; margin-top: 100px;\">\n <div id=\"text\">Connecting...</div>\n <audio id=\"output\"></audio>\n </body>\n </html> ","x":377.5,"y":408,"wires":[["a6212bb3.02802"]]},{"id":"a6212bb3.02802","type":"http response","z":"1cb7579d.55d42","name":"","x":706.5,"y":407,"wires":[]},{"id":"33aaa87b.5ac66","type":"websocket out","z":"1cb7579d.55d42","name":"","server":"9996f125.7aead8","client":"","x":725.5,"y":264,"wires":[]},{"id":"24f4abc0.eada5c","type":"function","z":"1cb7579d.55d42","name":"speech to payload","func":"msg.payload = msg.speech;\nreturn msg;","outputs":1,"noerr":0,"x":497.5,"y":264,"wires":[["33aaa87b.5ac66"]]},{"id":"3aa49b94.7ada4c","type":"websocket in","z":"1cb7579d.55d42","name":"","server":"9996f125.7aead8","client":"","x":490.5,"y":327,"wires":[["33aaa87b.5ac66"]]},{"id":"9996f125.7aead8","type":"websocket-listener","z":"1cb7579d.55d42","path":"ws/audio","wholemsg":"false"}]
De tekst van het html-document in de hello-html node is als volgt:
<!DOCTYPE html>
<html>
<head>
<title>IBM Watson - Text To Speech</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script type="text/javascript">
var socketaddy = "wss://" + window.location.host + "/ws/audio";
$(document).ready(function(){
var output = document.getElementById('output')
$('#output').on('playing', function () {
$('#text').text('Playing audio.')
});
$('#output').on('ended', function () {
$('#text').text('Waiting for audio...')
});
sock = new WebSocket(socketaddy);
sock.onopen = function(){
$('#text').text('Waiting for audio...');
console.log("Connected websocket");
};
sock.onerror = function(){
console.log("Websocket error");
};
sock.onclose = function () {
$('#text').text('Not connected. Refresh the page?')
}
sock.onmessage = function(evt){
console.log("Websocket message", evt);
output.src = window.URL.createObjectURL(evt.data);
output.play();
};
});
</script>
</head>
<body style="font-size: 56px; font-family: helvetica; text-align: center; margin-top: 100px;">
<div id="text">Connecting...</div>
<audio id="output"></audio>
</body>
</html>
- importeer en deploy deze flow in je NodeRed-app in BlueMix
- open in je browser een webpagina met de URL van je app, met als extra pad:
/hello
- bijvoorbeeld:
https://iot123.eu-gb.mybluemix.net/hello
- bijvoorbeeld:
- je krijgt de melding in de browser: waiting for audio;
- activeer in NodeRed de inject-node (klik op de button links);
- als het goed is, krijg je dan de tekst als spraak te horen;
- je kunt de tekst in de inject-node veranderen.
Opmerkingen:
- gebruik als browser Firefox of Chrome; bij andere browsers kan dit problemen geven (websockets?)
- controleer eventueel of de websockets-nodes goed geconfigureerd zijn:
- deze moeten als path ("listener")
/ws/audio
hebben. - je kunt ook controleren of je config-nodes correct zijn: zie in het hamburger-menu rechts boven, "Configuration nodes".
- deze moeten als path ("listener")
Vervolgstap
Je kunt de tekst op eenzelfde manier parametriseren als een e-mail. Hiervoor gebruik je een template-node waarin de payload uit een andere node ingevuld wordt.
Stuur een LED aan
Voor het aansturen van een LED in een sensor-node moeten we een MQTT-bericht naar de broker sturen. Via de MQTT-subscribe van de sensornode komt het bericht dan op de bestemming.
Met de volgende flow kun je een LED van je sensor-node aansturen:
[{"id":"1cb97f97.19bdb","type":"mqtt out","z":"f900c06c.a92a48","name":"","topic":"node/e1bd/actuators","qos":"","retain":"","broker":"68911311.d14e14","x":428.5,"y":351,"wires":[]},{"id":"31953437.bb76e4","type":"inject","z":"f900c06c.a92a48","name":"","topic":"","payload":"{\"led0\": 1}","payloadType":"json","repeat":"","crontab":"","once":false,"x":117.5,"y":346,"wires":[["1cb97f97.19bdb"]]},{"id":"18ca8a7e.254f8e","type":"inject","z":"f900c06c.a92a48","name":"","topic":"","payload":"{\"led0\": 0}","payloadType":"json","repeat":"","crontab":"","once":false,"x":116.5,"y":405,"wires":[["1cb97f97.19bdb"]]},{"id":"68911311.d14e14","type":"mqtt-broker","z":"f900c06c.a92a48","broker":"infvoplein.nl","port":"1883","clientid":"","usetls":false,"compatmode":true, "keepalive":"60","cleansession":true,"willTopic":"","willQos":"0","willRetain":"false","willPayload":"", "birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":""}]
- de inject-nodes gebruiken als type: JSON (
"{}"
).- je kunt dan als waarde een JSON-object invullen, bijvoorbeeld
{"Led0": 1}
- je kunt dan als waarde een JSON-object invullen, bijvoorbeeld
- de MQTT-output-node moet geconfigureerd worden
- vul de juiste broker in
- vul het juiste topic in, met de identificatie van je eigen sensornode.
N.B. door meerdere import-acties kun je meerdere configuraties voor dezelfde broker krijgen. Deze zitten elkaar in de weg. Dit kun je zien in het overzicht van de config-nodes. Eventuele duplicaten van dezelfde broker moet je verwijderen.
Data uit andere bronnen
Beslissingen die tot een actie leiden nemen we meestal niet alleen op grond van de sensordata: we gebruiken ook data uit andere bronnen. In deze opdrachten halen we als voorbeeld weer-data uit twee bronnen:
- via de NodeRed IBM/Weather Insights-node kunnen we de weergegevens voor een bepaalde locatie ophalen: historische data, actuele data, of voorspellingen;
- dit is een voorbeeld van een dienst die gestructureerde data aanbiedt, in JSON-formaat. We krijgen dit als JavaScript-object in de payload, waardoor we eenvoudig elementen hieruit kunnen selecteren.
- via een combinatie van een HTTP-request-knoop en knopen voor het extraheren van HTML-elementen kunnen we de weergegevens van het KNMI of Buienradar ophalen. Ook in dit geval kan het gaan om actuele data of om voorspellingen.
- dit is een voorbeeld van een dienst die ongestructureerde of semi-gestructureerde data aanbiedt: we moeten de data zelf extraheren en in een structuur (JavaScript-object) onderbrengen. Voor dit extraheren biedt NodeRed, vooral in combinatie met Watson, de nodige middelen.
Weather Insights
Voordat we deze dienst kunnen gebruiken, moeten we deze activeren en verbinden aan onze app. Dit gaat op eenzelfde manier als bij de Speech-to-text dienst.
We gebruiken de onderstaande flow:
[[{"id":"e1196304.7235b8","type":"weather_insights","z":"1cb7579d.55d42","name":"Waalre-weer","service":"/observations.json","geocode":"51.40,5.47","units":"m","language":"","x":371.5,"y":91,"wires":[["35545aa1.106586"]]},{"id":"35545aa1.106586","type":"debug","z":"1cb7579d.55d42","name":"","active":true,"console":"false","complete":"observation", "x":642.5,"y":92,"wires":[]}, {"id":"d6c8b914.8b845","type":"inject","z":"1cb7579d.55d42","name":"","topic":"","payload":"","payloadType":"date","repeat":"", "crontab":"","once":false,"x":144.5,"y":91,"wires":[["e1196304.7235b8"]]}]
Je kunt de weather-insights-node aanpassen:
- je kunt aangeven of je het huidige weer wilt, historische data, of een voorspelling;
- je kunt de locatie aangeven
- pas dit aan voor een eigen locatie, bijvoorbeeld in Amsterdam
- deze geef je aan in lengte- en breedtegraden, gescheiden door een komma, zonder spaties
- deze locatie kun je bijvoorbeeld via Google Maps vinden, voor een plaats waarvan je het weer wilt weten
- en nog meer, zoals de eenheden en de taal.
Je moet de debug-node eventueel aanpassen: afhankelijk van wat je opvraagt, heb je een andere msg-property nodig. Voor de huidige weerstoestand is dat msg.observation
.
Bekijk in het debug-venster het resultaat van deze flow (als gevolg van het activeren van de inject-node).
Voorbeeld van de uitvoer hiervan:
{ "key": "EHEH", "class": "observation", "expire_time_gmt": 1481729100, "obs_id": "EHEH", "obs_name": "Eindhoven", "valid_time_gmt": 1481721900, "day_ind": "D", "temp": 10, "wx_icon": 28, "icon_extd": 2800, "wx_phrase": "Mostly Cloudy", "pressure_tend": null, "pressure_desc": null, "dewPt": 7, "heat_index": 10, "rh": 82, "pressure": 1020.33, "vis": 9, "wc": 10, "wdir": null, "wdir_cardinal": "VAR", "gust": null, "wspd": 9, "max_temp": null, "min_temp": null, "precip_total": null, "precip_hrly": null, "snow_hrly": null, "uv_desc": "Low", "feels_like": 10, "uv_index": 1, "qualifier": null, "qualifier_svrty": null, "blunt_phrase": null, "terse_phrase": null, "clds": "BKN", "water_temp": null, "primary_wave_period": null, "primary_wave_height": null, "primary_swell_period": null, "primary_swell_height": null, "primary_swell_direction": null, "secondary_swell_period": null, "secondary_swell_height": null, "secondary_swell_direction": null }
Zoals je ziet zijn dit gestructureerde data: elementen als temp
en pressure
kun je direct selecteren.
- Eigenlijk hoort hierbij nog een beschrijving (schema) van deze data, waarin voor elk onderdeel precies de betekenis beschreven wordt.
KNMI data
Je kunt ook ongestructureerde of semi-gestructureerde data ophalen. Als voorbeeld behandelen we het huidige weer in Eindhoven, zoals weergegeven in de pagina van het KNMI: http://knmi.nl/nederland-nu/weer/waarnemingen
- via de HTTP-request-node halen we deze pagina op;
- uit het resultaat, een HTML-document, destilleren we de gegevens voor Eindhoven. Hiervoor gebruiken we de HTML-node: daarmee kunnen we HTML-elementen selecteren, bijvoorbeeld aan de hand van CSS-selectoren.
- De HTML-functieknoop gebruik Cheerio (https://github.com/cheeriojs/cheerio/blob/master/Readme.md), met CSS/jQuery-syntax voor het selecteren van elementen.
We gebruiken de volgende flow:
[{"id":"5b56c281.203484","type":"debug","z":"c9456f27.49186","name":"","active":true,"console":"false","complete":"false", "x":645.5,"y":105,"wires":[]}, {"id":"870ebbdd.1d4758","type":"inject","z":"c9456f27.49186","name":"","topic":"","payload":"","payloadType":"date","repeat":"", "crontab":"","once":false,"x":117.5,"y":104,"wires":[["d613812a.4e9e88"]]},{"id":"d613812a.4e9e88","type":"http request","z":"c9456f27.49186","name":"","method":"GET","ret":"txt","url":"http://knmi.nl/nederland-nu/weer/waarnemingen","tls":"","x":282.5,"y":105,"wires":[["255dd6f2.e81bfa"]]},{"id":"255dd6f2.e81bfa","type":"html","z":"c9456f27.49186","name":"Eindhoven","tag":"td:contains('Eindhoven') ~ td","ret":"html","as":"single","x":466.5,"y":105,"wires":[["5b56c281.203484"]]}]
Voor de HTML-node gebruiken we de selectie-expressie:
td:contains('Eindhoven') ~ td
Dit betekent: selecteer de "sibling" td-elementen van het td-element met inhoud "Eindhoven". Dit zijn te tabel-cellen (td) in de rij die met de cel met "Eindhoven" begint.
Het resultaat is zoiets:
[ "half bewolkt", "9.6", "8.7", "82", "ZO", "2", "12100", "1023.3" ]
We moeten dan nog weten wat de verschillende kolommen voorstellen. Als we de selectie-expressie iets veranderen, in
th.contains('Station') ~ th
dan krijgen we de tabel-kopjes:
[ "Weer", "Temp (°C)", "Chill (°C)", "RV (%)", "Wind", "Wind (m/s)", "Zicht (m)", "Druk (hPa)" ]
Hieruit zien we welke kolommen (waarschijnlijk) de temperatuur en de druk voorstellen.
Voordat we dergelijke data via zo'n selectie uit een HTML-document kunnen halen, moeten we eerst de structuur van die pagina analyseren. We kunnen deze bijvoorbeeld bekijken via de bronpagina-weergave in de browser.
Opmerking: het is op geen enkele manier gegarandeerd dat de pagina's van het KNMI dezelfde structuur blijven houden. We moeten onze flow dus in de toekomst mogelijk aanpassen.
Opdrachten:
- pas deze flow aan voor het weer in de buurt van Amsterdam;
- maak uit het resultaat een gestructureerde waarde met onder andere "temp" en "pressure" als properties. Lever dit op als payload-object
- voor het bovenstaande zou een deel van het resultaat zijn:
msq.payload.temp === 9.6
- voor het bovenstaande zou een deel van het resultaat zijn:
Watson
Onder BlueMix biedt Watson nog meer mogelijkheden, bijvoorbeeld om Word-, HTML-, of PDF documenten om te zetten in andere documenten, of in "answer units". Hiervoor moet je de betreffende Document Conversion-dienst inschakelen en koppelen aan je NodeRed-toepassing.
Beslissingen
Op grond van de sensordata en eventuele externe data kunnen we een beslissing nemen, waar we een actie aan koppelen. In deze opdrachten bekijken we een aantal mogelijkheden om met sensordata te rekenen en tot een beslissing voor een actie te komen.
Van ruwe sensordata naar fysische grootheden
Een analoge sensor levert een spanning, die via een A/D omzetter resulteert in een getal. Dit getal moeten we omzetten in een fysische grootheid.
Voorbeeld: een temperatuursensor van het type LM36DZ levert een spanning op die evenredig is met de temperatuur, typisch 10mV per graad Celcius. 0V komt overeen met 0 graden Celcius, en 1V met 100 graden Celcius.
Deze spanning zetten we via de A/D omzetter van de Arduino om in een getal x, tussen 0 (0V) en 1023 (5V; 10-bits resolutie). Dit getal moeten we omrekenen naar graden Celcius:
t = x * 500 / 1023
We kunnen ook met gehele getallen rekenen, in 1/10 graad Celcius: t = x * 5000 / 1023
.
Deze berekening kunnen we in de Arduino-code doen, bijvoorbeeld:
long temp = sensorvalue * 5000L / 1023L;
(We rekenen hier met long waarden omdat de tussenresulaten groter zijn dan een 16-bits int.)
Voor dit doel kun je ook de Arduino library-function map
gebruiken:
long temp = map(sensorvalue, 0, 1023, 0, 5000);
We kunnen deze berekening ook in NodeRed uitvoeren. Hiervoor kun je de range-node gebruiken.
Opdracht: verwerk het MQTT-bericht van je sensornode, met een waarde van een analoge sensor (bijvoorbeeld voor temperatuur). Reken deze in de Arduino om naar een waarde in 1/10 graden Celcius, als geheel getal (bijv. 213 voor 21,3'C), en stuur dit getal als de waarde van sensor0 in het MQTT-bericht. Reken deze waarde in NodeRed om naar 'C voor de eigenschap "temperatuur".
- gebruik voor dit omrekenen een function-node (naam bijvoorbeeld "Get temperature"), met als code:
msg.payload.temperature = ...expressie met msg.payload.sensor0 ...
Switch, Report by Exception
Vaak hoef je alleen actie te ondernemen als een sensor-waarde verandert, of buiten een bepaald bereik komt. Hiervoor kun je de "report by exception" (RbE) node gebruiken, of de switch-node
Opdracht: stuur een bericht (naar keuze) als de temperatuur van je "ding" buiten een vooraf ingesteld bereik komt.
Hint: gebruik de switch-node, en splits de input in messages die binnen het bereik vallen, en messages die daarbuiten vallen (otherwise). Alleen deze laatste koppel je aan een actie.
Een mogelijke flow (in test-mode):
Koppelen van een LED aan twee knoppen
Opdracht: maak een flow waarin je de LED van je sensornode (of van iemand anders) bestuurt door middel van de knoppen op je eigen sensornode (als aan- en uitknop).