Blog der Heimetli Software AG

Zustand der Raspberry GPIOs auf einer Webseite anzeigen

Den Zustand der GPIOs auf einer Webseite darzustellen ist relativ enfach. Schwieriger ist es, den aktuellen Zustand ohne Reload der Seite zu zeigen.

Screenshot des Browsers

Um das zu bewerkstelligen gibt es diverse Methoden wie Websockets, Server Sent Events oder AJAX. Weil auf meinem Raspberry bereits ein Apache-Server lief, habe ich mich für AJAX entschieden. Ein weiterer Vorteil von AJAX ist, dass es alle auch nur einigermassen aktuellen Browser unterstützen. Der nachfolgend gezeigte Code funktioniert sogar mit einem IE8 auf einem Windows XP!

Falls Ihnen AJAX gar nichts sagt, dann lesen Sie besser zuerst die Beschreibung zum einfacheren Beispiel für AJAX mit ausführlichen Erklärungen.

Voraussetzungen

Die Applikation läuft auf einem Raspberry Pi 3 mit einem etwa 6 Monate alten Raspbian und einem vor drei Monaten installierten Apache. Das CGI läuft wahrscheinlich nicht auf einem Raspberry 2 weil die GPIO-Register dort anders gemappt sind.

Die HTML-Seite mit dem JavaScript

Die folgende HTML-Seite liegt in /var/www/html und sie muss zumindest für www-data lesbar sein:

<!DOCTYPE html>
<html>
 <head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="author" content="P. Tellenbach">
  <title>Yet another AJAX demo</title>
  <script>
   var timer = setInterval( pollServer, 500 ) ;
   var ids   = [ "S1", "S2", "S3", "S4", "S5" ] ;

   function process( obj )
   {
      for( var i = 0; i < 5; i++ )
      {
         var element = document.getElementById( ids[i] ) ;

         if( (obj.inputs & (1 << i)) != 0 )
         {
            element.innerHTML        = "EIN" ;
            element.style.background = "#00FF00" ;
         }
         else
         {
            element.innerHTML        = "AUS" ;
            element.style.background = "#FF0000" ;
         }
      }
   }

   function statechange()
   {
      if( this.readyState == 4 )
      {
         if( this.status == 200 )
         {
            process( JSON.parse(this.responseText) ) ;
         }
      }
   }

   function pollServer()
   {
      try
      {
         var request = new XMLHttpRequest() ;

         if( request )
         {
            request.onreadystatechange = statechange ;
            request.open( "GET", "/cgi-bin/getpins", true ) ;
            request.send( null ) ;
         }
         else
         {
            alert( "XMLHttpRequest failed" ) ;

            clearInterval( timer ) ;
         }
      }
      catch( err )
      {
         alert( err.description ) ;

         clearInterval( timer ) ;
      }
   }
  </script>
 </head>
 <body>
  <h1>Yet another AJAX demo</h1>
  <table>
   <tr><th>Schalter</th><th>Zustand</th></tr>
   <tr><td>Schalter 1</td><td id="S1">AUS</td></tr>
   <tr><td>Schalter 2</td><td id="S2">AUS</td></tr>
   <tr><td>Schalter 3</td><td id="S3">AUS</td></tr>
   <tr><td>Schalter 4</td><td id="S4">AUS</td></tr>
   <tr><td>Schalter 5</td><td id="S5">AUS</td></tr>
  </table>
 </body>
</html>

Es ist eine ganz normale HTML-Seite mit etwas JavaScript. Das Script fragt im Hintergrund den Server nach dem aktuellen Status und aktualisiert die Anzeige.

Die Funktion pollserver wird alle 500ms aufgerufen und schickt einen GET-Request an den Server. Sie enthält ausser der URL nichts was spezifisch für dieses Projekt ist. Sie kann also auch für ganz andere Projekte benutzt werden.

So lange der Server Daten im JSON-Format schickt, braucht auch statechange nicht verändert zu werden. statechange prüft ob der Request erfolgreich beendet wurde, und wandelt das JSON vom Server in ein Objekt um.

process dagegen ist sehr spezifisch. Die Funktion erwartet ein Objekt mit dem Attribut inputs als Parameter. In diesem Attribut muss der Zustand der GPIOs als binär codierte Zahl stehen.

Mit einer Schleife wird diese Zahl Bit für Bit zerlegt und sowohl Inhalt als auch Hintergrundfarbe der entsprechenden Tabellenzelle nachgeführt. Um die Aufgabe zu vereinfachen, habe ich ein Array mit den IDs der Zellen definiert, so dass das Script mühelos auf die Zellen zugreifen kann.

