Introduction à Node JS sur Raspberry PI

C’est quoi Nodejs ?

Nodejs, c’est un portage open source en C++ du moteur d’interpretation et d’exécution javascript que l’on trouve habituellement dans les navigateurs (chrome) : le V8 de google. Nodejs permet de sortir le javascript des navigateurs pour l’exécuter sur une machine windows, mac ou linux en standalone (desktop, serveur ou embarqué).

Les navigateurs étant sécurisés, par exemple, on ne peut pas accéder aux fichiers, à la ligne de commande ou écouter un port. Nodejs ajoute au javascript les fonctions  d’accès au système qui lui faisaient défaut. Nodejs suis les évolutions de la norme ECMAScript (spécifications du langaue) et est devenu une plateforme à la fois simple, léger et mature. Surtout depuis ES6. La doc de l’API exposée par NodeJS en plus du Javascript : https://nodejs.org/d…-v6.x/docs/api/

NodeJS est maintenant couramment utilisé dans le monde de l’IT pour des middleware, des web services temps-réel ou en full-stack (coté serveur et coté client) pour des applications web. On le voit aussi beaucoup dans le monde des objets connectés. Il arrive naturellement sur le raspberry PI pour piloter un robot.

Le langage javascript n’est pas un langage simple contrairement à ce qu’on pense généralement. Ce tuto ne couvre pas le javascript. Il faudra pour cela voir de ce coté : http://javascript.de…ppez.com/cours/. C’est faux de croire que l’on connait javascript si on a affiché un bouton dans une page web. En effet le navigateur masque toute la partie interessante de javascript : sa boucle d’évènements. Le code javascript est exécuté dans un seul thread. Mais derrière tous les appels asynchrones, la mécanique du V8 est parallélisée. Mieux que des mots, la meilleure video que je connaisse pour expliquer comment fonctionne javascript.

Par exemple, hello.js

// attends 1s et affiche le texte
setTimeout (function() {
  console.log("les makers");
},1000);
console.log ("Hello");

$node hello.js (ou C:\node.exe hello.js)

hello.js affiche :
Hello
​les makers
(et pas « les makers Hello »)

Avertissement

Le tuto qui suit montre comment piloter un robot avec NodeJS. Vous l’aurez compris, NodeJS devient intéressant quand on connait javascript.

Ce tuto pré-suppose que le raspberry est installé avec la raspbian : https://www.raspberr…/documentation/

Ce tuto est destiné aux débutants en robotique. Il est réalisé dans le cadre de la conception du robot Ash : http://www.robot-mak…alancing-robot/

Pourquoi PAS NodeJS ?

Javascript n’est pas un langage temps-réel.
Javascript n’est pas compilé. Il n’y a pas de vérification du code avant l’exécution.
Javascript est faiblement typé. Il demande un peu d’attention.

Alors, plus concrètement,

Pourquoi NodeJS ?

  • javascript n’attend pas

Il continue à exécuter le code pendant qu’une instruction asynchrone est en attente. Une lecture de ficher, d’un port série ou réseau. Cela le rend bien plus efficace et performant qu’un système multi-threadé.

  • javascript est un langage à évènements.

Ce n’est pas avec une librairie ou un addon. C’est au coeur du langage.

  • Les streams

L’API stream de NodeJS combine la puissance des évènements avec celle des pipes d’unix. Si vous connaissez les pipes entre les commandes unix, vous savez que c’est une aide précieuse et facile à mettre en place.

  • javascript est full stack

Un seul langage pour le serveur, pour le client et pour les middlewares orienté messages (évènements).

Si on connait son langage, NodeJS est un pont facile à mettre en place entre arduino et une interface de contrôle web. Et disposer d’une interface web permet de ne rien installer sur le terminal de commande (sauf un navigateur) et il est compatible avec tous les smartphone et sur toutes les machines desktop habituels.

NodeJS ouvre le raspberry (et donc le robot qui en est doté) à tout l’Internet domestique ou extérieur.

Enfin, Nodejs bénéficie d’une très large communauté. Il est accompagné d’un gestionnaire de packages (librairies) npm. Pour vous faire une idée, voilà ce qu’on trouve déjà concernant le port gpio chez nodejs : https://www.npmjs.com/search?q=gpio

NPM

Node s’install avec son gestionnaire de packages. Voir : https://docs.npmjs.com/
On se contentera de faire des npm install pour obtenir des modules.

Installation de NodeJS et NPM sur Raspberry PI

Ajouter le repository nodesource :

sudo curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -

Cette commande va aussi  mettre à jour le catalogue.

NB. Adafuit mets aussi node à disposition sur un repository similaire. Avec nodesource, on a une version plus à jour.
NB. Quand la version 8 de NodeJS sera disponible, l’url ci-dessus devra être adaptée. Voir https://github.com/nodejs/LTS/.

Installer nodejs :

sudo apt-get install nodejs

NB. npm (le gestionnaire de package de node) est installé automatiquement avec nodejs.

Vérification :

pi@raspberrypi:~ $ nodejs -v
v6.2.1
pi@raspberrypi:~ $ npm -v
3.9.3

Source : https://nodejs.org/e…x-distributions

Installation d’un module NPM

$npm install <nom du module>

npm va alors créer un répertoire module dans le répertoire courant. Vous devez donc vous placer à la racine de votre projet pour lancer cette commande.

Pour plus d’infos sur NPM : https://docs.npmjs.com/

Interface avec Arduino

Le plus simple est de conserver le câble USB entre le raspberry et arduino dans le montage définitif du robot.

Compilation
Cela permet de compiler le code arduino sur le raspberry directement et d’uploader le programme compilé sur l’arduino directement depuis le raspberry. Voir http://www.robot-mak…ne-de-commande/.

Communication
Pour communiquer avec arduino, on utilise toujours le câble USB.

Coté raspberry, on utilise le module NPM « serialport » dans une classe utilitaire qui fait l’interface avec Arduino :

"use strict";

var serialPortModule = require("serialport");
var EventEmitter = require('events').EventEmitter;
var util = require('util');

function Arduino() {
  EventEmitter.call(this);

  this.serialIsOpened = false;
  var self = this;

  this.serialPort = new serialPortModule.SerialPort("/dev/ttyACM0", {
    baudrate: 115200,
  	parser: serialPortModule.parsers.readline('\n')
  });

  this.serialPort.on("open", function () {
  	self.serialIsOpened = true;
    self.emit('ready', {});

    self.serialPort.on('data', function(data) {
      self.deserializeWhatDuinoSays(data);
    });
  });
}
util.inherits(Arduino, EventEmitter);

Arduino.prototype.deserializeWhatDuinoSays = function(data) {
  // Le message attendu est de la forme COMMANDE:DONNEE:DONNEE:DONNEE\n
  var splitedData = data.split(':');
  var command = splitedData.shift();
  this.emit(command, {args: splitedData});
}

Arduino.prototype.writeSerial = function(message, next) {
  if(this.serialIsOpened) {
    this.serialPort.write(message+"\n", function(err, results) {
      if(next)next();
    });
  }
}

module.exports = Arduino;

On utilise cette classe comme ceci :

var Arduino = require('./modules/arduino.js');

Pour envoyer un message à arduino :

arduino.writeSerial('COMMANDE:DONNEE:DONNEE:DONNEE');

Pour recevoir des messages de arduino :

arduino.on('ready', function() {
  console.log('Arduino ready');
});

arduino.on('CONSOLE', function(data) {
  console.log('[ARDUINO CONSOLE]'+data.args);
});

arduino.on('COUNTCODER', function(data) {
  console.log('Arduino COUNT left: '+data.args[0])
  console.log('Arduino COUNT right: '+data.args[1])
  (...)
})

Du coté Arduino, pour écrire un message à raspberry, on utilisera simplement

Serial.print();

Pour lire les message envoyé par raspberry, il faut les dé-sérialiser et dispatcher les commandes. Par exemple :

String dataFromPI = "";
/*
  Format : COMMAND:value[:value[:value[...]]]
  Command DIST : 0
  Command SERVOH : pos
  Command SERVOV : pos
  Command MOTOR : dirA:pwmA:dirB:pwmB
*/

void parseAndDispatch(String dataFromPI) {
  // get Command
  int dataLength = dataFromPI.length();
  int firstSep = dataFromPI.indexOf(SEPARATOR);
  if(firstSep == -1) return;
  if(firstSep + 1 >= dataLength) return;
  String cmd = dataFromPI.substring(0,firstSep);
  // get args
  String arrayArgs[MAX_ARGS];
  unsigned int index = 0;
  while(firstSep + 1 < dataLength && index < MAX_ARGS) {
    int secondSep = dataFromPI.indexOf(SEPARATOR, firstSep+1);
    // pour le dernier argument
    if(secondSep == -1) {
      secondSep = dataLength;
    }
    String arg = dataFromPI.substring(firstSep+1, secondSep);
    firstSep = secondSep;
    arrayArgs[index] = arg;
    index++;
  }
  if(cmd == "MOTOR") {
    doMotorCommand(arrayArgs);
  }
}


void loop() {
  if (Serial.available()>0)  {
    dataFromPI = Serial.readStringUntil('\n');
    parseAndDispatch(dataFromPI);
  }
}

Alimentation
Le raspberry peut alimenter l’aduino par le port USB donc dans la limite de 500mA.

Interface avec les programmes en python

Par exemple, on va intercepter les valeurs qui arrivent du capteur IMU du senseHAT. Le senseHAT est fourni avec une API en python. On va appeler la librairie du senseHAT et écrire une valeur sur la sortie standard.

#!/usr/bin/python
# -*- coding: utf-8 -*-

from sense_hat import SenseHat
import time
sense = SenseHat()
sense.set_imu_config(False, True, True)  # compass_enabled, gyro_enabled, accel_enabled
while True:
	# Get orientation from hat
	orientation = sense.get_orientation_degrees()

	roll = orientation["roll"]
	roll = round(roll, 2)
        print(roll)
	time.sleep(.005)

Dans NodeJS, on va utiliser le module « python-shell » pour instancier ce programme en python et lire la sortie standard.

var PythonShell = require('python-shell');

var pyOptions = {
  mode: 'text',
  pythonPath: '/usr/bin/python',
  pythonOptions: ['-u'],
  scriptPath: '/home/pi/SBR'//,
  //args: ['value1', 'value2', 'value3']
};

var imu = new PythonShell('imu.py', pyOptions);

imu.on('message', function(data) {
    console.log(data); // data contient une ligne de la sortie standard données par imu.py
  });

Interface web

Pour servir une page static HTML avec node, c’est très simple. On va utiliser le module « express » pour créer un mini serveur web sur le raspberry. On y ajoutera tout de suite une websocket pour obtenir des informations du robot sur la page web en mode push (à l’initiative du serveur).

var app = require('express')();
var server = require('http').createServer(app);
var io = require('socket.io')(server);

io.on('connection', function(socket){

    socket.on('command', function(data) {
      // data contient les données envoyées depuis la page web
    });

    // Pour envoyer des données au navigateur
    io.sockets.emit("COMMAND", data);
});

// Pour servir la page static, on la place dans un répertoire dédié
app.use(express.static(__dirname + '/../static'));

server.listen(3000);

Placez une un fichier nommé index.html dans le répertoire static.

Pour se connecter l’interface web, il faudra se connecter à l’IP du raspberry sur le port 3000 depuis un navigateur. (http://w.x.y.z:3000/). Votre navigateur affichera le contenu html de votre index.html.
A noter, socket.io sert son client websocket au navigateur de façon transparente.

Votre fichier html de commande peut ressembler à ceci :

[attachment=4794:Sans titre.png]

<html>
<head>
  <meta charset="utf-8">
  <title>Ash Control</title>
  <link rel="icon" type="image/png" href="/raspberry_pi_logo.png">
</head>
<body>
<script src="/jquery-2.1.4.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
KP : <input id="input-kp" value="" /><br/>
KI : <input id="input-ki" value="" /><br/>
KD : <input id="input-kd" value="" /><br/>
<button id="buttonPID">Ok</button><br/>
<button id="buttonStopMotor">Stop moteurs</button><br/>
<button id="buttonStartMotor">Start moteurs</button><br/>
<br/>
<button id="buttonForward">Avancer</button><br/>
<button id="buttonBackward">Reculer</button><br/>
<button id="buttonTurnLeft">Gauche</button><br/>
<button id="buttonTurnRight">Droite</button><br/>
<button id="buttonStop">Stop</button>
<script>
  var socket = io.connect('http://raspberrywlan:8080/');
  socket.on('reset', function (data) {
    $('#input-kp').val(data.kp);
    $('#input-ki').val(data.ki);
    $('#input-kd').val(data.kd);
  });
  $('#buttonPID').click(function() {
    var data = {};
    data.kp = $('#input-kp').val();
    data.ki = $('#input-ki').val();
    data.kd = $('#input-kd').val();
    socket.emit('command', {action: 'changePID', data: data});
  });

  $('#buttonStopMotor').click(function() {
    socket.emit('command', {action: 'stopMotors', data: {}});
  });
  $('#buttonStartMotor').click(function() {
    socket.emit('command', {action: 'startMotors', data: {}});
  });

  $('#buttonForward').click(function() {
    socket.emit('command', {action: 'forward', data: {}});
  });
  $('#buttonBackward').click(function() {
    socket.emit('command', {action: 'backward', data: {}});
  });
  $('#buttonTurnLeft').click(function() {
    socket.emit('command', {action: 'left', data: {}});
  });
  $('#buttonTurnRight').click(function() {
    socket.emit('command', {action: 'right', data: {}});
  });
  $('#buttonStop').click(function() {
    socket.emit('command', {action: 'stop', data: {}});
  });

</script>
</doby>
</html>

Pour plus d’infos sur la websocket : https://github.com/socketio/socket.io
Pour plus d’infos sur express : http://expressjs.com/fr/

NB. Il s’agit d’un embryon de serveur web. Mais suffisant pour les besoins de pilotage d’un robot.

Pour aller plus loin

Le V8 de google : https://fr.wikipedia…eur_JavaScript)
NodeJS : https://nodejs.org/en/about/
Liens utiles NodeJS : https://github.com/s…NodeJS-Learning
Javascript : https://developer.mo…/Web/JavaScript

Mise à jour du 9 décembre 2018

nvm : pour faciliter les mises à jour de nodejs.

pm2 : pour garantir la fiabilité de nodejs.

le module serialport a été mis à jour. Un exemple d’utilisation de la nouvelle version.