Cours:LPTraitDonnee2
Partie 2 : traitement temps réel de données avec NodeRed
L'objectif de cette partie est d'approfondir l'écriture de bloc NodeRed en Javascript.
Sommaire
Données utilisées
L'IUT est équipé depuis peu de modules de mesure en temps réel de la consommation électrique. Ces mesures sont publiées sur le serveur MQTT d'adresse 10.98.35.245. Voici les topics utilisés :
Topics MQTT mesure consommation électrique IUT
elec/general/courant/i1 elec/mmi/courant/i1 elec/geii/courant/i1 elec/gmp/courant/i1 elec/appartements/courant/i1 elec/batA/courant/i1 elec/batE/courant/i1 elec/chaufferie/courant/i1 elec/general/courant/i2 elec/mmi/courant/i2 elec/geii/courant/i2 elec/gmp/courant/i2 elec/appartements/courant/i2 elec/batA/courant/i2 elec/batE/courant/i2 elec/chaufferie/courant/i2 elec/general/courant/i3 elec/mmi/courant/i3 elec/geii/courant/i3 elec/gmp/courant/i3 elec/appartements/courant/i3 elec/batA/courant/i3 elec/batE/courant/i3 elec/chaufferie/courant/i3
- La nomenclature est la suivante :
elec/[bât]/courant/i{1,2,3}
. - Le courant est triphasé :
i1
,i2
eti3
désignent les courants de chaque phase. - Les courants sont donnés en mA.
Travail à faire
Exercice 1
- Vérifier que vous arrivez à lire, dans un terminal, les données de consommation
- Lire le guide suivant (Javascript en NodeRed) et reproduire les exemples proposés. Plusieurs techniques seront utiles pour la suite (
join
etcontext
par exemple) : http://noderedguide.com/node-red-lecture-5-the-node-red-programming-model/
Exercice 2
- Tracer dans un graphe la consommation en temps réel de l'iut (
elec/general/courant/*
: les trois phases). - Essayer de tracer pour une phase (par exemple i1), la somme de courants de tous les bâtiments.
- Il sera probablement nécessaire d'exploiter le nœud "join". Vous pourrez recherche des indications sur le net, comme par exemple avec les mots-clés "node-red join sum".
- Confronter cette valeur à la mesure générale. Qu'en pensez-vous ?
Exercice 3
Des erreurs peuvent venir perturber aléatoirement les mesures. Il s'agit ici d'identifier deux types de perturbation (bruit) et de chercher à les corriger (filtrage).
1. Les topics data/d0
et data/d1
publient des versions bruitées du courant publié dans le topic elec/general/courant/i1
. Afficher ces courants dans des graphes, puis essayer de caractériser les deux bruits.
2. Pour les corriger, il va falloir filtrer les données en temps réel dans un nœud de type "function". Deux filtres vont être mis en œuvre : un filtre linéaire (la moyenne) et un filtre non-linéaire (la médiane).
- Il s'agit dans un premier temps de réaliser la moyenne des trois dernières valeurs. Voici le code du nœud qui réalise cette opération :
var v2 = context.get('v2') || 0;
var v1 = context.get('v1') || 0;
var v0 = Number(msg.payload);
context.set('v2',v1);
context.set('v1',v0);
msg.payload = Math.floor((v0+v1+v2)/3);
return msg;
- Créer le nœud correspondant et afficher les graphes des deux courants bruités filtrés avec cette moyenne. La moyenne est-elle efficace ? Sur les deux bruits ?
- Modifier le code afin de réaliser la moyenne sur les cinq dernières valeurs. Le filtrage est-il de meilleure qualité ?
3. Voici un extrait de code permettant de calculer la médiane des valeurs d'un tableau :
function median(arr){
arr.sort(function(a, b){ return a - b; });
var i = arr.length / 2;
return i % 1 == 0 ? (arr[i - 1] + arr[i]) / 2 : arr[Math.floor(i)];
}
Vous pouvez la placer en début du code d'un nœud afin de l'utiliser
- Créer un nouveau nœud de type "function" afin de réaliser un filtrage par médiane. Ce filtrage est-il efficace ? Sur les deux bruits ?
4. Conclure sur la nature des deux bruits et sur la meilleure façon de corriger chacun d'eux.
Outils potentiellement utiles
- Test en javascript (dans un nœud function) : https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Statements/if...else
- Buffer circulaire (ou pile FIFO) : https://flows.nodered.org/node/node-red-contrib-ring-buffer.
Exemple d'utilisation de ce buffer circulaire (5 dernières valeurs)
[{"id":"cd90371f.731b38","type":"tab","label":"Flow 3","disabled":false,"info":""},{"id":"5daf2445.fc766c","type":"mqtt in","z":"cd90371f.731b38","name":"","topic":"elec/general/courant/i1","qos":"2","datatype":"utf8","broker":"b2923e3a.7b32c","x":140,"y":80,"wires":"a3ade5d6.5885f8"},{"id":"e8d3dc2a.1031b","type":"ring-buffer","z":"cd90371f.731b38","name":"","capacity":"5","order":"old-to-new","sendOnlyIfFull":false,"pushAfterClear":false,"extra":false,"perTopic":false,"x":370,"y":200,"wires":"e1488506.f31938"},{"id":"a3ade5d6.5885f8","type":"function","z":"cd90371f.731b38","name":"toNum => val","func":"msg.payload = Number(msg.payload);\nmsg.topic = \"val\";\nreturn msg;","outputs":1,"noerr":0,"x":160,"y":200,"wires":"e8d3dc2a.1031b"},{"id":"e1488506.f31938","type":"function","z":"cd90371f.731b38","name":"mean","func":"arr = msg.payload;\nmean = arr.reduce((s, n) => s + n) / arr.length;\nmsg.payload = mean;\nmsg.topic = \"mean\";\nreturn msg;","outputs":1,"noerr":0,"x":530,"y":200,"wires":"8bcebb56.86cf78"},{"id":"8bcebb56.86cf78","type":"debug","z":"cd90371f.731b38","name":"","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","x":710,"y":200,"wires":[]},{"id":"b2923e3a.7b32c","type":"mqtt-broker","z":"","name":"Broker IUT","broker":"10.98.35.245","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""}]
- Utilisation du contexte pour préserver la valeur d'une variable entre les appels de fonctions :
// initialise the counter to 0 if it doesn't exist already
var count = context.count||0;
count += 1;
// store the value back
context.count = count;
// make it part of the outgoing msg object
msg.payload = count;
return msg;
- Calcul de la moyenne des valeurs d'un tableau en entrée du nœud :
arr = msg.payload;
mean = arr.reduce((s, n) => s + n) / arr.length;
msg.payload = mean;
return msg;
- Calcul d'écart-type des valeurs d'un tableau en entrée du nœud :
arr = msg.payload;
mean = arr.reduce((s, n) => s + n) / arr.length;
variance = arr.reduce((s, n) => s + (n - mean) ** 2, 0) / (arr.length - 1);
msg.payload = Math.sqrt(variance);
return msg;
- Exemple d'un flow combinant deux flux dans un nœud "function" (technique basée sur `msg.topic`):
Code à dérouler et importer dans Node-Red
[
{
"id": "b4145e7.a5e95a",
"type": "tab",
"label": "Flow 4",
"disabled": false,
"info": ""
},
{
"id": "69f2b086.abc8e",
"type": "data-generator",
"z": "b4145e7.a5e95a",
"name": "",
"field": "payload",
"fieldType": "msg",
"syntax": "text",
"template": "{{int 1 10}}",
"x": 260,
"y": 80,
"wires": [
[
"7087813.b11c98"
]
]
},
{
"id": "220be564.c0876a",
"type": "debug",
"z": "b4145e7.a5e95a",
"name": "",
"active": true,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"x": 770,
"y": 80,
"wires": []
},
{
"id": "f31017dd.8c0fa8",
"type": "inject",
"z": "b4145e7.a5e95a",
"name": "",
"topic": "",
"payload": "",
"payloadType": "date",
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"x": 100,
"y": 120,
"wires": [
[
"69f2b086.abc8e",
"46f115c9.5b036c"
]
]
},
{
"id": "46f115c9.5b036c",
"type": "data-generator",
"z": "b4145e7.a5e95a",
"name": "",
"field": "payload",
"fieldType": "msg",
"syntax": "text",
"template": "{{int 1 10}}",
"x": 260,
"y": 160,
"wires": [
[
"dae1a336.56fa8"
]
]
},
{
"id": "7087813.b11c98",
"type": "function",
"z": "b4145e7.a5e95a",
"name": "toNum => v1",
"func": "val = Number(msg.payload);\nmsg.payload = val;\nmsg.topic = \"v1\";\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 430,
"y": 80,
"wires": [
[
"220be564.c0876a",
"91fcb0ff.7859d"
]
]
},
{
"id": "dae1a336.56fa8",
"type": "function",
"z": "b4145e7.a5e95a",
"name": "toNum => v2",
"func": "val = Number(msg.payload);\nmsg.payload = val;\nmsg.topic = \"v2\";\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 430,
"y": 160,
"wires": [
[
"8cb704a3.30ab78",
"91fcb0ff.7859d"
]
]
},
{
"id": "8cb704a3.30ab78",
"type": "debug",
"z": "b4145e7.a5e95a",
"name": "",
"active": true,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"x": 770,
"y": 160,
"wires": []
},
{
"id": "91fcb0ff.7859d",
"type": "join",
"z": "b4145e7.a5e95a",
"name": "",
"mode": "custom",
"build": "object",
"property": "payload",
"propertyType": "msg",
"key": "topic",
"joiner": "\\n",
"joinerType": "str",
"accumulate": false,
"timeout": "",
"count": "2",
"reduceRight": false,
"reduceExp": "",
"reduceInit": "",
"reduceInitType": "",
"reduceFixup": "",
"x": 610,
"y": 260,
"wires": [
[
"d08c4fd0.63748",
"82b2ca26.169818"
]
]
},
{
"id": "d08c4fd0.63748",
"type": "debug",
"z": "b4145e7.a5e95a",
"name": "",
"active": true,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"x": 770,
"y": 260,
"wires": []
},
{
"id": "82b2ca26.169818",
"type": "function",
"z": "b4145e7.a5e95a",
"name": "v1-v2",
"func": "v = msg.payload;\nmsg.payload = v.v1 - v.v2;\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 610,
"y": 340,
"wires": [
[
"30b2c7f8.5054c8"
]
]
},
{
"id": "30b2c7f8.5054c8",
"type": "debug",
"z": "b4145e7.a5e95a",
"name": "",
"active": true,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"x": 770,
"y": 340,
"wires": []
}
]
- Flow de génération des infos d1 à d4 (afin de terminer le travail à la maison si nécessaire. Attention, ce flow exploite un nœud de type 'data-generator' : https://flows.nodered.org/node/node-red-node-data-generator
Code à dérouler et importer dans Node-Red
[{"id":"2b0e003a.3e538","type":"tab","label":"Flow 2","disabled":false,"info":""},{"id":"d33da5ac.fb0108","type":"inject","z":"2b0e003a.3e538","name":"","topic":"","payload":"","payloadType":"date","repeat":"0.2","crontab":"","once":false,"onceDelay":0.1,"x":110,"y":180,"wires":"d6d0db1.7477728"},{"id":"d6d0db1.7477728","type":"data-generator","z":"2b0e003a.3e538","name":"","field":"payload","fieldType":"msg","syntax":"text","template":"Modèle:Int 100 100","x":280,"y":180,"wires":"100f437b.c507fd"},{"id":"fecac641.353818","type":"debug","z":"2b0e003a.3e538","name":"","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","x":730,"y":180,"wires":[]},{"id":"ba6d1e93.daebf","type":"ui_chart","z":"2b0e003a.3e538","name":"","group":"79b96145.ddeab","order":1,"width":0,"height":0,"label":"data/d1","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"80","ymax":"120","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"60","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":720,"y":140,"wires":[[]]},{"id":"fe905f74.70bb5","type":"mqtt out","z":"2b0e003a.3e538","name":"","topic":"data/d1","qos":"","retain":"","broker":"34a88a4.f836a76","x":720,"y":240,"wires":[]},{"id":"17c708a7.5d0ef7","type":"inject","z":"2b0e003a.3e538","name":"","topic":"","payload":"","payloadType":"date","repeat":"0.2","crontab":"","once":false,"onceDelay":0.1,"x":110,"y":380,"wires":"5ceeadbe.70ba14"},{"id":"41df97d6.318a58","type":"inject","z":"2b0e003a.3e538","name":"Clear charts","topic":"","payload":"[]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":530,"y":80,"wires":"ba6d1e93.daebf","5bfea12a.d2224","f274511e.4849c"},{"id":"100f437b.c507fd","type":"function","z":"2b0e003a.3e538","name":"Generate data/d1","func":"function getRandomInt(max) {\n return Math.floor(Math.random() * max);\n}\n\nval = getRandomInt(30);\nold = parseFloat(msg.payload);\n\nif (val==0) {\n noiseimp = Math.random()*100-50;\n}\nelse {\n noiseimp = 0;\n}\namp = 5;\nnoise = Math.random()*5-amp/2;\nmsg.payload = old + noise + noiseimp;\nreturn msg;","outputs":1,"noerr":0,"x":490,"y":180,"wires":"ba6d1e93.daebf","fecac641.353818","fe905f74.70bb5"},{"id":"92077b88.1d0808","type":"debug","z":"2b0e003a.3e538","name":"","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","x":730,"y":320,"wires":[]},{"id":"5ceeadbe.70ba14","type":"function","z":"2b0e003a.3e538","name":"Cos(t)","func":"let now = new Date().getTime()/1000 // seconds\nmsg.payload = Math.cos(now/10)\nreturn msg;","outputs":1,"noerr":0,"x":250,"y":380,"wires":"bb991d79.568bc"},{"id":"5bfea12a.d2224","type":"ui_chart","z":"2b0e003a.3e538","name":"","group":"79b96145.ddeab","order":2,"width":0,"height":0,"label":"data/d2","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"-1.5","ymax":"1.5","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"60","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":720,"y":380,"wires":[[]]},{"id":"bb991d79.568bc","type":"function","z":"2b0e003a.3e538","name":"Generate data/d2","func":"function getRandomInt(max) {\n return Math.floor(Math.random() * max);\n}\n\nval = getRandomInt(10);\nold = parseFloat(msg.payload);\n\namp = 2.0;\nif (val==0) {\n noise = Math.random()*amp-amp/2;\n}\nelse {\n noise = 0;\n}\nmsg.payload = old + noise;\nreturn msg;","outputs":1,"noerr":0,"x":490,"y":380,"wires":"92077b88.1d0808","5bfea12a.d2224","d6b77e6d.caacc"},{"id":"d6b77e6d.caacc","type":"mqtt out","z":"2b0e003a.3e538","name":"","topic":"data/d2","qos":"","retain":"","broker":"34a88a4.f836a76","x":720,"y":420,"wires":[]},{"id":"bfacb7a5.461768","type":"inject","z":"2b0e003a.3e538","name":"","topic":"","payload":"","payloadType":"date","repeat":"0.2","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":700,"wires":"950b5185.f64ea"},{"id":"e8626103.0ad67","type":"debug","z":"2b0e003a.3e538","name":"","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","x":730,"y":700,"wires":[]},{"id":"950b5185.f64ea","type":"function","z":"2b0e003a.3e538","name":"Cos(t)","func":"let now = new Date().getTime()/1000 // seconds\nmsg.payload = 10*Math.cos(now/300)\nreturn msg;","outputs":1,"noerr":0,"x":270,"y":700,"wires":"17a78ce2.99d863"},{"id":"f274511e.4849c","type":"ui_chart","z":"2b0e003a.3e538","name":"","group":"79b96145.ddeab","order":4,"width":0,"height":0,"label":"data/d4","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"60","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":720,"y":760,"wires":[[]]},{"id":"17a78ce2.99d863","type":"function","z":"2b0e003a.3e538","name":"Generate data/d4","func":"derive = parseFloat(msg.payload);\n\namp = 2 \nsignal = Math.random()*amp-amp/2;\n\nmsg.payload = signal + derive;\nreturn msg","outputs":1,"noerr":0,"x":490,"y":780,"wires":"e8626103.0ad67","f274511e.4849c","ac66ba93.bdc448"},{"id":"ac66ba93.bdc448","type":"mqtt out","z":"2b0e003a.3e538","name":"","topic":"data/d4","qos":"","retain":"","broker":"34a88a4.f836a76","x":720,"y":800,"wires":[]},{"id":"dc4578cf.2be168","type":"inject","z":"2b0e003a.3e538","name":"","topic":"","payload":"","payloadType":"date","repeat":"0.2","crontab":"","once":false,"onceDelay":0.1,"x":110,"y":560,"wires":"72beea8c.71cae4"},{"id":"eb09cd36.6964c","type":"debug","z":"2b0e003a.3e538","name":"","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","x":730,"y":500,"wires":[]},{"id":"72beea8c.71cae4","type":"function","z":"2b0e003a.3e538","name":"Cos(t)","func":"let now = new Date().getTime()/1000 // seconds\nmsg.payload = Math.cos(now/10)\nreturn msg;","outputs":1,"noerr":0,"x":250,"y":560,"wires":"efac392a.e33f88"},{"id":"87b9b24.916515","type":"ui_chart","z":"2b0e003a.3e538","name":"","group":"79b96145.ddeab","order":3,"width":0,"height":0,"label":"data/d3","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"-1.5","ymax":"1.5","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"60","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":720,"y":560,"wires":[[]]},{"id":"efac392a.e33f88","type":"function","z":"2b0e003a.3e538","name":"Generate data/d3","func":"function getRandomInt(max) {\n return Math.floor(Math.random() * max);\n}\n\nval = getRandomInt(10);\nold = parseFloat(msg.payload);\n\namp = 3.0;\nif (val==0) {\n noiseimp = Math.random()*amp-amp/2;\n}\nelse {\n noiseimp = 0;\n}\namp2 = 1.0;\nnoise = Math.random()*amp2-amp2/2;\nmsg.payload = old + noise + noiseimp;\nreturn msg;","outputs":1,"noerr":0,"x":490,"y":560,"wires":"eb09cd36.6964c","87b9b24.916515","6d51a515.9f97ec"},{"id":"6d51a515.9f97ec","type":"mqtt out","z":"2b0e003a.3e538","name":"","topic":"data/d3","qos":"","retain":"","broker":"34a88a4.f836a76","x":720,"y":600,"wires":[]},{"id":"79b96145.ddeab","type":"ui_group","z":"","name":"Données","tab":"61030fa.c1b88f","order":1,"disp":true,"width":"6","collapse":false},{"id":"34a88a4.f836a76","type":"mqtt-broker","z":"","name":"Broker IUT","broker":"10.98.35.245","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"61030fa.c1b88f","type":"ui_tab","name":"Tab 1","icon":"dashboard","order":1}]