par Alexandre Alapetite le 2012-06-23 ; mise-à-jour 2013-07-20

Raspberry Pi avec Node.js et Arduino

Est-ce qu’Arduino et Raspberry Pi peuvent être amis ?

Démo en direct (pas toujours branché… Lien secondaire)
English

Introduction

J’avais un Arduino Uno R3 avec un module Arduino Ethernet R3, mais je ne suis pas arrivé faire fonctionner le module Ethernet de manière stable sur plusieurs jours, même pour mon petit projet de température.

Arrive alors le Raspberry Pi, un ordinateur bon-marché (~27€) au format carte de visite qui tourne sous Linux, qui est stable, moins cher même que le module Arduino Ethernet seul (~32€), et à peine plus cher qu’un Arduino Uno (~22€).

D’un autre côté, pour interagir avec le monde physique et analogique, le Raspberry Pi dispose seulement de ports digitaux GPIO non-protégés, ce qui n’est pas suffisamment sûr pour un informaticien comme moi. Peu de modules d’extension pour le Raspberry Pi sont actuellement disponibles, ils peuvent être plus chers qu’un Arduino Uno, et ne disposent pas de la même quantité de bibliothèques et de la communauté d’Arduino.

L’idée : combiner Raspberry Pi pour la programmation de haut niveau avec Arduino pour l’électronique de bas niveau, grâce à une simple connexion USB !

De plus, je souhaite avoir sur le Raspberry Pi un serveur Web ultraléger qui puisse à la fois réagir à des événements générés par l’Arduino ainsi qu’envoyer des requêtes sur le Web.
Ma sélection : Node.js.

Cela fait un ensemble cohérent, clefs-en-main, et avec une bonne documentation pour les différentes briques.


Sommaire


Architecture

[Raspberry Pi / USB / Arduino]

J’utilise un chargeur de tablette ou de téléphone pour alimenter le Raspberry Pi par micro-USB (5V, 500mA ou plus), qui est aussi connecté à Internet via un câble Ethernet, et à l’Arduino Uno via micro-USB. L’unité de stockage consiste en une carte SD (Samsung 32GB SDHC Classe 10), dont je consomme moins de 2Go.
L’Arduino est connecté à une platine Labdec pour former un simple circuit électronique (diviseurs de tension) permettant des mesures de température (pas détaillé).
Le module Arduino Ethernet n’est plus utilisé.

Sommaire

Préparation de l’Arduino

Dans l’esprit de Node.js qui a une architecture orienté événement, et afin d’économiser de l’énergie, l’Arduino est en veille la plupart du temps et quand il se réveille (ici, toutes les 5 minutes), il envoie un texte qui génère un événement dans Node.js.

Avec JavaScript comme langage natif de Node.js, il est pratique d’envoyer les données depuis l’Arduino au Raspberry Pi en utilisant du texte au format JSON.

Le programme Arduino suivant est destiné à mesurer deux différents thermistors (Capteur de température avec tête en acier) afin de rapporter la température intérieure et extérieure.

Ce document de détaille pas les détails électroniques, car l’intérêt est focalisé sur l'interaction entre l’Arduino et le Raspberry Pi.

TemperatureSerial.ino

#include <math.h>

unsigned long UpdateDelay = 1000UL * 60 * 5;	//Fréquence de mise à jour (5 minutes)
const byte Temperature1Pin = 0;	//Thermistor 1 (Extérieur)
const byte Temperature2Pin = 1;	//Thermistor 2 (Intérieur)
const int Resistance1 = 9900;	//Ohms (mesurés sur la résistance R10K du diviseur de tension 1)
const int Resistance2 = 9980;	//Ohms (mesurés sur la résistance R10K du diviseur de tension 2)
const byte NbSamples = 8;	//Nombre de mesures pour faire une moyenne

