Pi BOAt – Radar de surface

pi_boat_modules_radar

Je n’ai pas pu résister, ce module radar de surface me faisait vraiment trop triper 🙂

Au menu, de l’I2C pour le télémètre ultrason SRF02, du ServoBlaster pour le pilotage du servo moteur, du Python pour orchestrer tout ça, et beaucoup de JavaScript et d’HTML 5 (canvas) pour afficher un bel écran radar 🙂

Principe de fonctionnement

Un radar c’est quoi ? C’est un truc qui envoi des ondes (sonores, lumineuses, électromagnétiques) et qui attend un retour de cette onde. Si retour il y a, c’est que l’onde a rebondi sur un objet, en mesurant le temps de retour de l’onde, on en déduit à quelle distance se trouve l’objet. En répétant l’opération tout en faisant tourner le radar dans toutes les directions, on peut détecter tous les objets présents aux alentours et les représenter sur un écran 🙂

pi_boat_radar

Donc, prenez un capteur à ultrason tel que le SRF02, faites le tourner sur lui même à l’aide d’un servo moteur, et vous avez votre radar 🙂

A chaque position d’angle du servo moteur, on note la distance mesurée par le télémètre, on enregistre tout ça dans un registre pour ensuite pouvoir l’afficher. On répète la manip à l’infinie pour avoir un balayage continu 🙂

Pour plus d’information sur les télémètres, je vous invite à lire cet article : GPIO – Télémètre à ultrason SRF02

Bien entendu, le temps de réponse et la qualité de mesure des télémètres à ultrason n’étant pas extra-ordinaires, il ne faut pas s’attendre à un haut niveau de précision. Mais, le résultat est, comme vous allez le voir, plus que satisfaisant 🙂 De plus, il ne faut pas oublier que nous sommes ici pour apprendre et découvrir des principes de fonctionnement. Si votre budget vous le permet, vous pourrez remplacer le télémètre à ultrason par un télémètre laser, et là, vous aurez un radar de surface très précis 🙂

Matériel, GPIO et branchements

Au départ, je voulais utiliser le module PCA9685 de chez Adafruit pour piloter le servo moteur, comme dans cet article. Finalement, après avoir testé l’excellente lib ServoBlaster, j’ai décidé de me passer totalement de cette carte et de brancher le servo directement sur le Pi 🙂

Au final, j’utilise donc uniquement un télémètre SRF02 et un servo moteur Futaba en les branchant sur le GPIO d’un Pi A+, sans carte intermédiaire 🙂

Raspberry Pi A+ Télémètre SRF02 Servo Futaba
raspberry_pi_a_carre srf02 servo

Niveau branchement, rien de plus simple, le télémètre se connecte sur les broches I2C du GPIO comme expliqué dans cet article : GPIO – Télémètre à ultrason SRF02

SRF02_RaspberryPi_GPIO

Pour le servo moteur, une seule broche suffit. Nous utiliserons une alimentation séparée pour le servo dont vous pouvez voir le convertisseur de tension sur la droite de la photo ci-dessous.

IMG_0222

raspberry_pi_touchscreen_officiel

Pour info, j’utilise le nouvel écran officiel de la fondation Raspberry Pi. Pour le maquettage, c’est vraiment très pratique je trouve 🙂

Je ferai un article dédié à cet écran très prochainement 🙂

Logiciel, Scripts et affichage

L’architecture logicielle du Pi BOAt n’est pas encore définitive mais le principe général est le suivant :

  1. Chaque module est piloté par un script Python dédié
  2. Le script Python inscrit ou lit des données dans un registre au format JSON
  3. Une page HTML est mise à disposition grâce à un mini serveur web en Python
  4. Un script JavaScript lit ou écrit dans le registre et gère l’affichage dans la page web

Note : Pour info, à l’heure où je réalise ce prototype, j’utilise un Raspberry Pi A+ avec une Raspbian Lite Jessie (2015-11-21).

script Python radar.py : gestion du servo et du télémètre à ultrason

Pour piloter le servo moteur, nous utiliserons la lib ServoBlaster qui fonctionne très bien et très simplement 🙂

Pour l’installer, rien de plus simple, faites un clone du dépôt GIT suivi d’un make pour compiler les sources :

git clone https://github.com/richardghirst/PiBits
cd ./PiBits/ServoBlaster/user/
sudo make servod

