Pilotage d’une LED RVB par la liaison Série

L’objectif est ici de faire interagir une carte Arduino et une application C#. Les échanges entre l’application et la carte Arduino s’effectuent via la liaison série. Le programme C# permet de sélectionner et d’ouvrir le port série de la carte Arduino. Ensuite le déplacement des curseurs de l’application envoie des messages sur la liaison série. Messages qui sont interprétés par la carte Arduino afin de commander l’intensité lumineuse l’une Led RVB.

câblage

La plus grande broche de la LED correspond à la cathode à relier à la masse

De gauche à droite

  1. Anode Bleu (+)
  2. Anode Vert (+)
  3. Cathode commune (-)
  4. Anode Rouge (+)
1234

Programme Arduino

Le programme consiste à lire les données reçues sur la liaison série, d’en extraire les données concernant la commande des leds. Le pilotage des leds à proprement parlé s’effectue à l’aide des instructions analogWrite.

Le programme est le suivant :

#define ROUGE 3
#define GND 4
#define VERT 5
#define BLEU 6
void setup() {
  // put your setup code here, to run once:
  pinMode(GND, OUTPUT);
  digitalWrite(GND, LOW);
  Serial.begin(115200);

}

void loop() {
  if (Serial.available() > 0)
  {
    byte valRouge = Serial.parseInt();
    byte valVert = Serial.parseInt();
    byte valBleu = Serial.parseInt();
    while(Serial.available() > 0) Serial.read();
    analogWrite(ROUGE, valRouge);
    analogWrite(VERT, valVert);
    analogWrite(BLEU, valBleu);
  }

}

 

programme C#

l’application ressemble à ceci :

Le code final est le suivant :

using System;
using System.Windows.Forms;
using System.IO.Ports;

namespace pilotage_led_RVB
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (serialPort1.IsOpen)
            {
                button1.Text = "Connexion";
                serialPort1.Close();
                trackBar1.Enabled = false;
                trackBar2.Enabled = false;
                trackBar3.Enabled = false;
            }
            else {
                button1.Text = "Déconnexion";
                serialPort1.PortName = comboBox1.SelectedItem.ToString();
                serialPort1.Open();
                trackBar1.Enabled = true;
                trackBar2.Enabled = true;
                trackBar3.Enabled = true;
            }
            
        }

        private void trackBar1_Scroll(object sender, EventArgs e)
        {
            serialPort1.WriteLine(trackBar1.Value + "/" + trackBar2.Value + "/" + trackBar3.Value);
        }

        private void trackBar2_Scroll(object sender, EventArgs e)
        {
            serialPort1.WriteLine(trackBar1.Value + "/" + trackBar2.Value + "/" + trackBar3.Value);
        }

        private void trackBar3_Scroll(object sender, EventArgs e)
        {
            serialPort1.WriteLine(trackBar1.Value + "/" + trackBar2.Value + "/" + trackBar3.Value);
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            String[] ports = SerialPort.GetPortNames();
            foreach (var port in ports)
            {
                comboBox1.Items.Add(port);
            }
        }
    }
}

 

Variables

Déclaration des variables dans arduino

variables globales, locales, static

Variables globales

Ce sont des variables déclarées au début du programme, en dehors de toute fonction. Ces variables ont une portée globale, c’est à dire que chaque fonction constituant le programme peut les utiliser, les manipuler.

Avantages :

  • On déclare les variables une fois pour toute, on n’a plus à y revenir…
  • Comme les variables sont accessibles à toutes les fonctions, on peut faire l’économie de passage de paramètres.

Inconvénients :

  • Les variables mobilisent l’espace mémoire durant toute la vie du programme, on peut ainsi saturer la mémoire vive avec des variables qui n’ont servi qu’une fois (à l’initialisation par exemple)
  • Si on utilise les mêmes variables dans diverses fonctions, il se peut qu’une fonction modifie la valeur d’une manière qui n’était pas attendue par une autre fonction qui utilisait la même variable, c’est un source importante de bugs…

On évite donc le plus possible d’utiliser les variables globales et en particulier pour des compteurs de boucles.

Variables locales

Ce sont des variables déclarées au sein d’une fonction. Ces variables ont une portée locale, c’est-à-dire qu’une fois sortie de la fonction elles sont détruites libérant ainsi de l’espace mémoire. Ce sont les variables internes à une fonction, nécessaire à son fonctionnement.

Avantage :

  • Variables uniquement utilisables par la fonction à laquelle elles appartiennent, les autres fonctions ne peuvent pas interférer.
  • Des fonctions diverses peuvent avoir des variables portant le même nom sans risque d’interférence.

Inconvénients :

  • On doit utiliser un passage paramètre pour renvoyer une variable de ce genre à une autre fonction.
  • Les variables sont détruites à la sortie de la fonction, il n’y a pas de mémorisation de la dernière valeur de la variable.

Variables statiques

Ce sont des variables déclarées au sein d’une fonction, mais qui ne sont pas détruites à la sortie de cette dernière, de cette manière au prochain rappel de la fonction cette variable n’est pas recrée, mais elle est toujours là avec la dernière valeur obtenue. Ces variables associent le coté permanent des variables globales et le fait que cette variable est isolée des autres fonctions.

Variables volatiles

Ce sont des variables globales amenées à être modifiées par un processus extérieur au programme principal. Le mot volatile est une indication donnée au compilateur de ne pas simplifier son code et supprimer cette variable même si elle semble inutile.

Par exemple Routine d’interruption
Les routines d’interruptions utilisent souvent des variables testées dans le programme principal. Par exemple, l’interruption d’un port série en réception : le programme d’interruption stocke et teste les caractères reçus, signalant la fin du message par une variable globale au programme principal :

int End_Message = FALSE; // variable globale
void main ( void )
{
    ....
        while (! End_Message) 
/* mal réglé le compilateur va supprimer ce test car pour lui 
!End_Message= !FALSE= TRUE et comme While(TRUE) est toujours vrai le compilateur simplifie en supprimant ce test « inutile », or End_Message est modifié dans sous programme d’interruption ce test ne doit donc pas être simplifié */
        {
        .....
        }
}
interrupt void Isr_Receive ( void )
        {
            .....
                if ( RX_CHAR == ETX ) { End_Message = TRUE;}
                .....
                    .....
        }

 

Si l’optimisation du compilateur est dé-validée, ce programme fonctionne correctement. Par contre n’importe quel compilateur “décent” avec l’optimisation validée, va supprimer le code après la boucle while dans le main(), la condition ( ! End_Message) étant toujours vraie pour lui : en effet le compilateur n’a pas idée que la variable End_Message est modifiée par la routine d’interruption !
Dans ce cas-là et pour que tout rentre dans l’ordre il suffit de déclarer la variable End_Message avec l’attribut volatile.

bibliothèque Ethernet et Udp

Fonctions de bases nécessaires à l’utilisation du protocole UDP

L’utilisation de la carte réseau avec Arduino est subordonnée à l’utilisation de la bibliothèque Ethernet. Nous passerons en revue les fonctions nécessaire à l’utilisation du protocole UDP.

Bibliothèques nécessaires :

La communication entre la carte Ethernet et l’Arduino s’appuie sur la liaison série synchrone (SPI) , suivant la version de Arduino il peut être nécessaire d’inclure la bibliothèque SPI.h. Les autres fichiers d’entête à inclure au projet sont Ethernet.h et Udp.h

Le début de notre programme contiendra donc au minimum les entêtes suivants :

#include <SPI.h>         
#include <Ethernet.h>
#include <EthernetUdp.h>

 Bibliothèque ethernet

Un objet « Ethernet » est auto-instancié lors de l’inclusion de la bibliothèque Ethernet. Pour utiliser le protocole UDP, une seule fonction de la bibliothèque Ethernet est nécessaire :

Ethernet.begin(mac,IP,gateway,subnet);

Cette fonction initialise la bibliothèque et les paramètres du réseau. Les paramètres sont les suivants :

  • mac: l’adresse MAC (Media Access Control) pour le composant (tableau de 6 octets) : c’est l’adresse Ethernet matérielle de votre module. Pour de nombreux composants Ethernet, cette adresse est prédéfinie par le fabricant. Pour le module Ethernet Arduino, vous devez choisir votre propre adresse MAC.
  • ip: l’adresse IP du composant (tableau de 4 octets).
  • gateway: l’adresse IP de la passerelle du réseau (tableau de 4 octets). Optionnel: la valeur par défaut est l’adresse IP de la carte avec le dernier octet à 1.
  • subnet: le masque du sous-réseau du réseau (tableau de 4 octets). Optionnel : la valeur par défaut est 255.255.255.0.

pour plus d’informations et les autres fonctions exposées, voir la documentation en français ici : http://www.mon-club-elec.fr/pmwiki_reference_arduino/pmwiki.php?n=Main.BibliothèqueEthernet#toc2