void setup()
{
	delay(1000);
	Serial.begin(9600);	//Démarre le port série
	pinMode(Temperature1Pin, INPUT);
	pinMode(Temperature2Pin, INPUT);
	digitalWrite(Temperature1Pin, LOW);
	digitalWrite(Temperature2Pin, LOW);
	analogRead(Temperature1Pin);
	analogRead(Temperature2Pin);
}

void loop()
{
	float rawADC1 = 0.0;
	float rawADC2 = 0.0;
	for (byte i = NbSamples; i > 0; i--)
	{//Moyenne sur plusieurs mesures
		rawADC1 += analogRead(Temperature1Pin);
		rawADC2 += analogRead(Temperature2Pin);
		delay(100);
	}
	rawADC1 /= NbSamples;
	rawADC2 /= NbSamples;

	//Envoi une chaîne de texte au format JSON par communication série/USB comme : {"ab":"123","bc":"234","cde":"3546"}
	Serial.println("{\"adc\":\"" + String((long)round(100.0 * rawADC1)) +
			"\", \"celsius\":\"" + String((long)round(100.0 * thermistor(rawADC1, Resistance1))) +
			"\", \"adc2\":\"" + String((long)round(100.0 * rawADC2)) +
			"\", \"celsius2\":\"" + String((long)round(100.0 * thermistor(rawADC2, Resistance2))) +
			"\"}");

	delay(UpdateDelay);
}

float thermistor(float rawADC, float rSeries)
{//http://arduino.cc/playground/ComponentLib/Thermistor2
	//Cette méthode n’est pas très avancée
	long resistance = (1024 * rSeries / rawADC) - rSeries;
	float temp = log(resistance);
	temp = 1 / (0.001129148 + (0.000234125 * temp) + (0.0000000876741 * temp * temp * temp));
	return temp - 273.15;	//Kelvin to Celsius
}

Puis il faut brancher un câble USB standard type A-B entre le Raspberry Pi (hôte) et l’Arduino (périphérique). Il ne devrait pas être nécessaire d’alimenter l’Arduino en électricité par ailleurs.

Sommaire

Préparation du Raspberry Pi

Il existe déjà plusieurs distributions Linux pour le the Raspberry Pi, surnommé RasPi. J’ai choisi la dernière Raspbian, une Linux Debian optimisée pour le Raspberry Pi, qui est fournie avec un assistant raspi-config bien pratique au premier démarrage qui s’occupe de redimensionner les partitions pour utiliser au mieux la carte SD, etc.

Il faut juste suivre les instructions pour préparer la carte SD, et après le premier démarrage, exécuter les commandes suivantes :

sudo nano /etc/ssh/sshd_config
	PermitRootLogin no	#Changer cette configuration pour empêcher l’accès administrateur à distance par SSH

sudo apt-get update && sudo apt-get dist-upgrade && sudo apt-get autoremove

sudo apt-get install localepurge	#Pour récupérer beaucoup d’espace disque en supprimant les langues inutilisées
sudo localepurge

sudo apt-get install fail2ban	#Pour ajouter un peu de sécurité lorsque le Raspberry Pi est connecté à Internet
sudo fail2ban-client status
Sommaire

Préparer Node.js

Note.js est une plateforme JavaScript ultra légère côté serveur utilisant le moteur V8 de Google (comme Chrome). Pour l’installer :

sudo apt-get update && sudo apt-get install nodejs npm

Néanmoins, au moment d’écrire ces lignes, les versions sz Node.js et de npm disponibles dans les dépôts étaient trop vieilles.
Aussi, voici mon approche pour installer nodejs (version 0.10.6) et npm (version 1.12.18) depuis les dépôts en amont, après avoir vérifié s’il n’y a pas une version plus récente dans :
http://nodejs.org/dist/latest/.

