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