bibliothèque UDP

Un objet Udp s’instancie de la manière suivante :

EthernetUDP Udp;

On peut alors utiliser les méthodes suivantes :

  • Udp.begin(localPort);cette fonction initialise le protocole UDP et démarre l’écoute sur le port désigné
  • Udp.stop(); cette fonction stoppe l’écoute du port. Utile si le traitement d’un paquet est long. Cela évite le débordement du buffer d’entrée de la carte Ethernet. (cette fonction a été rajoutée dans la bibliothèque Eiffel)
  • Udp.parsePacket(); cette fonction retourne la taille d’un paquet en octets ou 0 si aucun paquet n’est disponible
  • Udp.available(); cette fonction permet de savoir s’il y a des données dans le buffer de reception. Elle retourne un entier (int) qui vaut 0 si le buffer est vide et le nombre d’octets disponible s autrement.
  • Udp.read(unsigned char* buffer, size_t len); cette fonction permet de lire le paquet reçu et de le placer dans buffer (dont la longueur maximum est spcécifiée par buflen).
    Avant d’appeler cette fonction, il faut s’assurer que le buffer de reception n’est pas vide , il faut donc faire un test avec la fonction Udp.parsePacket()
    La fonction renvoie un entier (int) qui correspond au nombre d’octets lus. Si le nombre d’octets contenus dans le paquet est supérieur à la longueur du buffer, il y a troncature, le nombre renvoyé est alors négatif.
  • Udp.beginPacket(IPAddress ip, uint16_t port); permet d’initier l’envoi d’un paquet vers l’hôte distant dont on spécifie l’adresse IP et le port. La fonction retourne 1 en cas de succès et 0 en cas d’échec.
  • Udp.beginPacket(const char *host, uint16_t port); autre version en passant en argument le nom d’hôte au lieu de l’adresse IP.
  • Udp.endPacket(); ferme le paquet et l’envoie à l’hôte distant.
  • Udp.write(byte data); écrit un octet unique dans le paquet à envoyer.
  • Udp.write(const byte *buffer, size_t taille); Envoie « taille » octets du buffer vers le paquet.

exemple

UDPSendReceiveString.ino

#include <SPI.h>         // needed for Arduino versions later than 0018
#include <Ethernet.h>
#include <EthernetUdp.h>         // UDP library from: bjoern@cs.stanford.edu 12/30/2008


// Enter a MAC address and IP address for your controller below.
// The IP address will be dependent on your local network:
byte mac[] = {
    0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED
};
IPAddress ip(192, 168, 1, 177);

unsigned int localPort = 8888;      // local port to listen on

// buffers for receiving and sending data
char packetBuffer[UDP_TX_PACKET_MAX_SIZE];  //buffer to hold incoming packet,
char  ReplyBuffer[] = "acknowledged";       // a string to send back

// An EthernetUDP instance to let us send and receive packets over UDP
EthernetUDP Udp;

void setup() {
    // start the Ethernet and UDP:
    Ethernet.begin(mac, ip);
    Udp.begin(localPort);

    Serial.begin(9600);
}

void loop() {
    // if there's data available, read a packet
    int packetSize = Udp.parsePacket();
    if (packetSize) {
        Serial.print("Received packet of size ");
        Serial.println(packetSize);
        Serial.print("From ");
        IPAddress remote = Udp.remoteIP();
        for (int i = 0; i < 4; i++) {
            Serial.print(remote[i], DEC);
            if (i < 3) {
                Serial.print(".");
            }
        }
        Serial.print(", port ");
        Serial.println(Udp.remotePort());

        // read the packet into packetBufffer
        Udp.read(packetBuffer, UDP_TX_PACKET_MAX_SIZE);
        Serial.println("Contents:");
        Serial.println(packetBuffer);

        // send a reply to the IP address and port that sent us the packet we received
        Udp.beginPacket(Udp.remoteIP(), Udp.remotePort());
        Udp.write(ReplyBuffer);
        Udp.endPacket();
    }
    delay(10);
}

 

 

Bibliothèques et débogage

bibliothèques

Définition

Une bibliothèque permet de mettre à disposition du programmeur des fonctions pré-écrites. On parle de fonctions haut-niveau. Par exemple dans la bibliothèque math.h (incluse par défaut dans tout programme Arduino) on trouvera des fonctions telles que sin() et cos() qui calculent le sinus ou le cosinus d’un angle exprimé en radiant.

La bibliothèque peut aussi être un pilote (driver) pour un composant que l’on veut interfacer avec la carte Arduino. Par exemple la bibliothèque LiquidCrystal pilote automatiquement un afficheur cristaux liquides de 2 lignes 16 caractères.

L’intérêt d’une telle bibliothèque est qu’on n’a pas à connaitre la manière dont l’afficheur est commandé pour l’utiliser. C’est la bibliothèque qui va prendre en charge la génération des signaux électriques (horloge, données..) pour piloter l’afficheur en fonction de la nature de la fonction.

Utilisation d’une bibliothèque dans un programme.

 Dans l’interface de développement (IDE) Arduino c’est on ne peut plus simple : depuis le menu « Croquis » sélectionner « Inclure une bibliothèque » puis la sélectionner dans la liste déroulante.

On voit alors apparaitre une directive de type « #include… » dans le programme

Dans l’exemple ci-dessus, on a importé  la bibliothèque LiquidCrystal.

Il faut souvent lire la documentation de la bibliothèque pour connaitre la manière de l’utiliser

Par exemple la bibliothèque LiquidCrystal expose les méthodes (ou fonctions) suivantes :

Fonctions d’initialisation

Fonctions de gestion de l’écran

Fonctions de positionnement du curseur

Fonctions modifiant l’aspect du curseur

 

Fonctions d’écriture

Fonctions de contrôle du mode d’affichage

Fonction de création de caractère personnalisé

createChar()

En général il faut initialiser la bibliothèque en instanciant un objet c’est-à-dire en créant une variable du type correspondant à l’objet.

Explication : dans le cas d’une variable simple on la crée en écrivant :

int valeur = 3;

On instancie un objet simple  de type Int (entier) auquel on affecte la valeur 3.
Pour un objet plus complexe on procède de manière analogue :

LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

On créer ici un objet « lcd » de type LiquidCrystal, de plus on passe en paramètres les numéros de broches de l’Arduino permettant la connexion avec l’afficheur (voir la documentation de la bibliothèque)

Remarque : la création d’objet (que ce soit de simples variables ou des objets complexes) revient à faire des réservations d’espace mémoire et fournir des valeurs initiales.

L’appel des fonctions liées à l’objet se fait en indiquant le nom de l’objet suivi d’un « . » (point) suivi lui-même du nom de la fonction.

exemple

lcd.begin(16, 2); // initialise l’afficheur: 16 caractères, 2 lignes
lcd.print("hello, world!"); // affiche un message.

En général une bibliothèque est fournie avec des exemples d’utilisation. On y accède depuis le menu « Fichiers » puis « Exemples » enfin on choisit l’exemple dans la bibliothèque qui nous intéresse.

Installation d’une bibliothèque

Pour installer une bibliothèque il faut fermer l’IDE Arduino.

L’installation d’une bibliothèque est simple : il faut la copier dans le dossier « Libraries » contenu dans le dossier Arduino de l’utilisateur (« chemin de l’utilisateur »/documents/Arduino). Les bibliothèques « natives » c’est dire fournies d’office avec Arduino sont localisées dans « dossier d’installation d’Arduino »/libraries. Les bibliothèques natives contiennent par exemple Ethernt, Firmata,Keyboard, LiquidCrystal, Stepper, Wifi…

En général le dossier de la bibliothèque contient les éléments suivants :

  • Les fichiers codes de la bibliothèque (LiquidCrystal.h et LiquidCristal.cpp)
  • Un fichier texte keywords.txt permetaant la coloration syntaxique
  • Un dossier « examples » contenant lui-même des sous dossiers nommés avec le nom des exemples.

Le nom des exemples doit être le même que le dossier qui les contient . Par exemple le dossier  « HelloWorld » contient « HelloWorld.ino »

débogage

L’IDE Arduino ne possède pas d’outils tels qu’exécution en mode pas à pas, mode trace, visualisation des registres…

Il faut user de stratagèmes pour déboguer un programme. Le plus simple est d’utiliser le moniteur série et d’y faire afficher les valeurs de variables, les résultats de calculs…

Pour utiliser le moniteur il faut initialiser la liaison série. Puis à chaque fois qu’on désire afficher une valeur, il faut appeler la fonction « print » ou « println »

/* Blink without Delay*/
// set pin numbers:
const int ledPin =  13;      // the number of the LED pin
int ledState = LOW;             // ledState used to set the LED
long previousMillis = 0;        // will store last time LED was updated
long interval = 1000;           // interval at which to blink (milliseconds)
void setup() { 
    pinMode(ledPin, OUTPUT);       // set the digital pin as output:
    // -------------------- débogage ----------------------------//
    //  initialisation de la liaison série 
    // la ligne suivante pourra être commentée pour le passage en production
    Serial.begin(9600);
    //-------------------- fin débogage -------------------------//
}
void loop()
{
    unsigned long currentMillis = millis();
    if(currentMillis - previousMillis > interval) 
    {   
        // -------------------- débogage ----------------------------//
        // affichage de la valeur de CurrentMillis
        // la ligne suivante pourra être commentée pour le passage en production
        Serial.print("time: "); Serial.println(currentMillis,DEC); 
        //-------------------- fin débogage -------------------------//
        previousMillis = currentMillis;    // save the last time you blinked the LED 
        if (ledState == LOW)// if the LED is off turn it on and vice-versa
            ledState = HIGH;
        else
            ledState = LOW;

        // -------------------- débogage ----------------------------//
        // affichage de la valeur de ledState
        // la ligne suivante pourra être commentée pour le passage en production
        Serial.print("led state: "); Serial.println(ledState);  
        //-------------------- fin débogage -------------------------//   
        digitalWrite(ledPin, ledState); // set the LED with the ledState of the variable
    }
}

 

La fonction Serial.begin(9600); initialise la liaison série à un débit de 9600 bauds

Les fonctions Serial.print et Serial.println sont deux fonctions similaires : elles affichent des données sur la liaison série. À la différence de print, println affiche les données sur le port série suivi d’un caractère de “retour de chariot” (ASCII 13, or ‘\r’) et un caractère de “nouvelle ligne” (ASCII 10, or ‘\n’).

Serial.print(val)
Serial.print(val, format)

 

Serial.print(78); // affiche "78"
Serial.print(1.23456); // affiche "1.23"
Serial.print(byte(78)); // affiche "N" (dont la valeur ASCII est 78)
Serial.print('N'); // affiche "N"
Serial.print("Hello world."); // affiche "Hello world." 

Un second paramètre optionnel (format) spécifie :

  • pour les nombres entiers, la base à utiliser. Les valeurs autorisées sont BYTE, BIN (binaire, ou base 2), OCT (octal, ou base 8), DEC (décimal, ou base 10), HEX (hexadécimal, ou base 16),
  • pour les nombres à virgules (float), le paramètre précise le nombre de décimales après la virgule à utiliser.
Serial.print(78, BYTE); // affiche "N"
Serial.print(78, BIN) ; // affiche  "1001110"
Serial.print(78, OCT); // affiche "116"
Serial.print(78, DEC); // affiche "78"
Serial.print(78, HEX); // affiche "4E"
Serial.println(1.23456, 0); // affiche "1"
Serial.println(1.23456, 2); // affiche "1.23"

 

Lorsqu’on compile et qu’on lance le progamme, il faut appuyer sur l’icône « Serial Monitor »  dans l’IDE Arduino on voit alors une fenêtre qui s’ouvre et qui affiche les informations demandées dans le programme

À noter que le débit du moniteur doit être ajusté avec le débit de la liaison série ( 9600 baud)

En visualisant les variables et en plaçant des espions aux bons endroits du programme on se sort de quasiment toutes les situations.

Freeboard et Node-Red

Freeboard est un projet de tableau de bord gratuit et open source avec des abonnements hébergés optionnels, facile à intégrer à diverses sources de données. Ce projet a été développé par Bug Labs, les créateurs d’un puissant service appelé dweet , un service de messagerie conçu pour l’Internet des Objets. Il existe un nœud Node-Red qui permet de mettre en relation un flux et le tableau de bord freeboard. Nous allons voir ici comment l’installer et l’utiliser.

IPX800-V4

IPX800 est un automate programmable orienté domotique. Il possède 4 entrées analogiques, 8 entrées logiques et 8 sorties sur relais. Le paramétrage et la programmation se font via une interface WEB. Un système de bus permet de connecter divers périphériques et on peut accéder aux données via une API. La plateforme est donc polyvalente et ouverte, ce qui permet de l’inclure à de nombreux projets.

Présentation de l’IPX800-V4

Programmation d’un scénario : thermostat d’ambiance

Contrôle d’accès par lecture de plaques d’immatriculation (ALPR)

Ce système permet d’autoriser l’accès à un parking par lecture des plaques d’immatriculation. Il s’appuie sur une caméra IP associée à une carte Raspberry Pi et le logiciel de reconnaissance de plaques openALPR. Une base de donnée mongoDB contient la liste des plaques autorisées. Le middleware est assuré par un “flow” node-red. (liaison openALPR / mongoDB, déclenchement de l’ordre d’ouverture et IHM pour la gestion de la base)

Diagramme de cas d’utilisation

La lecture des plaques d’immatriculation va s’intégrer dans le cas d’utilisation « Contrôle d’accès ». L’ajout et la mise à jour de nouveaux véhicules sera lié au cas d’utilisation « Mise à jour de la base données ». Le système autorise l’accès au parking aux véhicules dont le numéro de plaque d’immatriculation est inscrit dans la base de données.

Constitution du système

  • Une caméra IP, elle sera positionnée afin de capter la plaque d’immatriculation du véhicule lorsque ce dernier de présentera à l’entrée du parking.
  • Une barrière automatisée. Elle est fermée par défaut et s’ouvre si la plaque du véhicule est reconnue.
  • Un ordinateur qui contient la base de données des plaques autorisées, l’IHM web permettant la maintenance de la BDD, un logiciel de reconnaissance de plaques en relation avec la caméra IP et un interlogiciel (middleware) permettant de faire communiquer les différentes applications.

Topologie du système

Solution retenue

Matériels

  • Une carte Raspberry (2 ou 3) / 16GB raspbian JESSIE (nov 2016) -> serveur
  • Une carte Arduino avec le firmware Firmata Standard -> extension E/S
  • Une caméra IP. On doit pouvoir accéder à l’image via le web. Pour l’exemple nous utiliserons une petite caméra motorisée db power clone d’une caméra FOSCAM FI8908W[1]. Sur cette caméra, d’après la documentation[2], l’accès au flux s’effectue à l’URL : http://<IP_CAMERA>:<PORT_CAMERA>//videostream.cgi?user=admin&pwd=&resolution=32&rate=0
  • Une barrière automatique pilotée par un signal logique (« 1 » ouverture, « 0 » fermeture). La barrière gère elle-même la sécurité de passage.

Logiciels

  • openALPR pour Raspberry : logiciel open source de reconnaissance de plaques d’immatriculation.
  • MongoDB et mongoExpress : serveur de base de données et client web.
  • NODE-RED avec node-red-contrib-gpio, node-red-dashboard, node-red-contrib-mongoDB2, ObjectID et mongoDB2 : logiciel de programmation orienté flux, permettant l’interconnexion d’applications hétérogènes devant échanger des données.

Installation Logicielle.

Node-red

dans une console lancer node-red : node-red-start puis à l’aide d’un navigateur  accéder à http://<ip raspberry>:1880  . Si le noeud Johnny5 est visible, il n’y a rien à faire sinon voir Réinstallation de node-red avec Johnny5

OpenAlpr

voir openAlpr sur Raspberry Pi

Base de données mongoDB

voir installation de mongoDB et mongo-express

Mise en oeuvre.

Test de openALPR avec la caméra

Compléter le fichier /etc/openalpr/alprd.conf et indiquer le chemin d’accès au flux vidéo de la caméra IP

; This configuration file overrides the default values specified 
; in /usr/local/share/openalpr/config/alprd.defaults.conf
[daemon]

; country determines the training dataset used for recognizing plates. Valid values are us, eu
country = eu
pattern = fr

; text name identifier for this location
site_id = rpi01

; Declare each stream on a separate line
; each unique stream should be defined as stream = [url]
; adresse de la camera IP accessible par le web
stream = http://192.168.5.23:8080/videostream.cgi?user=admin&pwd=&resolution=32&rate=0

; topn is the number of possible plate character variations to report
topn = 5

; Determines whether images that contain plates should be stored to disk
store_plates = 0
store_plates_location = /home/pi/plateimages/

; upload address is the destination to POST to
upload_data = 1
upload_address = http://localhost:1880/plate/

Lancer la commande alprd -fet placer une image comportant une plaque d’immatriculation devant la caméra

Observer la console :

