Kampis Elektroecke

Portables GPIO-Interface für den Raspberry Pi

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.

2 Kommentare

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

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert