Manchmal möchte man den kontinuierlichen Datenstrom auf einer seriellen Verbindung als Webseite in einem Browser anzeigen. Wie kann man dies erreichen? Ganz einfach mit einem Skript...


Im Grunde geht es nur darum, regelmäßig eine Webseite mit den neuesten seriellen Daten zu erstellen. Ein Webbrowser kann die Seite dann auf dem lokalen Computer oder über ein Netzwerk anzeigen. Daher wird ein kleines Programm benötigt, das die seriellen Daten kontinuierlich beispielsweise in HTML-Dateien umwandelt. Ist PHP nicht ebenfalls eine Programmiersprache für internetbezogene Anwendungen? Ja, und weil dies so ist, kann man mit PHP diese Aufgabe einfach und elegant erledigen. Aber es gibt auch andere Wege, wie wir sehen werden.

Automatischer Refresh

HTML kennt ein Meta-Tag, das den Browser anweist, eine Webseite mit einer gewissen Verzögerung zu laden:
 
<meta http-equiv="refresh" content="10">
 

Ist keine URL angegeben, weist dieses Tag im Head-Bereich der Webseite den Browser an, die aktuelle Seite alle zehn Sekunden neu zu laden. Der Timeout kann natürlich auch ein anderer Wert sein. Falls Ihr Browser dieses Tag nicht unterstützt, kann es durch ein kurzes Stück JavaScript ersetzt werden (Download siehe unten). Und wenn die Seite alle zehn Sekunden mit neuen seriellen Daten neu aufgebaut wird, zeigt der Browser diese auch an.

Teilen Sie die Aufgabe in zwei Teile!

Das Refresh-Tag kann statt in einer HTML- auch in einer PHP-Datei verwendet werden. Der Browser wird identisch reagieren. Diese PHP-Datei könnte auch ein Skript enthalten, das die Daten von der seriellen Schnittstelle liest. Dies ist der Punkt, an dem die Dinge kompliziert werden, weil PHP serielle Schnittstellen nicht von Haus aus unterstützt. Und selbst wenn es so wäre, müsste das Skript jedes Mal, wenn der Browser die neueste Version der Seite anfordert, die serielle Schnittstelle öffnen, die aktuellen Daten abrufen und die Schnittstelle wieder schließen. Daten, die außerhalb dieses Fensters über die Schnittstelle laufen, würden verloren gehen. Zu allem Überfluss könnten arduinoide Systeme beim Öffnen der seriellen Schnittstelle zurückgesetzt werden, was den ganzen Ansatz unmöglich macht. Eine Lösung hierfür ist die Aufteilung des Prozesses in zwei Abschnitte:

Abschnitt 1: Ein Skript zum kontinuierlichen Lesen des seriellen Ports und zum Aktualisieren der Daten in einer Datei, die von der PHP-Webseite importiert wird (Listing 1).

Listing 1. Ein PHP-Skript, das die Daten von der seriellen Schnittstelle liest und in die Datei „data.txt“ schreibt.​ 


<?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);
    }
  }
}

?>

Abschnitt 2: Ein Browser, der die PHP-Webseite periodisch neu lädt, damit die aktuellen Daten dargestellt werden (Listing 2, Bild 1).
 
Bild 1. Die vom PHP-Skript generierte dynamische PHP-Datei wird von einem WAMP-Webserver an unseren Browser geleitet (siehe Adressleiste). Sie stellt die Daten der seriellen Schnittstelle als CSV in einer Basistabelle dar. Werte unter 500 sind rot, die anderen grün. Der Titel des Terminalfensters zeigt den Befehl zum Ausführen des Skripts.
 
Listing 2. Diese PHP-Webseite formatiert den Inhalt der Datei „data.txt“ als Tabelle.

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

Webserver benötigt

