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 : 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.