Communication série : Arduino et Pure Data

Pour communiquer entre une carte Arduino et le logiciel Pure Data, il existe plusieurs solutions, listées ici. La plus répandue est d'utiliser l'objet [arduino] dans Pure Data avec le firmware Firmata dans Arduino. Voir aussi la page Arduino et pd sur flossmanuals.

Cependant il peut arriver que nous ayons besoin de plus de fonctionnalités comme l'utilisation des capteurs de distance avec la bibliothèque UltraSonic, des capteurs capacitifs, d'utiliser la bibliothèque Tone pour changer la fréquence PWM, etc. Donc, il faut soit reprendre et donc comprendre les exemples de Firmata, soit comprendre la base de la communication série. Vous l'aurez compris, on va plutôt choisir la deuxième solution.

Téléchargement de tous les codes de cette page : arduino-pd-serial.zip
Un autre tutoriel (en anglais) détaillant la plupart des cas : arduino_for_pders.tar.gz

Prérequis

Si vous n'êtes pas à l'aise avec les notions de liaison série, je vous invite à lire le tutoriel d'openclassrooms.

Une autre notion technique très utilisée ici est celle de String, littéralement “chaîne” en français. Il s'agit d'un type de données comme les nombres entiers (int), nombres à virgule flottante (float) ou caractère (char) que l'on retrouve en programmation. On l'appelle chaîne de caractères, c'est une suite de caractères, l'équivalent d'un tableau de caractères. Par exemple, le mot “BONJOUR” est une String composée de sept caractères 'B', 'O', 'N', 'J', 'O', 'U', 'R' auquel il est parfois ajouté au niveau informatique un caractère de fin, en langage C par exemple.

Une valeur

L'exemple le plus simple est d'envoyer une valeur avec Pure Data et de la recevoir après être passée par la carte Arduino. Une boucle en somme.

Quand il s'agit d'un nombre pas de problème, mais on peut aussi vouloir envoyer des caractères. Pour ce faire le caractère est converti en nombre entre 0 et 255 soit 8 bits. La conversion suit le standard ASCII que nous utiliserons souvent. Une table permet de visualiser ces correspondances.

// Serial messages (1)
// Recevoir un nombre et l'envoyer
 
// conteneur (int) pour recevoir la donnée série
int incomingByte = 0; 
 
void setup() {
  // ouverture du port série avec un taux de 9600 bauds
  Serial.begin(9600);
}
 
void loop() {
  // on reçoit au moins 1 octet dans le buffer (.available())
  if (Serial.available() > 0) {
    // lecture d'un octet et effacement dans le buffer
    incomingByte = Serial.read();
    // ecriture d'un octet
    Serial.write(incomingByte);    
  }
}

Une commande

En utilisant les opérateurs de comparaisons, on peut déclencher une fonction très simplement. Ici, on allume et éteind la LED 13 de la Arduino avec l'envoi d'un chiffre ou d'une lettre.

// Serial messages (2)
// Allumer une LED avec un nombre
// 72 pour allumer, 76 pour éteindre
 
const int ledPin = 13; // pin de la LED
int incomingByte; // variable 
 
void setup() {
  Serial.begin(9600); // port série
}
 
void loop() {
  // on reçoit quelque chose
  if (Serial.available() > 0) { 
 
    incomingByte = Serial.read();
 
    // allumer la LED (H=72 en ASCII)
    if (incomingByte == 'H')  digitalWrite(ledPin, HIGH);
 
    // eteindre la LED (L=76 en ASCII)
    if (incomingByte == 'L') digitalWrite(ledPin, LOW);
  }
}

Serial.available()

La fonction Serial.available() est toujours utilisée pour connaître combien d'octets restent dans le buffer. Celui-ci est limité à 63 octets et si il n'y pas de Serial.read() ou de Serial.parseInt() pour enlever petit à petit les octets, alors il atteindra son maximum.

Dans le code Pure Data, il y a un petit algorithme très pratique pour afficher dans Pure Data les données venant de la Arduino. Il consiste à stocker dans un objet [list] toutes les données les unes à la suite des autres ([list prepend]), puis de l'envoyer sous forme de liste quand arrive le chiffre 10 équivalent au retour à la ligne dans Arduino, le ln dans Serial.println(). L'objet [bytes2any] convertit cette liste en caractères courant.

// Serial messages (3) : test available
// imprime le nombre de caractères reçus
 
int inBytes = 0;
int lastInBytes = 0;
 
void setup() {
  Serial.begin(9600); // port série
}
 
