Eine beliebte Anwendung des Raspberry Pi’s ist daraus einen Webserver zu bauen. Hierfür können wir unterschiedliche Technologien wie Python, Node.JS oder auch PHP verwenden. Da die allermeisten Skripte für den Raspberry Pi mit Python geschrieben sind, ergibt es Sinn eine REST API Schnittstelle ebenfalls damit zu erstellen. Anschließend können wir spezifische Funktionen aufrufen und bspw. GPIOs steuern oder auslesen. Damit lassen sich LEDs oder auch andere Sensoren/Module einfach steuern. Das Schöne daran ist, dass wir fast jeglichen Code, den wir bisher in Python für den Raspberry Pi geschrieben haben, einfach per REST API aufrufbar machen können.
Daher erstellen wir in diesem Tutorial mit FastAPI eine solche Schnittstelle und schauen uns an, wie wir sie erweitern und absichern können.
Benötigtes Zubehör
Im Prinzip ist nicht viel Zubehör für dieses Tutorial nötig. Da wir allerdings testen wollen, ob unser Aufbau auch funktioniert, empfehle ich folgene Teile:
Du kannst natürlich das ganze beliebig erweitern und bspw. Sensoren (Temperatur, etc.) anschließen, die wir über unsere API abfragen.
Was ist eine REST API?
Eine API (Application Programming Interface) ist eine Schnittstelle, die z.B. über eine URL aufgerufen werden kann. Unter REST (Representational State Transfer) sind einige Prinzipien zusammengefasst, wie sich Schnittstellen zu verhalten haben, z.B. dass eine GET Anfrage lediglich lesend sein soll und auf dem Server nichts verändert. Anderseits dürfen POST Befehle neue Entitäten (bspw. eine neue Instanz eines Buches) erstellen. Mehr zu der Umsetzung kannst du hier erfahren.
Aufbau am Raspberry Pi
Der Aufbau am Raspberry Pi ist in diesem Tutorial sehr einfach, da wir lediglich eine LED und einen Taster verwenden. Dein Szenario kann (sollte!) natürlich von diesem abweichen, da es nur als Beispiel gelten soll und daher auch sehr simpel ist.
Die LED geht über einen 330 Ω Vorwiderstand an GPIO 17 und der Taster an 3.3V und GPIO 21.
Außerdem nutzen wir im Folgenden die BCM Nummern der GPIOs und nicht die Board-Nummerierung:
Softwarekomponenten der Python REST API
Nun werden wir die API Schritt für Schritt erstellen. Zunächst bereiten wir die benötigten Tools vor. Danach erstellen und erweitern wir unsere REST API, um unsere GPIOs zu schalten bzw. auszulesen. Zu guter Letzt sichern wir unsere API noch ab, sodass nicht jeder darauf Zugriff hat.
Übrigens: Den gesamten Code, den wir gleich Schritt für Schritt durchgehen, findest du auch auf Github.
Installation der Bibliotheken
Bevor wir anfangen, benötigen wir Python3 sowie ein paar Bibliotheken, die wir über den Package-Installer pip laden.
sudo apt-get install python3 python3-pip
Anschließend können wir die benötigten Python-Bibliotheken installieren:
pip3 install fastapi uvicorn[standard] rpi.gpio
Mehr Informationen findest du auf den jeweiligen Dokumentationsseiten (fastapi, uvicorn, rpi.gpio).
Einstieg: Erster Test zum Status auslesen per API
Starten wir unseren ersten Test. Dazu erstellen wir ein einfaches Python-Skript.
sudo nano main.py
Dieses bekommt folgenden Inhalt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
from fastapi import FastAPI import RPi.GPIO as GPIO GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) app = FastAPI() @app.get("/read/{gpio}") def read_root(gpio: int): GPIO.setup(gpio, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) return {"gpio": gpio, "on": GPIO.input(gpio)} |
Mit STRG+O speichern wir die Datei und mit STRG+X schließen wir den nano-Editor. Danach können wir unser Programm bereits starten.
uvicorn main:app --reload
Jetzt kannst du im Browser des Raspberry Pi’s folgende URL öffnen: http://127.0.0.1:8000/read/17
Die GPIO Nummer kannst du anpassen, falls der Taster an einem anderen Pin angeschlossen ist. Das Ergebnis sieht folgendermaßen aus, sofern du den Taster nicht drückst:
{"gpio":17,"on":false}
Das ist die Antwort, die wir unter diesem Endpunkt definiert haben. Wenn du den Taster drückst und die URL erneut aufrufst, wird sich das Ergebnis ändern. Mit wenigen Zeilen Code haben wir unseren ersten REST Endpunkt geschrieben. Nun wollen wir diesen aber noch erweitern.
Erweiterung unserer Python API – GPIO Status setzen
Das reine Auslesen ist etwas langweilig, daher wollen wir die GPIOs natürlich auch steuern und setzen. Daher erstellen wir einen weiteren Endpunkt (diesmal PATCH, da wir etwas verändern wollen):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
from fastapi import FastAPI from pydantic import BaseModel import RPi.GPIO as GPIO GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) app = FastAPI() class SetGPIO(BaseModel): on: bool @app.get("/read/{gpio}") def read_root(gpio: int): GPIO.setup(gpio, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) return {"gpio": gpio, "on": GPIO.input(gpio)} @app.patch("/set/{gpio}") def read_item(gpio: int, value: SetGPIO): if value.on: GPIO.setup(gpio, GPIO.OUT, initial=GPIO.HIGH) else: GPIO.setup(gpio, GPIO.OUT, initial=GPIO.LOW) return {"gpio": gpio, "on": value.on} |
Wie du siehst, ist der erste Parameter wieder die GPIO Nummer,
Du kannst entweder Postman oder eine Browser-Erweiterung verwenden, oder aber cURL (sudo apt-get install curl
). Ich nutze letzteres:
curl -X PATCH http://127.0.0.1:8000/set/21 -H "Content-Type: application/json" -d '{"on": true}'
Und damit geht die LED an!
Übrigens findest du unter http://127.0.0.1:8000/docs
eine API-Dokumentation, welche automatisch mit Swagger/OpenAPI erstellt wird. Um die Antworten etwas lesbarer zu machen, definieren wir Modelle (response_model
). Das ist einfach eine Klasse mit Attributen, wobei wir in der Definition des Endpunkts sagen, dass wir dieses Model zurückgeben werden.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
from fastapi import FastAPI from pydantic import BaseModel import RPi.GPIO as GPIO GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) app = FastAPI() class GpioStatusResponse(BaseModel): gpio: int on: bool class SetGPIO(BaseModel): on: bool @app.get("/read/{gpio}", response_model=GpioStatusResponse) def read_root(gpio: int): GPIO.setup(gpio, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) return GpioStatusResponse(gpio=gpio, on=GPIO.input(gpio)) @app.patch("/set/{gpio}", response_model=GpioStatusResponse) def read_item(gpio: int, value: SetGPIO): if value.on: GPIO.setup(gpio, GPIO.OUT, initial=GPIO.HIGH) else: GPIO.setup(gpio, GPIO.OUT, initial=GPIO.LOW) return GpioStatusResponse(gpio=gpio, on=value.on) |
Als abschließende Übung: Baue einen Endpunkt (POST), der PWM auf einem GPIO aktiviert, sodass wir eine LED dimmen können (hier geht es zur PWM Doku).
Außerdem hast du nun genügend Mittel in der Hand, um bspw. ein User-Interface zu bauen, welches mit den GPIOs interagiert.
Sicherheit – Basic Auth und andere Methoden
Falls wir den Port 8000 des Raspberry Pi’s freigeben würden, so könnte jeder auf die API zugreifen. Das wollen wir verhindern und eine Authentifizierung einbauen. Dafür haben wir verschiedene Möglichkeiten:
- Ein User/Passwort wird bei jedem Aufruf der API als Header mitgesendet. Dafür nutzen wir Basic Auth und verifizieren beim Aufrufen, ob die Daten korrekt sind.
- Alternativ können wir oauth2 mit JWT (JSON Web Tokens) nutzen, welche ebenfalls per Header mitgesendet werden. Der Einfachheit halber verzichten wir darauf in diesem Tutorial. Falls du es dennoch (als Übung) umsetzen willst, findest du hier mehr dazu.
Wir öffnen nun das Skript erneut und passen es entsprechend an:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
from fastapi.security import HTTPBasic, HTTPBasicCredentials from pydantic import BaseModel from fastapi import Depends, FastAPI, HTTPException, status import RPi.GPIO as GPIO import secrets GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) app = FastAPI() security = HTTPBasic() class GpioStatusResponse(BaseModel): gpio: int on: bool class SetGPIO(BaseModel): on: bool def get_current_username(credentials: HTTPBasicCredentials = Depends(security)): correct_username = secrets.compare_digest(credentials.username, "admin") correct_password = secrets.compare_digest(credentials.password, "passw0rd") if not (correct_username and correct_password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect email or password", headers={"WWW-Authenticate": "Basic"}, ) return credentials.username @app.get("/read/{gpio}", response_model=GpioStatusResponse) def read_root(gpio: int, username: str = Depends(get_current_username)): GPIO.setup(gpio, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) return GpioStatusResponse(gpio=gpio, on=GPIO.input(gpio)) @app.patch("/set/{gpio}", response_model=GpioStatusResponse) def read_item(gpio: int, value: SetGPIO, username: str = Depends(get_current_username)): if value.on: GPIO.setup(gpio, GPIO.OUT, initial=GPIO.HIGH) else: GPIO.setup(gpio, GPIO.OUT, initial=GPIO.LOW) return GpioStatusResponse(gpio=gpio, on=value.on) |
Damit die Abfrage nun durchgeht, müssen wir Username und Passwort (Zeile 24/25) mit angeben. Dies geht mit curl oder Postman sehr einfach:
curl -X PATCH http://127.0.0.1:8000/set/21 -H "Content-Type: application/json" -d '{"on": false}' -u "admin:passw0rd"
Zu guter Letzt: Falls du die API über das Internet zugänglich machen willst (sprich deinen Port nach außen hin öffnen), empfiehlt sich die Nutzung eines SSL Zertifikats (Let’s Encrypt), sodass die Verbindung verschlüsselt ist. Mehr dazu findest du auf dieser Seite.
Fazit
Mit FastAPI können wir sehr einfach und schnell eine REST Schnittstelle erzeugen und auf Raspberry Pi spezifische Funktionen aufrufen. Damit können wir unter anderem die GPIOs steuern, Sensoren auslesen und vieles mehr. Falls der Raspberry Pi von einem externen System aufgerufen werden muss, so ist das eine einfache und saubere Lösung. Du solltest jedoch auf entsprechende Authentifizierung achten, sodass nicht jeder den Raspberry Pi quasi fernsteuern kann.
Als Alternative zur Python REST API gibt es einerseits Lösungen wie MQTT: Sofern nur Daten übertragen/empfangen werden sollen und es keine öffentliche API geben soll, ist das eine gute Alternative. Für den Fall, dass wir dennoch eine API brauchen, können wir ebenfalls Node.JS nutzen, um GPIOs zu schalten. Alles in allem finde ich die Lösung per Python aber um einiges angenehmer, auch da es hierfür die meisten kompatiblen Erweiterungen gibt.
Ein Kommentar
Hallo
Ich hab Raspberry 4 mit Bookworm. Python 3 lt. Anweisung installiert. Die fastapi über:
sudo apt-get install python3-fastapi. Keine Fehlermeldung.
Beim Ausführen des ersten scripts readgpio (mit geany) erscheint Fehlermeldung:
program exited with code: 0
Was kann man machen?
mfg Steffen