wget http://nodejs.org/dist/v0.10.6/node-v0.10.6-linux-arm-pi.tar.gz
tar xvzf node-v* && rm node-v*.gz
sudo mv node-v* /opt/node
sudo mkdir /opt/bin
sudo ln -s /opt/node/bin/* /opt/bin/

Si ce n’est pas déjà le cas, ajouter /opt/bin/ à la liste des chemins par défaut :

sudo nano /etc/profile
/etc/profile

	PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/bin"

Créez deux dossiers ~/public_html/ and ~/log/nodejs/ , puis déplacez-vous dans ~/public_html/ où il faudra télécharger 2 scripts :

mkdir -p /home/pi/public_html	//Pour les scripts
mkdir -p /home/pi/log/nodejs	//Pour les logs
cd /home/pi/public_html
wget http://alexandre.alapetite.fr/doc-alex/raspberrypi-nodejs-arduino/index.js
wget http://alexandre.alapetite.fr/doc-alex/raspberrypi-nodejs-arduino/arduinoTemperature.js

Avec Node.js, il est nécessaire d’écrire à la fois la logique du serveur Web (la mission d’Apache par exemple), de même que la logique des pages Web (ce que PHP fait), et tout en JavaScript.
Voici mon fichier principal, en charge d’écouter les requêtes Web, de servir des réponses dynamiques, des fichiers statiques, et des erreurs 404 :

index.js

"use strict";

function escapeHtml(text)
{
	return text.replace(/&/g, "&amp;")
		.replace(/</g, "&lt;")
		.replace(/>/g, "&gt;")
		.replace(/"/g, "&quot;")
		.replace(/'/g, "&#039;");
}

var fs = require('fs');
var os = require('os');
var path = require('path');
var util = require('util');

var serverSignature = 'Node.js / Debian ' + os.type() + ' ' + os.release() + ' ' + os.arch() + ' / Raspberry Pi';

function done(request, response)
{
	util.log(request.connection.remoteAddress + '\t' + response.statusCode + '\t"' + request.method + ' ' + request.url + '"\t"' +
		request.headers['user-agent'] + '"\t"' + request.headers['accept-language'] + '"\t"' + request.headers['referer'] + '"');
}

function serve404(request, response, requestUrl)
{//Quand un fichier statique n’est pas trouvé
	response.writeHead(404,
	{
		'Content-Type': 'text/html; charset=UTF-8',
		'Date': (new Date()).toUTCString(),
		'Server': serverSignature
	});
	response.end('<!DOCTYPE html>\n\
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB">\n\
<head>\n\
<meta charset="UTF-8" />\n\
<title>404 Not Found</title>\n\
</head>\n\
<body>\n\
<h1>Not Found</h1>\n\
<p>The requested <abbr title="Uniform Resource Locator">URL</abbr> <kbd>' +
	escapeHtml(requestUrl.pathname) + '</kbd> was not found on this server.</p>\n\
</body>\n\
</html>\n\
');
	done(request, response);
}

function serveStaticFile(request, response, requestUrl)
{
	var myPath = '.' + requestUrl.pathname;
	if (myPath && (/^\.\/[a-z0-9_-]+\.[a-z]{2,4}$/i).test(myPath) && (!(/\.\./).test(myPath)))
		fs.stat(myPath, function (err, stats)
		{
			if ((!err) && stats.isFile())
			{
				var ext = path.extname(myPath);
				var mimes = { '.css': 'text/css', '.html': 'text/html', '.ico': 'image/x-icon', '.jpg': 'image/jpeg',
					'.js': 'application/javascript', '.json': 'application/json', '.png': 'image/png', '.txt': 'text/plain', '.xml': 'application/xml' };
				var modifiedDate = new Date(stats.mtime).toUTCString();
				if (modifiedDate === request.headers['if-modified-since'])
				{
					response.writeHead(304,
					{
						'Content-Type': ext && mimes[ext] ? mimes[ext] : 'application/octet-stream',
						'Date': (new Date()).toUTCString()
					});
					response.end();
				}
				else
				{
					response.writeHead(200,
					{
						'Content-Type': ext && mimes[ext] ? mimes[ext] : 'application/octet-stream',
						'Content-Length': stats.size,
						'Cache-Control': 'public, max-age=86400',
						'Date': (new Date()).toUTCString(),
						'Last-Modified': modifiedDate,
						'Server': serverSignature
					});
					fs.createReadStream(myPath).pipe(response);
				}
				done(request, response);
			}
			else serve404(request, response, requestUrl);
		});
	else serve404(request, response, requestUrl);
}

function serveHome(request, response, requestUrl)
{
	var now = new Date();
	response.writeHead(200,
	{
		'Content-Type': 'text/html; charset=UTF-8',
		'Date': now.toUTCString(),
		'Server': serverSignature
	});
	response.end('<!DOCTYPE html>\n\
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB">\n\
<head>\n\
<meta charset="UTF-8" />\n\
<title>Test of Node.js on Raspberry Pi</title>\n\
<meta name="robots" content="noindex" />\n\
<meta name="viewport" content="initial-scale=1.0,width=device-width" />\n\
<link rel="author" href="http://alexandre.alapetite.fr/cv/" title="Alexandre Alapetite" />\n\
</head>\n\
<body>\n\
<pre>\n\
Hello ' + request.connection.remoteAddress + '!\n\
This is <a href="http://nodejs.org/">Node.js</a> on <a href="http://www.raspberrypi.org/">Raspberry Pi</a> :-)\n\
It is now ' + now.toISOString() + '.\n\
</pre>\n\
<ul>\n\
<li><a href="./temperature">Temperature</a></li>\n\
<li><a href="index.js">Source code</a></li>\n\
</ul>\n\
</body>\n\
</html>\n\
');
	done(request, response);
}

var arduino = require('./arduinoTemperature.js');	//Connexion avec Arduino

function serveTemperature(request, response, requestUrl)
{
	var temperatureResponse = arduino.temperatureResponse();
	response.writeHead(200,
	{
		'Content-Type': 'text/html; charset=UTF-8',
		'Date': (new Date()).toUTCString(),
		'Server': serverSignature,
		'Last-Modified': temperatureResponse.dateLastInfo.toUTCString()
	});
	response.end(temperatureResponse.html);
	done(request, response);
}

var http = require('http');
var url = require('url');

var server = http.createServer(function (request, response)
{
	var requestUrl = url.parse(request.url);
	switch (requestUrl.pathname)
	{
		case '/': serveHome(request, response, requestUrl); break;
		case '/temperature': serveTemperature(request, response, requestUrl); break;
		default: serveStaticFile(request, response, requestUrl); break;
	}
}).listen(8080);

console.log('Node.js server running at %j', server.address());

Il est possible de tester que cela marche en commentant la ligne //var arduino = require… vers la fin du fichier ci-dessus, puis exécuter la commande suivante, et enfin naviguer vers http://ip-de-votre-raspberry.example:8080/

cd /home/pi/public_html
node index.js

Communication série entre Node.js et Arduino

Maintenant, il faut que Node.js réagisse lorsqu’Arduino parle par USB. La liaison série de l’Arduino, lorsqu’il est connecté par USB au Raspberry Pi, est détectée comme quelque chose du style /dev/ttyACM0 et cela peut varier légèrement selon la configuration.
Il faut ajouter les fonctionnalités de communication série à Node.js, comme suit :

cd /home/pi/public_html
sudo npm install serialport

Et voici le script que j’utilise pour réagir aux messages JSON envoyés par l’Arduino (c.a.d. orienté événément) en mettant à jour les températures.
Il transmet aussi une copie de cette nouvelle information à un serveur Web dédié grâce à une requête HTTP POST, de telle manière que le petit Raspberry Pi (qui plus est sur une connexion Internet de maison) puisse éviter d’être en première ligne pour servir les requêtes Web si nécessaire.

arduinoTemperature.js

"use strict";

var arduinoSerialPort = '/dev/ttyACM0';	//Port série par connexion USB entre le Raspberry Pi et l’Arduino

var fs = require('fs');
function writeFile(text)
{
	fs.writeFile('temperature.json', text, function(err)
	{
		if (err) console.warn(err);
	});
}

var os = require('os');
var serverSignature = 'Node.js / Debian ' + os.type() + ' ' + os.release() + ' ' + os.arch() + ' / Raspberry Pi B + Arduino Uno R3';

var postOptions =
{
	host: 'posttestserver.com',	//À changer pour votre propre serveur
	path: '/post.php',
	method: 'POST',
	headers:
	{
		'Content-Type': 'application/x-www-form-urlencoded',
		'Connection': 'close',
		'User-Agent': serverSignature
	}
};

var http = require('http');
function postData(s)
{//Requète HTTP POST standard
	var myOptions = postOptions;
	postOptions.headers['Content-Length'] = s.length;

	var requestPost = http.request(myOptions, function(res)
	{
		res.setEncoding('utf8');
		res.on('data', function (chunk)
		{
			console.log(chunk);
		});
	});

	requestPost.on('error', function(e)
	{
		console.warn(e);
	});

	requestPost.write(s);
	requestPost.end();
}

var serialport = require('serialport');
var serialPort = new serialport.SerialPort(arduinoSerialPort,
{//Écoute du port série pour réagir aux données provenant d’Arduino par USB
	parser: serialport.parsers.readline('\n')
});

var lastTemperatureIndoor = NaN;
var lastTemperatureOutoor = NaN;
var dateLastInfo = new Date(0);

var querystring = require('querystring');
serialPort.on('data', function (data)
{//Quand une nouvelle ligne de texte est reçue en provenance d’Arduino par USB
	try
	{
		var j = JSON.parse(data);
		lastTemperatureOutoor = j.celsius / 100.0;
		lastTemperatureIndoor = j.celsius2 / 100.0;
		dateLastInfo = new Date();
		writeFile('{"outdoor":"' + lastTemperatureOutoor + '","indoor":"' + lastTemperatureIndoor + '"}');
		//Transmet une copie de l’information émise par Arduino à un autre serveur Web
		postData(querystring.stringify(j));
	}
	catch (ex)
	{
		console.warn(ex);
	}
});


function colourScale(t)
{//Génère une couleur HTML en fonction de la température
	if (t <= -25.5) return '0,0,255';
	if (t <= 0) return Math.round(255 + (t * 10)) + ',' + Math.round(255 + (t * 10)) + ',255';
	if (t <= 12.75) return Math.round(255 - (t * 20)) + ',255,' + Math.round(255 - (t * 20));
	if (t <= 25.5) return Math.round((t - 12.75) * 20) + ',255,0';
	if (t <= 38.25) return '255,' + Math.round(255 - (t - 25.5) * 20) + ',0';
	return '255,0,0';
}

function temperatureResponse()
{
	return {
		'lastTemperatureIndoor': lastTemperatureIndoor,
		'lastTemperatureOutoor': lastTemperatureOutoor,
		'html': '<!DOCTYPE html>\n\
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB">\n\
<head>\n\
<meta charset="UTF-8" />\n\
<meta http-equiv="refresh" content="300" />\n\
<title>Temperature - Arduino - Raspberry Pi</title>\n\
<meta name="keywords" content="Temperature, Arduino, Raspberry Pi" />\n\
<meta name="viewport" content="initial-scale=1.0,width=device-width" />\n\
<link rel="alternate" type="application/json" href="temperature.json"/>\n\
<meta name="robots" content="noindex" />\n\
<style type="text/css">\n\
html, body {background:black; color:white; font-family:sans-serif; text-align:center}\n\
.out {font-size:48pt}\n\
.in {font-size:36pt}\n\
.r, .sb {bottom:0; color:#AAA; position:absolute}\n\
.r {left:0.5em; margin-right:5em; text-align:left}\n\
.sb {right:0.5em}\n\
a {color:#AAA; text-decoration:none}\n\
a:hover {border-bottom:1px dashed}\n\
</style>\n\
</head>\n\
<body>\n\
<h1>Temperature somewhere</h1>\n\
<p>Outdoor<br /><strong class="out" style="color:rgb(' + colourScale(lastTemperatureOutoor) + ')">' +
	(Math.round(lastTemperatureOutoor * 10) / 10.0) + '°C</strong></p>\n\
<p>Indoor<br /><strong class="in" style="color:rgb(' + colourScale(lastTemperatureIndoor) + ')">' +
	(Math.round(lastTemperatureIndoor * 10) / 10.0) + '°C</strong></p>\n\
<p>' + dateLastInfo.toISOString() + '</p>\n\
<p class="r"><a href="http://alexandre.alapetite.fr/doc-alex/raspberrypi-nodejs-arduino/" title="Based on">Arduino + Raspberry Pi</a></p>\n\
</body>\n\
</html>\n\
',
	dateLastInfo: dateLastInfo
	};
}

module.exports.temperatureResponse = temperatureResponse;

Faire tourner Node.js comme serveur

Maintenant, le petit Raspberry Pi est prêt à faire tourner Node.js comme serveur, c’est-à-dire que Node.js ne doit pas s’arrêter lorsque vous fermez votre session. Pour ce faire, il y a différentes manières, mais voici une simple approche (qui ne se relance pas en cas de plantage, ni au redémarrage du Raspberry Pi) :

cd /home/pi/public_html/
nohup node /home/pi/public_html/index.js >> /home/pi/log/nodejs/console.log 2>&1 &

Vous pouvez arrêter le serveur en faisant :

killall node

Si votre Node.js plante d’une manière ou d’une autre, vous pouvez le tester régulièrement et le relancer. Similairement, si votre Raspberry Pi perd sa connexion Ethernet, il est possible de tester régulièrement et de faire une action comme le redémarrer :

sudo nano /etc/cron.d/temperature
/etc/cron.d/temperature

# Lance au démarrage
@reboot pi cd /home/pi/public_html/ && /opt/bin/node /home/pi/public_html/index.js >> /home/pi/log/nodejs/console.log 2>&1 &

# Teste toutes les 7 minutes et ssi le fichier JSON n’a pas été mis à jour depuis plus de 11 minutes, alors relance Node.js
*/7 * * * * pi [ `find /home/pi/public_html/temperature.json -mmin +11` ] && touch /home/pi/public_html/cronNode.txt && (killall -q node || true) && sleep 2 && cd /home/pi/public_html/ && node /home/pi/public_html/index.js >> /home/pi/log/nodejs/console.log 2>&1 &

# Teste toutes les 13 minutes si la connexion au réseau fonctionne, sinon redémarre
*/13 * * * * root [ `ping -c3 -q  192.168.1.1 > /dev/null 2>&1` ] && touch /home/pi/public_html/cronReboot1.txt && reboot

# Teste toutes les 17 minutes et ssi le fichier JSON n’a pas été mis à jour depuis plus de 23 minutes, alors redémarre
*/17 * * * * root [ `find /home/pi/public_html/temperature.json -mmin +23` ] && touch /home/pi/public_html/cronReboot2.txt && reboot
sudo chmod +x /etc/cron.d/temperature
sudo service cron restart
Sommaire

Conclusion

C’est tout !
Voyez la démo en direct depuis le Raspberry Pi (quand il est allumé) ou la copie sur le serveur Web dédié.

Sommaire

FAQ

Problèmes pour installer serialport ?
Installez une version plus récente de Node.js et de npm. Voir les problèmes actuels, comme le problème #81.
Index

Commentaires

Si vous attendez une réponse ou afin de rapporter un problème, contactez-moi plutôt par courriel.

object : voir les commentaires

http://alexandre.alapetite.fr

Retour