Interaktiver Webserver mit dem Raspberry Pi

Kurze Einführung

Auf einem eingebetteten System (z.B.: Raspberry Pi) soll ein Webserver es ermöglichen interaktiv auf LEDs oder angeschlossene elektronische Schaltkreise zuzugreifen.

Statische Webseiten

Siehe auch Kapitel "Webseiten mit HTML und CSS". Wird eine Internetadresse (URL, Uniform resource locator) im Browser angegeben, so sucht dieser nach einer Datei mit dem Namen "index.html". Diese HTML-Datei (HyperText Markup Language) besteht im einfachsten Fall aus dem Kopf mit Titel und dem Body mit Inhalt:

<!DOCTYPE html>
<html lang="en">
  <head>
  <meta charset="utf-8"/>
    <title>Meine statische Webseite</title>
  </head>
  <body>
    <h1>Dies ist die Überschrift</h1>
    <p>Das ist ein Absatz</p>
  </body>
</html>

Den obigen HTML-Code speichern wir auf dem Raspi unter dem Namen: "index.html" im Verzeichnis "/home/pi/webserver" das wir neu erstellen (mkdir webserver). Nachdem wir in dieses Verzeichnis gewechselt sind (cd ~/webserver) starten wir den Webserver mit dem folgenden Befehl:

python3 -m http.server 8000

Wir verwenden hier nicht den Standardport 80 für Webserver, da wir sonst Root- Rechte auf dem Raspi benötigen. Unser Raspi-Server lauscht nun auf Port 8000. Der Schalter "-m" ist nötig damit nachher Module als Skript ausgeführt werden können. Auf dem PC können wir die Webseite jetzt mit folgender URL aufrufen, wobei natürlich die richtige IP-Adresse einzugeben ist:

http://172.16.249.30:8000