Durch dieses verteilte Vorgehen wird das Problem mit dem Öffnen und Schließen des Ports und dem sich daraus ergebenen Datenverlust zwar gelöst, aber es wird ein Skript benötigt, das im Hintergrund ausgeführt wird. Wenn es sich um ein PHP-Skript handelt, dann muss der Computer auch in der Lage sein, PHP-Skripte auszuführen. Außerdem ist ein Webserver erforderlich, um die PHP-Webseite an den Browser zu übergeben. Ohne Webserver würde der Browser einfach den PHP-Code anzeigen, aus dem die Seite besteht. Die traditionelle Art, dies zu erreichen, ist die Installation eines sogenannten AMP-Pakets. AMP steht für Apache-MySQL-PHP. Windows-Webservern ist ein oft „W“ vorangestellt, Linux-Versionen ein „L“, also WAMP oder LAMP (es gibt aber zahllose andere Webserver von Mini bis ausgewachsen im Netz).

Nehmen Sie kein PHP, ...

Wir haben PHP ausprobiert und es geschafft, das Skript zum Laufen zu bringen, aber nicht ohne Probleme. Neben den Schwierigkeiten bei der Einrichtung des Webservers war das Hauptproblem, dass PHP eine serielle Schnittstelle nicht zuverlässig öffnet, um Daten zu empfangen. Im Internet scheint es nur eine Bibliothek namens PHP Serial für die serielle Kommunikation zu geben, alle anderen sind wohl von dieser abgeleitet. Wie der Autor auf der GitHub-Seite erwähnt [2] „Windows: it seems to be working for some people, not working for some others.“ Wir gehörten eindeutig zu der zweiten Gruppe. Um die serielle Kommunikation mit PHP zum Laufen zu bringen, mussten wir zuerst die Schnittstelle mit einem seriellen Terminalprogramm wie TeraTerm öffnen und sofort wieder schließen. Wir haben daher die PHP-Methode aufgegeben und uns stattdessen für Python entschieden.

... nehmen Sie Python!

Python 3 mit pySerial hat sich auf unserem Testcomputer mit Windows 10 als perfekt erwiesen. Wir haben ein Skript geschrieben, um Daten von der seriellen Schnittstelle zu lesen und die Webseite mit den Daten zu füllen. Da PHP nicht mehr benötigt wird, kann das Python-Skript auch eine reine HTML-Datei erzeugen (Listing 3, Bild 2).

 
Bild 2. Wie aus der Adressleiste des Browsers hervorgeht, erzeugt ein Python-Skript eine dynamische HTML-Datei.
Listing 3. Ein Python-Skript, das Daten von der seriellen Schnittstelle liest und eine entsprechende HTML-Datei erzeugt.

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)

Alle Datenformatierungen können auch direkt in Python vorgenommen werden. Die Webseite kann zwar immer noch von einem Webserver ausgeliefert werden, aber ein Browser kann die Seite auch lokal anzeigen und aktualisieren. Daher ist auch kein (W/L)AMP-Paket mehr erforderlich, was alles wesentlich einfacher macht.

 

Bild 3. Dieser Arduino-Sketch erzeugt einen seriellen Datenstrom, der für die Entwicklung, das Debuggen und den Test des Skripts verwendet werden kann.

Zum Abschluss

In diesem Artikel haben wir eine Methode zur Anzeige serieller Daten in einem Webbrowser vorgestellt. Die Methode ist weder neu noch unbedingt die Beste. Wenn Sie einen anderen Weg kennen – einfacher, eleganter oder was auch immer – bitte teilen Sie ihn uns mit. Und natürlich brauchen Sie das Skript nicht in Python zu schreiben, Sie können jede andere Programmiersprache verwenden, die in der Lage ist, eine serielle Kommunikation abzuwickeln und Dateien zu schreiben. Der Vorteil von Python (und pySerial) ist, dass es unter Windows, macOS und Linux (und mehr) läuft.

Der für diesen Artikel entwickelte Code in Form von PHP- und Python-Skripts sowie ein Arduino-Sketch können unten heruntergeladen werden.
(170111)
 
Wollen Sie weitere ElektorLabs-Artikel lesen? Jetzt Elektor-Mitglied werden!