mercredi 6 mai 2015

Arduino, réaliser un objet connecté sans Shield


Objectif :

Après une rapide prise en main de l'Arduino à travers les exemples présents dans le Arduino Starter Kit, il m'est venu la nécessité d'expérimenter quelque chose de plus personnel. Je me suis donc décidé à réaliser un objet connecté. Le but est de transmettre de l'information propre à la carte jusqu'à son back-end et inversement permettre à un utilisateur connecté d’interagir avec la carte depuis une interface distante dédiée. Même que l'objet connecté devra s'interface à travers une API pour communiquer avec le back-end.

Pour mettre en place la connectivité entre l'Arduino et le web, il y a des shields qui facilitent grandement le travail (Ethernet, Wifi voire même Akeru pour interagir avec le réseau Sigfox même si je ne suis pas certain que ça gère une communication descendante vers leur modem...). Mais voila, j'ai pas tout ça. Comme rien est impossible, l'outil de développement Processing s'est révélé idéal pour la tâche à accomplir (IDE similaire à celle de l'Arduino, on ne sent pas dépaysé) malgré la perte d'autonomie de mon objet connecté (échange via USB entre la carte et l'appli Processing qui tourne en tâche de fond).

Aucune idée intelligente arrêtée pour un quelconque use case, je me contente d'une démonstration technique pour voir que ça marche. L'objet sera donc constitué de trois leds (rouge, bleue et verte) et un bouton poussoir. L'utilisateur connecté sera en mesure de choisir une couleur sur une interface distante, la led associée sur la carte s'allumera. J'ai fait le choix que mon objet connecté (via Processing) soit uniquement un client web. Le client sollicitera l'API du back-end de façon régulière (toutes les minutes) ou de manière forcée par l'appui du bouton situé sur la carte.

Architecture :


Ca me parait clair, non? On notera quand même la présence d'un fichier de stockage qui fera office d'EEPROM car la carte Arduino seule ne peut pas stocker de données non volatiles (autre que le code qu'on flash). Ce fichier contiendra un numéro de transaction qui sera transmis à l'API.

Implémentation :

Arduino : On ne va pas s'attarder sur le montage physique. Trois leds avec des résistances de 220, le bouton poussoir avec celle de 10k, chaque composant relié à une entrée/sortie digitale du micro-contrôleur. Concernant sa programmation, pas bien compliqué non plus, on initie une transmission série. On reste à l'écoute d'un ordre d'allumer une led et on envoie un signal quand on détecte l'appui sur le bouton poussoir.

Afficher / Masquer
const int buttonPin = 2; const int ledPin = 13; const int greenLed = 3; const int blueLed = 4; const int redLed = 5; int buttonState = 0 ; int ordre = 0; void setup() { pinMode(buttonPin, INPUT); pinMode(ledPin, OUTPUT); pinMode(greenLed, OUTPUT); pinMode(blueLed, OUTPUT); pinMode(redLed, OUTPUT); Serial.begin(9600); } void loop() { // La gestion des leds if (Serial.available()>0){ ordre = Serial.read(); if (ordre == 2){ digitalWrite(greenLed,LOW); digitalWrite(blueLed,LOW); digitalWrite(redLed,HIGH); } else if (ordre == 3){ digitalWrite(blueLed,HIGH); digitalWrite(redLed,LOW); digitalWrite(greenLed,LOW); } else if (ordre == 4){ digitalWrite(redLed,LOW); digitalWrite(blueLed,LOW); digitalWrite(greenLed,HIGH); } } // La gestion du bouton buttonState = digitalRead(buttonPin); if (buttonState == 1){ digitalWrite(ledPin,HIGH); Serial.println("1"); delay(1000); } else{ digitalWrite(ledPin,LOW); } }
Processing 2.2.1 : A l'exact opposé du code pour l'Arduino, on écoute l'appui sur le bouton et on transmet les ordres d'allumer une led. L'obtention de numéro courant de transaction se fera grâce à la fonction loadStrings tout comme sa mise à jour avec saveStrings. Le plus intéressant reste la mise en place du client web (que j'ouvre pour chaque transaction du fait qu'il se déconnecte très vite). Le format de la requête étant très précis, il faut penser à calculer la taille du body (en l’occurrence ici le nom de l'opération appelée, le numéro de transaction et le type d'appel (bouton, schédulé) transmis à l'API).

Afficher / Masquer
import processing.net.*; import processing.serial.*; // Objet de connexion Client clientWeb; Serial portSerial; // Variables de travail String dataClientWeb; String dataSerial; int ordre; int numeroTransaction = 0; String linesEeprom[] ; char couleur ; int savedTime; int totalTime = 60000; // Constantes final int ORDRE_BOUTTON = 1; final int ORDRE_LED_ROUGE = 2; final int ORDRE_LED_BLEU = 3; final int ORDRE_LED_VERT = 4; final char TYPE_ACTION_BOUTON = 'B'; final char TYPE_ACTION_SCHEDULEE = 'S'; void setup() { // On récupère le numéro de la transaction en cours depuis l'EEPROM linesEeprom = loadStrings("C:\\Users\\MonUser\\Documents\\Arduino\\Projets\\projetPerso_AppelAPI\\eeprom.txt"); numeroTransaction = Integer.parseInt(linesEeprom[0]); // Définition port Série String portName = Serial.list()[0]; portSerial = new Serial(this, portName, 9600); } void draw() { // Gestion au timer int passedTime = millis() - savedTime; if (passedTime > totalTime) { savedTime = millis(); gestion_numero_transaction(); // Ouverture client HTTP clientWeb = new Client(this, "monserveur_distant.net", 80); couleur = call_get_color(TYPE_ACTION_SCHEDULEE); allumer_led(couleur); clientWeb.stop(); } // Gestion appui du bouton if (portSerial.available() > 0){ dataSerial = portSerial.readStringUntil('\n'); // On récupère les infos de la carte if (dataSerial!=null) { // On récupère l'ordre envoyé par l'Arduino ordre = Integer.parseInt(dataSerial.substring(0, dataSerial.length()-2)); print("Ordre : "); println(ordre); gestion_numero_transaction(); // Si on détecte un appui de bouton alors on envoi la requête POST à l'API if (ordre == ORDRE_BOUTTON){ // Ouverture client HTTP clientWeb = new Client(this, "monserveur_distant.net", 80); couleur = call_get_color(TYPE_ACTION_BOUTON); allumer_led(couleur); clientWeb.stop(); } } } } char call_get_color(char type_action) { // On créé le body String body = "act=get_color&num="+str(numeroTransaction)+"&type="+str(type_action); println("On appelle le get_color !"); clientWeb.write("POST http://monserveur_distant.net/arduino/arduino_api.php HTTP/1.1\r\n"); clientWeb.write("Host: monserveur_distant.net\r\n"); clientWeb.write("Content-Type: application/x-www-form-urlencoded\r\n"); clientWeb.write("Content-Length: "+body.length()+"\r\n"); clientWeb.write("\r\n"); clientWeb.write(body); clientWeb.write("\r\n"); // Attente d'une seconde delay(1000); dataClientWeb = clientWeb.readString(); char c = dataClientWeb.substring(dataClientWeb.indexOf("color:")+6,dataClientWeb.indexOf("color:")+7).charAt(0); print("Couleur : "); println(c); return c; } void allumer_led(char c){ if (c == 'r') { // Allumer led rouge portSerial.write(ORDRE_LED_ROUGE); } else if (c == 'b') { // Allumer led bleu portSerial.write(ORDRE_LED_BLEU); } else if (c == 'v') { // Allumer led verte portSerial.write(ORDRE_LED_VERT); } } void gestion_numero_transaction(){ // On incrémente le numéro de la transaction numeroTransaction = numeroTransaction + 1; print("Numero transaction : "); println(numeroTransaction); // On enregistre le numéro sur l'eeprom String listNumero[] = {str(numeroTransaction)}; saveStrings("C:\\Users\\MonUser\\Documents\\Arduino\\Projets\\projetPerso_AppelAPI\\eeprom.txt", listNumero); }
API : C'est le gros point faible de mon système. Pas d'authentification/identification, ni d'échange sécurisé. Ca se veut API REST, on y accède par POST HTTP. J'ai même pas eu le courage d'encoder la réponse en JSON pour ne pas avoir à me pencher sur son décodage côté Processing.

Afficher / Masquer
<?php // Encodage en UTF-8 header('Content-Type: text/html; charset=utf-8'); $act = $_POST['act']; $numero = $_POST['num']; $type_action = $_POST['type']; include 'utils/is_dev.php'; include 'utils/fonctions_db.php'; include 'utils/fonctions_arduino.php'; if ($act == "get_color") { // On renvoie la valeur $db = db_connect(); $couleur = get_couleur($db); set_trace($numero, $couleur, $type_action, $db); echo "color:".$couleur; } ?>
Interface : J'ai essayé de réaliser quelque chose de sexy et minimal. Vu que le type color de html5 ne permet pas de customisage, j'ai utilisé le module customColorPicker pour permettre la sélection des trois couleurs proposées à l'utilisateur. Simple à prendre en main, je l'ai choisi avant tout pour son design sobre. Je me suis de plus payé le luxe de rajouter de l'AJAX quant à l'affichage de l'historique pour qu'il apparaisse sans refresh de la page. Ca a été particulièrement douloureux, jQuery pouvant être parfois très mystérieux. J'ai penché pour le module utf8.js-master pour gérer l'affichage des caractères spéciaux depuis Javascript.


Le php :

Afficher / Masquer
<?php // Encodage en UTF-8 header('Content-Type: text/html; charset=utf-8'); // On regarde si on est en mode développement include 'utils/is_dev.php'; include 'utils/fonctions_db.php'; include 'utils/fonctions_arduino.php'; // On lance l'échanges avec la bdd $db = db_connect(); $trace = get_trace($db); $nb_trace = count($trace)-1; $couleur = get_couleur($db); $carre_couleur['r'] = "<span class=\"colorbox\"><b style=\"background:#D42436\"></b></span>"; $carre_couleur['b'] = "<span class=\"colorbox\"><b style=\"background:#00CED1\"></b></span>"; $carre_couleur['v'] = "<span class=\"colorbox\"><b style=\"background:#74d600\"></b></span>"; $type['S'] = "Mise à jour schédulée"; $type['B'] = "Mise à jour au bouton"; ?> <html> <head> <link rel="stylesheet" media="screen" href="css/arduino.css"> <script type="text/javascript" src="script/jquery-1.9.0.min.js"></script> </head> <body id="css-zen-garden"> <div class="page-wrap"> <section class="intro" id="zen-intro"> <table id="tab"> <tr><td COLSPAN=2><h3>Aide <br/></td><tr> <tr><td COLSPAN=2> ><?php echo utf8_encode("♣ Interface connectée à une carte Arduino dôtée de trois leds (rouge, bleu, vert).<br/> ♣ Clickez sur la couleur de votre choix dans «Selection» pour changer la couleur de la led de l'Arduino.<br/> ♣ «Historique» trace les mises à jour des leds sur la carte Arduino. Elles peuvent êtres schédulées (toutes les minutes) ou déclenchées par l'appui du bouton situé sur la carte.<br/><br/><br/>"); ?> </td><tr> <tr><td><h3>Selection <br/></h3></td> <td><h3>Historique <br/></h3></td></tr> <tr> <td> <span class="colorpicker"> <span class="bgbox"></span> <span class="hexbox"></span> <span class="clear"></span> <span class="colorbox"> <b name="couleur" id-valeur="r" <?php if ($couleur == "r") { echo "class=\"selected\""; } ?> style="background:#D42436" title="Rouge"></b> <b name="couleur" id-valeur="b" <?php if ($couleur == "b") { echo "class=\"selected\""; } ?> style="background:#00CED1" title="Bleu"></b> <b name="couleur" id-valeur="v" <?php if ($couleur == "v") { echo "class=\"selected\""; } ?> style="background:#74d600" title="Vert"></b> </span> </span> </td> <td> <?php for ($i=1; $i<=$nb_trace; $i++) { echo utf8_encode("<p>".$trace[$i]['numero'].' '.$carre_couleur[$trace[$i]['couleur']].' - '.$type[$trace[$i]['type']].' à '.$trace[$i]['date'].'<br/></p>') ; } ?> </td> </tr> </table> </section> </div> <link href="colorpicker/customColorPicker.css" rel="stylesheet" type="text/css" /> <script src="colorpicker/customColorPicker.js" type="text/javascript"></script> <script src="script/utf8.js-master/utf8.js" type="text/javascript"></script> <script src="script/arduino.js" type="text/javascript"></script> </body> </html>
Le Javascript :

Afficher / Masquer
$(document).ready( function() { // Ajoute une nouvelle ligne à l'historique (et supprime la dernière) toutes les 10s si nécessaire setInterval(function () { $.ajax({ type: "POST", url: "act_arduino.php", data: "act=get_trace", success: function(msg){ var o = JSON.parse(msg); var msg_parsed = Object.keys(o).map(function(k) { return o[k] }); var screen = $('p:first-child').text(); var num_screen = screen.substring(0,screen.indexOf("-")-2); var num_base = msg_parsed[0].numero; if (num_screen != num_base) { if (msg_parsed[0].type == "S"){ var type = "Mise \xC3\xA0 jour sch\xC3\xA9dul\xC3\xA9e"; } else if (msg_parsed[0].type == "B"){ var type = "Mise \xC3\xA0 jour au bouton"; } if (msg_parsed[0].couleur == "r"){ var couleur = "<span class=\"colorbox\"><b style=\"background:#D42436\"></b></span>"; } else if (msg_parsed[0].couleur == "b"){ var couleur = "<span class=\"colorbox\"><b style=\"background:#00CED1\"></b></span>"; } else if (msg_parsed[0].couleur == "v"){ var couleur = "<span class=\"colorbox\"><b style=\"background:#74d600\"></b></span>"; } var child = '<p>'+couleur+msg_parsed[0].numero+' - '+type+' \xC3\xA0 '+msg_parsed[0].date+' <strong>NEW!</strong></p>'; $('p:first-child').before(utf8.decode(child)); $('p:last-child').remove(); } }, error: function(msg){ } }); }, 10000); // Post synchrone de la mise à jour de la couleur $("b[name = 'couleur']").click(function(){ post_couleur = $(this).attr('id-valeur'); postData('act_arduino.php',{couleur:post_couleur,act:"maj_couleur"}); }); // Réalise un post synchrone function postData(page,data) { var form = document.createElement('form'); form.setAttribute('action', page); form.setAttribute('method', 'post'); for (var n in data) { var inputvar = document.createElement('input'); inputvar.setAttribute('type', 'hidden'); inputvar.setAttribute('name', n); inputvar.setAttribute('value', data[n]); form.appendChild(inputvar); } document.body.appendChild(form); form.submit(); } } );
Back-end : Une table pour historiser les appels à l'API. Une autre pour stocker la couleur choisie par l'utilisateur. Des fonctions php pour requêter tout ça.

Demo :


Use case 1 :
- Sélectionner une couleur de led dans l'interface
- Presser le bouton poussoir et observer la led de la couleur choisie s'allumer
- Observer dans l'interface la trace de la mise à jour

Use case 2 :
(- Sélectionner une couleur de led dans l'interface)
- Attendre 1min
(- Observer la led de la couleur choisie s'allumer)
- Observer dans l'interface la trace de la mise à jour

Axe d'amélioration :

- Faire de l'API le point central de l’interaction avec le back-end. Dans mon cas, les fonctionnalités de mise à jour de la couleur et de l'obtention de l'historique présentes dans l'interface sont effectués directement avec le back-end. L'API gagnerait à être enrichie pour servir de base à la création d'une nouvelle interface par exemple.

- Monter une API sécurisée. Proposer une authentification (Basic, Digest ou OAuth) et envisager la sécurisation de l'échange avec SSL. J'ai vu que c'était impossible de mettre en place l'implémentation associée directement sur l'Arduino (avec les shield wifi et ethernet) du fait de la trop faible mémoire (SRAM) du micro-contrôleur. Peut-être est-ce possible via Processing ? Il n'empêche qu'il y a un trou dans la raquette en terme de sécurisation si on se projette avec l'Arduino pour réaliser un objet connecté.

- Réfléchir à l'alternative de transformer Processing en server web pour permettre à l'utilisateur de le solliciter quand il le souhaite (et non de façon récurrente comme effectuée en tant que client). Il est possible d'y héberger une interface minimale mais le travail pour la rendre dynamique semble une perte de temps. A minima, on peut envisager d'y héberger un point de connexion (API) mais ce dernier resterait figé.

- Mettre en place une meilleure gestion de l'appel à l'API. En l'occurrence, actuellement on ne gère aucun des codes retour HTTP. De plus, le delay d'1s après l'envoi de la requête en espérant recevoir la réponse est un peu limite.

- Réfléchir à un use case intelligent. Allumer des leds à distance, c'est bien, mais ça ne sert pas à grand chose.