In diesem Beitrag stelle ich eine Realisierungsmethode für ein portables Webinterface, mit dem die GPIOs des Raspberry Pi geschaltet werden, vor. Die Applikation wird in Python mittels Flask realisiert und anschließend in einem Docker-Container untergebracht.
Docker auf dem Raspberry Pi:
Eine Kernkomponente dieses Projektes ist die Containerverwaltung Docker. Docker ermöglicht es einzelne Anwendungen zu virtualisieren, sodass diese ohne Installation etc. auf jedem System lauffähig sind. Das Prinzip ist dem einer virtuellen Maschine (z. B. VirtualBox) sehr ähnlich, nur das Docker ohne Hypervisor arbeitet und das Betriebssystem als Ausgangsbasis für die Virtualisierung verwendet.
Damit Docker auf dem Raspberry Pi genutzt werden kann, muss es erst installiert werden. Dazu muss als erstes der GPG-Schlüssel von Docker hinzugefügt werden:
$ sudo apt-get update $ curl -fsSL https://download.docker.com/linux/raspbian/gpg | sudo apt-key add –
Jetzt wird die Paketliste /etc/apt/sources.list
erweitert:
deb https://download.docker.com/linux/raspbian/ buster stable
Die erweiterte Paketliste muss nun noch eingelesen werden. Im Anschluss daran kann Docker installiert werden:
$ sudo apt-get update $ sudo apt-get install docker-ce
Docker ist nun einsatzbereit und der Virtualisierung von Containern steht nichts mehr im Weg.
Das Webinterface:
Mit Hilfe des Webinterface sollen die I/Os des Raspberry Pi geschaltet werden können. Für die Erstellung des Webinterface wird Flask verwendet. Dieses Modul habe ich, zusammen mit einem SocketIO Modul, in einer separaten Klasse für den Webservice untergebracht. In dieser Klasse wird der Webservice initialisiert und die Routing-Regeln für den Webzugriff festgelegt:
self.__app = Flask(__name__) self.__socketio = SocketIO(self.__app, async_mode = None) self.__app.add_url_rule("/", "index", self.__showIndex) self.__app.add_url_rule("/shutdown", "shutdown", self.__shutdown, methods = ["GET"])
Der komplette Webserver läuft in einem eigenen Thread und kann mit der Methode Run() gestartet werden.
Über die ObserveableData
-Klasse kann die Applikation auf Daten, die mittels JavaScript an den Webserver gesendet werden, reagieren. Jedes ObserveableData
-Objekt kann mit Callbacks versehen werden, die immer dann ausgelöst werden, sobald die Daten geändert wurden. Dazu wird eine Variable in einem globalen Daten-Dictionary, welches an den Webserver gegeben wird, angelegt:
Data = {} def gpioOnChanged(self, Value): GPIO.output(int(Value), GPIO.HIGH) Data.update({ "gpio_on" : ObserveableData("") }) Data["gpio_on"].set_Callback(gpioOnChanged)
Das Dictionary Data wird mit einem ObserveableData
-Objekt erweitert, welches mit dem Schlüssel gpio_on
versehen wird. Anschließend wird der dazu gehörige Callback festgelegt. Zu guter letzt wird der Variablenname noch an den Webservice weitergegeben:
Webservice = Webservice(Data) Webservice.Run() Webservice.GetDataFromWeb("gpio_on")
Damit wäre der Python-Code vollständig. Jetzt muss nur noch das Webinterface designed und die entsprechende Javascript-Applikation entworfen werden.
Die Kommunikation mit dem Webservice wird durch die Javascript-Methode SendJSON
realisiert.
function SendJSON(Element, Value) { var xmlhttp = new XMLHttpRequest(); xmlhttp.open("POST", "/transmitData"); xmlhttp.setRequestHeader("Content-Type", "application/json"); xmlhttp.send(JSON.stringify({ "Element" : Element, "Value" : Value })); }
Diese Methode erwartet zwei Parameter, wobei der erste Parameter der Schlüssel der jeweiligen Variable ist (z. B. gpio_on
) und der zweite Parameter ist der Wert, der an den Webservice gesendet werden soll (in diesem Beispiel die Nummer des I/Os).
Damit die einzelnen I/Os geschaltet werden können habe ich für jeden I/O zwei Radio-Buttons vorgesehen, einen zum Einschalten und den anderen zum Ausschalten. Der Name der Radio-Buttons entspricht dabei immer der I/O-Bezeichnung, wodurch die Radio-Buttons entsprechend gruppiert werden können. Zudem wird der Name für die Identifizierung des I/Os im Python-Skript verwendet.
<tr> <td> <label>17</label> </td> <td> <input type = "radio" id = "On_17" name = "17" value = "On"> </td> <td> <input type = "radio" id = "Off_17" name = "17" value = "Off" checked = "checked"> </td> </tr> <tr> <td> <label>27</label> </td> <td> <input type = "radio" id = "On_27" name = "27" value = "On"> </td> <td> <input type = "radio" id = "Off_27" name = "27" value = "Off" checked = "checked"> </td> </tr> <tr> <td> <input type = "Button" id = "ButtonCloseApplication" value = "Close" /> </td> </tr>
Die Radio-Buttons, die zum Einschalten vorgesehen sind, bekommen das onChange-Event OnClick
und die Radio-Buttons, die zum Ausschalten vorgesehen sind, bekommen das onChange-Event OffClick
zugewiesen.
var Inputs = document.querySelectorAll("input"), i; for (i = 0; i < Inputs.length; ++i) { if(Inputs[i].value == "On") { Inputs[i].onclick = OnClick } else if(Inputs[i].value == "Off") { Inputs[i].onclick = OffClick } }
Über den Elementnamen gpio_on
und gpio_off
der Methode SendJSON
werden im Python-Code die entsprechenden Callbacks ausgelöst, die dann einen I/O an- bzw. abschalten. Die Identifizierung des I/Os findet über den Namen des aufrufenden Objektes (this.name
) statt.
function OnClick() { SendJSON("gpio_on", this.name); } function OffClick() { SendJSON("gpio_off", this.name); }
Damit ist auch das Webinterface fertig erstellt und kann ausgeführt werden:
$ python3 app.py
Über die IP-Adresse localhost:5000 ist das Webinterface anschließend zu erreichen:
root@Raspberry:/home/pi/Desktop/GPIO_Interface# python3 app.py WebSocket transport not available. Install eventlet or gevent and gevent-websocket for improved performance. * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit) 192.168.178.51 - - [03/May/2018 16:55:20] "GET /socket.io/?EIO=3&transport=polling&t=1525359313999-45 HTTP/1.1" 200 - 192.168.178.51 - - [03/May/2018 16:55:20] "POST /socket.io/?EIO=3&transport=polling&t=1525359314179-46&sid=ab3af116cf5f46279e5db109820568b7 HTTP/1.1" 200 - 192.168.178.51 - - [03/May/2018 16:55:20] "GET /socket.io/?EIO=3&transport=polling&t=1525359314179-47&sid=ab3af116cf5f46279e5db109820568b7 HTTP/1.1" 200 -
Erstellen eines Docker-Containers:
Nun soll die Anwendung in einen Docker-Container portiert werden. Dazu wird im dem root-Verzeichnis der Application eine Datei namens dockerfile angelegt und wie folgt ausgefüllt:
FROM arm32v7/python:3.5-jessie # Set the working directory to /app WORKDIR /app # Copy the current directory contents into the container at /app ADD . /app # Install any needed packages specified in requirements.txt RUN pip install --no-cache-dir -r requirements.txt # Make port 3000 available to the world outside this container EXPOSE 3000 # Run app.py when the container launches CMD ["python", "app.py"]
Die Datei requirements.txt beinhaltet alle zusätzlichen Pakete, die mit Hilfe des RUN-Befehls bei der Containererstellung durch pip installiert werden sollen:
rpi.gpio Flask flask_socketio
Mit diesen beiden Dateien kann der Container erstellt werden:
$ docker build -t webinterface .
Dieser Vorgang dauert beim ersten Ausführen relativ lange, da alle notwendigen Pakete noch heruntergeladen werden müssen. Wenn alles geklappt hat, erscheint die folgende Ausgabe:
root@Raspberry:/home/pi/Desktop/GPIO_Interface# docker build -t webinterface . Sending build context to Docker daemon 157.7kB Step 1/7 : FROM arm32v7/python:3.5-jessie ---> 41877d8c33a7 Step 2/7 : WORKDIR /app ---> Using cache ---> 0320fda5c715 Step 3/7 : MAINTAINER Daniel Kampert <danielkampert@kampis-elektroecke.de> ---> Using cache ---> e8da460ce3f0 Step 4/7 : ADD . /app ---> 269b74619bb6 Removing intermediate container 88ae531c3a29 Step 5/7 : RUN pip install --no-cache-dir -r requirements.txt ---> Running in 5d5101a51411 ... ---> 22d484f992d7 Removing intermediate container 5d5101a51411 Step 6/7 : EXPOSE 3000 ---> Running in 40c706570e61 ---> 4e5cfd070055 Removing intermediate container 40c706570e61 Step 7/7 : CMD python app.py ---> Running in de9b9d6c3287 ---> 568c33ef8d59 Removing intermediate container de9b9d6c3287 Successfully built 568c33ef8d59 Successfully tagged webinterface:latest
Jetzt kann der Container gestartet werden:
$ docker run -p 3000:5000 --privileged webinterface
Da der Container auf die Hardware des Prozessors (hier die I/Os) zugreifen will muss entweder der Zusatz --privileged
oder --device /dev/gpiomem
verwendet werden. Über den Parameter -p 3000:5000
wird der Port 3000, der über das Dockerfile verfügbar gemacht werden sollte, des Hosts auf den Port 5000 des Containers (der Default-Port von Flask) gemappt. Der Container ist nun im Netzwerk erreichbar und die I/Os können geschaltet werden.
Das komplette Projekt ist bei GitHub zum Download verfügbar.
Hallo,
vielen Dank für deine ausführlichen Beiträge. Sie erleichtern den Einstieg in neue Projekte und das spart viel Zeit. Aus gegebenen Anlass möchte ich ein wenig Zeitersparnis bei der Pflege deiner Webside zurückgeben.
Beim Ausprobieren deines Projektes „Portables GPIO-Interface für den Raspberry Pi“ bin ich heute auf ein Update-Problem gestoßen, da die ursprüngliche Webside vom Projekt „yum.dockerproject.org“ umgezogen ist. Du könntest deine Webside wie folgt aktualisieren:
Die Zeile:
$ curl -fsSL https://yum.dockerproject.org/gpg | sudo apt-key add –
wäre aktualisiert:
curl -fsSL https://download.docker.com/linux/raspbian/gpg | sudo apt-key add –
und aus der bisherigen Zeile:
deb https://apt.dockerproject.org/repo/ raspbian-jessie main
wäre aktualisiert z. B. :
deb https://download.docker.com/linux/raspbian/ buster stable
Danach können die Paketlisten vom Raspberry wieder eingelesen werden.
Viele Grüße
Roland
Hallo Roland,
vielen Dank. Ich habe es geändert.
Gruß
Daniel