pi@Raspberry Pipi:~ $ alprd -f
INFO - Running OpenALPR daemon in the foreground.
INFO - Using: /etc/openalpr/alprd.conf for daemon configuration
Missing config value for company_id
Missing config value for pattern
INFO - Using: /home/pi/plateimages/ for storing valid plate images
INFO - country: fr -- config file: /etc/openalpr/openalpr.conf
INFO - pattern:
INFO - Stream 1: http://192.168.5.23:8080/videostream.cgi?user=admin&pwd=&resolution=32&rate=0
INFO - Starting camera 1
INFO - Video stream connecting...
INFO - Video stream connected
DEBUG - Writing plate AV865XG (rpi01-cam1-1483036354101) to queue.

Video de Guy TechLab : mise en oeuvre de ce tuto.

OpenALPR et node-red : lecture et affichage d’une plaque.

Node-red permet d’interconnecter des applications hétérogènes devant échanger des informations. On parle d’échange de messages. Aplrd permet d’envoyer une requête POST à un serveur web lors de la lecture d’une plaque. Nous allons donc créer un flux qui intercepte cette requête POST, va en extraire la donnée et publier cette dernière dans une fenêtre de débogage.

Vérification du fichier fichier /etc/openalpr/alprd.conf

Vérifier que les 3 dernières lignes du fichier alprd.cong correspondent à :

; upload address is the destination to POST to
upload_data = 1 ; « 1 » = envoi des données
upload_address = http://localhost:1880/plate/ ; adresse du serveur web où sont envoyées les requêtes POST

Ce qui impliquera que les données seront postées à l’adresse http://localhost:1880/plate

Flux node-red

Flux composé d’un node « http in », « http response » et « debug »

Lors de la reconnaissance d’une plaque on obtient un objet msg.payload en sortie du node « http in » qui une fois remis en forme ressemble à ceci :

{
    "version": 2,
    "data_type": "alpr_results",
    "epoch_time": 1483090959429,
    "img_width": 640,
    "img_height": 480,
    "processing_time_ms": 395.173157,
    "regions_of_interest": [],
    "results": [
        {
            "plate": "AV865XG",
            "confidence": 90.980667,
            "matches_template": 0,
            "plate_index": 0,
            "region": "",
            "region_confidence": 0,
            "processing_time_ms": 171.828781,
            "requested_topn": 5,
            "coordinates": [
                {…

On peut observer qu’il s’agit d’un objet JSON dont la propriété « results » est un tableau contenant les plaques reconnues avec leur score « confidence ».

On peut donc interroger l’objet msg.payload afin d’en extraire le numéro de plaque. Pour cela on introduit un script

 

On peut alors voir le numéro de la plaque apparaitre dans la fenêtre de débogage

Utilisation de mongoDB et ALPR dans node-red

L’objectif est de comparer la plaque lue par alprd avec une plaque contenue dans la base de données.

Ajout du node mongoDB2 à la palette de nodes :

Sélectionner le Menu de Node-red -> Manage palette

Sélectionner l’onglet « Install »

Dans l’espace de recherche taper « mongo »

On voit alors apparaitre les nodes disponibles. Cliquer sur le bouton « install » du node « node-red-contrib-mongoDB2 »

Le nœud s’installe est devient alors disponible dans la palette des nodes.

Installer aussi le node « node-red-contrib-objectid », il permet de gérer les propriétés « _id » des documents.

Paramétrage du nœud mongoDB2 et test de connexion

Faire glisser un node mongoDB2 dans l’espace de travail et l’éditer.

Sélectionner « External service » puis cliquer sur le petit crayon afin d’ajouter une connexion au serveur mongoDB Ajouter un serveur, l’URI est normalement : mongodb://localhost:27017/db
Une fois le serveur paramétré, sélectionner « db » dans l’option « Server », compléter l’option « Collection » avec le nom de la collection « LicencePlate » enfin dans « Operation » sélectionner « findOne » et cliquer sur « Done » Ajouter un nœud « Inject » dans l’espace de travail et le paramétrer de la manière suivante :

Compléter le flow de la manière suivante : ajouter un node « json » et un node « debug ». Lier l’ensemble.

Quand on clique sur le bouton du nœud inject, on voit apparaitre le document au format JSON dans la fenêtre de debug

code du Flow (à importer via le clipboard)
[{"id":"fcc8de73.b9a8e","type":"mongodb2 in","z":"de8a40ec.19323","service":"_ext_","configNode":"1d02b15c.4cf227","name":"","collection":"LicencePlate","operation":"findOne","x":1234.7665710449219,"y":157.85000610351562,"wires":[["c37c79dd.bbd448"]]},{"id":"5934a9db.1c3ee8","type":"json","z":"de8a40ec.19323","name":"","x":996.7665710449219,"y":158.01666259765625,"wires":[["fcc8de73.b9a8e"]]},{"id":"c37c79dd.bbd448","type":"debug","z":"de8a40ec.19323","name":"","active":true,"console":"false","complete":"false","x":1474.7666015625,"y":157.01666259765625,"wires":[]},{"id":"fea87bf7.46af38","type":"inject","z":"de8a40ec.19323","name":"","topic":"","payload":"{\"nom\":\"Hollande\"}","payloadType":"str","repeat":"","crontab":"","once":false,"x":774.7666320800781,"y":158.38333129882812,"wires":[["5934a9db.1c3ee8"]]},{"id":"1d02b15c.4cf227","type":"mongodb2","z":"","uri":"mongodb://localhost:27017/db","name":"db","options":"","parallelism":"-1"}]

 

Nous venons d’effectuer une requête dans la base mongoDB

Reconnaissance d’une plaque d’immatriculation.

En associant la gestion de la requête POST fournit par le logiciel alprd et une requête dans la base de données, on peut déterminer si l’accès est autorisé.

code du Flow (à importer via le clipboard)
[{"id":"cd7d103b.308a78","type":"mongoDB2 in","z":"1ee78c77.4f0c84","service":"_ext_","configNode":"d3816939.e303e8","name":"","collection":"LicencePlate","operation":"count","x":1201,"y":144,"wires":[["ae24a545.fb9ea","3fc0b1.16e58f5"]]},{"id":"ae24a545.fb9ea","type":"debug","z":"1ee78c77.4f0c84","name":"","active":true,"console":"false","complete":"false","x":1477.7666015625,"y":66.71665954589844,"wires":[]},{"id":"d96d3dcc.a4c3a8","type":"http in","z":"1ee78c77.4f0c84","name":"","url":"/plate","method":"post","swaggerDoc":"","x":697.7666015625,"y":147.18333435058594,"wires":[["e36dbbe7.4063c8"]]},{"id":"e36dbbe7.4063c8","type":"function","z":"1ee78c77.4f0c84","name":"extraction num plaque","func":"var plaque={}; //objet vide\nplaque.immatriculation=msg.payload.results[0].plate;\nmsg.payload=plaque;\nreturn msg;","outputs":1,"noerr":0,"x":939.7666015625,"y":145.9499969482422,"wires":[["cd7d103b.308a78","b918e1ce.d2721"]]},{"id":"b918e1ce.d2721","type":"debug","z":"1ee78c77.4f0c84","name":"","active":true,"console":"false","complete":"false","x":1184.7666015625,"y":61.716644287109375,"wires":[]},{"id":"3fc0b1.16e58f5","type":"http response","z":"1ee78c77.4f0c84","name":"","x":1457.7666015625,"y":142.4166717529297,"wires":[]},{"id":"d3816939.e303e8","type":"mongoDB2","z":"","uri":"mongoDB://localhost:27017/db","name":"db","options":"","parallelism":"-1"}]

 

Le script dans le node function (extraction num plaque) est le suivant :

var plaque={}; 
plaque.immatriculation=msg.payload.results[0].plate;
msg.payload=plaque;
return msg;

Le message de sortie sera donc de la forme : { "immatriculation": "AV865XG" }. Ce message sert d’argument d’entrée à la requête « count » du node mongoDB2. Autrement dit le node mongoDB2 va compter les documents qui contiennent un champ « immatriculation » ayant pour valeur « AV865XG ». Le message de sortie du node mongoDB sera un nombre. Si ce dernier vaut 0, c’est que le numéro de plaque n’est pas enregistré dans la base. Reste à commander l’ouverture de la barrière en fonction de cette information.

Pour cela nous allons utiliser une carte Arduino qui sera pilotée par la carte Raspberry.

Compléter le flow de la manière suivante en ajoutant les nœuds « switch », « trigger » et « gpio ».

code du Flow (à importer via le clipboard)
[{"id":"7ec1525b.cd9fac","type":"trigger","z":"1ee78c77.4f0c84","op1":"1","op2":"0","op1type":"str","op2type":"str","duration":"2000","extend":false,"units":"ms","reset":"","name":"","x":1270.7666015625,"y":243.18331909179688,"wires":[["1a99c7a1.ed34b8"]]},{"id":"1a99c7a1.ed34b8","type":"gpio out","z":"1ee78c77.4f0c84","name":"arduino","state":"OUTPUT","pin":"13","i2cDelay":"0","i2cAddress":"","i2cRegister":"","outputs":0,"board":"4a53a7ab.eb3e7","x":1475.7666015625,"y":242.11666870117188,"wires":[]},{"id":"fd89d95c.482bb","type":"switch","z":"1ee78c77.4f0c84","name":"","property":"payload","propertyType":"msg","rules":[{"t":"gte","v":"1","vt":"num"}],"checkall":"true","outputs":1,"x":1043.7666015625,"y":243.76663208007812,"wires":[["7ec1525b.cd9fac"]]},{"id":"4a53a7ab.eb3e7","type":"nodebot","z":"","name":"","username":"","password":"","boardType":"firmata","serialportName":"/dev/ttyUSB0","connectionType":"local","mqttServer":"","socketServer":"","pubTopic":"","subTopic":"","tcpHost":"","tcpPort":"","sparkId":"","sparkToken":"","beanId":"","impId":"","meshbluServer":"https://meshblu.octoblu.com","uuid":"","token":"","sendUuid":""}]

 

Le switch sera paramétré de la manière suivante :

Le trigger :

Le nœud gpio quant à lui sera paramétré pour piloter une broche digital d’une carte Arduino. La carte Arduino sera programmée avec le programme Firmata : (Menu fichier > Exemples > Firmata > StandardFirmata ) puis connectée à la carte Raspberry Pi par un câble USB.

Editer le nœud gpio puis cliquer sur le crayon afin de définir la cible :
Cliquer sur la loupe afin de trouver la carte Arduino connectée à la carte Raspberry (dans l’exemple c’est /dev/tty/USB0, mais cela peut être autre chose…). Configurer le reste comme suit :
Compléter alors le nœud gpio comme suit :

Si la configuration est correcte la broche 13 (et la led qui y est rattachée) passe à l’état logique « 1 » pour une durée de 2 sec lorsqu’une plaque est reconnue. Reste à interconnecter l’ensemble à la vraie barrière…

Outil d’administration de la base de donnée

voir : Gestion de mongoDB à l’aide de jsGrid et node-red

code du Flow (à importer via le clipboard)

il est disponible ici : http://flows.nodered.org/flow/e32f2b942bce77ef6079c0642b93c036

Ce flow fabrique une interface homme machine qui permet de maintenir la base de données des utilisateurs autorisés à accéder au parking. Il s’appuie sur un template html et le plugin jsGrid (dépend de jQuery). Son fonctionnement est dit « RESTfull » c’est-à-dire que la grille utilise une requête ajax de type POST pour insérer des valeurs dans la base, une requête de type PUT pour effectuer la mise à jour et une requête DELETE pour effacer un document. Pour fonctionner ce flow a besoin du node « ObjectId », qu’il faut installer si ce n’est pas déjà fait.

capture

Le format des documents doit être :

{
    "_id": ObjectID("585a4732ac11400192a60b85"),
    "nom": "nameOfUser",
    "prenom": "FirstNameOfUser",
    "immatriculation": "123456",
    "heure": "13",
    "minute": "30"
}

La grille est accessible à l’adresse : http://<node-red>:<port>/plates

Le code du node template est le suivant :

<!DOCTYPE html>
<html lang="fr">
<head>
    <title>Licence Plate</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-timepicker/0.5.2/css/bootstrap-timepicker.min.css" />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
    <link type="text/css" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jsgrid/1.5.3/jsgrid.min.css" />
    <link type="text/css" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jsgrid/1.5.3/jsgrid-theme.min.css" />
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jsgrid/1.5.3/jsgrid.min.js"></script>

    <script type="text/javascript">
        $(function () {
            var db = {{#payload}}{{{.}}}{{/payload}};
                $("#jsgrid").jsGrid({
                    width: "100%",
                    inserting: true,
                    editing: true,
                    sorting: true,
                    paging: true,

                    data: db,

                    fields: [
                        { title:"Nom", name: "nom", type: "text", width: 50 },
                        { title:"Prénom", name: "prenom", type: "text", width: 50 },
                        { title:"Plaque", name: "immatriculation", type: "text", width: 50 },
                        { title:"Heure", name: "heure", type:"number", width: 25},
                        { title:"Minute", align:"left",  name: "minute", type:"number" , width: 25},
                        { type: "control" }
                    ],
               
                    controller: {
                        insertItem: function(item) {
                            return $.ajax({
                                type: "POST",
                                url: "/insert",
                                data: item
                            });
                        },
                        updateItem: function(item) {
                            return $.ajax({
                                type: "PUT",
                                url: "/update",
                                data: item
                            });
                        },
                        deleteItem: function(item) {
                            return $.ajax({
                                type: "DELETE",
                                url: "/delete",
                                data: item
                            });
                        }
                    }   
                });
            });
    
  </script>
</head>
<body class="container">
    <section class="row">

        <div class="col-md-6"></div>
        <div class="col-md-6" id="jsgrid">
        </div>
    </section>
</body>
</html>

La ligne var db = {{#payload}}{{{.}}}{{/payload}};utilise la notation « mustache » c’est la notation qui permet de à la page web de récupérer les informations contenues dans le message d’entrée du node. Dans notre cas msg.payload est le résultat de la requête effectuée dans la base de données. Il s’agit d’un tableau d’objets JSON le format est le suivant :

[
    {
        "nom": "TestName",
        "prenom": "Toto",
        "immatriculation": "AV865XG",
        "_id": "58667499d93f5401a2b4d2d5"
    },
    {
        "_id": "586666b73a3b9e2011963257",
        "nom": "Hollande",
        "prenom": "Francois",
        "immatriculation": "HE123LY"
    }
]

Le moteur de template mustache va itérer le tableau et passer les objets qu’il contient au rendu de la page web

Lors du rendu de la page, le texte du template

…
        $(function () {
            var db = {{#payload}}{{{.}}}{{/payload}};
                $("#jsgrid").jsGrid({
                    width: "100%",
                    inserting: true,
…

est remplacé par

…
        $(function () {
            var db = [{"nom":"TestName","prenom":"Toto","immatriculation":"AV865XG","_id":"58667499d93f5401a2b4d2d5"},{"_id":"586666b73a3b9e2011963257","nom":"Hollande","prenom":"Francois","immatriculation":"HE123LY"}];
                $("#jsgrid").jsGrid({
                    width: "100%",
                    inserting: true,
…

{{{.}}}(Avec triple « mustache ») signifie que l’objet entier doit être passé de manière brute, c’est-à-dire sans être mise au format html

Avec une {{.}}(double mustache) le texte serait remplacé par

var db = [{&quot;nom&quot;:&quot;TestName&quot;,&quot;prenom&quot…

c’est-à-dire que les « ” » sont remplacés par leur code html « &quot ; » de ce fait la variable « db » n’est plus un tableau JSON au vu du script.

Voir https://github.com/janl/mustache.js/#non-empty-lists et https://github.com/janl/mustache.js/#variables pour plus d’informations.

  1. http://www.gadgetvictims.com/2008/08/foscam-fi8908w-firmware-history-page.html
  2. https://docs.google.com/fileview?id=0B9kFXSWngqLWYjBlYjcyOWUtYzgxYy00MWYxLThhYjAtOTgwODgzOTRmOGM5&hl=en

jQuery et AJAX

Gérer une liste à l’aide de jQuery

La page que nous allons utiliser ressemble à celle-ci. Il s’agit d’une liste de véhicules ayant participé au challenge EducEco en 2012. Chaque véhicule est accompagné de sa vignette. La page est construite avec le framework Bootstrap. La barre de menu supérieure permet changer la couleur de fond une ligne sur deux, de mettre le nom de la voiture en vert. Le premier lien quant à lui permet de restituer l’état initial.

Dans ce document la liste est statique, c’est à dire qu’elle est inscrite “en dur” dans le code html, ce qui n’est pas très pratique si on désire faire une mise à jour (par exemple rajouter ou enlever un véhicule de la liste). La page statique est test-liste-educEco (pour la télécharger faire un clic droit et enregistrer la cible du lien sous…)

AJAX[1]

L’idée va donc être de séparer les données de la page html. La page html assure la mise en forme, les données, elles seront contenues dans un fichier séparé au format json (fichier de données javascript). C’est un script jQuery qui va formuler une requête asynchrone (AJAX) afin de récupérer le jeu de données et intégrer ces dernières dans la page html.

Utilisation de JQUERY pour manipuler des jeux de données

Jquery peut effecteur des requêtes de type AJAX afin de récupérer des jeux de données puis les injecter dans la page web. Le diagramme de séquence est le suivant :

Le client initialise la requête web, le code HTML est alors renvoyé. Le code HTML contient des liens vers les feuilles de styles CSS et les scripts JS, ce qui provoque d’autres requêtes. Une fois le(s) script(s) rapatrié(s), il(s) s’exécute(nt). Un script peut effectuer des requêtes asynchrones auprès du serveur (c’est-à-dire en tâche de fond). Cela permet de mettre à jour la page sans la recharger complètement ce qui apporte un confort visuel pour l’utilisateur et une moindre occupation de la bande passante

Format d’une requête AJAX dans JQUERY :


$(document).ready(function () {
$.ajax({
type: "GET", // type de requête : GET ou POST
url: "http://.../data.json", // l'URL pour accéder aux données
dataType: "json", // le format des données JSON ou XML
success: parseData, // fonction appelée en cas de succès les données seront passée en argument de cette fonction
error: function (jqXHR, textStatus, errorThrown) { // fonction traitée en cas d'erreur
alert(textStatus);
alert(errorThrown);
}
});

function parseData(data) {…} // l’argument data correspond au jeu de données en retour de la requête AJAX
});

La fonction parseData récupère en argument les données au format JSON issu de la requête asynchrone. Et peut donc modifier le document HTML à l’aide de ce jeu de données.

Imaginons que le jeu de données JSON soit de la forme :

[
{
"Numero": "4",
"Nomvehicule": "MIRELEC09",
"Ville": "Mirepoix",
"Groupe": "EcoCitadin",
"Motorisation": "Electrique"
},
{
"Numero": "5",
"Nomvehicule": "Vincicar",
"Ville": "Blois",
"Groupe": "EcoCitadin",
"Motorisation": "Electrique"
},…
]

On voit qu’il s’agit d’un tableau (balises « [] ») contenant des objets JSON (balises « {} »). Chaque objet possède les propriétés “Numero”, “Nomvehicule”, “Ville”, “Groupe” et “Motorisation”. Si on désire afficher tous les éléments du tableau, il faut prendre les objets un par un et afficher chacune des propriétés. Pour cela, on peut s’aider de la boucle « for/in » propre au langage JS : Cette boucle peut itérer tous les index d’un tableau ou d’un objet.

function parseData(data) {
for (var i in data) {
$("#content").append(
"<li class='col-md-6' data-filter=" + data[i].Groupe + ">"
+ "<div class='vignette'>"
+ "<a href='#'>"
+ "<img width='150' height='80' alt='Photo vehicule' src='http://www.rgot.org/wp-content/uploads/img/" + data[i].Numero + ".jpg'>"
+ "</a>"
+ "<h4>"
+ "<a href='#'>" + data[i].Nomvehicule + "</a>"
+ "</h4>"
+ "<h4>" + data[i].Motorisation + "</h4>"
+ "<p>" + data[i].Ville + "</p>"
+ "</div>"
+ "</li>"
);
};
};

Dans le code précédent « data[i] » représente l’objet de position « i » dans le tableau.
La page web et le fichier json sont téléchargeables ici :
jquery manipulation de données

  1. Acronyme d’Asynchronous JAvascript and Xml, AJAX  permet en autreMise à jour d’une page web sans avoir à la recharger.Demander (requête) des données au serveur, après chargement de la page.Recevoir des données du serveur, après chargement de la page.Envoyer des données au serveur en tâche de fond.

jQuery

Scripts coté client : JS / JQUERY

JavaScript est un langage de scripts qui peuvent s’exécuter depuis le navigateur. Les scripts permettent d’agrémenter l’expérience de l’utilisateur (effet visuels, vérification d’une saisie…) , ils peuvent interagir avec le DOM afin de modifier la mise en page (ajout/masquage de textes, modification dynamique de la mise en page, interaction avvec les feuilles CSS…). Les scripts peuvent aussi effectuer des requêtes asynchrones (c’est-à-dire des requêtes qui n’ont pas lieux en même temps que le chargement de la page) afin de compléter un contenu de page ( effet de lazy loading (chargement différé) pour les images…).

Javascript a un formaliste proche du langage C/C++. Mais c’est un langage faiblement typé (pas besoin de déclaré le type d’une variable lors de sa création) ce qui semble alléchant à priori, mais se révèle très lourd à l’usage. Par exemple maVariable=0000 : le moteur sera incapable de savoir si 0 est un nombre ou une chaine de caractères…

Depuis quelques années on voit apparaitre des frameworks (structure ou environnement logiciel) JavaScript. Ces frameworks regroupent des bibliothèques JavaScript créées pour faciliter l’écriture de scripts côté client dans le code HTML des pages web. Une des plus connue est jQuery.

La bibliothèque jQuery contient notamment les fonctionnalités suivantes :

  • Parcours et modification du DOM (y compris le support des sélecteurs CSS 1 à 3 et un support basique de XPath) ;
  • Événements ;
  • Effets visuels et animations ;
  • Manipulations des feuilles de style en cascade (ajout/suppression des classes, d’attributs…) ;
  • Ajax (requêtes asynchrones formulées par le navigateur pour, par exemple, récupérer un jeu de données ;
  • Plugins ;
  • Utilitaires (version du navigateur web…).

1. bases de jquery

2. Notes de cours

Fondamentalement le Framework jQuery est un script JavaScript. Pour l’utiliser il suffit juste d’intégrer ce script dans la page html. Pour cela il faut télécharger jQuery (https://jquery.com/download/ ), placer le script dans le dossier du site (l’idéal est de créer un sous dossier nommé JS et qui contiendra tous les fichiers de script).

Dans la page html, il suffit alors de charger ce script et, pour pouvoir utiliser jQuery, on rajoute le lien vers le fichier qui contiendra les scripts actifs sur la page (demo.js dans l’exemple ci-dessous).

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title></title>
    <!-- déclaration du moteur jQuery. Dans cet exemple il se nomme jquery.js et est localisé dans le dossier JS-->
    <script src="Scripts/jquery-1.9.1.min.js"></script>
    <!-- script de la page-->
    <script>
        $(function () {
            //ici on place les fonctions qui permettent de personnaliser la page
            console.log("ready!"); // indication dans la console que jQuery est correctement initialisé
            $("#cachemoi").hide(3000); // au chargement de la page la division d'id "cahemoi" disparait en 3000ms
            //…
        });
    </script>
</head>
<body>
    <div id="conteneur">
        <h1>Le titre de la page</h1>
        <p>bonjour, comment ça va ? Je suis sûr que si tu <span>cliques ici</span> ça fait quelque chose</p>
        <p>2ème paragraphe</p>
        <div id="makemegreen">Fait-moi devenir tout vert</div>
        <div id="cachemoi">Cache moi !</div>
    </div>
</body>
</html> 

 

 

Pour initialiser les scripts de la page le contenu du fichier de script de la page (demo.js) doit avoir la forme suivante :

$(function () {
  //ici on place les fonctions qui permettent de personnaliser la page
  console.log("ready!"); // indication dans la console que jQuery est correctement initialisé
  $("#cachemoi").hide(3000); // au chargement de la page la division d'id "cahemoi" disparait en 3000ms
//…
});

 

Pour plus d’information : http://learn.jquery.com/

    1. Sélection des éléments du DOM

Voir : http://learn.jquery.com/using-jquery-core/selecting-elements/

Les balises html contiennent souvent des attributs. Par exemple un lien hypertexte contient des attributs tels que « href » (lien hypertexte), « title » (info bulle)… jQuery permet d’interroger ces attributs pour lire leur valeur ou pour la changer

Exemple avec la balise img (image)

<img id="greatphoto" src="brush-seller.jpg" alt="brush seller">

L’attribut « alt » (comme alternative) est le texte qui apparait à la place de l’image si cette dernière, pour une quelconque raison, ne peut être affichée.

Avec le jQuery suivant :

$("#greatphoto").attr("alt", "Brush Manquant");

L’image d’id « greatphoto », verra son attribut alt remplacé par « Brush Manquant »

Avec jQuery on peut modifier le contenu de la page

Autre exemple pour remplacer du code html

$("#conteneur").html("<p>Texte de remplacement</p>");

Dans cet exemple le contenu de la div d’id « conteneur » sera remplacé par un paragraphe de texte « Texte de remplacement ».

Si on désire rajouter du texte à la fin d’un paragraphe :

$("#conteneur p:first").append(" texte rajouté à la fin");

jQuery cherche le premier paragraphe contenu dans la div « conteneur » et rajoute à la fin le texte «  texte rajouté à la fin »

De la même manière,

$("#conteneur p:first").prepend("Texte rajouté au début<br/>");

Rajoute du texte au début du même paragraphe

Les évènements permettent de capturer les interactions entre l’utilisateur et le navigateur (déplacement ou clic de souris, saisie clavier…) et d’exploiter ces informations pour modifier le contenu ou l’apparence de la page.

Exemple détection d’un clic de souris :

$(document).keyup(function (e) {
      console.log(e.keyCode); // option de debug : permet d'afficher la valeur de la touche pressée.
         if (e.keyCode == 82) {  // 82 valeur de la touche 'r'
          $("#makemegreen").css("color", "red");// modification de la couleur
          var nouveauTexte = $("#makemegreen").text().replace("vert", "rouge"); // remplacement de  "vert" par "rouge"
          $("#makemegreen").text(nouveauTexte);// affichage du texte remplacé
     };
});

 

Instrumentation modbus

L’instrumentation consiste ici à équiper un véhicule électrique de sondes afin de mesurer la vitesse, la température du moteur ainsi que sa vitesse de rotation. Les sondes sont construites autour de cartes Arduino. Les cartes Arduino sont reliées à un bus RS485. Une application Node-red embarquée dans un Raspberry Pi permet de communiquer via le protocole modBus avec les cartes Arduino. Les données recueillies sont affichées sur un tableau de bord : une page web développée elle aussi avec Node-red équipée de jauges.

Une Carte Raspberry équipée de node-red se comporte comme un maitre modBus. Une carte Arduino effectue une mesure et se comporte comme un esclave modBus. Le maitre interroge cycliquement la carte Arduino qui envoie alors ses résultats. La trame modBus est constituée d’un paquet de 7 données (voir le tableau dans le code Arduino). Node-red récupère ces données et les encapsule dans un objet json qui est stocké dans le contexte du flux. Une page web est fabriquée à l’aide de node-red cette page affiche une jauge grâce au script sonicGauge. Des requêtes Ajax à intervalles réguliers récupèrent l’objet stocké dans le contexte, ce qui permet la mise à jour de la jauge en temps réel.

Vidéos

La playlist suivante contient 4 vidéos :

  1. Présentation du projet
  2. Intégration du nœud Slave Modbus, extraction des données et sauvegarde dans le contexte de flux.
  3. Création d’une page web exploitant le contexte de flux à l’aide d’AJAX.
  4. Intégration de sonicGauge pour afficher les valeurs.

Code Arduino

La bibliothèque ModbusRtu.h est accessible ici : https://github.com/smarmengol/Modbus-Master-Slave-for-Arduino

Le code Arduino simule une acquisition de température allant de 0 à 42°C ce manière cyclique.

#include <ModbusRtu.h>
#include <SoftwareSerial.h>
#define SERIAL 0
#define MODBUS_ADR 1
#define TXEN 3
#define ID 0xCAFE
#define UNITE 0x09 // 
#define TYPE 0x03
#define MIN 0
#define MAX 100 
// data array for modbus network sharing
// format : {Id_appareil,id_unite,id_type,min,max,valEntiere,valDeci}
// 
// id_appareil : numéro unique
/* 

╔═════════╦════════════════╦══════════╦═════════╦═════╦═════╦════════════╦═════════╗
║  index  ║       0        ║    1     ║    2    ║  3  ║  4  ║     5      ║    6    ║
╠═════════╬════════════════╬══════════╬═════════╬═════╬═════╬════════════╬═════════╣
║ trame   ║ id_appareil    ║ id_unite ║ id_type ║ min ║ max ║ valEntiere ║ valDeci ║
╠═════════╬════════════════╬══════════╬═════════╬═════╬═════╬════════════╬═════════╣
║ exemple ║ 51966 (0xCAFE) ║ 9        ║ 3       ║ 5   ║ 95  ║ 33         ║ 90      ║
╚═════════╩════════════════╩══════════╩═════════╩═════╩═════╩════════════╩═════════╝

tableau des unité :
╔══════════╦══════╦═════╦══════╦═══╦════╦═══╦═══╦═════════╦═══════════╦════╗
║ id_unite ║  0   ║  1  ║  2   ║ 3 ║ 4  ║ 5 ║ 6 ║    7    ║     8     ║ 9  ║
╠══════════╬══════╬═════╬══════╬═══╬════╬═══╬═══╬═════════╬═══════════╬════╣
║ unité    ║ sans ║ m/s ║ Km/h ║ m ║ km ║ s ║ h ║ Tours/s ║ Tours/min ║ °C ║
╚══════════╩══════╩═════╩══════╩═══╩════╩═══╩═══╩═════════╩═══════════╩════╝
tableau des types :
╔═════════╦══════╦══════╦═════╦═════════╗
║ id_type ║  0   ║  1   ║  2  ║    3    ║
╠═════════╬══════╬══════╬═════╬═════════╣
║ Type    ║ Char ║ Byte ║ Int ║ decimal ║
╚═════════╩══════╩══════╩═════╩═════════╝
*/
// min :                       valeur minimum
// max :                       valeur max                             
// valEntiere:                 valeur entière
// valDeci :                   valeur decimale
 
uint16_t mesure[] = { ID, UNITE, TYPE, MIN, MAX, 0, 0 };
/**
 *  Modbus object declaration
 *  u8id : node id = 0 for master, = 1..247 for slave
 *  u8serno : serial port (use 0 for Serial)
 *  u8txenpin : 0 for RS-232 and USB-FTDI 
 *               or any pin number > 1 for RS-485
 */
Modbus slave(MODBUS_ADR,SERIAL,TXEN); // this is slave @1 and RS-232 or USB-FTDI
unsigned long previousMillis = 0;        // will store last time LED was updated
 
// constants won't change :
const long interval = 75;
void setup() {
  slave.begin( 19200 ); // baud-rate at 19200
}
 
void loop() {
        static long temp = 0;
        unsigned long currentMillis = millis();
 
        if (currentMillis - previousMillis >= interval) {
                // save the last time you blinked the LED
                previousMillis = currentMillis;
                temp += 10;
                int entier = temp / 100;
                int decimal = temp - (entier *100);
                mesure[5] = entier;
                mesure[6] = decimal;
                if (temp>4200)
                {
                        temp = 0;
                }
 
        }
 
  slave.poll( mesure,sizeof(mesure) );
}

Fichier JS du nœud “data2obj”

/* 

╔═════════╦════════════════╦══════════╦═════════╦═════╦═════╦════════════╦═════════╗
║  index  ║       0        ║    1     ║    2    ║  3  ║  4  ║     5      ║    6    ║
╠═════════╬════════════════╬══════════╬═════════╬═════╬═════╬════════════╬═════════╣
║ trame   ║ id_appareil    ║ id_unite ║ id_type ║ min ║ max ║ valEntiere ║ valDeci ║
╠═════════╬════════════════╬══════════╬═════════╬═════╬═════╬════════════╬═════════╣
║ exemple ║ 51966 (0xCAFE) ║ 9        ║ 3       ║ 5   ║ 95  ║ 33         ║ 90      ║
╚═════════╩════════════════╩══════════╩═════════╩═════╩═════╩════════════╩═════════╝

tableau des unité :
╔═══════╦══════╦═════╦══════╦═══╦════╦═══╦═══╦═════════╦═══════════╦════╗
║ index ║  0   ║  1  ║  2   ║ 3 ║ 4  ║ 5 ║ 6 ║    7    ║     8     ║ 9  ║
╠═══════╬══════╬═════╬══════╬═══╬════╬═══╬═══╬═════════╬═══════════╬════╣
║ unité ║ sans ║ m/s ║ Km/h ║ m ║ km ║ s ║ h ║ Tours/s ║ Tours/min ║ °C ║
╚═══════╩══════╩═════╩══════╩═══╩════╩═══╩═══╩═════════╩═══════════╩════╝
tableau des types :
╔═══════╦══════╦══════╦═════╦═════════╗
║ index ║  0   ║  1   ║  2  ║    3    ║
╠═══════╬══════╬══════╬═════╬═════════╣
║ Type  ║ Char ║ Byte ║ Int ║ decimal ║
╚═══════╩══════╩══════╩═════╩═════════╝
par exemple data = [ 51966, 9, 3, 5, 95, 33, 90 ]
id = 51966 = 0xCAFE
unité = 9 => °C
type = 3 => decimal
min = 5 => minimum indiqué sur la gauge
max = 95 => max indiqué sur la gauge
valEntiere = 33 et vaDeci = 90 => tempétature = 33.90°C

format de l'objet de sortie :
4 propriétés : unite, valeur, min et max 
{ "unite": "°C", "valeur": 33.9, "min": 5, "max": 95 }
*/
var data = msg.payload;
var obj={};
var unite = ['','m/s','km/h','m','km','s','h','tours/s','tours/min','°C'];

if(data[0]==0xCAFE)
{
    obj.unite=unite[data[1]];
    if(data[2]==3){
        obj.valeur= (data[5]*100 + data[6])/100.0;
    }
    obj.min=data[3];
    obj.max=data[4];
}
flow.set('message',obj);
msg.payload=obj;
return msg;

Fichier html du noeud “html”

<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
<script>{{{script}}}</script>
<div id='thermometre' class="gauge"></div>
<script>
    var thermometre= $('#thermometre').SonicGauge();
    
    $.getJSON(
    "/data",
    function(data){
        var options ={};
        options.label= data.unite;
        options.start = {angle: -225, num: data.min};
        options.end		= {angle: 45, num: data.max};
        thermometre.SonicGauge('setOptions', options);
        thermometre.SonicGauge ('draw');
    });
                
                
setInterval(function () {
$.getJSON(
    "/data",
    function(data){
        thermometre.SonicGauge('val',data.valeur);
    });
},200);


</script>

Script SonicGauge à coller dans le nœud “script”

/**
 * Sonic Gauge jQuery Plugin v0.3.0
 * jQuery plugin to create and display SVG gauges using RaphaelJS
 * 
 * Copyright (c) 2013 Andy Burton (http://andyburton.co.uk)
 * GitHub https://github.com/andyburton/Sonic-Gauge
 * 
 * Licensed under the MIT license (http://andyburton.co.uk/license/mit.txt)
 */

(function(b){var a={init:function(c){if(!this.length){return this}this.options=b.extend(true,{},b.fn.SonicGauge.defaultOptions);this.settings={};this.SonicGauge("setOptions",c);this.SonicGauge("draw");return this},setOptions:function(c){if(c){b.extend(true,this.options,c)}this.settings.canvas_d=this.options.diameter;this.settings.canvas_r=this.settings.canvas_d/2;this.settings.speedo_d=this.settings.canvas_d-this.options.margin*2;this.settings.speedo_r=this.settings.speedo_d/2;this.settings.increment=(this.options.end.angle-this.options.start.angle)/(this.options.end.num-this.options.start.num);return this},draw:function(){var f=this;this.width(this.settings.canvas_d);this.height(this.settings.canvas_d);this.gauge=Raphael(this.attr("id"),this.settings.canvas_d,this.settings.canvas_d);var e=this.gauge.circle(this.settings.canvas_r,this.settings.canvas_r,this.settings.speedo_r).attr(this.options.style.outline);var i=this.settings.canvas_r;var h=this.settings.canvas_r-(this.settings.canvas_r/4);if(typeof this.options.label=="object"){if(this.options.label.margin_x){i+=this.options.label.margin_x}if(this.options.label.margin_y){h+=this.options.label.margin_y}}else{if(typeof this.options.label=="string"){this.options.label={value:this.options.label}}}if(this.options.label){var d=this.gauge.text(i,h,this.options.label.value).attr(this.options.style.label)}this.sectors=[];b.each(this.options.sectors,function(n){this.style=b.extend(true,f.options.style.sector,this.style);if(!(isNaN(this.start)||isNaN(this.end))){var p=f.settings.increment*(this.start-f.options.start.num)+f.options.start.angle;var m=f.settings.increment*(this.end-f.options.start.num)+f.options.start.angle;var j=this.radius?this.radius:f.settings.speedo_r;var q=Math.PI/180;var l=f.settings.canvas_r+j*Math.cos(p*q),k=f.settings.canvas_r+j*Math.cos(m*q),t=f.settings.canvas_r+j*Math.sin(p*q),s=f.settings.canvas_r+j*Math.sin(m*q);var o=f.gauge.path(["M",f.settings.canvas_r,f.settings.canvas_r,"L",k,s,"A",j,j,0,+(m-p>180),0,l,t,"z"]).attr(this.style);f.sectors.push(o)}});var g=[];b.each(this.options.markers,function(){if(this.line){if(!this.line.width){this.line.width=10}if(!this.line.height){this.line.height=1}var v=f.gauge.rect(f.settings.canvas_r+f.settings.speedo_r-this.line.width,f.settings.canvas_r-Math.floor(this.line.height/2)).attr(this.line).hide()}var n=1;while(this.gap<1){n*=10;this.gap*=10}var j=f.options.start.num*n;var l=f.options.end.num*n;for(var o=j;o<=l;o+=this.gap){var k=n>1?o/n:o;if(this.toFixed){k=k.toFixed(this.toFixed)}if(this.toPrecision){k=k.toPrecision(this.toPrecision)}var t=f.settings.increment*(k-j)+f.options.start.angle;if(t+Math.abs(f.options.start.angle)>=360){t=(t+Math.abs(f.options.start.angle))%360+f.options.start.angle}if(b.inArray(t,g)>=0){continue}g.push(t);if(this.line){var p=v.clone().rotate(t,f.settings.canvas_r,f.settings.canvas_r)}if(this.text){if(!this.text.space){this.text.space=0}var m=k;if(typeof this.value=="object"){if(this.value.divide){m/=this.value.divide}if(this.value.multiply){m*=this.value.multiply}}var q=t.toRadians();var s=f.settings.canvas_r+(this.text.space+f.settings.speedo_r)*Math.cos(q);var r=f.settings.canvas_r+(this.text.space+f.settings.speedo_r)*Math.sin(q);var u=f.gauge.text(s,r,m).attr(this.text)}}if(this.line){v.remove()}});if(this.options.digital){this.digital=b("<div>").addClass("digital").css({"margin-top":Math.ceil(this.settings.speedo_r/2),width:"20%","font-family":"Arial","font-size":20,color:"#fff","text-align":"center",border:"2px solid #777","border-radius":10,padding:5,"background-color":"#111"}).css(this.options.digital).text(this.options.default_num).appendTo(this).center()}this.needles=[];b.each(this.options.needles,function(k){if(!this.default_num){this.default_num=f.options.default_num}this.style=b.extend(true,f.options.style.needle,this.style);var m=this.default_num-f.options.start.num;if(typeof this.value=="object"){if(this.value.divide){m/=this.value.divide}if(this.value.multiply){m*=this.value.multiply}}var j=f.settings.increment*m+f.options.start.angle;var l=f.gauge.rect(f.settings.canvas_r,f.settings.canvas_r,f.settings.speedo_r).attr(this.style).transform("r"+j+","+f.settings.canvas_r+","+f.settings.canvas_r);f.needles.push(l)});if(typeof this.options.style.center=="object"){var c=this.gauge.circle(this.settings.canvas_r,this.settings.canvas_r,this.options.style.center.diameter).attr(this.options.style.center)}this.trigger("drawn");return this},val:function(e){if(this.digital){var c=e;if(this.options.digital_toFixed){c=c.toFixed(this.options.digital_toFixed)}if(this.options.digital_toPrecision){c=c.toPrecision(this.options.digital_toPrecision)}this.digital.text(c)}var d=this;b.each(this.needles,function(h){var f=e;if(typeof d.options.needles[h].value=="object"){var g=d.options.needles[h].value;if(g.divide){f/=g.divide}if(g.multiply){f*=g.multiply}}this.animate({transform:"r"+(d.settings.increment*(f-d.options.start.num)+d.options.start.angle)+","+d.settings.canvas_r+","+d.settings.canvas_r},d.options.animation_speed)});this.trigger("update",e);return this}};b.fn.SonicGauge=function(c){if(a[c]){return a[c].apply(this,Array.prototype.slice.call(arguments,1))}else{if(typeof c==="object"||!c){return a.init.apply(this,arguments)}else{b.error("Method "+c+" does not exist on jQuery.SonicGauge")}}};b.fn.SonicGauge.defaultOptions={margin:35,diameter:350,start:{angle:-225,num:0},end:{angle:45,num:100},default_num:0,animation_speed:1000,digital:{},digital_toFixed:0,needles:[{}],sectors:[{}],markers:[{gap:10,line:{width:20,stroke:"none",fill:"#eeeeee"},text:{space:22,"text-anchor":"middle",fill:"#333333","font-size":18}},{gap:5,line:{width:8,stroke:"none",fill:"#999999"}}],style:{outline:{fill:"#333333",stroke:"#555555","stroke-width":8},center:{fill:"#eeeeee",diameter:10},needle:{height:1,stroke:"none",fill:"#cc0000"},label:{"text-anchor":"middle",fill:"#fff","font-size":16}}}})(jQuery);if(typeof(Number.prototype.toRadians)==="undefined"){Number.prototype.toRadians=function(){return this*Math.PI/180}}if(typeof(Number.prototype.decimalPlaces)==="undefined"){Number.prototype.decimalPlaces=function(){return(this.toFixed(20)).replace(/^-?\d*\.?|0+$/g,"").length}}if(typeof(jQuery.fn.center)==="undefined"){jQuery.fn.center=function(a){if(typeof a==="undefined"){a=this.parent()}a.css("position","relative");return this.css("position","absolute").css({top:Math.max(0,((a.height()-this.outerHeight())/2)+a.scrollTop()),left:Math.max(0,((a.width()-this.outerWidth())/2)+a.scrollLeft())})}};

Le code complet du flow node-red : http://flows.nodered.org/flow/f0d416178add2b981ac21afeaf0bc0f7