void loop() {
  // nombre d'octets reçus dans le buffer
  inBytes = Serial.available(); 
 
  // pour n'imprimer le nombre d'octets reçus
  // que lorsque ce nombre a changé
  if (inBytes != lastInBytes && inBytes > 0) {
    Serial.println(inBytes);
  }
  lastInBytes = inBytes;
  delay(10);
}

Méthodes pour recevoir les caractères ASCII

serialEvent()

C'est l'occasion d'introduire la fonction serialEvent() du langage Arduino qui permet de récupérer les données séries, très pratique pour ne pas avoir du code éparpillé. La méthode pour recevoir un message consiste à concaténer (ajouter les unes à la suite des autres) les données pour former une chaine de caractères (String). On définit un caractère de fin de message pour pouvoir ensuite l'utiliser. Il est souvent convenu que celui-ci soit le caractère de retour de ligne “\n”, équivalent à 10 en ASCII.

L'étape suivante est d'extraire du message la fonction et l'argument à l'aide des méthodes indexOf(' ') et substring().

/*
 * Serial messages (4)
 * Réception des données avec Serial Event et commande avec un argument
 */
 
String inputString = "";   // chaine de caractères pour contenir les données
boolean stringComplete = false;  // pour savoir si la chaine est complète
 
void setup() {
  Serial.begin(9600); // port série
  pinMode(13,OUTPUT);
  pinMode(9,OUTPUT);
}
 
void loop() {
  // 2 - Utilisation du message
  if (stringComplete) {
    //Serial.println(inputString);
 
    // on récupère la position du séparateur (l'espace " ")
    int index = inputString.indexOf(' ');
 
    // on coupe la chaine en deux : la fonction d'un côté et l'argument de l'autre
    String fct = inputString.substring(0,index); 
    String arg = inputString.substring(index,inputString.length());
 
    // appel de ma fonction en transformant la chaine en nombre
    if (fct == "LED13") {
      light(13, arg.toInt()); 
    }
    else if (fct == "LED9") {
      light(9, arg.toInt());
    }
 
    // on vide la chaine pour utiliser les messages suivants
    inputString = "";           
    stringComplete = false;
  }
}
 
/*
  1 - Réception des données
  SerialEvent est déclenchée quand de nouvelles données sont reçues. 
  Cette routine tourne entre chaque loop(), donc utiliser un 
  delay la fait aussi attendre.
 */
void serialEvent() {
  while (Serial.available()) {
    // récupérer le prochain octet (byte ou char) et l'enlever
    char inChar = (char)Serial.read(); 
    // concaténation des octets reçus
    inputString += inChar;
    // caractère de fin pour notre chaine
    if (inChar == '\n') {  
      stringComplete = true;
    }
  }
}
 
// fonction personnalisable
void light(int pin, int brightness) {
  Serial.print("Light function : ");
  Serial.print(pin);
  Serial.print(", ");
  Serial.println(brightness);
  analogWrite(pin,brightness);
}

Avec deux arguments

On découpe encore une fois les données avec les espaces.

Pour le code Arduino, on découpe une nouvelle fois pour récupérer le second argument. On ajoute aussi nos fonctions.