Den Webserver kann man auch lokal testen, also auf dem Gerät auf dem der Webserver gestartet wurde. Die Standard- IP-Adresse des lokalen Geräts lautet dann 127.0.0.1 (http://127.0.0.1:8000).

Aufgabe WS1:
  1. Teste den Webserver wie oben beschrieben.
  2. Teste den Webserver mit deinen Dateien aus dem vorigen Kapitel.

Dynamische Webseite mit CGI und Python

Mit dem Common Gateway Interface (CGI) ist es möglich Python- Programme auf dem Webserver auszuführen. Durch diese besteht dann die Möglichkeit dynamische Webseiten zu erstellen, also Seiten, deren Inhalt durch den Nutzer verändert werden kann. Die Programme müssen sich zwingend im Unterverzeichnis "cgi-bin" im Webserver- Verzeichnis befinden (/home/pi/webserver/cgi-bin). Das erste Python-Programm soll die gleiche statische Webseite wie vorhin erzeugen.

Damit CGI weiß, welche Programmiersprache wir einsetzen, müssen wir dies in der ersten Zeile mitteilen. Die Zeile teilt CGI zusätzlich mit, dass die Python Umgebung (Programme) im Verzeichnis "/usr/bin" zu finden ist. Da wir deutsche Umlaute verwenden, müssen wir Python zusätzlich mitteilen mit welcher Kodierung wir arbeiten (dies muss in einer der ersten beiden Zeilen erfolgen!). Der HTML-Code wird mit der print()-Methode ausgegeben. Mit drei Hochkomma besteht die Möglichkeit mehrere Zeilen zu drucken. Der ausgegebene Code landet, wenn CGI ihn ausführt, allerdings nicht auf der Standardausgabe, sondern gleich im Browser.

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    #webserver_WS2.py
    print('''
    <!DOCTYPE html>
    <html lang="de">
      <head>
      <meta charset="utf-8"/>
        <title>Meine statische Webseite</title>
      </head>
      <body>
        <h1>Dies ist die Überschrift</h1>
        <p>Das ist ein Absatz</p>  
      </body>
    </html>
    ''')
!!Achtung:

Erstelle die Python-Dateien auf dem Raspi (Linux). Die Zeilen müssen mit einem Linefeed enden (LF, 0x0A). Dateien die in Windows erstellt wurden, enthalten ein zusätzliches Carriage Return (CR, 0x0D) Zeichen, das bewirkt, dass ein Fehler auftritt und die Datei nicht angezeigt wird.

Aufgabe WS2:

Erstelle das Verzeichnis "cgi-bin". Speichere das obige Programm mit dem Namen "webserver_WS2.py" im Verzeichnis "/cgi-bin" ab. Wechsle in das Verzeichnis und teste das Programm (python3 webserver_WS2.py).

Damit CGI die Datei ausführt, benötigt man die nötigen Rechte für eine ausführbare Datei. Führe den folgenden Befehl im Unterverzeichnis "/cgi-bin" aus:

chmod u+x webserver_WS2.py

(Auf der graphischen Oberfläche kann dies auch im Dateimanager erledigt werden (Rechtsklick Properties) oder mit dem Midnight Commander (MC) in einem Terminal-Fenster).

Danach können wir im Server-Verzeichnis webserver ("cd ..") den Server starten. Dazu erstellen wir eine python Datei mit folgendem Inhalt:

    #!/usr/bin/env python
    import http.server
    http.server.HTTPServer(("", 8000),http.server.CGIHTTPRequestHandler).serve_forever()

und nennen sie webserver_cgi.py.

Wir starten dann den Server mit dem Befehl in einem eigenen Fenster:

python3 webserver_cgi.py

Achtung, dies muss im Verzeichnis webserver passieren, da der Server sonst die Dateien nicht findet. Im Terminal-Fenster des Webservers tauchen nun auch Fehlermeldungen auf, sollte das Python Programm aus dem cgi-bin Verzeichnis nicht richtig funktionieren.

Im Webbrowser rufen wir jetzt unsere Seite mit der folgenden URL auf (eigene IP-Adresse einsetzen!):

http://172.16.249.30:8000/cgi-bin/webserver_WS2.py

Die Ausgaben unseres Python Programms landen nicht mehr auf der Standardausgabe, sondern werden zum Webserver gesendet. Über diesen Weg kann ein eingebettetes System uns jetzt Informationen mitteilen.

Sollte ein Programm nicht

Informationen vom eingebetteten System zum Browser

An einem Raspberry Pi ist, über die 1-Wire Schnittstelle, ein Temperatursensor vom Typ DS18B20 angeschlossen. Die Temperatur soll im Webbrowser angezeigt werden, und bei jedem Reload der Webseite aktualisiert werden. Der Sensor ist mit 3,3V, Masse und Pin Nummer 4 zu verbinden. Der Pull-Up- Widerstand von 4,7k zieht die Datenleitung auf 3,3V (siehe Kapitel: Schnittstellen mit dem Raspberry Pi). Hier das entsprechende Programm.

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    # webserver_WS3.py

    import glob

    deviceFolder = glob.glob('/sys/bus/w1/devices/28*')  # find the file
    deviceFolderString = deviceFolder[0]
    deviceFile = deviceFolderString + '/w1_slave'

    def readTemp():
        try:
            f = open(deviceFile, 'r')
        except IOError:
            print('IOError')
        line1 = f.readline()
        line2 = f.readline()
        f.close()
        pos = line2.find('t=')
        if pos != -1:
            tempString = line2[pos + 2:]
            temp = round(float(tempString) / 1000.0, 1)
        else:
            print('error')
        return temp

    temperature = (readTemp())

    print('''
    <!DOCTYPE html>
    <html lang="en">
      <head>
      <meta charset="utf-8"/>
        <title>Mein Raspi Fieberthermometer</title>
      </head>
      <body>
        <h1>Mein Raspi Fieberthermometer</h1>
        <p>Die Temperatur beträgt: <b>%s Grad Celsius<b/></p>  
      </body>
    </html>
    ''' % temperature)

Sollen mehrere Variablen ausgegeben werden, so sind diese in Klammern, getrennt durch Komma hinter dem Prozentzeichen anzugeben. Beispiel:

    print('''
    ...
    <p>Der erste  Schalter ist <b>%sgeschaltet</b>.</p>
    <p>Der zweite Schalter ist <b>%sgeschaltet</b>.</p>
    <p>Der dritte Schalter ist <b>%sgeschaltet</b>.</p>
    ...
    ''' % (var0,var1,var2))
Aufgabe WS3:

Speichere das obige Programm mit dem Namen "webserver_WS3.py" im Verzeichnis "/cgi-bin" und mache es ausführbar (chmod u+x webserver_WS3.py. Verdrahte die Hardware am Raspberry Pi und teste das Programm indem du es im Webbrowser mit der folgenden URL aufrufst: (eigene IP-Adresse einsetzen!):

http://172.16.249.30:8000/cgi-bin/webserver_WS3.py
Aufgabe WS4:

Schreibe ein eigenes Programm, das am Raspberry Pi vier Schalter abfragt und den Zustand der Schalter im Browser darstellt. Nutze dazu den 8-Bit I/O Port-Expander-Chip PCF8574 (siehe Kapitel: Schnittstellen mit dem Raspberry Pi). Er ermöglicht es den Raspi über den I²C- Bus um 8 digitale Ein- bzw. Ausgänge zu erweitern. Ein über den I²C-Bus gesendetes Byte wird parallel an 8 Pins ausgegeben bzw eingelesen. Verbinde den Chip mit dem Raspi und schließe vier Taster (lila Drähte dienen als Schalter) an P0 bis P3 an. Weitere Informationen zum PCF8574: http://www.nxp.com/documents/data_sheet/PCF8574.pdf Nenne das Programm "webserver_WS4.py" (Verzeichnis "/cgi-bin" und chmod u+x webserver_WS4.py!) und teste es.

Schalterzustände

Verdrahtungsplan PCF8574P:

Verdrahtungsplan PCF8574P

Informationen vom Webbrowser zum eingebetteten System

Ein Parameter

Um unserem eingebetteten System Informationen zukommen zu lassen, nutzen wir Befehlszeilen-Parameter, die wir dem Python-Programm mitgeben. Auf den ersten Parameter kann mit sys.argv[1] zugegriffen werden.Dazu muss nur das Modul sys mit import sys eingebunden werden. Der Parameter wird als Zeichenkette (string) übergeben.

Es soll im folgenden eine LED ein- und ausgeschaltet werden. Die Ausgänge des PCF8574 sind "open collector" Ausgänge und liefern maximal 25mA. Beim Anschließen von LEDs (gegen 5V) sind deshalb Vorwiderstände nicht unbedingt nötig. Die LEDs werden durch Ausgabe einer Null eingeschaltet. Um einzelne LEDs zu schalten wird eine klassische Maskierung (einlesen, maskieren, ausgeben) verwendet (siehe "Die Maskierung von Daten" in http://weigu.lu/tutorials/avr_assembler/pdf/MICEL_MODUL_A.pdf Kapitel 3). Mit der ODER-Verknüpfung (Python |) werden einzelne Pins auf Eins gesetzt und mit der UND-Verknüpfung (Python &) werden einzelne Pins auf Null gesetzt. Um falsche Eingaben mit einer Fehlermeldung abfangen zu können, verwenden wir eine try/except-Anweisung.

Hier der Code eines Programms, das eine LED an P4 des Port-Expander-Chip PCF8574 ein bzw. ausschaltet:

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    # webserver_WS5.py

    # install i2c-tools and smbus with:
    # sudo apt-get install i2c-tools python-smbus
    # reboot:                   sudo reboot
    # test with:                i2cdetect -y 1 (for Pi Model B rev. 2)

    import smbus
    import sys

    port = 1  # (0 for rev.1, 1 for rev 2!)
    bus = smbus.SMBus(port)
    expanderAddr = 0x20

    din = bus.read_byte(expanderAddr)

    try:
        state = sys.argv[1]
        if (state == '0'):
            din = din | 0x10    # P4 = 1 LED ausschalten
            comment = "Die <b>LED</b> wurde <b>ausgeschaltet<b/>."
        elif (state == '1'):
            din = din & 0xEF    # P4 = 0 LED einschalten
            comment = "Die <b>LED</b> wurde <b>eingeschaltet<b/>."
        else:
            comment = "Nur die 0 und die 1 sind als Parameter gültig!"
    except:
        comment = "Fehler: Kein gültiger Parameter!"

    bus.write_byte(expanderAddr, din)

    print('''
    <!DOCTYPE html>
    <html lang="en">
      <head>
      <meta charset="utf-8"/>
        <title>Der erleuchtete Raspi</title>
      </head>
      <body>
        <h1>Der erleuchtete Raspi</h1>
        <p>%s</p>
      </body>
    </html>
    ''' % comment)

Im Webbrowser rufen wir jetzt unsere Seite mit der folgenden URL auf (eigene IP-Adresse einsetzen!):

http://172.16.249.30:8000/cgi-bin/webserver_WS5.py?0

zum Ausschalten und mit

http://172.16.249.30:8000/cgi-bin/webserver_WS5.py?1

zum Einschalten. Das Fragezeichen wurde zur Trennung verwendet, da Leerzeichen nicht erlaubt sind.

Aufgabe WS5:

Teste das Programm. Denke daran es zuerst ausführbar zu machen.

Um die komplizierten URLs nicht auswendig kennen zu müssen bauen wir klickbare Links mit den jeweiligen URLs in unsere Seite mit ein:

print('''
<!DOCTYPE html>
<html lang="en">
  <head>
  <meta charset="utf-8"/>
    <title>Der erleuchtete Raspi</title>
  </head>
  <body>
    <h1>Der erleuchtete Raspi</h1>
    <p><a href="http://172.16.249.30:8000/cgi-bin/webserver_WS6.py?0">LED an P4
ausschalten<a/></p>
    <br>
    <p><a href="http://172.16.249.30:8000/cgi-bin/webserver_WS6.py?1">LED an P4
einschalten<a/></p>
    <br>
    <hr>
    <p>%s</p>
  </body>
</html>
''' % comment)
Aufgabe WS6:

Erweitere das Programm um die Links und teste es als webserver_WS6.py.

Aufgabe WS7:
  1. Erweitere das Programm auf vier schaltbare LEDs und um einen Link, der zusätzlich die Zustände der vier Schalter anzeigt. Teste das Programm als webserver_WS7.py.

  2. Verpasse der Seite ein wenig "Style" sowie ein favicon. Nutze Bilder statt Text für die Links (Entsprechende Icons findest du hier: http://www.clker.com/clipart-red-led-off-1.html.).
    Dies lässt sich mit folgendem HTML-Code verwirklichen:

<a href="http://172.16.249.30:8000/cgi-bin/webserver_WS7.py?L4_0">
<img src="../led_off.png" alt="LED off"></a>

Die Datei style.css und die Bilddateien müssen sich im Verzeichnis webserver befinden.

LEDs schalten über die Webseite

Aufgabe WS8: (für Fleißige)

Schreibe ein Programm, das einen als Parameter übergebenen Text als Lauflicht auf dem 14-Segment Display (siehe Kapitel: Schnittstellen mit dem Raspberry Pi) ausgibt. Teste das Programm als webserver_WS8.py.

Mehrere Parameter

Leider sind mehrere Fragezeichen zur Trennung von mehreren Parametern nicht erlaubt. Möchte man mehrere Parameter übergeben, so ist es am einfachsten diese in einen Parameter zu packen. Dazu verwendet man das &-Zeichen. Um vier LEDs gleichzeitig zu schalten könnte man zum Beispiel:

http://172.16.249.30:8000/cgi-bin/webserver_WS8.py?1&0&1&1&Hallo

versenden (1. LED ein, 2. LED aus, 3. LED ein, 4. LED ein, 5. Parameter Text"Hallo"). Mit der split()-Methode wird der gepackte Parameter dann in seine Einzelteile zerlegt:

    try:
        para = sys.argv[1]
        state = para.split ('&')
        comment5 = state[4]
        maskOff = 0x00
        maskOn = 0xFF
        if (state[0] == '0'):
            maskOff = maskOff | 0x10    # P4 = 1 LED ausschalten
            comment1 = "Die <b>LED P4</b> wurde <b>ausgeschaltet</b>."
        elif (state[0] == '1'):
            maskOn = maskOn & 0xEF    # P4 = 0 LED einschalten
            comment1 = "Die <b>LED P4</b> wurde <b>eingeschaltet</b>."
        else:
            comment1 = "Nur die 0 und die 1 sind als Parameter gültig!"
        if (state[1] == '0'):
            maskOff = maskOff | 0x20
            comment2 = "Die <b>LED P5</b> wurde <b>ausgeschaltet</b>."
        elif (state[1] == '1'):
            maskOn = maskOn & 0xDF
            comment2 = "Die <b>LED P5</b> wurde <b>eingeschaltet</b>."
        else:
            comment2 = "Nur die 0 und die 1 sind als Parameter gültig!"
        if (state[2] == '0'):
            maskOff = maskOff | 0x40
            comment3 = "Die <b>LED P6</b> wurde <b>ausgeschaltet</b>."
        elif (state[2] == '1'):
            maskOn = maskOn & 0xBF
            comment3 = "Die <b>LED P6</b> wurde <b>eingeschaltet</b>."
        else:
            comment3 = "Nur die 0 und die 1 sind als Parameter gültig!"
        if (state[3] == '0'):
            maskOff = maskOff | 0x80
            comment4 = "Die <b>LED P7</b> wurde <b>ausgeschaltet</b>."
        elif (state[3] == '1'):
            maskOn = maskOn & 0x7F
            comment4 = "Die <b>LED P7</b> wurde <b>eingeschaltet</b>."
        else:
            comment4 = "Nur die 0 und die 1 sind als Parameter gültig!"

    except:
        comment1 = "Fehler: Kein gültiger Parameter!"
        comment2 = "Fehler: Kein gültiger Parameter!"
        comment3 = "Fehler: Kein gültiger Parameter!"
        comment4 = "Fehler: Kein gültiger Parameter!"
Aufgabe WS9:

Teste die Variante mit der gleichzeitigen Übergabe mehrerer Parameter. Teste das Programm als webserver_WS9.py.

Aufgabe WS10: (für Fleißige)

Schreibe ein Programm, das den Inhalt der Parameter über die serielle Schnittstelle des Raspi versendet.

Aufgabe WS11: (für sehr Fleißige)

Erweitere die vorige Aufgabe, so dass zusätzlich der Text aus einer Textdatei (serial_in.txt) im Browser angezeigt wird. Ein zweites Python-Programm soll im Hintergrund laufen, die serielle Schnittstelle der Raspi kontinuierlich abfragen, und empfangene Zeilen in die obige Textdatei schreiben.

Quellen: