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 [shell]/etc/openalpr/alprd.conf[/shell]

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

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

Jeu de pong C#

Pong est un des premiers jeux vidéo d’arcade (1972). Au travers de quelques vidéos, nous allons recréer ce jeu dans l’environnement Windows, à l’aide du langage C#. Nous verrons comment intégrer des objets graphiques (pictureBox) et les faire se mouvoir, comment gérer les rebonds sur les bords et les raquettes. Nous verrons aussi comment ajouter du son.

PictureBox

le fichier contenant les images gif des balles est à télécharger ici

Déplacement d’une pictureBox

Reste à faire :

ajouter les boutons et le code afin que la balle puisse se déplacer en diagonale dans les 4 sens.

Résumé des informations concernant les timer :

Un composant Timer permet d’implémenter une minuterie déclenchant un événement selon un intervalle de temps défini par l’utilisateur.

Propriété Type Description
https://sites.google.com/site/notionscsharpcem/_/rsrc/1412335790755/guicontroles/timer/Propri%C3%A9t%C3%A9.png (Name) Indique le nom utilisé dans le code pour identifier l’objet. Préfixe : tmr Exemple : tmrHorloge
https://sites.google.com/site/notionscsharpcem/_/rsrc/1412335790755/guicontroles/timer/Propri%C3%A9t%C3%A9.png Enabled bool Obtient ou définit l’état de la minuterie, soit active ou non. true si la minuterie est active ; sinon, false.
L’état par défaut est false.
https://sites.google.com/site/notionscsharpcem/_/rsrc/1412335790755/guicontroles/timer/Propri%C3%A9t%C3%A9.png Interval int Obtient ou définit la fréquence de temps en millisecondes du déclenchement de l’événement Tick.
Événement Description
https://sites.google.com/site/notionscsharpcem/_/rsrc/1282076684193/guicontroles/button/Evenements.png Tick Se produit lorsque l’intervalle de temps spécifié s’est écoulé.
Méthodes Description
https://sites.google.com/site/notionscsharpcem/_/rsrc/1401026160445/instanciation_objet/M%C3%A9thode.png Start Démarre la minuterie. Exemple : tmrHorloge.Start();
https://sites.google.com/site/notionscsharpcem/_/rsrc/1401026160445/instanciation_objet/M%C3%A9thode.png Stop Arrête la minuterie. Exemple : tmrHorloge.Stop();

Déplacement et rebond de la balle

Reste à faire :
  • Modifier le code afin que la balle se déplace en diagonale et rebondisse sur les 4 bords du conteneur.
  • A l’aide des propriétés Height, Width, Location.X et Location.Y des divers composants modifier le code afin que les butées (les valeurs limites permettant de définir s’il y a rebond) soit indépendantes de la tailles des éléments : si l’utilisateur redimensionne la fenêtre de l’application, l’application doit toujours fonctionner.

Ajout d’une raquette et gestion de son déplacement à l’aide des touches du clavier.

L’image gif de la raquette peut être téléchargée ici

Reste à faire :
  • Modifier le code afin d’ajouter la raquette de gauche.
  • Provoquer le rebond de la balle sur les 2 raquettes, pour cela on utilisera les propriétés Location.X, Location.Y, ainsi que les propriétés Height et Width de la raquette.

Le rebond à lieu lorsque plusieurs conditions sont réunies :

  • horizontalement : lorsque pbBalle.Location.X + pbBalle.Width devient supérieur ou égale à pbRqDroite.Location.X
  • verticalement : pbBalle.Location.Y + pbBalle.Height supérieur ou égal à pbRqDroite.Location.Y ET pbBalle.Location.Y inférieure ou égal à pbRqDroite.location.Y + pbRqDroite.Height

gestion des scores :

  • Placer des labels afin d’afficher les scores : chaque fois que la balle rebondit sur les lignes verticales du conteneur, le score du joueur opposé s’incrémente ;
  • Ajouter une fonction indiquant une fin de partie lorsqu’un des scores arrive à 10.
  • Proposer une solution pour réinitialiser le jeu en fin de partie.

Simplification de  la gestion du rebond.

Ajouter du son[1]

  1. http://stackoverflow.com/questions/4125698/how-to-play-wav-audio-file-from-resources

Lecteur WebRadio avec node-red, MPD et MPC

Cet article propose de créer une IHM pour piloter un lecteur de radio internet. Le système est embarqué dans une carte Raspberry Pi équipée de node-red. Nous verrons d’abord comment installer MPD et MPC qui permettent de transformer la carte en lecteur audio. Nous verrons ensuite comment créer une interface web à l’aide de node-red qui permette de piloter le lecteur.

MPD est un lecteur média qui fonctionne comme un service (daemon). Il met en relation des sources de musiques, des codecs et une API de pilotage via le réseau. En fait il se comporte comme un serveur auquel on envoie des commandes de type « play », « stop »,… afin qu’il les exécute.
MPD / MPC[3]
Nous allons installer MDP (Music Player Daemon) qui va servir de lecteur de musique embarqué, ainsi que MPC (Music Player Command) qui servira à piloter MPD (telle une télécommande) :
MPD
Pré-requis
Vérifier que le driver audio est correctement installé :

[shell]
lsmod[/shell]

Il doit apparaître le driver snd_bcm2835 dans le résultat. Si ce n’est pas le cas, essayer :

[shell]sudo modprobe snd_bcm2835[/shell]

Forcer la sortie son sur la prise casque :

[shell]amixer cset numid=3 1
sudo alsactl store
[/shell]

Pour que la sortie audio soit toujours la prise Jack (au lieu de la prise HDMI), éditer le fichier /boot/config.txt et dé-commenter la ligne  hdmi_drive=2 en enlevant le dièse en début de ligne.

[shell]
# uncomment to force a HDMI mode rather than DVI. This can make audio work in
# DMT (computer monitor) modes
hdmi_drive=2
[/shell]

Installation

Sous Raspbian stretch, l’installation est facile (après mise à jour des dépôts) :

sudo apt update
sudo apt install mpc mpd

Il faut ensuite éditer le fichier /etc/mpd.conf et retirer le commentaire de la ligne “device” dans la section “audio_output”

# An example of an ALSA output:
#
audio_output {
    type		"alsa"
    name		"My ALSA Device"
    device		"hw:0,0"	# optional

il faut ensuite redémarrer MPD :

sudo /etc/init.d/mpd restart

Télécharger le fichier radio.m3u. Ce fichier contient la liste de quelques radios, il peut être édité. Pour ajouter d’autres radios rendez-vous à : www.ecouter-en-direct.com/
Coller le fichier playlist radio.m3u dans /var/lib/mpd/playlists/
Le fichier de configuration de mdp est /etc/mdp.conf. On pourra y paramétrer les dossiers où MPD ira chercher la musique et les playlist par exemple. Une fois les réglages effectués, redémarrer le service : [shell]sudo /etc/init.d/mpd restart[/shell]

charger le fichier playlist : [shell]mpc load radio[/shell]

lancer la diffusion : [shell]mpc play 1[/shell]

Vidéos

La playlist suivante contient 5 vidéos :

  1. installation de MPD et MPC
  2. Affichage de la liste des radio (DahBoard node-red)
    Remarque : bien décocher les cases “Pass through…” et “Add output…” dans le template  de la liste des radios.
  3. Déclenchement de la lecture au clic sur un élément de la liste.
  4. Ajout de boutons de commandes
  5. Affichage du nom de la radio et du titre en cours

Annexe

Le fichier css pour la mise en forme du tableau:

table {
    color: #fff;
    font-family: Helvetica, Arial, sans-serif;
    width: 100%;
    border-collapse: collapse;
    border-spacing: 0;
}

td, th {
    border: 1px solid transparent;
    /* No more visible border */
    height: 30px;
    transition: all 0.3s;
    /* Simple transition for hover effect */
}

th {
    background: #DFDFDF;
    /* Darken header a bit */
    font-weight: bold;
}

td {
    background: #3a3a3a;
    text-align: center;
}

/* Cells in even rows (2,4,6...) are one color */

tr:nth-child(even) td {
    background: #313131;
}

/* Cells in odd rows (1,3,5...) are another (excludes header cells)  */

tr:nth-child(odd) td {
    background: #3e3e3e;
}

tr td:hover {
    background: #F1F1F1;
    color: #000;
}

 

  1. Voir : http://www.peyregne.info/wp/radio-internet-sur-raspberry-pi-partie-1-mpdmpc/