Il convient enseuite d’activer l’I2C pour pouvoir utiliser le télémètre SRF02. Ceci se fait, à présent, très simplement via la commande raspi-config 🙂

Ci-dessous, le script python radar.py dont le rôle est de faire aller et venir le servo d’un extrême à l’autre en continu, tout en prenant les mesures de distance avec le télémètre. A chaque position du servo on associe une distance mesurée, et on enregistre le tout dans un fichier radar.json.

Il y a un paramètre set_pas que vous pouvez ajuster en fonction de la finesse des mesures que vous souhaitez réaliser et de la capacité de votre servo moteur. Pour ma part, le servo moteur dont je dispose n’a une course que de 200 degrés environ. Le pas est converti en pourcentage de cet angle. En choisissant un pas de 1, je prendrais donc 100 mesures sur 200 degrés, soit une mesure tous les 2 degrés d’angle.

Bien entendu, plus vous augmentez le nombre de mesures, plus le balayage sera lent. Après quelques essais, je pense qu’il vaut mieux prendre moins de mesures mais avoir un balayage plus rapide. Une mesure tous les 10 degrés d’angle est largement suffisante pour détecter les objets aux alentours et offre un balayage très rapide.

Ceci est d’autant plus vrai avec un télémètre à ultrason dont le cône de détection est relativement large (plus de 30 degré pour le SRF02). Avec un télémètre laser en revanche, il serait plus pertinent d’avoir un balayage plus fin pour avoir des mesures très précises.

#!/usr/bin/python

import RPi.GPIO as GPIO
import smbus, time, os, srf02

radar_data_file = "/home/pi/www/data/radar.json"

data = []
set_pas = 1

fo = open(radar_data_file, "w")
fo.write(str(data));
fo.close()

nb = int((100/set_pas)+1)
for num in range(0,nb):
  data.append(0);

# SRF02

s = srf02.srf02()

# SERVO

os.system("sudo /home/pi/PiBits/ServoBlaster/user/servod")

p_min = 0
p_mid = 50
p_max = 100

p = p_min
pas = set_pas

while True:
  try:
    if p >= p_max:
      pas = -set_pas
    if p <= p_min:
      pas = set_pas
    p = p + pas
    
    os.system("echo 0="+str(p)+"% > /dev/servoblaster")

    #time.sleep (0.1)

    dst = s.getValue()
    idx = int((100-p)/set_pas)

    print(str(p)+' - '+str(idx)+' - '+str(dst))

    data[idx]=dst

    fo = open(radar_data_file, "w")
    fo.write(str(data));
    fo.close()

  except KeyboardInterrupt:
    os.system("echo 0=50% > /dev/servoblaster")
    quit()

En exécutant ce script, vous obtiendrez ce résultat :

radar_table

script JavaScript et page HTML

C’est là que ça devient réellement fun car on va voir le résultat s’afficher à l’écran 🙂

Le principe est relativement simple puisque nous avons une simple page web radar.html qui contient deux canvas. Un pour dessiner le fond du radar et un pour afficher les points correspondant aux objets détectés.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">

    <title>Pi BOAt - Radar</title>
    
    <link rel="stylesheet" type="text/css" href="css/radar.css" />
    
    <script src="//code.jquery.com/jquery-1.12.0.min.js"></script>
    <script src="//code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
    <script src="js/radar.js"></script>

  </head>
  <body>
  
    <div id="radar">
      <canvas id="radar_back" width="500" height="500"></canvas>
      <canvas id="radar_1" width="500" height="500"></canvas>
    </div>

  </body>
</html>

Ajoutons à cela une feuille de style radar.css, très basique, pour positionner et styliser les éléments principaux :

body {
  background-color : #FFF;
}

div#radar {
  position : relative;
  margin : 10px;
  width : 500px;
  height : 500px;
  border : solid 0px #555;
}

div#radar canvas {
  position:absolute;
  left:0px;
  top:0px;
}

canvas#radar_back {
  z-index : 1;
}

canvas#radar_1 {
  z-index : 2;
}

Enfin, le script le plus important, en JavaScript, radar.js.

Quelques explications s’imposent. Dans un premier temps, le script se charge de dessiner le fond du radar dans le premier canvas. Puis, une fonction appelée toutes les 500ms est chargée de lire le registre radar.json, d’en extraire les données et de dessiner les points correspondants sur le deuxième canvas.