Der Code auf dem Raspberry

Mein erster Plan war ein PHP-Script das die /sys/class/gpio/gpioxx/value Files liest und als JSON codiert. Ich habe viel Zeit investiert, aber es ist mir nicht gelungen, diese Files zu lesen.

Ein Problem dabei sind die Rechte der Files ab ../gpio/.. Man kann sie zwar anpassen, aber bei jedem Neustart gehen sie verloren. Einige Elemente in diesem Pfad sind zudem Links, was bedeutet dass die Rechte auf den gelinkten Directories und Files geändert werden müssen.

Schlussendlich ist es mir gelungen, die Files als www-data zu lesen, aber PHP meldete weiterhin Fehler beim Filezugriff. Anscheinend beschränkt PHP den Zugriff auf Files und Directories ausserhalb von /var/www. Weil ich keine offensichtlichen Einstellungen von PHP gefunden habe, musste ich diesen Ansatz aufgeben.

Also habe ich mich von elinux Code Samples inspirieren lassen und ein CGI geschrieben...

/*********************************************************************************************/
/*                                                                                           */
/*                                                                         File: getpins.cpp */
/*                                                                                           */
/*     Reads the GPIO pins and encodes the state of selected pins in JSON format             */
/*     =========================================================================             */
/*                                                                                           */
/*     V0.1     28-DEC-2016      P. Tellenbach                                               */
/*                                                                                           */
/*********************************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

// Define the bit masks for the pins
#define GPIO17   0x0020000
#define GPIO18   0x0040000
#define GPIO22   0x0400000
#define GPIO23   0x0800000
#define GPIO24   0x1000000

/***********/
 int main( )
/***********/
{
   // Write the header lines first
   printf( "Content-type: application/json\r\n" ) ;
   printf( "Expires: Tue, 03 Jul 2001 09:00:00\r\n" ) ;
   printf( "Cache-Control: no-store, no-cache, must-revalidate, max-age=0\r\n" ) ;
   printf( "Pragma: no-cache\r\n\r\n" ) ;

   // Open a file descriptor for /dev/mem
   int fd = open( "/dev/mem", O_RDWR|O_SYNC ) ;
   
   if( fd == -1 )
   {
      // Return error messages in a JSON object
      printf( "{ \"error\": \"opening /dev/mem failed\" }" ) ;
      return 0 ;
   }
 
   // Map the memory to get access to the GPIO registers
   void *map = mmap( NULL, 4096, PROT_READ, MAP_SHARED, fd, 0x3F200000 ) ;
 
   close( fd ) ;
 
   if ( map == MAP_FAILED )
   {
      printf( "{ \"error\": \"mmap failed\" }" ) ;
      return 0 ;
   }
 
   // Read the input register
   unsigned reg = *(((volatile unsigned *)map) + 13) ;

   munmap( map, 4096 ) ;

   int inputs = 0 ;

   if( (reg & GPIO17) != 0 )
      inputs |= 0x01 ;
    
   if( (reg & GPIO18) != 0 )
      inputs |= 0x02 ;
    
   if( (reg & GPIO22) != 0 )
      inputs |= 0x04 ;
    
   if( (reg & GPIO23) != 0 )
      inputs |= 0x08 ;
    
   if( (reg & GPIO24) != 0 )
      inputs |= 0x10 ;
    
   printf( "{ \"inputs\": %d }", inputs ) ;
   
   return 0;
}

Das ging recht glatt und lief in kurzer Zeit.

Installation des CGI-Programmes

Das Builden des Programmes geht wie erwartet:

g++ -o getpins getpins.cpp

Danach muss es ins richtige Directory:

sudo cp getpins /usr/lib/cgi-bin

Und braucht bestimmte Rechte:

sudo chown root.root /usr/lib/cgi-bin/getpins
sudo chmod 4755 /usr/lib/cgi-bin/getpins

Es läuft aber noch nicht, weil der Apache CGIs per Default nicht ausführt. Also muss der auch noch konfiguriert werden:

sudo a2enmod cgi

Dann empfiehlt sich ein Reboot, damit alle Einstellungen auch wirklich übernommen werden.

Nach dem Reboot können Sie die Seite mit einem Browser aufrufen und alles sollte richtig funktionieren.

Die Sourcen für die HTML-Seite und das CGI

In diesem ZIP finden Sie index.html und getpins.cpp.