void loop() {
  if (stringComplete) {
 
    ...
 
    // deuxième découpage pour le second argument
    index = arg.lastIndexOf(' ');
    String arg2 = arg.substring(index,arg.length());
    arg = arg.substring(0,index);
 
    // appel des fonctions en transformant la chaine en nombre
    if (fct == "LED") {
      light(arg.toInt(), arg2.toInt()); 
    }
    else if (fct == "MODE") {
      mode(arg.toInt(), arg2.toInt());
    }
 
...
 
// fonctions personnalisables
void light(int pin, int brightness) {
  ...
}
 
void mode(int pin, int state) {
...
}

Découper le message (parser)

Jusqu'ici, le nombre d'arguments est fixe et la méthode n'est pas modulaire. On peut concevoir une fonction qui parcourt le message et le découpe à chaque espace, qui sera le séparateur. Ainsi, le message “LED 9 120”, pourra être décomposé en trois “bouts” : “LED”, “9”, “120”. Le premier sera le sélecteur de la commande et les deux autres, les arguments dans Arduino.

Le patch Pure Data est le même que précédemment :

Le code Arduino est aussi presque le même, nous ajoutons la fonction splitString() :

...
 
int cnt = 0; // nombre de données découpées
String data[10]; // stockage des données découpées
 
...
 
void loop() {
  // si le message est complet
  if (stringComplete) {
 
    // on le découpe à chaque espace ' '
    // et on stocke les bouts dans un tableau
    splitString(inputString, ' ');
 
    // appel des fonctions selon le premier sélecteur
    if (data[0] == "LED") {
      light(data[1].toInt(), data[2].toInt()); 
    }
    else if (data[0] == "MODE") {
      mode(data[1].toInt(), data[2].toInt()); 
    }
 
    // vide la chaine
    inputString = "";           
    stringComplete = false;
  }
}
 
...
 
// méthode pour découper le message avec un séparateur (ou "parser")
void splitString(String message, char separator) {
  int index = 0;
  cnt = 0;
    do {
      index = message.indexOf(separator); 
      // s'il y a bien un caractère séparateur
      if(index != -1) { 
          // on découpe la chaine et on stocke le bout dans le tableau
          data[cnt] = message.substring(0,index); 
          cnt++;
          // on enlève du message le bout stocké
          message = message.substring(index+1, message.length());
      } else {
         // après le dernier espace   
         // on s'assure que la chaine n'est pas vide
         if(message.length() > 0) { 
           data[cnt] = message.substring(0,index); // dernier bout
           cnt++;
         }
      }
   } while(index >=0); // tant qu'il y a bien un séparateur dans la chaine
}
 
...

Autres exemples

// Exemple avec strtok
// http://forum.arduino.cc/index.php/topic,41215.0.html
#include <string.h>
 
char sz[] = "Here; is some; sample;100;data;1.414;1020";
 
void setup()
{
  char *p = sz;
  char *str;
  Serial.begin(9600);
  while ((str = strtok_r(p, ";", &p)) != NULL) // delimiter is the semicolon
    Serial.println(str);
}
 
void loop(){}

SerialCommand

Télécharger la bibliothèque ArduinoSerialCommand et la placer dans le dossier “~/sketchbook/libraries”.

// Demo Code for SerialCommand Library - Steven Cogswell - May 2011
 
#include <SerialCommand.h>
#define arduinoLED 13 // Arduino LED on board
 
SerialCommand sCmd;   // The demo SerialCommand object
 
void setup() {
  pinMode(arduinoLED, OUTPUT);      // Configure the onboard LED for output
  digitalWrite(arduinoLED, LOW);    // default to LED off
 
  Serial.begin(9600);
 
  // Setup callbacks for SerialCommand commands
  sCmd.addCommand("ON", LED_on); // Turns LED on
  sCmd.addCommand("OFF", LED_off);// Turns LED off
  sCmd.addCommand("HELLO", sayHello);// Echos the string argument back
  sCmd.addCommand("P", processCommand);// Echos two arguments converted to integers
  sCmd.setDefaultHandler(unrecognized);// Handler for command that isn't matched 
  Serial.println("Ready");
}
 
void loop() {
  sCmd.readSerial(); // We don't do much, just process serial commands
}
 
void LED_on() {
  Serial.println("LED on");
  digitalWrite(arduinoLED, HIGH);
}
 
void LED_off() {
  Serial.println("LED off");
  digitalWrite(arduinoLED, LOW);
}
 
void sayHello() {
  char *arg;
  arg = sCmd.next(); // Get the next argument from the SerialCommand object buffer
  if (arg != NULL) { // As long as it existed, take it
    Serial.print("Hello ");
    Serial.println(arg);
  }
  else {
    Serial.println("Hello, whoever you are");
  }
}
 
void processCommand() {
  int aNumber;
  char *arg;
  Serial.println("We're in processCommand");
  arg = sCmd.next();
  if (arg != NULL) {
    aNumber = atoi(arg);    // Converts a char string to an integer
    Serial.print("First argument was: ");
    Serial.println(aNumber);
  }
  else {
    Serial.println("No arguments");
  }
 
  arg = sCmd.next();
  if (arg != NULL) {
    aNumber = atol(arg);
    Serial.print("Second argument was: ");
    Serial.println(aNumber);
  }
  else {
    Serial.println("No second argument");
  }
}
 
// This gets set as the default handler, and gets called when no other command matches.
void unrecognized(const char *command) {
  Serial.println("What?");
}

CmdMessenger

Plus compliqué mais à noter : http://playground.arduino.cc/Code/CmdMessenger

Réception de valeurs

Même technique, l'espace est un séparateur et le retour à la ligne le caractère de fin de message

/*
 * Serial messages (8)
 * Envoie de données de trois capteurs
 */
 
void setup() {
  Serial.begin(9600);
}
 
void loop() {
  Serial.print(analogRead(0));
  Serial.print(" ");
  Serial.print(analogRead(1));
  Serial.print(" ");
  Serial.println(analogRead(2));
  delay(20);
}