Ce script contient quatre paramètres qu’il convient d’ajuster en fonction de vos besoins :

  • radar_width : largeur et hauteur en pixel du radar affiché dans la page web
  • max_range : distance maximale en centimètre affichable sur le radar (la distance maximale théorique du SRF02 est de 6 mètres, en pratique, 4 mètres soit 400 cm, c’est bien)
  • grid_step : pas de la grille radar en centimètre (permet une meilleure lisibilité des distances)
  • angle_range : largeur du balayage en degré (200 degrés dans mon cas)
/**********************/
/*     paramètres     */
/**********************/

var radar_width  = 500; // taille du radar en px
var max_range    = 400; // distance max en cm
var grid_step    = 50;  // pas de la grille en cm
var angle_range  = 200; // largeur du balayage en degré

/**********************/

Math.radians = function(degrees) {
  return degrees * Math.PI / 180;
};

var radar_timeout;

var radar_back, radar_back_ctx;
var radar_1, radar_1_ctx;

var radar_radius = radar_width/2; // rayon du radar en px
var range_ratio  = radar_radius/max_range; // ratio px/cm
var start_angle  = -Math.radians((angle_range/2)+90);
var end_angle    = -Math.radians(90-(angle_range/2));

function measureConvert (nb_measure, pos, dst) {
  if(dst > max_range) { dst = max_range; }
  var angle_deg  = -90-(angle_range/2)+Math.round((angle_range/(nb_measure-1))*pos);
  var angle_rad  = Math.radians(angle_deg);
  var dst_px     = Math.round(dst*range_ratio);
  var x          = Math.round(dst_px*Math.cos(angle_rad)+radar_radius);
  var y          = Math.round(dst_px*Math.sin(angle_rad)+radar_radius);
  var coor       = {x:x, y:y, angle:angle_deg}
  return coor;
}

$(function() {
  console.log( "radar ready !" );
  initRadar ();
  drawRadar ();
});

function getRadarData () {
  var data;
  
  $.ajax({
    async : false,
    type: "GET",
    url: 'data/radar.json',
    cache: false,
    dataType: "json",
    success: function (result) {
      data = result;
    }
  });
  
  return data;
}

function initRadar () {
  
  /**********************/
  /*  Radar Background  */
  /**********************/
  
  radar_back = document.getElementById("radar_back");
  radar_back_ctx = radar_back.getContext("2d");
  
  // background
  radar_back_ctx.beginPath();
  radar_back_ctx.arc(250,250,250,0,Math.PI*2,true);
  radar_back_ctx.fillStyle = "#000000";
  radar_back_ctx.fill();
  // center
  radar_back_ctx.fillStyle = "#FFFFFF";
  radar_back_ctx.fillRect(249,249,2,2);
  // grid
  radar_back_ctx.strokeStyle = "#888888";
  radar_back_ctx.lineWidth = 1;
  var grid_radius = grid_step;
  while(grid_radius < max_range) {
    var grid_radius_px = Math.round(grid_radius*range_ratio);
    radar_back_ctx.beginPath();
    radar_back_ctx.arc(250,250,grid_radius_px,start_angle,end_angle,false);
    radar_back_ctx.stroke();
    grid_radius = grid_radius+grid_step;
  }
  
  /**********************/
  /*  Radar Layer 1     */
  /**********************/
  
  radar_1 = document.getElementById("radar_1");
  radar_1_ctx = radar_1.getContext("2d");
  
}

function drawRadar () {
  console.log('drawRadar');
  
  var measures   = getRadarData();
  var nb_measure = measures.length;
  
  radar_1_ctx.clearRect(0, 0, radar_1.width, radar_1.height);

  radar_1_ctx.fillStyle = "#00FF00";
  radar_1_ctx.strokeStyle = "#00FF00";
  radar_1_ctx.lineWidth = 1;
  radar_1_ctx.beginPath();
  for(var i = 0; i < measures.length; i++) {
    var measure_px = Math.round(measures[i]*range_ratio);
    var coor = measureConvert (nb_measure, i, measures[i]);
    radar_1_ctx.fillRect(coor.x-1,coor.y-1,3,3);
    if(i==0) {
      radar_1_ctx.moveTo(coor.x,coor.y);
    }
    else{
      radar_1_ctx.lineTo(coor.x,coor.y);
    }
    //console.log (i+" - dst "+measures[i]+" - dst_px "+measure_px+" - x : "+coor.x+" - y : "+coor.y+" - angle : "+coor.angle);
  }
//  radar_1_ctx.stroke();
  
  radar_timeout = setTimeout("drawRadar()", 500);
}

Pour que la page HTML et le script JavaScript soient accessibles via un navigateur, il convient de les publier grâce à un serveur web (autrement appelé serveur de publication web). Inutile de sortir l’artillerie lourde en installant Apache, nous pouvons mettre en place un serveur web minimaliste grâce une petite commande Python :

python -m SimpleHTTPServer

Et voilà ce que ça donne :

radar_table radar_scan_large

J’ai fais ce test sur une table, sur des distances très courtes (moins de 1 mètre). On distingue bien les trois objets sur l’avant et la gauche du radar. Les points qui apparaissent tout à droite correspondent au dossier d’une chaise qui était contre la table 🙂

On observe quelques artefacts inhérents à ce type de télémètre à ultrason. Ce qui confirme l’intuition évoquée plus haut qui dit qu’il vaut mieux avoir un balayage plus rapide quitte à être moins précis 🙂

Avec un servo capable de faire un balayage à 360 degrés, ou un petit système de démultiplication, nous pourrions afficher un écran radar complet en modifiant le paramètre angle_range. Encore mieux, avec deux télémètres placés dos à dos sur le servo moteur, on pourrait faire un balayage rapide à 180 degrés tout en ayant une détection à 360 degrés 🙂 C’est probablement ce que je ferai sur la version finale du Pi BOAt.

Voilà pour ce premier module du Pi BOAt qui expose un principe qui peut être utilisé dans de nombreux autres projets.

Retrouvez la liste de tous les modules du Pi BOAt : Pi BOAt – Description des modules

A très bientôt pour la suite 😉

15 réflexions au sujet de « Pi BOAt – Radar de surface »

  1. StEx

    Super, ça ressemble à un vrai 🙂
    Sur le bateau, il faudra peut-être penser à un dispositif mécanique pour le garder bien horizontal car une petite coque va être fort sujette au tangage et au roulis qui risque de fausser les résultats (écho sur l’eau et non sur un objet potentiel). À tester…

    Répondre
    1. Olivier Auteur de l’article

      Effectivement, sur l’eau ça va tanguer pas mal. Pour la caméra je vais faire un petit stabilisateur mécanique. Je verrai ce que ça donne avec le radar 🙂

      Répondre
      1. StEx

        J’étais opérateur radar du temps de mon service militaire et, même sur un très gros navire avec un radar perché à plusieurs dizaines de mètres, on voyait les vagues par mauvais temps. Dans ce cas précis, c’était plus dû à la hauteur des vagues (en méditerranée !) qu’à l’inclinaison du navire… Bonne continuation, nous avons hâte de lire la suite.

        Répondre
  2. Vincent

    J’ai quelques questions au sujet du capteur :

    1) au début du tuto, tu montres un sonar sans l’habituel récepteur (les seuls que je connaisse sont en deux parties, deux « bulbes ») est-ce que celui là fonctionne de manière autonome avec la seule partie montée sur le servo ?

    2) à la fin du tuto sur les gifs, le capteur sonar a disparu et est remplacé par une « barre » qui ne ressemble ni à un sonar ni à un télémètre infrarouge, du coup je suis perdu, qu’est-ce que c’est ?

    Répondre
    1. Olivier Auteur de l’article

      Salut Vincent,
      1) oui, le SRF02 émet et reçoit dans le même module, contrairement à d’autres qui sont séparés en deux bulbes comme tu dis.
      2) sur le gif du bas, c’est le même capteur mais j’ai ajouté un tube en papier pour tenter de limiter le champ de réception pour « affiner » en quelque sort les mesures.

      Répondre
        1. Olivier Auteur de l’article

          Effectivement, et comme je le mentionnais dans l’article, l’utilisation d’un télémètre optique serait beaucoup plus précise. Mais le principe est strictement identique, j’avais le SRF02 sous la main au moment de faire mon prototype, mais rien ne dit que pour la version finale du Pi BOAt je ne vais pas en changer 😉

          Répondre
  3. Pascal LE TRUONG

    Bonjour,

    Intéresant comme projet. J’ai juste un petit pb avec l’importation du module srf02 dans le code python:
    import smbus, time, os, srf02

    J’aimerais savoir comment l’installer, svp?

    Répondre

Laisser un commentaire