Il est parfois utile de visualiser sur une page web dans un navigateur un flux continu de données récupérées sur un port série. Mais comment s’y prendre ? Pourquoi pas avec un petit script ?

Fondamentalement, le problème se résume à la création périodique d’une page web contenant les données série les plus récentes. Un navigateur web peut alors afficher la page, que ce soit sur le même ordinateur ou au travers d’un réseau. Par conséquent, ce qu’il faut c’est un petit programme qui convertit en permanence les données série par exemple en fichiers HTML ou PHP. Ah ! PHP n’est-il pas un langage de programmation pour les applications en relation avec l’internet ? Si, il l’est. Donc, pouvons-nous utiliser PHP pour faire le travail ? Oui, nous pouvons. Mais il y a aussi d’autres moyens, comme nous le verrons.

Utiliser le rafraîchissement automatique de page

Pour mémoire, le langage HTML permet l’utilisation d’une métabalise qui demande au navigateur de recharger périodiquement une page :

 
<meta http-equiv="refresh" content="10">
 

Cette balise indique au navigateur de recharger la page contenant la balise toutes les dix secondes. (Au cas où votre navigateur ne prendrait pas en charge cette balise, remplacez-la par un bout de JavaScript. Voyez le téléchargement au-dessous de l'article. Si nous créons une page web qui comporte cette balise et pointons le navigateur sur elle, ce dernier la rechargera toutes les dix secondes (bien sûr, vous pouvez modifier le délai d’expiration). Si nous réécrivons la page toutes les dix secondes avec des données rafraîchies, alors le navigateur nous les montrera aussi.

Partage du travail

On peut aussi mettre la balise de rafraîchissement dans un fichier PHP au lieu d’un fichier HTML et le navigateur fera la même chose. Le fichier PHP contiendra également un script de lecture des données sur le port série. C’est là que les choses se compliquent parce que le langage PHP ne prend pas en charge nativement les ports série. Et même s’il le faisait, cela signifierait que chaque fois que le navigateur demanderait la (dernière version de la) page, le script devrait ouvrir le port série, récupérer quelques données et refermer de nouveau le port. Les données reçues en dehors de cette fenêtre seraient perdues. De plus, certains systèmes de type Arduino peuvent se réinitialiser à l’ouverture du port série, ce qui rend la configuration inutile. Une solution consiste à diviser le processus en deux sous-processus :

Processus 1 : un script pour lire en permanence le port série et mettre à jour un fichier importé par la page web PHP (listage 1):

Listage 1. Un script PHP qui lit des données depuis le port série et les écrit dans un fichier nommé « data.txt ».


<?php

// Linux $comPort = "/dev/ttyACM0";
$comPort = "COM15";

include "php_serial.class2.php";
$serial = new phpSerial;
$serial->deviceSet($comPort);

// On Windows (10 only?) all mode settings must be done in one go.
$cmd = "mode " . $comPort . " baud=115200 parity=n data=8 stop=1 to=off xon=off";
$serial->_exec($cmd);
$serial->deviceOpen();

echo "Waiting for data...\n";
sleep(2); // Wait for Arduino to finish booting.
$serial->serialflush();

while(1)
{
  $read = $serial->readPort();

  if (strlen($read)!=0)
  {
    $fp = fopen("data.txt","w");
    if ($fp!=false)
    {
      fwrite($fp,trim($read));
      fclose($fp);
    }
  }
}

?>


Processus 2 : un navigateur qui recharge périodiquement la page web PHP de façon à rafraîchir les données (listage 2, fig. 1).

 
Figure 1. Le fichier PHP dynamique produit par le script PHP est servi par un serveur web WAMP à votre
navigateur (voir sa barre d’adresse). Il présente, séparées par des virgules dans un tableur élémentaire,
les données lues sur le port série. Les valeurs inférieures à 500 sont affichées en rouge, les autres en vert.
Le titre de la fenêtre d’invite de commande montre la commande pour exécuter le script.
 

Listage 2. Cette page web PHP transforme le contenu d’un fichier nommé « data.txt » en tableau.


<?php

$page_title = "Arduino on PHP";

// Start of HTML page.
echo "<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01//EN' 'http://www.w3.org/TR/html4/strict.dtd'>";
echo "<html>"; // Page begin.
echo "<head><title>",$page_title,"</title>"; // Head begin.
echo "<meta http-equiv='refresh' content='1'>";
echo "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>";
echo "<link rel='shortcut icon' href='favicon.ico' />";
echo "<link rel='icon' type='image/x-icon' href='favicon.ico' />";
echo "<link rel='icon' type='image/png' href='favicon.png' />";
echo "</head>"; // Head end.
echo "<body><center>"; // Body begin.

echo "<p>",$page_title,"</p>"; // Page title.

// Create a table from data file.
$handle = fopen("data.txt","r");
if ($handle!=NULL) 
{
  // Read one line from the file, then close it.
  $data = fgets($handle);
  fclose($handle);
  
  // Synchronise to the data.
  if ($data[0]=='$')
  {
    // Remove whitespace.
    str_replace(' ','',$data);
    // Split data in fields separated by ','.
    // Expected format: "$,id1,value1,id2,value2,CRLF"
    list($startchar,$id1,$value1,$id2,$value2,$newline) = explode(",",$data);
    // Create array from list.
    $numbers = array($id1=>$value1,$id2=>$value2);
    // Sort array in ascending key order.
    ksort($numbers);
    
    // Table begin.
    echo "<table border='1' border-spacing='5' style='text-align:center;'>";
    echo "<tr><th>ID</th><th>Value</th></tr>";
    foreach ($numbers as $x => $x_value) 
    {
      echo "<tr>"; // Table row begin.
      echo "<td>", $x, "</td>"; // Table column 1.
      echo "<td>"; // Table column 2 begin.
      if ($x_value>=500) echo "<font color='green'>";
      else echo "<font color='red'>";
      echo $x_value;
      echo "</font></td>"; // Table column 2 end.
      echo "</tr>"; // Table row end.
    }
    // Table end.
    echo "</table>";
  }
}
echo "</body>"; // Body end.
echo "</html>"; // Page end.
?>

 

Oups, on demande un serveur web...

Ce stratagème résout le problème de l’ouverture et de la fermeture du port série et de la perte de données qui en résulte, mais il faut un script qui tourne en arrière-plan. Si c’est un script PHP, alors l’ordinateur doit être capable d’exécuter des scripts PHP. Il faut aussi un serveur web pour envoyer la page web PHP à un navigateur. Sinon, le navigateur va simplement montrer le code PHP de création de la page au lieu de la page elle-même. La manière traditionnelle d’y parvenir est d’installer ce qu’on appelle un package « AMP » ou « WAMP ». AMP signifie Apache-MySQL-PHP, le W est pour « Windows », et ensemble ils forment un serveur web tout-en-un.

Oubliez PHP...

Nous avons essayé cette méthode et avons réussi à la faire marcher, mais pas sans problèmes. Outre les difficultés dans l’installation du serveur web, le problème principal rencontré a été d’obtenir de PHP qu’il ouvre de façon fiable un port série pour recevoir des données. En cherchant sur l’internet, il semble qu’il n’y ait qu’une seule bibliothèque PHP pour les communications série, PHP Serial. Toutes les autres paraissent en avoir été dérivées. Comme le mentionne l’auteur sur la page GitHub « Windows : cela semble fonctionner pour certaines personnes, pas pour d’autres ». Nous étions clairement dans le second groupe. Pour que les communications série avec le PHP fonctionnent correctement, nous avons dû ouvrir puis immédiatement fermer le port série avec un programme de Terminal série (Tera Term par exemple) ; autrement c’est impossible. Nous avons donc abandonné la méthode PHP pour nous diriger vers le langage Python.

... utilisez plutôt Python

Le langage Python 3 avec la bibliothèque pySerial s’est révélé parfaitement fonctionnel sur notre ordinateur sous Windows 10, donc nous avons écrit un script pour lire les données sur le port série et créer une page web avec ces données. Maintenant qu’il n’y a plus besoin de PHP, le script Python peut également produire un fichier HTML complet (listage 3, fig. 2). 
 

Figure 2. Cette fois-ci, un script Python produit un fichier HTML dynamique
(comme on peut le voir sur la barre d’adresse du navigateur).

Listage 3. Un script Python qui lit des données depuis le port série et produit un fichier HTML à partir de ces données.


import serial
import time

file_name = "serial.html" # Once created, open this file in a browser.

# Adapt serial port nr. & baud rate to your system.
serial_port = 'COM15'
baudrate = 115200

page_title = "Arduino on Python";

def write_page(data_list):
    fo = open(file_name,"w+")
    # Start of HTML page.
    fo.write("<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01//EN' 'http://www.w3.org/TR/html4/strict.dtd'>")
    fo.write("<html><head><title>"+page_title+"</title>") # Page & Head begin.
    fo.write("<meta http-equiv='refresh' content='1'>")
    fo.write("<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>")
    fo.write("<link rel='shortcut icon' href='favicon.ico' />")
    fo.write("<link rel='icon' type='image/x-icon' href='favicon.ico' />")
    fo.write("<link rel='icon' type='image/png' href='favicon.png' />")
    fo.write("</head><body><center><p>"+page_title+"</p>") # Head end, body begin.

    # Table begin.
    fo.write("<table border='1' border-spacing='5' style='text-align:center;'>")
    fo.write("<tr><th>ID</th><th>Value</th></tr>")
    for i in range(0,len(data_list),2):
        fo.write("<tr>") # Table row begin.
        fo.write("<td>"+data_list[i]+"</td>") # Table column 1.
        fo.write("<td>") # Table column 2 begin.
        fo.write("<font color='")
        # Values >= 500 will be printed in green, smaller values will be red.
        if (int(data_list[i+1])>=500): fo.write("green")
        else: fo.write("red")
        fo.write("'>")
        fo.write(data_list[i+1])
        fo.write("</font></td>") # Table column 2 end.
        fo.write("</tr>") # Table row end.
    fo.write("</table>") # Table end.
    fo.write("</body>") # Body end.
    fo.write("</html>") # Page end.
    # Done, close file.
    fo.close()

s = serial.Serial(serial_port,baudrate) # Open serial port.
s.dtr = 0 # Reset Arduino.
s.dtr = 1
print("Waiting for data...");
time.sleep(2) # Wait for Arduino to finish booting.
s.reset_input_buffer() # Delete any stale data.

while 1:
    data_str = s.readline().decode() # Read data & convert bytes to string type.
    # Clean up input data.
    # Expected format: "$,id1,value1,id2,value2,...,CRLF"
    data_str = data_str.replace(' ','') # Remove whitespace.
    data_str = data_str.replace('\r','') # Remove return.
    data_str = data_str.replace('\n','') # Remove new line.
    data_str += '123,65,1,999,cpv,236' # Add some more data
    print(data_str)
    # Split data in fields separated by ','.
    data_list = data_str.split(",")
    del data_list[0] # Remove '$'
    # Write HTML page.
    write_page(data_list)

Tout le formatage de données effectué par la page web PHP peut se faire directement en Python. Tout est maintenant au même endroit. La page web peut être servie par un serveur web (jeu de mots volontaire ;-), mais un navigateur peut aussi afficher et rafraîchir la page sans serveur. Par conséquent, il n’y a plus besoin non plus de package (W)AMP, ce qui simplifie tout.

Figure 3. Ce croquis Arduino produit un flux de données série qu’on
peut utiliser pour le test, le débogage ou l’écriture de scripts.

Nous avons présenté ici une méthode d’affichage de données série dans un navigateur web. La méthode n’est ni nouvelle, ni exclusive, ni « la meilleure ». Si vous en connaissez une autre – plus simple, plus élégante, peu importe – laissez un commentaire ci-dessous. Et, bien sûr, pas besoin d’écrire le script en Python, n’hésitez pas à utiliser n’importe quel langage capable d’établir des communications série et d’écrire des fichiers. L’avantage de Python avec pySerial est qu’il fonctionnera sur des machines Windows, macOS et Linux (ou autres).

Le code conçu pour cet article sous forme de scripts PHP et Python ainsi qu’un croquis Arduino peuvent être téléchargés après l'article ci-dessous.

(170111)
 

Intéressé(e) par la lecture d'autres articles du magazine Elektor ? Devenez membre dès maintenant !