Soms kan het handig zijn om een continue stroom seriële data op een webpagina in een browser weer te geven. Hoe u dit kunt doen? Eenvoudig, met een paar scripts.

We moeten hiervoor periodiek een webpagina maken die de meest recente seriële data bevat. Deze pagina kan dan op dezelfde computer of via een netwerk in een webbrowser worden weergegeven. Er is dus een programma nodig dat de seriële data continu omzet naar bijvoorbeeld HTML- of PHP-bestanden. PHP, is dat niet een programmeertaal voor internet-gerelateerde toepassingen? Kunnen we daarmee dit probleem oplossen? Ja, dat kan, maar we zullen zien dat er ook andere methoden zijn.

Automatische pagina-refresh

We beginnen met HTML, dat met een meta-tag een browser kan opdragen om regelmatig een pagina te herladen:
 
<meta http-equiv="refresh" content="10">
 
Deze tag vertelt de browser dat de pagina met die tag iedere tien seconden moet worden herladen. (Als uw browser de tag niet ondersteunt, kunt u deze vervangen door een stukje JavaScript. Zie hiervoor de download) Als we nu een webpagina met deze tag maken, dan zal deze door de browser iedere tien seconden opnieuw worden geladen (een ander tijdsinterval is ook mogelijk). Herschrijven we de pagina nu iedere tien seconden met recente seriële data, dan zal de browser deze recente data weergeven.

Taken verdelen

Als de refresh-tag zich in een PHP-bestand in plaats van in een HTML-bestand bevindt, zal de browser dezelfde actie uitvoeren. In dit PHP-bestand zouden we een script kunnen opnemen waarmee de data van de seriële poort worden gehaald. Maar dit wordt lastig omdat PHP van huis uit geen seriële poorten ondersteunt. En zelfs als het dit wel zou doen, dan zou het script als de browser de recente versie van de pagina opvraagt de seriële poort moeten openen, de data ophalen en de poort weer sluiten. Data die buiten dit venster bij de seriële poort aankomen gaan verloren. Ook kunnen sommige Arduino-achtige systemen zich resetten als de seriële poort wordt geopend en dat maakt deze opzet onbruikbaar. Een oplossing hiervoor is het proces te splitsen in twee subprocessen:
 
Proces 1: een script dat continu de seriële poort leest en de ontvangen data opslaat in een bestand dat door de PHP-webpagina wordt geïmporteerd (listing 1);
 

Listing 1: Een PHP-script dat data van de seriële poort leest, en deze vervolgens wegschrijft naar een bestand met de naam ‘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);
    }
  }
}

?>


Proces 2: een browser die periodiek de PHP-webpagina herlaadt zodat deze de data kan verversen (listing 2, figuur 1).

 
Figuur 1: Het door het PHP-script gegenereerde dynamische PHP-bestand wordt door een WAMP-server aan onze browser aangeboden (zie de adresregel). Het bevat een tabel met kommagescheiden waarden die van de seriële poort zijn gelezen. Waarden onder de 500 worden in rood weergegeven, de andere zijn groen. In de titel van het commandoprompt-venster ziet u het commando om het script uit te voeren.
 

Listing 2: Deze PHP-webpagina formatteert de inhoud van het bestand met de naam ‘data.txt’ als een tabel.


<?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.
?>

 

Er is een webserver nodig

Het probleem met het openen en sluiten van de seriële poort en het hiermee samenhangende dataverlies is nu opgelost, maar dit vereist een script dat op de achtergrond loopt. Als dit een PHP-script is, dan moet de computer PHP-scripts kunnen uitvoeren. Ook is een webserver nodig om de PHP-pagina aan een browser aan te bieden, anders geeft de browser niet de pagina maar de onderliggende PHP-code weer. De oplossing hiervoor is de installatie van een ‘AMP’- of ‘WAMP’-pakket. AMP is de afkorting van Apache-MySQL-PHP, de ‘W’ is van Windows. Met zo’n pakket krijgt u een complete webserver met alle toeters en bellen in huis.

Gebruik geen PHP...

We hebben deze methode geprobeerd en kregen het aan de praat, maar niet zonder problemen. Naast moeilijkheden bij het installeren van de webserver, was het een probleem om PHP betrouwbaar een seriële poort te laten openen om de data te ontvangen. Uit internet-research bleek dat er maar één PHP-bibliotheek voor seriële communicatie is: PHP Serial. Alle andere lijken hiervan te zijn afgeleid. Op de GitHub-pagina schrijft de auteur: “Windows: bij sommige mensen lijkt het te werken, maar voor sommige anderen niet.” Wij hoorden bij de tweede groep... Om met PHP de seriële communicatie werkend te krijgen, moesten we eerst met een terminalprogramma (zoals TeraTerm) de poort openen en meteen weer sluiten; een andere manier was er niet. Daarom hebben we besloten om PHP te verlaten en Python te gebruiken.

...maar Python

Python 3 met pySerial bleek op onze Windows-10-testcomputer prima te werken, en dus maakten we een script dat de data van de seriële poort leest en er een webpagina mee vult. Nu er geen reden meer is om PHP te gebruiken, kan met het pythonscript ook een eenvoudig HTML-bestand worden geproduceerd (listing 3, figuur 2).
 
Figuur 2: Hier produceert het Pythonscript een dynamisch HTML-bestand (zoals u kunt zien in de adresbalk van de browser).

Listing 3: Een Python-script dat data leest van de seriële poort en er een HTML-bestand voor genereert.


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)


Ook de PHP-dataformattering kan in Python worden gedaan en de webpagina kan zonder server door een browser worden weergeven en ververst zodat er geen (W)AMP-pakket meer nodig is. Dit maakt alles veel eenvoudiger.

Figuur 3: Deze Arduino-sketch genereert een seriële datastroom
die kan worden gebruikt voor het ontwikkelen, debuggen en testen van scripts.

Tot slot

Dit artikel beschrijft een methode om seriële data in een webbrowser weer te geven. Die methode is zeker niet nieuw, exclusief of ‘de beste’. Als u een andere manier weet – eenvoudiger, eleganter, of wat dan ook – deel deze dan met ons op onze website. En voor het script kunt u inplaats van Python iedere andere programmeertaal gebruiken die seriële communicatie en het schrijven van bestanden ondersteunt. Het voordeel van Python met pySerial is dat het draait op Windows-, macOS- en Linuxmachines (en nog meer).

De voor dit artikel ontwikkelde code in de vorm van PHP- en Pythonscripts en een Arduinosketch kan binnenkort van de Elektor website worden gedownload.
(170111)
 
Wilt u meer ElektorLabs artikelen lezen? Word dan nu lid van Elektor!