Projets systèmes communicants
Alexandre Boé, Xavier Redon, Thomas Vantroys, Nicolas Wichmann
1 Objectif
L'objectif du module "Projets IMA3 systèmes communicants" est de vous faire découvrir la notion de projet interdisciplinaire.
Le thème de ce projet est la conception et la réalisation d'une interface Web à un objet électronique connecté par liaison série.
Dans le cadre de ce thème vous devez :
-
définir votre objet en fonction des composants disponibles dans les salles de projets informatiques et électroniques,
à savoir des LEDs, des afficheurs n-segments, des matrices de LEDs, des servo-moteurs, des contrôleurs de moteurs (bras robotiques
et robots mobiles), des boutons, des accéléromètres, des capteurs de température, des capteurs de pression, des boussoles, des sonars
ultra-sons, etc ;
- réaliser une carte électronique permettant d'accéder aux composants de votre objet par une liaison série, a priori cette
carte est à réaliser en utilisant un FPGA ;
- définir le protocole de communication série pour actionner l'objet et récupérer ses données ;
- réaliser un site Web sur un système embarqué de type Raspberry Pi pour contrôler votre objet et afficher les données qu'il
collecte, le site est à écrire en HTML avec feuilles de style CSS, pour la partie dynamique les langages PHP et javascript sont à utiliser ;
- implanter un serveur websocket sur le système embarqué pour assurer la communication entre l'interface Web et le port série.
En fin de projet vous devez présenter une démonstration de votre réalisation en utilisant un PC portable connecté en Ethernet sur le
système embarqué, lui même connecté en série sur votre carte électronique de contrôle de l'objet.
2 Partie électronique
Pour réaliser le contrôle des capteurs et des actionneurs par la carte électronique, les procédés décrits ci-dessous peuvent être utilisés.
-
Sonar :
- La mesure de distance est basée sur la mesure du décalage entre l'émission d'un signal
ultrason et sa réception, après rebond sur l'obstacle. La carte électronique génère un signal carré à
la fréquence de résonance de l'émetteur d'ultrason afin de permettre l'émission
du signal. Le début d'émission fera démarrer un compteur qui sera arrêté lors de la
réception du signal ultrason sur le récepteur. Ainsi la valeur du compteur correspond à une
représentation de la distance parcourue par l’onde ultrasonore. L’écriture de la valeur du
compteur en mémoire sera permise grâce à un bit de permission d’écriture. La partie analogique
doit permettre d'adapter le signal carré à l'émetteur d'ultrasons avec une tension et une puissance
disponible compatible avec l'émetteur. De même pour la réception, l'onde reçue par le récepteur
doit être adaptée afin de la rendre compatible avec l'entrée du FPGA notamment son amplification et sa
mise en forme (signal 0-5v).
- Contrôleur de moteur, servo-moteurs, LED RGB, LED infrarouge :
- Il suffit de générer des signaux PWM
(Pulse Width Modulation, ou Modulation de Largeur d'Impulsions). Les signaux PWM
sont des signaux de fréquence constante mais dont on change le rapport cyclique
grâce à une donnée de commande. L’accès à la mémoire ne se faisant que sur un seul octet, il
conviendra donc d’effectuer un multiplexage pour accéder aux données de commande. De plus,
la lecture de ces données ne sera possible que lorsque le bit d'autorisation de lecture sera
validé. Chacun des signaux PWM générés est acheminés sur une sortie différente. Une partie
analogique est nécessaire pour obtenir une valeur moyenne dans le cas des LEDs.
- Accéléromètre, photo-transistor, capteur de pression, de température, de ligne :
-
Il s'agit de conversions analogique numérique. Une méthode possible est basée sur la
génération de signaux PWM puis par leur filtrage (filtre passe-bas) permettant d'obtenir une tension
continue variable représentant la valeur numérique. C'est la partie analogique qui permet de comparer
la valeur moyenne du signal PWM avec la tension provenant du capteur. Tant que la valeur moyenne du
signal PWM (réglée par la donnée de commande) est inférieure à la tension provenant du capteur, la
sortie du comparateur est à 0V. Lorsque la valeur moyenne du signal PWM devient supérieure ou égale à la
tension de l’accéléromètre, la sortie du comparateur passe à +Vcc. A ce moment là, la donnée de
commande correspond à la représentation numérique de la tension.
- Matrice de LEDs, afficheurs n-segments :
- La donnée provient du module mémoire et représente
de façon directe l'état allumé ou éteint des LEDs. Les bits représentent l’état des LEDS sur une colonne
de la matrice ou sur un afficheur. Les différentes colonnes ou afficheurs seront donc multiplexées.
La lecture de cette donnée ne sera possible que lorsque le bit d'autorisation de lecture sera validé.
Il conviendra de pouvoir afficher, de façon suffisamment rapide, les images transmises par le module
mémoire, tout en évitant les effets de scintillement.
Pour vérifier le fonctionnement de vos dispositifs numériques, l'utilisation de l'analyseur
logique est fortement recommandée.
Pour la gestion de la liaison série une IP (intellectual property) vous sera fournie qui réalise déjà la réception
et l'envoi des bits.
3 Partie informatique
Vous commencerez par développer votre interface Web sur une machine fixe de projets. Pour la mise en production,
votre site doit être déplacé sur un système embarqué (en l'occurence une Raspberry Pi). Pour que le site Web
du système embarqué puisse être accédé, il doit être connecté au réseau de l'école et être correctement
configuré (voir B en annexe).
La communication entre le navigateur et le port série se fait en utilisant un serveur WebSocket (voir
E.2 en annexe). Pour l'accès au port série voyez la bibliothèque proposée (C.1 en annexe).
Si votre objet nécessite une lecture non-bloquante sur le port série, utilisez l'option O_NONBLOCK
lors de l'appel à la primitive open (voir la page de manuel correspondante).
Pour pouvoir déverminer votre interface Web, il est fortement conseillé d'utiliser le navigateur
firefox avec son module firebug.
En attendant la réalisation de la carte électronique, un prototype de l'objet doit être rapidement être mis au point
avec une carte de type Arduino. La programmation de l'Arduino peut se faire à l'aide de l'environnement de
développement Arduino.
A NanoBoard
A.1 Tutoriel
Suivez le lien pour un tutoriel d'utilisation de la carte FPGA NanoBoard :
tutoriel_nanoboard.pdf
B Configuration de la Raspberry Pi
B.1 Configuration IP
Votre Raspberry Pi doit pouvoir communiquer avec votre ordinateur fixe (ordinateur de
salle de projet) mais également avec le réseau de l'école pour permettre l'installation
de paquetages. Vous allez donc mettre une adresse IP du réseau INSECURE de l'école sur
la Raspberry. Choisissez comme adresse IP 172.26.79.X avec X
correspondant à votre numéro de machine fixe. Pour configurer une Raspberry sous Linux/Debian,
remplacez les lignes correspondant à la configuration de la carte Ethernet dans le fichier
/etc/network/interfaces par les lignes :
auto eth0
iface eth0 inet static
address 172.26.79.X
netmask 255.255.240.0
gateway 172.26.79.254
Vous devez aussi indiquer le serveur DNS dans le fichier /etc/resolv.conf.
nameserver 193.48.57.34
Pour accéder à la Raspberry, utilisez un câble série et le logiciel minicom
qui permet d'établir une connexion série (vitesse de 115200 bauds et pas de contrôle
de flux matériel).
B.2 Lancement de programmes au démarrage
Vous devrez faire en sorte que la Raspberry Pi exécute des commandes à chaque
démarrage. Pour ce faire, Vous pouvez donc les inscrire dans le fichier /etc/rc.local.
C Programmes
C.1 Bibliothèque d'accès au port port série
Ces fonctions basiques en C permettent d'accèder au port série sous Unix.
Fichier d'entêtes serial.h :
/*
* Public definitions for serial library
*/
////
// Constants
////
#define SERIAL_READ 0
#define SERIAL_WRITE 1
#define SERIAL_BOTH 2
////
// Public prototypes
////
int serialOpen(char *device,int mode);
void serialConfig(int fd,int speed);
void serialClose(int fd);
Fichier C serial.c :
/*
* Serial library
*/
////
// Include files
////
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/file.h>
#include <linux/serial.h>
#include "serial.h"
////
// Functions
////
//
// Open serial port device
//
int serialOpen(char *device,int mode){
int flags=(mode==SERIAL_READ?O_RDONLY:(mode==SERIAL_WRITE?O_WRONLY:O_RDWR));
int fd=open(device,flags|O_NOCTTY);
if(fd<0){ perror(device); exit(-1); }
return fd;
}
//
// Serial port configuration
//
void serialConfig(int fd,int speed){
struct termios new;
bzero(&new,sizeof(new));
new.c_cflag=CLOCAL|CREAD|speed|CS8;
new.c_iflag=0;
new.c_oflag=0;
new.c_lflag=0; /* set input mode (non-canonical, no echo,...) */
new.c_cc[VTIME]=0; /* inter-character timer unused */
new.c_cc[VMIN]=1; /* blocking read until 1 char received */
if(tcsetattr(fd,TCSANOW,&new)<0){ perror("serialInit.tcsetattr"); exit(-1); }
}
//
// Serial port termination
//
void serialClose(int fd){
close(fd);
}
C.2 Exemple de programme C utilisant le port série
Le programme présenté ici envoie un octet nul au port série
puis récupère 8 octets sur le même port série. Le programme
utilise les fonctions présentées en C.1.
/*
* Test on serial device
*/
////
// Include files
////
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <termios.h>
#include "serial.h"
////
// Constants
////
#define SERIAL_DEVICE "/dev/ttyACM0"
////
// Global variables
////
////
// Main function
////
int main(void){
int c=0;
int sd=serialOpen(SERIAL_DEVICE,SERIAL_BOTH);
serialConfig(sd,B9600);
if(write(sd,&c,sizeof(char))!=1){ perror("main.write"); exit(-1); }
int i;
for(i=0;i<8;i++){
if(read(sd,&c,sizeof(char))!=1){ perror("main.read"); exit(-1); }
printf("%02x\n",c);
}
serialClose(sd);
exit(0);
}
D Pages dynamiques javascript
D.1 Page HTML
Voici un exemple de page HTML5 utilisant des canvas pour afficher des disques
dont la couleur peut être modifiée par un simple clic. Cette page utilise un
script javascript disque.js présenté plus loin.
<!DOCTYPE html>
<html>
<head> <title>LEDs</title> </head>
<body onload="dessinDisques();">
<script language="javascript" src="disques.js"> </script>
<canvas id="led0" width="20" height="20" onclick="changeCouleur(0);"></canvas>
<canvas id="led1" width="20" height="20" onclick="changeCouleur(1);"></canvas>
<canvas id="led2" width="20" height="20" onclick="changeCouleur(2);"></canvas>
<canvas id="led3" width="20" height="20" onclick="changeCouleur(3);"></canvas>
</body>
</html>
D.2 Script javascript
Voici le script javascript utilisé par la page HTML5 précédente. Les dessins sont
effectués en utilisant la technologie des canvas.
// Constantes
var TAILLE=20;
var DECALAGE=5;
var SPECULAIRE=3;
// Variables globales
var couleurs=['green','green','green','green'];
// Fonction de dessin de disque
function dessinDisque(id){
/* Récupération du canvas */
var canvas=document.getElementById('led'+id);
var context=canvas.getContext('2d');
/* Création d'un dégradé de couleur */
var gradient=context.createRadialGradient(
TAILLE/2-DECALAGE,TAILLE/2-DECALAGE,SPECULAIRE,
TAILLE/2-DECALAGE,TAILLE/2-DECALAGE,TAILLE+DECALAGE);
gradient.addColorStop(0,'white');
gradient.addColorStop(0.75,couleurs[id]);
/* Dessin du disque */
context.beginPath();
context.arc(TAILLE/2,TAILLE/2,TAILLE/2,0,2*Math.PI,false);
context.fillStyle=gradient;
context.fill();
/* Tracage du cercle pour lissage */
context.strokeStyle='white';
context.lineWidth = 2;
context.stroke();
}
// Fonction de dessin des disques
function dessinDisques(){
var i;
for(i=0;i<4;i++) dessinDisque(i);
}
// Fonction de modification de couleurs
function changeCouleur(id){
if(couleurs[id]=='green'){couleurs[id]='red';}
else{couleurs[id]='green';}
dessinDisque(id);
}
E WebSockets
Le protocole WebSocket permet une communication bi-directionnelle entre un navigateur et une application distante.
E.1 Bibliothèque JQuery
Vous pouvez trouver la bibliothèque Web 2.0 jquery.js sur le site
http://jquery.com/. Cette bibliothèque permet de lancer des requêtes
HTTP asynchrones mais aussi de faciliter la programmation JavaScript.
E.2 Exemple de serveur WebSocket
L'exemple ci-dessous est réalisé avec une bibliothèque C de WebSocket dont le site est http://libwebsockets.org.
Pour en disposer sur une machine Debian, installez le paquetage libwebsockets-dev. Lors de la compilation du
programme n'oubliez pas l'option -lwebsockets.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libwebsockets.h>
#define MAX_FRAME_SIZE 1024
#define WAIT_DELAY 50
static int callback_http(
struct libwebsocket_context *this,
struct libwebsocket *wsi,enum libwebsocket_callback_reasons reason,
void *user,void *in,size_t len)
{
return 0;
}
static int callback_my(
struct libwebsocket_context * this,
struct libwebsocket *wsi,enum libwebsocket_callback_reasons reason,
void *user,void *in,size_t len)
{
static char *message=NULL;
static int msize=0;
switch(reason){
case LWS_CALLBACK_ESTABLISHED:
printf("connection established\n");
message=NULL;
// Declenchement d'un prochain envoi au navigateur
libwebsocket_callback_on_writable(this,wsi);
break;
case LWS_CALLBACK_RECEIVE:
// Ici sont traites les messages envoyes par le navigateur
printf("received data: %s\n",(char *)in);
message=malloc(len+LWS_SEND_BUFFER_PRE_PADDING+LWS_SEND_BUFFER_POST_PADDING);
if(message==NULL){ perror("callback_my.malloc"); exit(EXIT_FAILURE); }
memcpy(message+LWS_SEND_BUFFER_PRE_PADDING,in,len);
// Declenchement d'un prochain envoi au navigateur
msize=len;
libwebsocket_callback_on_writable(this,wsi);
break;
case LWS_CALLBACK_SERVER_WRITEABLE:
// Ici sont envoyes les messages au navigateur
if(message!=NULL){
char *out=message+LWS_SEND_BUFFER_PRE_PADDING;
libwebsocket_write(wsi,(unsigned char *)out,msize,LWS_WRITE_TEXT);
free(message);
message=NULL;
}
break;
default:
break;
}
return 0;
}
static struct libwebsocket_protocols protocols[] = {
{
"http-only", // name
callback_http, // callback
0, // data size
0 // maximum frame size
},
{"myprotocol",callback_my,0,MAX_FRAME_SIZE},
{NULL,NULL,0,0}
};
int main(void) {
int port=9000;
struct lws_context_creation_info info;
memset(&info,0,sizeof info);
info.port=port;
info.protocols=protocols;
info.gid=-1;
info.uid=-1;
struct libwebsocket_context *context=libwebsocket_create_context(&info);
if(context==NULL){
fprintf(stderr, "libwebsocket init failed\n");
return -1;
}
printf("starting server...\n");
while(1){
libwebsocket_service(context,WAIT_DELAY);
}
libwebsocket_context_destroy(context);
return 0;
}
E.3 Exemple d'utilisation d'un serveur WebSocket
La page HTML ci-dessous se connecte sur le serveur WebSocket présenté ci-avant. Les chaînes tapés dans le champ
texte sont envoyés au serveur WebSocket puis retournées par le serveur au navigateur Web et affichées dans une
balise div.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="jquery.js"></script>
<script type="text/javascript">
window.WebSocket=(window.WebSocket||window.MozWebSocket);
var websocket=new WebSocket('ws://127.0.0.1:9000','myprotocol');
websocket.onopen=function(){ $('h1').css('color','green'); };
websocket.onerror=function(){ $('h1').css('color','red'); };
websocket.onmessage=function(message){
console.log(message.data);
$('#messages').append($('<p>',{ text: message.data }));
};
function sendMessage(){
websocket.send($('#message').val());
$('#message').val('');
}
</script>
</head>
<body>
<h1>WebSockets test</h1>
<input type="text" id="message"/>
<button onclick="sendMessage();">Send</button>
<div id="messages"></div>
</body>
</html>
This document was translated from LATEX by
HEVEA.