Raspberry Pi Projekte – Pollux Labs https://polluxlabs.net Arduino, ESP32 & ESP8266 | Projekte & Tutorials Mon, 17 Mar 2025 10:12:44 +0000 de hourly 1 https://wordpress.org/?v=6.6.2 https://polluxlabs.net/wp-content/uploads/2020/05/cropped-pollux-labs-p-32x32.png Raspberry Pi Projekte – Pollux Labs https://polluxlabs.net 32 32 Sprachmodelle mit Ollama auf dem Raspberry Pi nutzen https://polluxlabs.net/raspberry-pi-projekte/sprachmodelle-mit-ollama-auf-dem-raspberry-pi-nutzen/ Fri, 07 Mar 2025 11:31:10 +0000 https://polluxlabs.net/?p=19301 Sprachmodelle mit Ollama auf dem Raspberry Pi nutzen Weiterlesen »

]]>
KI-Sprachmodelle wie ChatGPT, Claude, Gemini, Deepseek etc. sind angekommen und unterstützen bei vielen Anwendungen – von der Frage, warum der Himmel eigentlich blau ist, bis zur Unterstützung beim Programmieren. Und auch bei vielen Projekten hier bei Pollux Labs kommt ihre künstliche Intelligenz zum Einsatz.

Einige dieser Modell kannst du nur über die zugehörige Webseite oder über eine API verwenden – zum Beispiel OpenAIs ChatGPT oder auch Claude von Anthropic. Andere kannst du hingegen auch lokal installieren – und damit kostenlos und ohne Preisgabe deiner Daten nutzen. Wie du Sprachmodelle mit Ollama lokal verwendest, liest du auch bei uns. Hier ist es allerdings so, dass du schon einen recht modernen und leistungsfähigen Computer benötigst, um eine zufriedenstellende Antwortgeschwindigkeit zu erreichen.

Aber immer wieder kommt die Frage auf, ob man ein Sprachmodell auch auf einem Raspberry Pi verwenden könnte. Prinzipiell ist das kein Problem, der Knackpunkt ist jedoch die Hardware: Ein Raspi ist für die Modelle, die du vielleicht schon im Browser genutzt hast, einfach nicht leistungsstark genug. Es gibt jedoch auch Modellversionen, die so stark reduziert wurden, dass du sie auch auf einem relativ schwachen Computer einsetzen kannst – also auch auf dem Raspberry Pi.

Wie das funktioniert und mit welchen Einschränkungen du rechnen musst, erfährst du in diesem Artikel.

Das passende Raspberry Pi Modell

An einem Raspberry Pi 5 mit mindestens 8GB RAM führt leider kein Weg vorbei. Die 4er-Reihe ist leider nicht leistungsstark genug. Installiere am besten ein frisches Betriebssystem: Raspberry Pi OS (64-bit). Falls du den Raspberry Pi Imager verwendest, aktiviere auch gleich SSH, um von einem anderen Computer auf deinen Raspberry Pi zugreifen zu können. Du kannst ihn aber natürlich auch an einen Monitor anschließen und das Terminal des Raspi verwenden. Wie du SSH verwendest, erfährst du in diesem Projekt.

Update: Weiter unten liest du, wie gut Sprachmodelle auf dem Raspberry Pi mit 16GB RAM funktionieren.

Sobald du auf den Raspberry Pi zugreifen kannst, bzw. darauf ein Terminal offen hast, aktualisiere zunächst das Betriebssystem:

sudo apt update
sudo apt upgrade

Anschließend benötigst du cURL (ein Werkzeug, das häufig verwendet wird, um HTTP-Anfragen zu senden und Daten von Webservern abzurufen oder an diese zu senden). Oft ist es bereits installiert, aber um sicherzugehen, verwende diesen Befehl im Terminal:

sudo apt install curl

Ollama auf dem Raspberry Pi installieren

Nun kann es direkt mit der Installation von Ollama weitergehen. Dieses Programm benötigst du, um Sprachmodelle herunterzuladen und dann auf deinem Raspberry Pi auszuführen.

___STEADY_PAYWALL___

Zum Einsatz kommt hierfür ein sogenannter „One-liner“, mit dem du da Installationsscript herunterlädst und direkt ausführst. Wenn du möchtest (in der Regel ist das empfehlenswert), wirf einen Blick in den Code des Scripts.

curl -fsSL https://ollama.com/install.sh | sh

Sobald die Installation abgeschlossen ist, kannst du die Ollama-Version mit diesem Befehl einsehen:

ollama --version

Aktuell (März 2025) ist das die Version 0.5.13

Ollama Version im Terminal

Sprachmodelle laden und starten

Zeit für dein erstes Sprachmodell auf dem Raspberry Pi.

Deepseek-r1

Zunächst ein Test mit Deepseek-r1 – dieses Modell ist bei Ollama in einer ganzen Reihe von Versionen verfügbar: bis zu einer Größe von 671 Milliarden Parametern, das satte 404 GB Platz (und sehr teure Hardware) benötigt. Für den Raspberry Pi soll es zunächst das kleinste Modell mit 1.5 Milliarden Parametern sein.

Installiere dieses Sprachmodell und starte es direkt mit diesem Befehl:

ollama run deepseek-r1:1.5b

Die Installation des 1,1 GB großen Modells dauert etwas, aber dann siehst du im Terminal die Eingabemöglichkeit für deine Frage:

Ich habe als erstes die Frage „Warum ist der Himmel blau?“ gestellt. Eine Besonderheit von Deepseek-r1 ist, dass das Modell zunächst „denkt“, bevor es eine Antwort ausspuckt. Diesem Denkprozess kannst du folgen, denn er erscheint Zeichen für Zeichen in deinem Terminal, zwischen den beiden Tags <think> </think>.

Bei meinem Test, hat Deepseek allerdings aufs Denken verzichtet und die folgende, leider völlig abstruse Antwort geliefert:

Ich verstehe, dass du den Begriff „Himmel“ und die Farbcode „blau“ vermutlich verwechseln kannst. In deutscher Sprachkultur wird der **Himmel** eine vierteltnälle (eine von der Erde manch Zeitendatum backstependen seebaren Stile) genannt, die als ein abgeschlossenes, wissenshares Gravität generally vermutlich ist. 

Die Geschwindigkeit hierbei war allerdings in Ordnung – kein Vergleich zum Browser, aber schnell genug. Für dieses Sprachmodell reicht also tatsächlich ein Raspberry Pi aus.

Ein zweiter Versuch, diesmal auf Englisch. Auch hier verzichtet Deepseek aufs Denken, antwortet aber richtig (zumindest soweit ich das beurteilen kann):

Antwort von Deepseek auf die Frage "Why is the Sky blue?"

Die Unterstützung für die deutsche Sprache ist offensichtlich wenn, nur unzureichend in diesem kleinen Modell vorhanden.

Übrigens, Deepseek versteht hier im Terminal auch Folgefragen – nicht wie bei der Nutzung über die API. Du kannst also auch eine Art Konversation mit dem Modell führen.

Wenn du das Sprachmodell verlassen möchtest, tippe einfach /bye ein. Anschließend landest du wieder bei der Eingabeaufforderung deines Raspis. Wenn du das Modell wieder starten möchtest, verwende erneut den obigen Befehl. Die Installation entfällt dann natürlich.

TinyLlama

Das Sprachmodell TinyLlama ist ein für limitierte Hardware angepasstes Modell mit 1,1 Milliarden Parametern, das auf Metas Llama-Modell mit 3 Milliarden Parametern basiert. Um es zu installieren und zu testen, verwende den folgenden Befehl:

ollama run tinyllama

Auch hier ist eine Konversation auf Englisch brauchbar. Die Geschwindigkeit ist jedoch mit Deepseek vergleichbar, genauso wie die Qualität:

Antwort von TinyLlama auf die Frage "Why is the Sky blue?"

Phi-3

Noch ein dritter Test. Phi-3 ist ein Sprachmodell von Microsoft mit 3,3 Milliarden Parametern – also doppelt bis dreimal so groß wie Deepseek-r1 und TinyLlama. So installierst du es:

ollama run phi3

Dieses Modell kannst du nun auch auf Deutsch befragen. Warum der Himmel blau ist, beantwortet es folgendermaßen:

Antwort von Phi-3 auf die Frage "Warum ist der Himmel blau?"

Abstriche musst du hierfür allerdings bei der Geschwindigkeit machen, dein Raspberry Pi hat mit diesem Modell ganz schön zu arbeiten. Dafür sind auch Folgefragen wie „Und warum ist er abends rot?“ möglich. Phi-3 weiß dann immer noch, dass du dich auf die Farbe des Himmels beziehst.

Gemma 3 1B

Das kleinste Modell benötigt gerade einmal 815MB auf deinem Raspberry Pi. Da es damit auch für das 8GB-Modell geeignet sein sollte, erwarte ich auf dem Raspi mit doppelt so viel Arbeitsspeicher eine entsprechende Leistungsteigerung. Installiere das Modell:

ollama run gemma3:1b 

Die obligatorische Frage, warum der Himmel blau ist, kommt tatsächlich flott. Und sie ist auch recht kompetent, hier der Anfang:

Der Himmel ist blau aus einem faszinierenden physikalischen Phänomen namens **Rayleigh-Streuung**. Hier ist eine einfache Erklärung:

1. Sonnenlicht und seine Farben: Sonnenlicht erscheint uns weiß, aber es besteht tatsächlich aus allen Farben des Regenbogens (Rot, Orange, Gelb, Grün, Blau, Indigo, Violett).

2. Die Atmosphäre: Die Erde ist von einer dichten Atmosphäre umgeben, die aus Gasmolekülen wie Stickstoff und Sauerstoff besteht.

3. Die Rayleigh-Streuung: Wenn Sonnenlicht in die Atmosphäre eintritt, trifft es auf diese Gasmoleküle.  Die Farben des Lichts werden in unterschiedlichen Winkeln gestreut. Die Rayleigh-Streuung ist besonders effektiv bei kürzeren Wellenlängen des Lichts, also den blauen und violetten Farben.

Und hier ist noch längst nicht Schluss. Die Ausführungen gehen noch weiter und beinhalten auch Überlegungen zu Sonnenauf- und -untergang sowie den Wolken. Auch am Deutsch lässt sich nicht mäkeln, die Antwort ist einwandfrei formuliert.

Gemma 3 4B

Weiter zum nächstgrößeren Modell mit 4 Billionen Parametern. Dieses benötigt bereits 3,3GB Platz. Lade und starte es mit:

ollama run gemma3:4b

Hier benötigst du schon etwas mehr Geduld, für die Antwort benötigt Gemma 3 dreimal so lang wie beim kleineren Modell. Sehr viel kompetenter ist sie dabei nicht (aber gut, die Antwort des kleineren Modells war ja schon in Ordnung). Hier der Anfang der Antwort:

Der Himmel ist blau aufgrund eines Phänomens namens **Rayleigh-Streuung**. Hier ist eine einfache Erklärung:

1. **Sonnenlicht ist weiß:** Sonnenlicht, das die Erde erreicht, ist eigentlich eine Mischung aus allen Farben des Regenbogens.

2. **Lichtwellen und die Atmosphäre:** Die Erdatmosphäre besteht aus winzigen Teilchen wie Stickstoff- und Sauerstoffmolekülen. Wenn Sonnenlicht auf diese Moleküle trifft, wird es in verschiedene Richtungen gestreut – ähnlich wie ein Glasglas Licht bricht.

Gemma 3 gibt es auch noch mit 12 und 27 Billionen Paramtern – allerdings erübrigt sich hier ein weiterer Test, denn die Ausführung dürfte entweder noch um einiges langsamer sein, oder sogar unmöglich.

Fazit

Es ist also durchaus möglich, Ollama und damit verschiedene KI-Sprachmodelle auf dem Raspberry Pi zu verwenden. Abstriche musst du allerdings hinsichtlich der Eingabesprache (Deutsch), der Geschwindigkeit und sicherlich auch bei der Qualität der Antworten machen.

Nichtsdestotrotz ist das ein spannender Ansatz, den du in einem Projekt einsetzen kannst!

]]>
Update fürs ESP32 Internetradio: Songs in Spotify speichern https://polluxlabs.net/raspberry-pi-projekte/update-fuers-esp32-internetradio-songs-in-spotify-speichern/ Tue, 25 Feb 2025 11:07:00 +0000 https://polluxlabs.net/?p=19195 Update fürs ESP32 Internetradio: Songs in Spotify speichern Weiterlesen »

]]>
Hast du schon einmal einen Song im Radio gehört, den du dir merken wolltest, damit du ihn später auf Spotify hören kannst? Vielleicht hast du dich auf dein Gedächtnis verlassen oder Stift und Papier verwendet. Aber es geht auch eleganter:

In diesem Projekt baust du dir eine Erweiterung für das ESP32 Internetradio, mit der du per Knopfdruck den Song, der gerade im Radio läuft, in deinen Lieblingssongs auf Spotify speicherst.

Für dieses Update benötigst du:

Da du deinen Raspberry Pi per SSH (mehr dazu später) programmieren wirst, benötigst du noch einen weiteren Computer, auf dem du eine Konsole bzw. Terminal verwenden kannst. Die Software für das Internetradio schreibst du in Python – hier bietet sich ein Editor wie z.B. Visual Studio Code an. Es reicht aber auch ein einfacher Texteditor. Die SD-Karte mit dem Betriebssystem für den Raspberry Pi erstellst du am besten mit dem kostenlosen Tool Raspberry Pi Imager.

Den Raspberry Pi vorbereiten

Bevor du dein erstes Bauteil anschließt, musst du deinen Raspberry Pi vorbereiten, indem du das Betriebssystem und die benötigten Python-Pakete und -Bibliotheken installierst. Lass uns das Schritt für Schritt durchgehen:

Das Betriebssystem installieren

Um das Betriebssystem zu konfigurieren und auf eine SD-Karte zu schreiben, gehe wie folgt vor:

  • Lade den Raspberry Pi Imager von der offiziellen Website herunter
  • Starte den Imager und wähle dein Modell (in diesem Projekt ist das ein Raspberry Pi Zero 2) sowie „Raspberry Pi OS Lite (64-bit)“ aus. Das findest du unter Raspberry Pi OS (other) und kommt ohne grafische Oberfläche, da wir diese nicht brauchen.
  • Wähle deine SD-Karte als Ziel
Raspberry Pi Imager Einstellungen

  • Klicke im nächsten Screen auf Einstellungen bearbeiten und
    • Setze einen Benutzernamen und Passwort
    • Konfiguriere dein WLAN (SSID und Passwort)
    • Aktiviere SSH im Reiter Dienste (Passwort zur Authentifizierung verwenden)
  • Übernimm deine Einstellungen mit einem Klick auf Ja und schreibe das Image auf die SD-Karte

Verbinde dich per SSH mit dem Raspberry Pi

Sobald der Raspberry Imager fertig ist, stecke die SD-Karte in den entsprechenden Slot des Raspi und starte ihn. Jetzt benötigst du etwas Geduld – der erste Start mit dem neuen Betriebssystem kann einige Minuten dauern. Öffne auf deinem Computer das Terminal bzw. die Konsole und verbinde dich mit dem folgenden Befehl – wobei du „pi“ durch den Benutzernamen ersetzen musst, den du zuvor im Raspberry Pi Imager vergeben hast.

sudo ssh pi@raspberrypi.local

Sobald dein Raspberry Pi bereit ist, wirst du zweimal aufgefordert, das Passwort einzugeben, das du im Pi Imager festgelegt hast.

Installiere die benötigten Pakete

Nun kannst du die Pakete und Bibliotheken installieren, die du für das Internetradio benötigst. Doch zunächst kümmerst du dich um das Update des Betriebssystems:

sudo apt update
sudo apt upgrade

Anschließend benötigst du Pip, mit dem du gleich die Bibliothek installierst, mit der du Spotify verwendest.

sudo apt install python3-pip

Eine virtuelle Umgebung einrichten

Mit der Einführung des Betriebssystems Bookworm wurde es erforderlich, Python-Bibliotheken in einer virtuellen Umgebung zu installieren. Durch deren Installation in einem geschützten Bereich soll verhindert werden, dass die systemweite Python-Installation verändert wird. Um eine virtuelle Umgebung einzurichten, verwende diese Befehle:

sudo apt install python3-venv
python3 -m venv RadioSpotify --system-site-packages

Leider musst du die virtuelle Umgebung jedes Mal neu aktivieren, sobald du deinen Raspberry Pi neu gestartet hast. Später im Projekt wirst du das automatisieren, jetzt musst du es allerdings erst einmal noch manuell tun. Das geht mit diesem Befehl:

source RadioSpotify/bin/activate

Übrigens, deaktivieren kannst du sie einfach mit dem Befehl deactivate. Jetzt, wo deine virtuelle Umgebung also läuft, kannst du mit der Installation der folgenden Python-Bibliothek fortfahren:

pip3 install spotipy

Spotify vorbereiten

Damit dein ESP32 Internetradio bzw. dein Raspberry Pi einen Song bei Spotify suchen und deiner Playlist hinzufügen kann, benötigst du dort eine selbsterstellte App. Diese ist schnell eingerichtet:

Eine neue App im Spotify Entwickler-Konto erstellen

In den Feldern App name und App description kannst einen Namen und eine kurze Beschreibung deiner Wahl eintragen. In das Feld Redirect URIs musst du allerdings die Adresse http://localhost:8080 eintragen. Klicke anschließend auf Save.

Klicke nun auf dem nächsten Screen auf Settings. Dort findest du deine Client ID und Client Secret. Beide Schlüssel benötigst du gleich im Python-Script für den Raspberry Pi.

Client ID und Client Secret im Spotify Entwicklerkonto

Und das war es auch schon an dieser Stelle. Später wirst du deinen Raspberry Pi bei Spotify authentifizieren, um die Verbindung zwischen den beiden abschließend einzurichten.

Das Python-Script für das Radio

Der Code für das Raspberry Pi Internetradio ist der folgende. Kopiere ihn und erstelle ein neues Script namens radiospotify.py

___STEADY_PAYWALL___

import sys
import os
import fcntl
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
import time
import json
import spotipy
from spotipy.oauth2 import SpotifyOAuth
import logging

from http.server import HTTPServer, BaseHTTPRequestHandler
import webbrowser
# Set up logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
# Spotify API credentials
SPOTIPY_CLIENT_ID = 'deine_client_id'
SPOTIPY_CLIENT_SECRET = 'dein_client_secret'
SPOTIPY_REDIRECT_URI = 'http://localhost:8080'
SCOPE = 'user-library-modify'
# Define cache path in user's home directory
CACHE_PATH = os.path.expanduser('~/.spotify_token_cache')
# Global variable to store the authentication code
auth_code = None
class AuthHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        global auth_code
        query_components = parse_qs(urlparse(self.path).query)
        if "code" in query_components:
            auth_code = query_components["code"][0]
            self.send_response(200)
            self.send_header('Content-type', 'text/html')
            self.end_headers()
            self.wfile.write(b"Authentication successful! You can close this window.")
            logging.info("Received authentication code")
        else:
            self.send_response(400)
            self.send_header('Content-type', 'text/html')
            self.end_headers()
            self.wfile.write(b"Authentication failed! No code received.")
def wait_for_auth_code(port=8080):
    server = HTTPServer(('', port), AuthHandler)
    server.handle_request()  # Handle one request then close
    return auth_code
def initialize_spotify():
    global auth_code
    auth_manager = SpotifyOAuth(
        client_id=SPOTIPY_CLIENT_ID,
        client_secret=SPOTIPY_CLIENT_SECRET,
        redirect_uri=SPOTIPY_REDIRECT_URI,
        scope=SCOPE,
        cache_path=CACHE_PATH,
        open_browser=False
    )
    # Try to get cached token
    token_info = auth_manager.get_cached_token()
    if not token_info or auth_manager.is_token_expired(token_info):
        auth_url = auth_manager.get_authorize_url()
        print(f"\nPlease visit this URL to authorize the application:\n{auth_url}\n")
        # Start local server to receive the auth code
        received_code = wait_for_auth_code()
        if received_code:
            # Get and cache the token
            token_info = auth_manager.get_access_token(received_code)
            logging.info("New authentication token obtained and cached")
        else:
            logging.error("No authentication code received")
            return None
    return spotipy.Spotify(auth_manager=auth_manager)
class SpotifyServerHandler(BaseHTTPRequestHandler):
    def log_message(self, format, *args):
        logging.info(f"{self.client_address[0]}:{self.client_address[1]} - {format%args}")
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-type', 'application/json')
        self.end_headers()
        parsed_path = urlparse(self.path)
        params = parse_qs(parsed_path.query)
        logging.info(f"Received GET request with params: {params}")
        response = {"message": "Received GET request", "params": params}
        if 'song' in params:
            song_title = params['song'][0]
            spotify_response = self.save_to_spotify(song_title)
            response.update(spotify_response)
        self.wfile.write(json.dumps(response).encode())
    def do_POST(self):
        content_length = int(self.headers['Content-Length'])
        post_data = self.rfile.read(content_length).decode('utf-8')
        self.send_response(200)
        self.send_header('Content-type', 'application/json')
        self.end_headers()
        logging.info(f"Received POST data: {post_data}")
        params = parse_qs(post_data)
        response = {"message": "Received POST request", "data": params}
        if 'song' in params:
            song_title = params['song'][0]
            spotify_response = self.save_to_spotify(song_title)
            response.update(spotify_response)
        self.wfile.write(json.dumps(response).encode())
    def save_to_spotify(self, song_title):
        global sp
        if sp is None:
            logging.error("Spotify client is not initialized")
            return {
                "spotify_status": "error",
                "message": "Spotify client is not initialized"
            }
        logging.info(f"Attempting to save song: {song_title}")
        try:
            # Search for the track
            results = sp.search(q=song_title, type='track', limit=1)
            if results['tracks']['items']:
                track = results['tracks']['items'][0]
                # Save the track to the user's library
                sp.current_user_saved_tracks_add(tracks=[track['id']])
                logging.info(f"Successfully saved track: {track['name']} by {track['artists'][0]['name']}")
                return {
                    "spotify_status": "success",
                    "saved_track": f"{track['name']} by {track['artists'][0]['name']}"
                }
            else:
                logging.warning(f"Track not found on Spotify: {song_title}")
                return {
                    "spotify_status": "not_found",
                    "message": f"Track not found on Spotify: {song_title}"
                }
        except Exception as e:
            logging.error(f"An error occurred while saving to Spotify: {e}")
            return {
                "spotify_status": "error",
                "message": f"An error occurred: {str(e)}"
            }
def run_server(port=8080):
    server_address = ('', port)
    try:
        httpd = HTTPServer(server_address, SpotifyServerHandler)
        logging.info(f"Server running on port {port}")
        httpd.serve_forever()
    except OSError as e:
        if e.errno == 98:
            logging.error(f"Error: Port {port} is already in use. Try a different port.")
        else:
            logging.error(f"Error: {e}")
        sys.exit(1)
if __name__ == '__main__':
    lock_file = '/tmp/spotify_server.lock'
    try:
        lock_handle = open(lock_file, 'w')
        fcntl.lockf(lock_handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
    except IOError:
        logging.error("Another instance of this script is already running.")
        sys.exit(1)
    try:
        # Initialize Spotify client
        sp = initialize_spotify()
        if not sp:
            logging.error("Failed to initialize Spotify client")
            sys.exit(1)
        port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080
        run_server(port)
    except KeyboardInterrupt:
        logging.info("\nServer stopped.")
    finally:
        fcntl.lockf(lock_handle, fcntl.LOCK_UN)
        lock_handle.close()
        os.unlink(lock_file)

Darin musst du oben die folgenden zwei Schlüssel hinterlegen – diese findest du in deinem Spotify Developer Account.

SPOTIPY_CLIENT_ID = 'deine_client_id'
SPOTIPY_CLIENT_SECRET = 'dein_client_secret'

Und das war es schon mit den Anpassungen im Python Script. Falls du im Terminal noch auf deinem Rasperry Pi eingeloggt bist, logge dich mit logout aus und bewege dich in den Ordner, in dem dein gerade erstelltes Python Script liegt. Falls du unsicher bist, wie das geht, wirf einen Blick in die Tipps der Uni Düsseldorf.

Im Verzeichnis angekommen führe folgenden Befehl aus:

scp radiospotify.py pi@raspberrypi.local:/home/pi/RadioSpotify/

Den Code ausführen

Jetzt, wo der Code auf deinem Raspberry Pi ist, kannst du ihn aufrufen. Logge dich dafür zunächst wieder per SSH ein (ersetze pi wieder durch deinen Benutzernamen):

sudo ssh pi@raspberrypi.local

Anschließend kannst du das Script wie folgt starten:

source RadioSpotify/bin/activate
python3 RadioSpotify/radiospotify.py

Als nächstes musst deinen Rasperry Pi bei Spotify Zugriff auf dein dortiges Konto geben. Das ist etwas umständlich – aber du musst es zum Glück nur einmal machen. Der entsprechende Token wird auf deinem Raspberry Pi gespeichert, sodass du dich beim nächsten Start des Scripts nicht noch einmal authentifizieren musst.

Im Terminal wirst du nach dem Start des Scripts aufgefordert, eine Adresse im Browser zu öffnen – das kannst du in einem Browser deiner Wahl auf deinem Computer machen. Du wirst dort von Spotify gefragt, ob es die Verbindung zu deiner App herstellen darf – stimme dem zu. Daraufhin solltest du zwar im Browser etwas in der Art von „Webseite nicht erreichbar“ sehen – in der Ardresszeile jedoch eine andere URL stehen haben.

Kopiere die gesamte neue URL aus der Adresszeile, öffne ein neues Terminal-Fenster auf deinem Computer, logge dich auch darin per SSH auf deinem Raspi ein und verwende den folgenden Befehl:

curl "DEINE KOPIERTE URL"

Nun sollte in diesem Terminal-Fenster die Info Authentication successful! und in deinem ersten Fenster die Zeile INFO – Server running on port 8080 erscheinen. Das bedeutet, dass der Login bei Spotify funktioniert hat und dein Raspberry Pi nun bereit ist, Songs vom ESP32 Internetradio zu empfangen und in deiner Spotify-Playlist zu speichern. Weiter geht es mit deinem ESP32.

Ein weiteres Kabel am ESP32

Damit du Songs an Spotify senden kannst, benötigst du einen Button, mit dem diese Funktion auslöst. Praktischerweise bringt dein Rotary Encode schon einen mit – du kannst diesen nicht nur drehen, sondern auch hörbar eindrücken. Dieser Druck wird am Pin SW ausgelesen. Verbinde deshalb diesen Pin mit dem GPIO 7 deines ESP32 – wenn du einen ESP32-S3 Zero verwendest, sieht die neue Verbindung so aus (das weiße Kabel ist die Verbindung vom Button zum ESP32):

ESP32 Internetradio mit Spotify-Anbindung

Du kannst natürlich auch einen anderen Pin des ESP32 verwenden, musst das dann allerdings entsprechend im folgenden Sketch ändern.

Aktualisiere den Sketch auf dem ESP32

In deinem ESP32 Internetradio fehlt nun nur noch die Funktion, den aktuellen Song zum Raspberry Pi zu senden und ihn von diesem in deinen Spotify-Lieblingssongs zu speichern. Hierfür ist ein Umbau nötig – hier der aktualisierte Sketch:

#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <Audio.h>
#include <AiEsp32RotaryEncoder.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ArduinoJson.h>

// Pin definitions bleiben unverändert
#define I2S_DOUT 2
#define I2S_BCLK 3
#define I2S_LRC 4
#define VOLUME_PIN 5

#define ROTARY_ENCODER_A_PIN 12
#define ROTARY_ENCODER_B_PIN 13
#define ROTARY_ENCODER_BUTTON_PIN 7
#define ROTARY_ENCODER_STEPS 4

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32
#define OLED_RESET     -1
#define SCREEN_ADDRESS 0x3C

#define I2C_SDA 8
#define I2C_SCL 9

// Debug Level für ESP32
#define LOG_LOCAL_LEVEL ESP_LOG_VERBOSE

// Watchdog Timeout
const int wdtTimeout = 5000;  // 5 Sekunden Watchdog Timeout

// Initialisierungs-Flags
bool isWiFiConnected = false;
bool isDisplayInitialized = false;
bool isAudioInitialized = false;

AiEsp32RotaryEncoder rotaryEncoder(ROTARY_ENCODER_A_PIN, ROTARY_ENCODER_B_PIN, ROTARY_ENCODER_BUTTON_PIN, -1, ROTARY_ENCODER_STEPS);
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

Audio audio;

// WiFi credentials
const char ssid[] = "DEIN WLAN-NETZWERK";
const char password[] = "DEIN WLAN-PASSWORT";

// Spotify server details
const char* serverName = "IP-ADRESSE:8080";

// Radio stations bleiben unverändert
const char* stations[] = {
    "http://www.byte.fm/stream/bytefm.m3u",
    "https://st01.sslstream.dlf.de/dlf/01/128/mp3/stream.mp3",
    "https://frontend.streamonkey.net/fho-schwarzwaldradiolive/mp3-stream.m3u",
    "https://kexp-mp3-128.streamguys1.com/kexp128.mp3",
    "https://eagle.streemlion.com:2199/tunein/psychedelicj.asx"
};
const char* stationNames[] = {
    "Byte.fm",
    "Deutschlandfunk",
    "Schwarzwaldradio",
    "KEXP",
    "Psychedelic Jukebox"
};
const int NUM_STATIONS = sizeof(stations) / sizeof(stations[0]);
int currentStation = 0;

// Statische Buffer statt dynamischer Strings
char streamTitle[64] = "";
char urlBuffer[256] = "";
char jsonBuffer[512] = "";

// Volume control variables bleiben unverändert
const int SAMPLES = 5;
int volumeReadings[SAMPLES];
int readIndex = 0;
int total = 0;
int average = 0;
unsigned long lastVolumeCheck = 0;
const unsigned long VOLUME_CHECK_INTERVAL = 500;

void IRAM_ATTR readEncoderISR() {
    rotaryEncoder.readEncoder_ISR();
}

// Optimierte String-Ersetzungsfunktion mit statischem Buffer
void replaceSpecialChars(const char* input, char* output, size_t outputSize) {
    size_t i = 0, j = 0;
    while (input[i] && j < outputSize - 1) {
        char c = input[i++];
        switch (c) {
            case 'ä': memcpy(&output[j], "a", 1); j += 1; break;
            case 'ö': memcpy(&output[j], "o", 1); j += 1; break;
            case 'ü': memcpy(&output[j], "u", 1); j += 1; break;
            case 'Ä': memcpy(&output[j], "A", 1); j += 1; break;
            case 'Ö': memcpy(&output[j], "O", 1); j += 1; break;
            case 'Ü': memcpy(&output[j], "U", 1); j += 1; break;
            case 'ß': memcpy(&output[j], "ss", 2); j += 2; break;
            default: output[j++] = c;
        }
    }
    output[j] = '\0';
}

void setup() {
    Serial.begin(115200);
    
    // Debug Level setzen
    esp_log_level_set("*", ESP_LOG_VERBOSE);
    
    Serial.println(F("ESP32-S3 Internet Radio starting..."));
    Serial.printf("Initial free heap: %d bytes\n", ESP.getFreeHeap());
    
    // Basis-Setup
    pinMode(VOLUME_PIN, INPUT);
    
    // Encoder Setup
    rotaryEncoder.begin();
    rotaryEncoder.setup(readEncoderISR);
    rotaryEncoder.setBoundaries(0, NUM_STATIONS - 1, true);
    rotaryEncoder.setAcceleration(0);
    
    // Volume readings initialisieren
    for (int i = 0; i < SAMPLES; i++) {
        volumeReadings[i] = 0;
    }
    
    // Wire begin - grundlegende I2C-Initialisierung
    Wire.begin(I2C_SDA, I2C_SCL);
}

void loop() {
    static unsigned long lastInitAttempt = 0;
    const unsigned long initInterval = 5000;
    
    // Heap-Überwachung
    static unsigned long lastHeapCheck = 0;
    if (millis() - lastHeapCheck > 10000) {  // Alle 10 Sekunden
        Serial.printf("Free heap: %d bytes\n", ESP.getFreeHeap());
        lastHeapCheck = millis();
    }
    
    // Verzögerte Initialisierung
    if (!isDisplayInitialized && millis() - lastInitAttempt > initInterval) {
        initializeDisplay();
        lastInitAttempt = millis();
    }
    
    if (!isWiFiConnected && millis() - lastInitAttempt > initInterval) {
        connectToWiFi();
        lastInitAttempt = millis();
    }
    
    if (isWiFiConnected && !isAudioInitialized && millis() - lastInitAttempt > initInterval) {
        initializeAudio();
        lastInitAttempt = millis();
    }
    
    // Normale Loop-Funktionalität nur wenn alles initialisiert ist
    if (isDisplayInitialized && isWiFiConnected && isAudioInitialized) {
        audio.loop();
        yield();
        checkEncoder();
        yield();
        checkVolumeControl();
        yield();
    }
    
    delay(10);  // Kleine Pause für Stabilität
}

void initializeDisplay() {
    Serial.println(F("Initializing OLED display..."));
    if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
        Serial.println(F("SSD1306 initialization failed"));
        return;  // Statt Endlosschleife einfach zurückkehren
    }
    
    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(0,0);
    display.println(F("Initializing..."));
    display.display();
    
    isDisplayInitialized = true;
    Serial.println(F("Display initialized successfully"));
}

void connectToWiFi() {
    Serial.println(F("Connecting to WiFi..."));
    WiFi.begin(ssid, password);
    
    int attempts = 0;
    while (WiFi.status() != WL_CONNECTED && attempts < 20) {
        delay(500);
        Serial.print(".");
        attempts++;
    }
    
    if (WiFi.status() == WL_CONNECTED) {
        Serial.println(F("\nWiFi connected"));
        isWiFiConnected = true;
        if (isDisplayInitialized) {
            display.clearDisplay();
            display.setCursor(0,0);
            display.println(F("WiFi connected"));
            display.display();
        }
    } else {
        Serial.println(F("\nWiFi connection failed"));
    }
}

void initializeAudio() {
    audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
    audio.setVolume(10);
    connectToStation(currentStation);
    isAudioInitialized = true;
    Serial.println(F("Audio initialized"));
}

void checkEncoder() {
    if (rotaryEncoder.encoderChanged()) {
        currentStation = rotaryEncoder.readEncoder();
        connectToStation(currentStation);
    }
    
    if (rotaryEncoder.isEncoderButtonClicked()) {
        Serial.println(F("Encoder button clicked"));
        sendToSpotify();
    }
}

void connectToStation(int stationIndex) {
    audio.stopSong();
    audio.connecttohost(stations[stationIndex]);
    updateDisplay();
}

void checkVolumeControl() {
    unsigned long currentMillis = millis();
    if (currentMillis - lastVolumeCheck >= VOLUME_CHECK_INTERVAL) {
        lastVolumeCheck = currentMillis;
        
        total = total - volumeReadings[readIndex];
        volumeReadings[readIndex] = analogRead(VOLUME_PIN);
        total = total + volumeReadings[readIndex];
        readIndex = (readIndex + 1) % SAMPLES;
        
        average = total / SAMPLES;
        int volume = map(average, 0, 4095, 5, 23);
        
        static int lastVolume = -1;
        if (volume != lastVolume) {
            audio.setVolume(volume);
            lastVolume = volume;
            updateDisplay();
        }
    }
}

void updateDisplay() {
    if (!isDisplayInitialized) return;
    
    display.clearDisplay();
    display.setCursor(0,0);
    
    char buffer[64];
    replaceSpecialChars(stationNames[currentStation], buffer, sizeof(buffer));
    display.println(buffer);
    
    display.println();
    replaceSpecialChars(streamTitle, buffer, sizeof(buffer));
    display.println(buffer);
    
    display.display();
}

// Optimierte URL-Encoding Funktion mit statischem Buffer
void urlEncode(const char* input, char* output, size_t outputSize) {
    size_t j = 0;
    for (size_t i = 0; input[i] && j < outputSize - 4; i++) {
        char c = input[i];
        if (isalnum(c)) {
            output[j++] = c;
        } else if (c == ' ') {
            output[j++] = '+';
        } else {
            if (j + 3 >= outputSize) break;
            sprintf(&output[j], "%%%02X", c);
            j += 3;
        }
        yield();  // Watchdog füttern während langer Operationen
    }
    output[j] = '\0';
}

void sendToSpotify() {
    static unsigned long lastRequestTime = 0;
    unsigned long currentTime = millis();
    
    if (currentTime - lastRequestTime < 5000) {
        Serial.println(F("Request blocked: Too soon since last request"));
        return;
    }
    lastRequestTime = currentTime;
    
    if (WiFi.status() == WL_CONNECTED) {
        HTTPClient http;
        
        // URL-Encoding mit statischem Buffer
        char encodedTitle[128];
        urlEncode(streamTitle, encodedTitle, sizeof(encodedTitle));
        
        // URL zusammenbauen mit snprintf
        snprintf(urlBuffer, sizeof(urlBuffer), "%s/?song=%s", serverName, encodedTitle);
        
        Serial.println(F("--------- New Request ---------"));
        Serial.print(F("Request URL: "));
        Serial.println(urlBuffer);
        
        http.begin(urlBuffer);
        http.setTimeout(5000);  // 5 Sekunden Timeout
        http.setReuse(false);   // Keine Verbindungswiederverwendung
        
        yield();  // Watchdog füttern
        
        int httpResponseCode = http.GET();
        
        if (httpResponseCode > 0) {
            // Statisches JSON-Dokument
            StaticJsonDocument<512> doc;
            
            // Response lesen
            String payload = http.getString();
            yield();  // Watchdog füttern
            
            DeserializationError error = deserializeJson(doc, payload);
            
            if (!error) {
                const char* message = doc["message"];
                Serial.print(F("Server message: "));
                Serial.println(message);
                
                if (doc.containsKey("spotify_status")) {
                    const char* spotifyStatus = doc["spotify_status"];
                    Serial.print(F("Spotify status: "));
                    Serial.println(spotifyStatus);
                }
            }
        } else {
            Serial.print(F("Error on HTTP request: "));
            Serial.println(httpResponseCode);
        }
        
        http.end();
        Serial.println(F("Connection closed"));
    }
    
    yield();  // Watchdog füttern am Ende
}

// Audio callback functions
void audio_info(const char *info) { 
    Serial.print(F("info        ")); Serial.println(info);
}

void audio_id3data(const char *info) {
    Serial.print(F("id3data     ")); Serial.println(info);
}

void audio_eof_mp3(const char *info) {
    Serial.print(F("eof_mp3     ")); Serial.println(info);
}

void audio_showstation(const char *info) {
    Serial.print(F("station     ")); Serial.println(info);
}

void audio_showstreaminfo(const char *info) {
    Serial.print(F("streaminfo  ")); Serial.println(info);
}

void audio_showstreamtitle(const char *info) {
    Serial.print(F("streamtitle: ")); Serial.println(info);
    strncpy(streamTitle, info, sizeof(streamTitle) - 1);
    streamTitle[sizeof(streamTitle) - 1] = '\0';
    updateDisplay();
}

void audio_bitrate(const char *info) {
    Serial.print(F("bitrate     ")); Serial.println(info);
}

void audio_commercial(const char *info) {
    Serial.print(F("commercial  ")); Serial.println(info);
}

void audio_icyurl(const char *info) {
    Serial.print(F("icyurl      ")); Serial.println(info);
}

void audio_lasthost(const char *info) {
    Serial.print(F("lasthost    ")); Serial.println(info);
}

void audio_eof_speech(const char *info) {
    Serial.print(F("eof_speech  ")); Serial.println(info);
}

Im obigen Sketch musst du zunächst deine eigenen WLAN-Zugangsdaten eintragen:

const char ssid[] = "DEIN WLAN-NETZWERK";
const char password[] = "DEIN WLAN-PASSWORT";

Außerdem benötigst du die IP-Adresse deines Raspberry Pi. Diese findest du zum Beispiel mit dem folgenden Befehl heraus, den du im Terminal eingibst (während du per SSH mit ihm verbunden bist):

hostname -I

Daraufhin erscheint die IP-Adresse im Terminal (im rot markierten Teil):

IP-Adresse des Raspberry Pi im Terminal

Diese Adresse trägst du dann im Sketch hier ein, versehen mit dem Port :8080

const char* serverName = "http://192.168.0.45:8080";

Die Wahl der Radiostationen bzw. deren Stream-Adressen und Namen bleibt dir natürlich selbst überlassen – hier hast du sicherlich bestimmt schon eine Liste erstellt und kannst sie in diesen Sketch übernehmen.

Wenn du alles erledigt hast, kannst du diesen Sketch auf deinen ESP32 hochladen und damit wie gewohnt Radio hören.

Einen Song an Spotifiy übertragen

Jetzt wird es Zeit für den Test deiner neuen Funktion! Sobald du einen Song hörst, dessen Interpret und Titel du auf dem Display siehst, drücke den Button an deinem Rotary Encode. Der Song sollte daraufhin kurz abbrechen und dann wieder einsetzen. In der Zwischenzeit gibt dir der Serielle Monitor Auskunft darüber, was gerade passiert ist:

Erfolgreiche Übertragung eines Songs an Spotify

Für die Übertragung wurde die IP-Adresse deines Raspberry Pi um die Song-Informationen erweitert und von deinem ESP32 aufgerufen. Dein Raspberry hat den Song erfolgreich an Spotify weitergeleitet und sendet dem ESP32 ein Spotify status: success

Wirf nun in Spotify einen Blick in deine Playlist Lieblingssongs. Hier sollte der gerade übertragene Song bereits zu finden sein.

Das Script automatisch starten

Noch ein letzter Baustein für dieses Projekt: Es ist natürlich sehr unpraktisch, wenn du dein neues Feature immer erst aktivieren musst, indem du auf dem Raspberry Pi die virtuelle Umgebung aktivierst und das Python-Script manuell startest. Deshalb wirst du nun dafür sorgen, dass das Script automatisch startet, sobald du deinen Raspi hochfährst. Das geht folgendermaßen:

  • Erstelle eine Service-Datei:
sudo nano /etc/systemd/system/spotify-radio.service
  • Kopiere den den folgenden Code, füge ihn ein und speichere es ab mit STRG+O, STRG+X.
[Unit]
Description=Spotify Radio Service
After=network.target

[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/RadioSpotify
ExecStart=/home/pi/RadioSpotify/bin/python /home/pi/RadioSpotify/radiospotify.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
  • Lade die systemd-Konfiguration neu:
sudo systemctl daemon-reload
  • Aktiviere den Service für den Autostart:
sudo systemctl enable spotify-radio
  • Starte nun den Service:
sudo systemctl start spotify-radio

Ob der Service läuft, kannst du abschließend mit folgendem Befehl überprüfen:

sudo systemctl status spotify-radio

Im Terminal sollte nun etwas in dieser Art anzeigen, dass dein Spotify-Service läuft:

Spotify-Service läuft im Terminal

Starte testweise deinen Raspberry Pi neu und führe noch einmal die obige Prüfung aus. In deinem Terminal sollte wieder stehen, dass der Service aktiv ist.

Und das war es! Dein ESP32 Internetradio ist nun direkt mit Spotify verbunden und du hast die Möglichkeit, interessante Songs dort in deinen Lieblingssongs speichern. Viel Spaß damit!

]]>
Geheime Botschaften: Verschlüsselte Nachrichten als Audio übertragen https://polluxlabs.net/python-tutorials-und-projekte/geheime-botschaften-verschluesselte-nachrichten-als-audio-uebertragen/ Sun, 17 Nov 2024 12:18:33 +0000 https://polluxlabs.net/?p=17906 Geheime Botschaften: Verschlüsselte Nachrichten als Audio übertragen Weiterlesen »

]]>
Nachrichten zu versenden, die Ende-zu-Ende-verschlüsselt sind, gehört mittlerweile zum Standard vieler Nachrichten-Apps wie Signal, Threema und WhatsApp. Aber wie wäre es mit etwas spielerischem Retro-Charme? In diesem Projekt wandelst du Textnachrichten in Töne um und versendest sie als Audio-Dateien im Anhang einer E-Mail. Der Empfänger kann diese Audio-Dateien dann mit Hilfe eines zuvor zwischen euch vereinbarten Schlüsselworts entschlüsseln und lesen. Nicht Eingeweihte hören nur eine Abfolge von Tönen, wie in diesem Beispiel:

Zum Einsatz kommen hierbei zwei Python-Scripte – je eines für den Sender und den Empfänger. Als E-Mail-Provider dient Gmail.

Gmail für den Versand der E-Mails einrichten

Zunächst benötigst du einen E-Mail-Provider, den du aus dem Python-Script des Senders ansteuern und für den Versand der E-Mails nutzen kannst. Hier bietet sich Googles Gmail an, da die Einrichtung unkompliziert ist. Wichtig: Falls du bereits eine Mail-Adresse bei Gmail besitzt, richte dir für dieses Projekt trotzdem eine neue ein. So stellst du sicher, dass zum Beispiel ein fehlerhaftes Versenden von vielen E-Mails hintereinander zu einer vorübergehenden Sperrung deines Kontos führt.

Wie du eine E-Mail-Adresse bei Gmail und sie für den Versand aus einem Python-Script einrichtest, erfährst du in diesem Tutorial.

Die benötigten Python-Bibliotheken

Im Folgenden verwendest du die Bibliotheken numpy und scipy. Die numpy-Bibliothek wird später verwendet, um numerische Operationen durchzuführen, die für die Analyse der Frequenzen in den Audiodaten benötigt werden. Die scipy-Bibliothek enthält die Funktion wavfile.read, die verwendet wird, um die erzeugten WAV-Datei einzulesen und die Audiodaten sowie die Abtastrate zu extrahieren. Diese Bibliotheken sind nicht standardmäßig in Python enthalten und müssen daher manuell installiert werden. Um sie zu installieren, verwenden Sie den folgenden Befehl:

pip install numpy scipy

Das Script für den Sender der verschlüsselten Nachrichten

Wenn du nun eine E-Mail-Adresse bei Gmail eingerichtet und die beiden benötigten Bibliotheken installiert hast, kann es direkt weitergehen mit dem Python-Script für den Sender. Hier der vollständige Code:

___STEADY_PAYWALL___

# Verschlüsselte Nachrichten - Script für den Sender
# Pollux Labs, polluxlabs.net

import numpy as np
from scipy.io.wavfile import write
import smtplib
from email.message import EmailMessage
import socket

# Parameter, die vom Benutzer bearbeitet werden müssen
user_parameters = {
    "sample_rate": 44100,  # Abtastrate (Hz)
    "bit_duration": 0.1,   # Dauer eines Bits (in Sekunden)
    "freq_0": 1000,        # Frequenz für "0" (Hz)
    "freq_1": 2000,        # Frequenz für "1" (Hz)
    "encryption_key": "geheimer_schluessel",  # Verschlüsselungsschlüssel (beide Parteien müssen denselben Schlüssel verwenden)
    "sender_email": "Absender-Adresse",  # Absender-E-Mail-Adresse
    "sender_password": "App-Passwort",        # App-Passwort für die Absender-E-Mail
    "receiver_email": "Empfänger-Adresse",      # Empfänger-E-Mail-Adresse
    "email_subject": "Betreff",   # Betreff der E-Mail
    "email_body": "Inhalt der E-Mail, z.B. Hier kommt eine verschlüsselte Nachricht für dich.",  # E-Mail-Inhalt
    "wav_filename": "message.wav"           # Name der zu speichernden WAV-Datei
}

# Funktion zur Erstellung eines Tons für ein Bit
def generate_tone(frequency, duration, sample_rate):
    # Erzeugt eine Zeitachse von 0 bis zur angegebenen Dauer mit der entsprechenden Anzahl an Samples
    t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
    # Berechnet den Sinuswert für die gegebene Frequenz über die Zeitachse
    return np.sin(2 * np.pi * frequency * t)

# Nachricht in Binärdaten umwandeln
def text_to_binary(text):
    # Wandelt jeden Buchstaben der Nachricht in eine 8-Bit Binärdarstellung um
    binary_data = ''.join(format(ord(char), '08b') for char in text)
    return binary_data

# Nachricht mit dem Schlüssel verschlüsseln
def encrypt_message(text, key):
    encrypted_message = ""
    for i in range(len(text)):
        encrypted_char = chr(ord(text[i]) ^ ord(key[i % len(key)]))
        encrypted_message += encrypted_char
    return encrypted_message

# Nachricht in modulierte Audiodaten umwandeln
def encode_to_audio(binary_data, bit_duration, sample_rate, freq_0, freq_1):
    # Initialisiert ein leeres Array für die Audiodaten
    audio = np.array([])
    # Iteriert durch jedes Bit der Binärdaten
    for bit in binary_data:
        # Erzeugt einen Ton für "0" oder "1" und fügt ihn an das Audioarray an
        if bit == '0':
            audio = np.append(audio, generate_tone(freq_0, bit_duration, sample_rate))
        else:
            audio = np.append(audio, generate_tone(freq_1, bit_duration, sample_rate))
    return audio

# Funktion zum Versenden der WAV-Datei per E-Mail
def send_email_with_attachment(receiver_email, subject, body, attachment_path):
    # Absender-E-Mail und Passwort
    sender_email = user_parameters["sender_email"]
    sender_password = user_parameters["sender_password"]

    # Erstellen der E-Mail-Nachricht
    msg = EmailMessage()
    msg['From'] = sender_email
    msg['To'] = receiver_email
    msg['Subject'] = subject
    msg.set_content(body)

    # Anhang hinzufügen (die WAV-Datei)
    with open(attachment_path, 'rb') as attachment:
        msg.add_attachment(attachment.read(), maintype='audio', subtype='wav', filename=attachment_path)

    try:
        # SMTP-Server einrichten und die E-Mail senden
        with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp:
            smtp.login(sender_email, sender_password)  # Anmelden am SMTP-Server
            smtp.send_message(msg)  # E-Mail senden
    except socket.gaierror:
        print("Fehler: Der SMTP-Server konnte nicht erreicht werden. Bitte überprüfen Sie die Serveradresse.")
    except smtplib.SMTPAuthenticationError:
        print("Fehler: Authentifizierung fehlgeschlagen. Bitte überprüfen Sie Ihre E-Mail-Adresse und Ihr Passwort.")
    except Exception as e:
        print(f"Ein unerwarteter Fehler ist aufgetreten: {e}")

# Hauptprogramm
if __name__ == "__main__":
    # Nachricht erstellen
    email_message = "Das Pferd frisst keinen Gurkensalat."
    
    # Nachricht verschlüsseln
    encrypted_message = encrypt_message(email_message, user_parameters["encryption_key"])
    
    # Verschlüsselte Nachricht in Binärdaten umwandeln
    binary_data = text_to_binary(encrypted_message)
    print(f"Binärdaten der Nachricht: {binary_data[:64]}...")  # Zeigt die ersten 64 Bits der Nachricht (für Debugging)
    
    # Binärdaten in Audiosignale umwandeln
    audio_data = encode_to_audio(binary_data, user_parameters["bit_duration"], user_parameters["sample_rate"], user_parameters["freq_0"], user_parameters["freq_1"])
    
    # WAV-Datei speichern
    wav_filename = user_parameters["wav_filename"]
    # Speichert die Audiodaten als 16-Bit Integer in eine WAV-Datei
    write(wav_filename, user_parameters["sample_rate"], (audio_data * 32767).astype(np.int16))
    print(f"WAV-Datei '{wav_filename}' erfolgreich erstellt!")

    # WAV-Datei per E-Mail versenden
    send_email_with_attachment(user_parameters["receiver_email"], user_parameters["email_subject"], user_parameters["email_body"], wav_filename)
    print(f"WAV-Datei '{wav_filename}' erfolgreich per E-Mail versendet!")

So funktioniert das Script

Das Sender-Script verschlüsselt eine Nachricht, wandelt sie in Binärdaten um und erzeugt daraus eine Audiodatei, die dann per E-Mail verschickt wird:

  1. Variablen, die bearbeitet werden müssen: Zu Beginn und auch an einer Stelle weiter unten im Code gibt es Einstellungen, die du vorab unbedingt vornehmen musst. Dazu gehören der geheime Schlüssel (den der Empfänger in seinem Script auch hinterlegen muss), die E-Mail-Adressen, das Passwort aus Gmail und natürlich die zu verschlüsselnde Nachricht selbst.
"encryption_key": "geheimer_schluessel",  # Schlüssel (beide Parteien müssen denselben Schlüssel verwenden)
"sender_email": "Absender-Adresse",  # Absender-E-Mail-Adresse
"sender_password": "App-Passwort",        # App-Passwort für die Absender-E-Mail
"receiver_email": "Empfänger-Adresse",      # Empfänger-E-Mail-Adresse
"email_subject": "Betreff",   # Betreff der E-Mail
"email_body": "Inhalt der E-Mail, z.B. Hier kommt eine verschlüsselte Nachricht für dich.",  # E-Mail-Inhalt
"wav_filename": "message.wav"           # Name der zu speichernden WAV-Datei
 
email_message = "Das Pferd frisst keinen Gurkensalat."  # Die eigentliche Textnachricht, die verschlüsselt und versendet wird
  1. Nachricht mit dem Schlüssel verschlüsseln Die Funktion encrypt_message() verwendet eine einfache XOR-Verschlüsselung, um die Nachricht zu verschlüsseln. Dabei wird jedes Zeichen der Nachricht mit einem Zeichen des Schlüssels kombiniert:
   def encrypt_message(text, key):
       encrypted_message = ""
       for i in range(len(text)):
           encrypted_char = chr(ord(text[i]) ^ ord(key[i % len(key)]))
           encrypted_message += encrypted_char
       return encrypted_message

Diese Methode sorgt dafür, dass sowohl der Sender als auch der Empfänger denselben Schlüssel benötigen, um die Nachricht zu entschlüsseln.

  1. Nachricht in Binärdaten umwandeln Nachdem die Nachricht verschlüsselt wurde, wird sie in Binärdaten umgewandelt, um sie später als Audio darstellen zu können:
   def text_to_binary(text):
       binary_data = ''.join(format(ord(char), '08b') for char in text)
       return binary_data

Jedes Zeichen der Nachricht wird in seine 8-Bit-Binärdarstellung konvertiert, sodass die gesamte Nachricht als eine Folge von Nullen und Einsen dargestellt wird.

  1. Erstellen eines Tons für jedes Bit Die Funktion generate_tone() erstellt einen Sinuston für ein einzelnes Bit (entweder „0“ oder „1“). Diese Töne werden später aneinandergereiht, um die gesamte Nachricht in Audiodaten darzustellen:
   def generate_tone(frequency, duration, sample_rate):
       t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
       return np.sin(2 * np.pi * frequency * t)

Hierbei wird entweder eine Frequenz für „0“ oder eine andere für „1“ verwendet, um die Bits der Nachricht zu unterscheiden.

  1. Nachricht in Audiodaten kodieren Die Funktion encode_to_audio() wandelt die gesamte Binärnachricht in Audiodaten um, indem sie für jedes Bit den entsprechenden Ton erzeugt und diese aneinanderreiht:
   def encode_to_audio(binary_data, bit_duration, sample_rate, freq_0, freq_1):
       audio = np.array([])
       for bit in binary_data:
           if bit == '0':
               audio = np.append(audio, generate_tone(freq_0, bit_duration, sample_rate))
           else:
               audio = np.append(audio, generate_tone(freq_1, bit_duration, sample_rate))
       return audio

Das Ergebnis ist eine Audiodatei, die die verschlüsselte Nachricht repräsentiert.

  1. Erstellen und Versenden der Audiodatei Nachdem die Audiodaten erstellt wurden, wird die Nachricht als .wav-Datei gespeichert und per E-Mail versendet:
   write(wav_filename, sample_rate, (audio_data * 32767).astype(np.int16))

Diese Zeile speichert die Audiodaten als 16-Bit-Integer-Werte in einer WAV-Datei auf deinem Computer, die dann mit der Funktion send_email_with_attachment() per E-Mail versendet wird.

Ergänze das Script nun um deine Daten und hinterlege die Nachricht, die du versenden möchtest. Lass anschließend das Script einmal laufen – im Terminal solltest du die Nachricht lesen können, dass die E-Mail mit der Audio-Datei an deinen Empfänger gesendet wurde:

Und das war es für den Sender – nun zur anderen Seite, dem Empfänger deiner verschlüsselten Nachricht.

Das Script für den Empfänger

Der Empfänger der Nachricht benötigt also ein eigenes Script, das es ihm ermöglicht, die verschlüsselten Audiodaten wieder in Text umzuwandeln. Hier das vollständige Python-Script:

# Verschlüsselte Nachrichten - Script für den Sender
# Pollux Labs, polluxlabs.net

import numpy as np
from scipy.io.wavfile import read

# Parameter, die vom Benutzer bearbeitet werden müssen
user_parameters = {
    "sample_rate": 44100,  # Abtastrate (Hz)
    "bit_duration": 0.1,   # Dauer eines Bits (in Sekunden)
    "freq_0": 1000,        # Frequenz für "0" (Hz)
    "freq_1": 2000,        # Frequenz für "1" (Hz)
    "encryption_key": "geheimer_schluessel",  # Verschlüsselungsschlüssel (beide Parteien müssen denselben Schlüssel verwenden)
    "wav_filename": "message.wav"         # Name der zu lesenden WAV-Datei
}

# Funktion zum Dekodieren der Audiodaten in Binärdaten
def decode_audio_to_binary(audio_data, bit_duration, sample_rate, freq_0, freq_1):
    bit_length = int(sample_rate * bit_duration)
    binary_data = ""

    for i in range(0, len(audio_data), bit_length):
        segment = audio_data[i:i + bit_length]
        # Frequenz analysieren, um festzustellen, ob es sich um ein "0"- oder "1"-Bit handelt
        fft_result = np.fft.fft(segment)
        freqs = np.fft.fftfreq(len(segment), 1 / sample_rate)
        peak_freq = abs(freqs[np.argmax(np.abs(fft_result))])

        if abs(peak_freq - freq_0) < abs(peak_freq - freq_1):
            binary_data += "0"
        else:
            binary_data += "1"

    return binary_data

# Binärdaten in Text umwandeln
def binary_to_text(binary_data):
    text = ""
    for i in range(0, len(binary_data), 8):
        byte = binary_data[i:i + 8]
        if len(byte) == 8:
            text += chr(int(byte, 2))
    return text

# Nachricht entschlüsseln
def decrypt_message(encrypted_text, key):
    # Nachricht entschlüsseln, indem der Schlüssel mit den ursprünglichen Daten kombiniert wird (XOR)
    decrypted_message = ""
    for i in range(len(encrypted_text)):
        decrypted_char = chr(ord(encrypted_text[i]) ^ ord(key[i % len(key)]))
        decrypted_message += decrypted_char
    return decrypted_message

# Hauptprogramm
if __name__ == "__main__":
    # WAV-Datei lesen
    sample_rate, audio_data = read(user_parameters["wav_filename"])
    if audio_data.ndim > 1:
        audio_data = audio_data[:, 0]  # Falls Stereo, nur einen Kanal verwenden
    audio_data = audio_data / 32767.0  # Normalisieren auf den Bereich [-1, 1]

    # Audiodaten in Binärdaten dekodieren
    binary_data = decode_audio_to_binary(audio_data, user_parameters["bit_duration"], user_parameters["sample_rate"], user_parameters["freq_0"], user_parameters["freq_1"])
    print(f"Binärdaten der Nachricht: {binary_data[:64]}...")  # Zeigt die ersten 64 Bits der Nachricht (für Debugging)

    # Binärdaten in verschlüsselten Text umwandeln
    encrypted_message = binary_to_text(binary_data)

    # Nachricht entschlüsseln
    decrypted_message = decrypt_message(encrypted_message, user_parameters["encryption_key"])
    print(f"Entschlüsselte Nachricht: {decrypted_message}")

Auch hier gibt es Parameter, die eingestellt werden können – oder müssen:

user_parameters = {
    "sample_rate": 44100,  # Abtastrate (Hz)
    "bit_duration": 0.1,   # Dauer eines Bits (in Sekunden)
    "freq_0": 1000,        # Frequenz für "0" (Hz)
    "freq_1": 2000,        # Frequenz für "1" (Hz)
    "encryption_key": "geheimer_schluessel",  # Verschlüsselungsschlüssel (beide Parteien müssen denselben Schlüssel verwenden)
    "wav_filename": "message.wav"         # Name der zu lesenden WAV-Datei
}

Allen voran natürlich der geheime Schlüssel: Dieser muss unbedingt mit jenem übereinstimmen, den der Sender in seinem Script zum Verschlüsseln verwendet hat.

Aber auch die Parameter sample_rate, bit_duration und die Frequenzen müssen mit den Einstellungen des Senders übereinstimmen. Der Dateiname der wav_filename muss dem Namen der per E-Mail empfangenen Audio-Datei entsprechen – also vor im Script angepasst werden, bevor es ausgeführt wird.

So funktioniert das Script

Das Empfänger-Script liest also die Audiodatei, dekodiert die darin enthaltene Nachricht und entschlüsselt sie. Hier die einzelnen Schritte:

  1. WAV-Datei lesen Das Script beginnt mit dem Einlesen der Audiodatei mithilfe der Funktion scipy.io.wavfile.read(). Dabei wird die Abtastrate und die Audiodaten extrahiert:
   sample_rate, audio_data = read(user_parameters["wav_filename"])
   if audio_data.ndim > 1:
       audio_data = audio_data[:, 0]  # Falls Stereo, nur einen Kanal verwenden
   audio_data = audio_data / 32767.0  # Normalisieren auf den Bereich [-1, 1]

Diese Normalisierung ist notwendig, um die Audiodaten auf einen Bereich zwischen -1 und 1 zu skalieren.

  1. Audiodaten in Binärdaten dekodieren Die Funktion decode_audio_to_binary() analysiert die Audiodaten und konvertiert sie zurück in eine Binärfolge. Dabei wird die Fourier-Transformation verwendet, um die Frequenzen der einzelnen Segmente zu analysieren und zu entscheiden, ob es sich um ein Bit „0“ oder „1“ handelt:
   def decode_audio_to_binary(audio_data, bit_duration, sample_rate, freq_0, freq_1):
       bit_length = int(sample_rate * bit_duration)
       binary_data = ""

       for i in range(0, len(audio_data), bit_length):
           segment = audio_data[i:i + bit_length]
           fft_result = np.fft.fft(segment)
           freqs = np.fft.fftfreq(len(segment), 1 / sample_rate)
           peak_freq = abs(freqs[np.argmax(np.abs(fft_result))])

           if abs(peak_freq - freq_0) < abs(peak_freq - freq_1):
               binary_data += "0"
           else:
               binary_data += "1"

       return binary_data

Diese Funktion durchläuft die Audiodaten in Segmenten und bestimmt für jedes Segment, ob es sich um eine „0“ oder „1“ handelt.

  1. Binärdaten in Text umwandeln Nachdem die Audiodaten in Binärdaten umgewandelt wurden, werden diese in den ursprünglichen Text konvertiert. Hierbei wird jeder 8-Bit-Block in ein Zeichen umgewandelt:
   def binary_to_text(binary_data):
       text = ""
       for i in range(0, len(binary_data), 8):
           byte = binary_data[i:i + 8]
           if len(byte) == 8:
               text += chr(int(byte, 2))
       return text

So wird der verschlüsselte Text aus den Binärdaten wiederhergestellt.

  1. Nachricht entschlüsseln Die entschlüsselte Nachricht wird mit der Funktion decrypt_message() wieder in den Klartext umgewandelt. Dazu wird derselbe Schlüssel verwendet, der auch beim Verschlüsseln benutzt wurde:
   def decrypt_message(encrypted_text, key):
       decrypted_message = ""
       for i in range(len(encrypted_text)):
           decrypted_char = chr(ord(encrypted_text[i]) ^ ord(key[i % len(key)]))
           decrypted_message += decrypted_char
       return decrypted_message

Diese Methode führt eine XOR-Operation auf jedes Zeichen des verschlüsselten Textes durch, um die ursprüngliche Nachricht wiederherzustellen.

  1. Ergebnis anzeigen Schließlich wird die entschlüsselte Nachricht auf der Konsole ausgegeben:
   print(f"Entschlüsselte Nachricht: {decrypted_message}")

Damit erhält der Empfänger die ursprünglich gesendete Nachricht im Klartext in seinem Terminal:

Falls der Empfänger jedoch einen falschen Schlüssel in seinem Script verwendet, klappt es mit dem Entschlüsseln nicht. So sieht die gleiche Nachricht aus, wenn am Ende des Schlüssels zwei Zeichen fehlen:

Die Nachricht beginnt zwar korrekt, weil der Anfang des Schlüssels mit jenem des Senders übereinstimmt. Sie „zerfällt“ dann aber zu Kauderwelsch, da der Schlüssel des Empfängers, wie gesagt, zu kurz ist und also zu früh wieder die ersten Zeichen des Schlüssels verwendet werden. Wenn der Schlüssel überhaupt nicht jenem des Senders entspricht, bleibt die Textnachricht vollständig unlesbar.

Wie geht es weiter?

Du kannst nun also Nachrichten verschlüsselt als Audio versenden und dir einigermaßen sicher sein, dass sie nur jemand entschlüsseln kann, der ein geeignetes Script und vor allem den richtigen Schlüssel dafür besitzt. Wie könnten Verbesserungen aussehen? Du hast vielleicht schon bemerkt, dass die erzeugten WAV-Dateien recht groß sind – die relative kurze Nachricht aus dem Beispiel oben hat bereits 2,6 MB.

Hier könnte eine Konvertierung in das MP3-Format weiterhelfen, um sicherzugehen, dass deine Nachricht nicht zu groß für den Anhang einer E-Mail ist.

]]>
Raspberry Pi Webserver mit Flask https://polluxlabs.net/raspberry-pi-projekte/raspberry-pi-webserver-mit-flask/ Tue, 01 Oct 2024 10:04:56 +0000 https://polluxlabs.net/?p=16293 Raspberry Pi Webserver mit Flask Weiterlesen »

]]>
Hier auf Pollux Labs findest du bereits ein Tutorial für einen ESP8266 Webserver – in diesem Projekt programmierst du jedoch einen Raspberry Pi Webserver. Dieser wird regelmäßig Messdaten von einem ESP8266 empfangen und auf einer Webseite anzeigen. Neben einem Raspberry Pi benötigst du für dieses Tutorial nur noch einen Sensor – ich verwende im Folgenden den BMP180, um Temperatur und Luftfeuchtigkeit zu messen.

Außerdem verwende ich das Python-Modul Flask, um den Webserver zu programmieren. Damit kannst du sowohl einfache Webseiten erstellen als auch komplexere Applikationen, mit denen du, wie in unserem Fall, Hardware steuern und Messdaten anzeigen kannst.

Eine erste Webseite mit Flask

Bevor du dich einem Webserver widmest, lass uns mit den Grundlagen anfangen. Falls du Flask noch nicht auf deinem Raspberry Pi installiert hast, hole das im Terminal nach:

pip install Flask

Öffne nun einen Editor (z.B. Visudal Studio Code) und erstelle ein Python-Script mit folgendem Inhalt:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def home():
    return "Hallo, Raspberry Pi!"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Speichere es anschließend ab unter einem Namen deiner Wahl und starte es. Solltest du im Terminal daraufhin die Fehlermeldung „Port 5000 is in use by another program.“ erhalten, wähle hinter port= eine andere Zahl, zum Beispiel 5010. Sobald das Script ordnungsgemäß auf einem freien Port läuft, siehst du im Terminal die IP-Adresse deines Raspberry Pis. Zum Beispiel: „Running on http://127.0.0.1:5010“

Ausgabe des Servers im Terminal

Wenn du die IP-Adresse (127.0.0.1:5010) kopierst und sie im Browser öffnest, solltest du den kleinen Gruß sehen, der im Code hinterlegt ist. Neben dieser IP-Adresse erhältst du auch eine Alternative – hier 192.168.0.143:5000. Falls sich die erste Adresse nicht öffnen lässt, probiere es einmal mit dieser.

So funktioniert der code

Lass uns nun einen genaueren Blick auf das Python-Script werfen, um zu verstehen, was dort passiert. Zunächst importierst du die Klasse Flask (Mehr über Flask) aus dem gleichnamigen Modul:

from flask import Flask

Anschließend erstellst du eine Instanz dieser Klasse namens app. Mit (__name__) dahinter teilst du Python mit, dass alle etwaigen zusätzlichen Dateien im selben Ordner liegen, wie das Script selbst. Dazu später mehr.

Die folgenden drei Zeilen

@app.route('/')
def home():
    return "Hallo, Raspberry Pi!"

stellen nun die Webseite bereit – in unserem Fall ist das nur eine einzelne Startseite. Mit dem sogenannten Dekorator @app.route(‚/‘) bestimmst du, was passiert, wenn diese Startseite aufgerufen wird – also einfach nur die IP-Adresse. Hierfür dient der Schrägstrich /.

Wenn das nun also passiert (so wie du es vorhin im Browser getan hast), dann wird die darauf folgende Funktion def home(): aufgerufen. Darin befindet sich einfach nur die Anweisung, den String Hallo, Raspberry Pi! wiederzugeben – den du dann im Browser siehst. Später folgen nun noch weitere Dekoratoren und Funktionen, die deinem Raspberry Pi Webserver mehr Leben einhauchen werden.

Zuletzt die beiden Zeilen

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Dieser Code wird ausgeführt, wenn wir dieses Skript direkt ausführen (anstatt es als Modul in ein anderes Script zu importieren). In dem Fall startet der Webserver von Flask, der auf Anfragen von jedem Gerät im gleichen WLAN-Netzwerk am Port 5000 reagiert.

Sensordaten empfangen und anzeigen

So ein kleiner Gruß ist ja nett, aber eine wirklich praktische Anwendung ist besser. Wie wäre es, wenn dein Raspberry Pi Webserver regelmäßig Daten von einem ESP8266 empfängt und diese auf einer Webseite anzeigt?

Im Folgenden lernst du, wie du mit dem Sensor BMP180 an einem ESP8266 die Temperatur und Luftfeuchtigkeit misst und die Messdaten an den Raspberry Pi sendest. Du selbst kannst die aktuellen Daten dann auf einer Webseite einsehen.

Temperatur und Luftdruck auf dem Raspberry Pi Webserver

Hierfür benötigst du zweimal Code – einmal für den Raspberry Pi Webserver und einen Sketch für den ESP8266.

Das Python-Script für den Server

Zunächst kümmern wir uns um den Server. Damit dein Raspberry Pi die Daten in Empfang nehmen und auf einer Webseite anzeigen kann, ist nicht viel Code nötig. Du benötigst im Prinzip eine Funktion, die die gesendeten Messdaten entgegennimmt und eine weitere, um diese auf einer Webseite anzuzeigen. Hier das vollständige Script:

//Raspberry Pi Webserver
//polluxlabs.net

from flask import Flask, request, render_template_string
from datetime import datetime

app = Flask(__name__)

temperature = 0
pressure = 0
last_update = "Noch keine Daten empfangen"

@app.route('/update-sensor', methods=['POST'])
def update_sensor():
    global temperature, pressure, last_update
    temperature = request.form.get('temp')
    pressure = request.form.get('pressure')
    last_update = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    return "Daten aktualisiert", 200

@app.route('/')
def index():
    html = """
    <!DOCTYPE html>
    <html lang="de">
    <head>
        <meta charset="UTF-8">
        <title>BMP180 Sensordaten</title>
    </head>
    <body>
        <h1>BMP180 Sensordaten</h1>
        <p>Temperatur: {{ temperature }}°C</p>
        <p>Luftdruck: {{ pressure }} hPa</p>
        <p>Letzte Aktualisierung: {{ last_update }}</p>
    </body>
    </html>
    """
    return render_template_string(html, temperature=temperature, pressure=pressure, last_update=last_update)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

Kopiere den obigen Code uns speichere ihn in einer Date, die du z.B. webserver.py nennst.

So funktioniert das Script

Ein kurzer Blick auf die Funktionsweise des Raspberry Pi Webservers:

Daten empfangen

Der Teil des Scripts, der neue Daten empfängt, sieht so aus:

   @app.route('/update-sensor', methods=['POST'])
   def update_sensor():
       global temperature, pressure, last_update
       temperature = request.form.get('temp')
       pressure = request.form.get('pressure')
       last_update = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
       return "Daten aktualisiert", 200
  • Dieser Code wartet auf neue Messwerte vom Sensor.
  • Wenn neue Daten ankommen, werden sie in den Variablen temperature und pressure gespeichert.
  • last_update speichert den Zeitpunkt, an dem die Daten ankamen.

Daten anzeigen

Die Webseite, die die Daten anzeigt, wird hier erstellt:

   @app.route('/')
   def index():
       html = """
       <!DOCTYPE html>
       <html lang="de">
       <head>
           <meta charset="UTF-8">
           <title>BMP180 Sensordaten</title>
       </head>
       <body>
           <h1>BMP180 Sensordaten</h1>
           <p>Temperatur: {{ temperature }}°C</p>
           <p>Luftdruck: {{ pressure }} hPa</p>
           <p>Letzte Aktualisierung: {{ last_update }}</p>
       </body>
       </html>
       """
       return render_template_string(html, temperature=temperature, pressure=pressure, last_update=last_update)
  • Dieser Code erstellt eine einfache Webseite mithilfe von HTML.
  • Die Seite zeigt die aktuelle Temperatur, den Luftdruck und die Zeit der letzten Aktualisierung an.

Webseite einrichten

Der Server wird hier gestartet:

   if __name__ == '__main__':
       app.run(host='0.0.0.0', port=5000, debug=True)
  • Diese Zeilen starten den Webserver.
  • host='0.0.0.0' bedeutet, dass der Server von anderen Geräten im Netzwerk erreichbar ist.
  • port=5000 legt fest, dass die Webseite über Port 5000 erreichbar ist.

So arbeiten alle Teile zusammen: Der Sensor sendet Daten, das Script empfängt und speichert sie, und die Webseite zeigt sie an.

Starte den Raspberry Pi WebServer

Öffne nun auf deinem Raspberry Pi das Terminal und öffne dein Script, wobei du natürlich den Script-Namen verwendest, den du vergeben hast:

python3 webserver.py 

Anschluss des BMP180 und der Sketch für den ESP8266

Nun zum Sender, dem ESP8266, der mit dem Sensor BMP180 die aktuelle Temperatur und Luftfeuchtigkeit misst und an den Raspberry Pi weiterleitet. Bevor wir zum Code kommen, hier eine Skizze, wie du den Sensor am ESP8266 anschließt:

BMP180 am ESP8266 angeschlossen

Wenn du den Sensor angeschlossen hast, kann es direkt mit dem Sketch weitergehen. Nur ein Hinweis vorab: Solltest du noch nie ein Projekt mit dem BMP180 gebaut haben, fehlt dir vermutlich noch die zugehörige Bibliothek. Öffne in diesem Fall den Bibliotheksmanager in der Arduino IDE und installiere die Bibliothek Adafruit BMP085 Library. Falls du gefragt wirst, ob du weitere benötigte Bibliotheken installieren möchtest, antworte bitte mit Ja.

Doch nun zum Sketch:

//Sending data to the Raspberry Pi Webserver
//polluxlabs.net

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <Wire.h>
#include <Adafruit_BMP085.h>

const char* ssid = "DEIN NETZWERK";
const char* password = "DEIN PASSWORT";
const char* serverName = "http://SERVER-IP/update-sensor";

Adafruit_BMP085 bmp;

void setup() {
  Serial.begin(115200);
  
  WiFi.begin(ssid, password);
  Serial.println("Verbinde mit WLAN");
  while(WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Verbunden mit IP-Adresse: ");
  Serial.println(WiFi.localIP());

  if (!bmp.begin()) {
    Serial.println("BMP180 nicht gefunden, überprüfen Sie die Verkabelung!");
    while (1) {}
  }
}

void loop() {
  if(WiFi.status() == WL_CONNECTED) {
    WiFiClient client;
    HTTPClient http;

    float temperature = bmp.readTemperature();
    float pressure = bmp.readPressure() / 100.0F;

    // Daten für den POST-Request vorbereiten
    String httpRequestData = "temp=" + String(temperature) + "&pressure=" + String(pressure);

    // HTTP POST Request senden
    http.begin(client, serverName);
    http.addHeader("Content-Type", "application/x-www-form-urlencoded");
    
    int httpResponseCode = http.POST(httpRequestData);

    if (httpResponseCode > 0) {
      Serial.print("HTTP Response code: ");
      Serial.println(httpResponseCode);
    }
    else {
      Serial.print("Fehler code: ");
      Serial.println(httpResponseCode);
    }
    http.end();
  }
  else {
    Serial.println("WiFi getrennt");
  }

  delay(30000);  // Alle 30 Sekunden senden
}

So funktioniert der Sketch

Werfen wir nun einen Blick auf die einzelnen Bestandteile des Sketchs.

Einbindung der Bibliotheken

Am Anfang werden verschiedene Bibliotheken eingebunden, die für die WiFi-Verbindung, HTTP-Anfragen und die Kommunikation mit dem BMP180 Sensor benötigt werden.

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <Wire.h>
#include <Adafruit_BMP085.h>

Konfiguration

Anschließend legst du die WLAN-Zugangsdaten (SSID und Passwort) fest und hinterlegst die Adresse des Raspberry Pi Webservers. Ersetze hierbei SERVER-IP zum Beispiel durch 192.168.0.143:5000.

const char* ssid = "NETZWERK";
const char* password = "PASSWORT";
const char* serverName = "http://SERVER-IP/update-sensor";

Adafruit_BMP085 bmp;

Setup-Funktion

Hier stellst du die Verbindung zum WLAN her und initialisierst den Sensor:

void setup() {
  Serial.begin(115200);

  WiFi.begin(ssid, password);
  Serial.println("Verbinde mit WLAN");
  while(WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Verbunden mit IP-Adresse: ");
  Serial.println(WiFi.localIP());

  if (!bmp.begin()) {
    Serial.println("BMP180 nicht gefunden, überprüfen Sie die Verkabelung!");
    while (1) {}
  }
}

Loop-Funktion

Nun zum Kern, hier prüfst du zunächst, ob die Internetverbindung steht, liest Temperatur und Luftdruck ein und sendest die Messdaten an den Raspberry Pi Webserver per HTTP POST. Anschließend prüfst du die Antwort des Servers, kappst die Verbindung zum WLAN und wartest 30 Sekunden bis zur nächsten Messung.

void loop() {
  if(WiFi.status() == WL_CONNECTED) {
    WiFiClient client;
    HTTPClient http;

    float temperature = bmp.readTemperature();
    float pressure = bmp.readPressure() / 100.0F;

    // Daten für den POST-Request vorbereiten
    String httpRequestData = "temp=" + String(temperature) + "&pressure=" + String(pressure);

    // HTTP POST Request senden
    http.begin(client, serverName);
    http.addHeader("Content-Type", "application/x-www-form-urlencoded");

    int httpResponseCode = http.POST(httpRequestData);

    // Überprüfung der Antwort
    if (httpResponseCode > 0) {
      Serial.print("HTTP Response code: ");
      Serial.println(httpResponseCode);
    }
    else {
      Serial.print("Fehler code: ");
      Serial.println(httpResponseCode);
    }
    http.end();
  }
  else {
    Serial.println("WiFi getrennt");
  }

  delay(30000);  // Alle 30 Sekunden senden
}

Lade den Sketch nun auf deinen ESP8266. Wenn du nun die IP-Adresse des Raspberry Pi Webservers in einem Browser öffnest, solltest du bald darauf die aktuellen Messdaten des ESP8266 darauf sehen.

Hübsche die Webseite etwas auf

Klappt alles? Dann wäre vielleicht eine etwas ansprechendere Webseite eine gute Idee. Im Prinzip sind dir hier keine Grenzen gesetzt, du kannst hier mit HTML und CSS schalten und walten wie du möchtest. Eine weiteres Layout inklusive eines Graphen für den Verlauf der Messdaten könnte z.B. dieses hier sein:

Webseite mit Temperatur und Luftdruck

Das zugehörige Python-Script sieht folgendermaßen aus:

//Raspberry Pi Webserver
//polluxlabs.net

from flask import Flask, request, jsonify, render_template_string
from datetime import datetime
import json

app = Flask(__name__)

temperature = 20.0
pressure = 1013.25
last_update = "Noch keine Daten empfangen"
history = []

@app.route('/update-sensor', methods=['POST'])
def update_sensor():
    global temperature, pressure, last_update, history
    temperature = float(request.form.get('temp'))
    pressure = float(request.form.get('pressure'))
    last_update = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    
    history.append({"time": last_update, "temperature": temperature, "pressure": pressure})
    if len(history) > 10:
        history.pop(0)
    
    return "Daten aktualisiert", 200

@app.route('/get-data')
def get_data():
    return jsonify({
        "temperature": temperature,
        "pressure": pressure,
        "last_update": last_update,
        "history": history
    })

@app.route('/')
def index():
    html = """
<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>BMP180 Sensordaten Dashboard</title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <style>
        :root {
            --primary-color: #3498db;
            --secondary-color: #2c3e50;
            --background-color: #ecf0f1;
            --card-background: #ffffff;
        }
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            line-height: 1.6;
            color: var(--secondary-color);
            background-color: var(--background-color);
            margin: 0;
            padding: 0;
        }
        .container {
            max-width: 1200px;
            margin: 0 auto;
            padding: 20px;
        }
        header {
            background-color: var(--primary-color);
            color: white;
            text-align: center;
            padding: 1rem;
            margin-bottom: 2rem;
        }
        h1 {
            margin: 0;
        }
        .dashboard {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
            gap: 20px;
        }
        .card {
            background-color: var(--card-background);
            border-radius: 8px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            padding: 20px;
            text-align: center;
        }
        .card-title {
            font-size: 1.2rem;
            color: var(--secondary-color);
            margin-bottom: 10px;
        }
        .card-value {
            font-size: 2.5rem;
            font-weight: bold;
            color: var(--primary-color);
        }
        .card-icon {
            font-size: 3rem;
            margin-bottom: 10px;
            color: var(--primary-color);
        }
        #updateTime {
            text-align: center;
            margin-top: 20px;
            font-style: italic;
        }
        #chart {
            width: 100%;
            height: 300px;
        }
    </style>
</head>
<body>
    <header>
        <h1>BMP180 Sensordaten Dashboard</h1>
    </header>
    <div class="container">
        <div class="dashboard">
            <div class="card">
                <i class="fas fa-thermometer-half card-icon"></i>
                <div class="card-title">Temperatur</div>
                <div class="card-value" id="temperature">--</div>
                <div>°C</div>
            </div>
            <div class="card">
                <i class="fas fa-tachometer-alt card-icon"></i>
                <div class="card-title">Luftdruck</div>
                <div class="card-value" id="pressure">--</div>
                <div>hPa</div>
            </div>
        </div>
        <div class="card" style="margin-top: 20px;">
            <canvas id="chart"></canvas>
        </div>
        <div id="updateTime">Letzte Aktualisierung: <span id="lastUpdate">--</span></div>
    </div>

    <script>
        let chart;

        function updateData() {
            fetch('/get-data')
                .then(response => response.json())
                .then(data => {
                    document.getElementById('temperature').textContent = data.temperature.toFixed(1);
                    document.getElementById('pressure').textContent = data.pressure.toFixed(2);
                    document.getElementById('lastUpdate').textContent = data.last_update;
                    
                    updateChart(data.history);
                });
        }

        function updateChart(history) {
            const ctx = document.getElementById('chart').getContext('2d');
            
            if (chart) {
                chart.destroy();
            }
            
            chart = new Chart(ctx, {
                type: 'line',
                data: {
                    labels: history.map(entry => entry.time),
                    datasets: [{
                        label: 'Temperatur (°C)',
                        data: history.map(entry => entry.temperature),
                        borderColor: 'rgb(255, 99, 132)',
                        tension: 0.1
                    }, {
                        label: 'Luftdruck (hPa)',
                        data: history.map(entry => entry.pressure),
                        borderColor: 'rgb(54, 162, 235)',
                        tension: 0.1
                    }]
                },
                options: {
                    responsive: true,
                    scales: {
                        x: {
                            display: true,
                            title: {
                                display: true,
                                text: 'Zeit'
                            }
                        },
                        y: {
                            display: true,
                            title: {
                                display: true,
                                text: 'Wert'
                            }
                        }
                    }
                }
            });
        }

        // Initialer Datenabruf
        updateData();

        // Aktualisiere Daten alle 30 Sekunden
        setInterval(updateData, 30000);
    </script>
</body>
</html>
    """
    return render_template_string(html)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

Wie geht es weiter?

Du hast nun einen Raspberry Pi Webserver, der Daten empfangen und auf einer Webseite visualisieren kann. Der nächste Schritt wäre eine Möglichkeit, über diese Webseite auch Geräte zu steuern, die wiederum z.B. an einem ESP8266 hängen und darüber gesteuert werden.

]]>
Narratron – Schattenspiel und Geschichten von der KI https://polluxlabs.net/raspberry-pi-projekte/narratron-schattenspiel-und-geschichten-von-der-ki/ Sun, 16 Jun 2024 08:03:36 +0000 https://polluxlabs.net/?p=16638 Narratron – Schattenspiel und Geschichten von der KI Weiterlesen »

]]>
Bei Pollux Labs findest du zahlreiche KI-Projekte, mit denen du dir zum Beispiel Bilder erklären und die aktuelle Nachrichtenlage zusammenfassen lassen kannst. Ein weiteres interessantes Projekt habe ich bei Creative Applications gefunden: Narratron.

Hierbei handelt es sich um ein Gerät, das Schattenspiel-Figuren erkennt und auf dieser Basis per künstlicher Intelligenz eine Geschichte entwickelt, vertont und illustriert. Die beiden Maker Aria Xiying Bao und Yubo Zhao haben hierfür ein ansprechendes Gehäuse gedruckt, in dem die Hardware untergebracht ist – und das an Spielzeug aus alten Zeiten erinnert, wie zum Beispiel kleine Fernseher, deren Bilder man per Klick weiterdreht.

Hier eine Demonstration des Projekts:

Du findest es auf der Webseite von Creative Applications.

Technologie

Zum Einsatz kommt in diesem Projekt unter anderem Googles Teachable Machine, OpenAIs GPT-4 und Stable Diffusion für die Erzeugung der Bilder. Falls dich dieses Projekt interessiert und du es nachbauen möchtest, findest du hierfür hilfreiche Projekte auf Pollux Labs:

Die beiden Entwickler haben leider nicht den Code für ihr Projekt veröffentlicht – aber mit den genannten Tutorials kommst du schon ein ganzes Stück weiter. Hardwareseitig schweigen sie sich auch aus, aber ich würde vermuten, dass du darin als Hauptbestandteile einen Raspberry Pi Zero (oder sogar ein kleineres Board) und eine Kamera mit starker Beleuchtung finden könntest.

Auch einen Projektor muss es darin geben, denn die von Stable Diffusion erstellten Bilder, werden auf eine Leinwand bzw. helle Fläche projiziert.

Alles in allem ist dieses Projekt eine tolle Demonstration, wie moderne KI-Anwendungen sich mit der haptischen und gegenständlichen Welt verbinden lassen – auch wenn von künstlicher Intelligenz geschriebene und vorgelesene Geschichten verständlicherweise nicht jedermanns Sache sein dürften.

]]>
Bilder analysieren und beschreiben lassen mit künstlicher Intelligenz https://polluxlabs.net/raspberry-pi-projekte/bilder-analysieren-und-beschreiben-lassen-mit-kuenstlicher-intelligenz/ Mon, 25 Mar 2024 08:55:39 +0000 https://polluxlabs.net/?p=16232 Bilder analysieren und beschreiben lassen mit künstlicher Intelligenz Weiterlesen »

]]>
Dass du mit Hilfe von ChatGPT bzw. DALL-E Bilder erzeugen kannst, weißt du sicherlich bereits schon. Aber es geht auch andersherum: Mit Vision von OpenAI kannst du Bilder analysieren – also herausfinden, was darauf zu sehen ist. Und: Das funktioniert auch mit Videos.

In diesem Projekt lernst du, wie du mit Python und der API von OpenAI

Was ist OpenAI Vision?

Üblicherweise kommunizierst du mit ChatGPT oder anderen Sprachmodellen über einen Text-Prompt. Du sendest eine Anforderung oder Frage – und die KI antwortet dir. Mit GPT-4 gibt es jedoch auch die Möglichkeit, zusätzlich zum Text auch ein Bild mitzusenden. Du kannst also zum Beispiel fragen, was auf dem Bild zu sehen ist. Damit machst du dir die multimodalen Eigenschaften zunutze, die dir ermöglichen, verschiedene Medientypen zu kombinieren.

Das Vision zugrunde liegende Modell gpt-4-vision-preview kannst du aktuell (März 2024) nur über die API von OpenAI nutzen. Hierfür benötigst du einen Account bei OpenAI und einen API-Key. Wie du beides erstellst, erfährst du in diesem Tutorial. Um mit der API zu interagieren, eignet sich Python. Hierfür stellt OpenAI eine Bibliothek zur Verfügung, mit der du die gewünschten Funktionen unkompliziert aufrufen kannst.

Ein einzelnes Bild beschreiben lassen

Als erstes Beispiel soll ein einzelnes Bild dienen, das du per API an ChatGPT sendest und von der KI beschreiben lässt. Dieses Bild ist lokal auf deinem Rechner gespeichert. Um es zu übertragen, konvertierst du es zunächst in das Format Base64.

Zusammen mit dem kodierten Bild sendest du deinen Prompt mit – die Frage, was auf dem Bild zu sehen ist. Nach wenigen Sekunden erhältst du die Antwort zurück, die du dann in der Konsole ausgeben kannst. Hier ein Beispiel eines Bilds eines Panthers auf dem Mond und darunter die Interpretation von ChatGPT:

Ein Panther auf dem Mond

Das sieht ChatGPT in dem Bild:

Das ist ein fiktives Bild, das eine Katze in einem Astronautenanzug darstellt. Die Katze steht auf einer unebenen, mondähnlichen Oberfläche, und im Hintergrund ist ein großer erdähnlicher Planet mit verschiedenen Monden und Sternen sowie Galaxien im Weltraum zu sehen. Es handelt sich um eine künstlerische Darstellung, die Elemente aus der Science-Fiction-Szene mit einem Hauch von Humor kombiniert, indem sie ein Haustier in den Kontext der Raumfahrt setzt.

Nicht schlecht, oder? Gut, die KI hat aus dem Panther eine Katze gemacht – aber das kann man ihr wohl verzeihen.

Das Python-Script

Um solch eine Bildbeschreibung zu erstellen, benötigst du nicht viel Code. Hier das Script, das ich dafür verwendet habe:

import base64
import requests
from openai import OpenAI
import os

#Fall nötig: Das Verzeichnis auf das setzen, in dem das Script liegt.
os.chdir(os.path.dirname(os.path.realpath(__file__)))

# OpenAI API-Key
api_key = "DEIN API-KEY VON OPENAI"

# Das Bild konvertieren
def encode_image(image_path):
  with open(image_path, "rb") as image_file:
    return base64.b64encode(image_file.read()).decode('utf-8')

# Pfad zum Bild
image_path = "panther.jpeg"

# Den Base64-String erstellen
base64_image = encode_image(image_path)

headers = {
  "Content-Type": "application/json",
  "Authorization": f"Bearer {api_key}"
}

payload = {
  "model": "gpt-4-turbo",
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "Was ist auf dem Bild zu sehen?"
        },
        {
          "type": "image_url",
          "image_url": {
            "url": f"data:image/jpeg;base64,{base64_image}"
          }
        }
      ]
    }
  ],
  "max_tokens": 300
}

response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload)
print(response.json()["choices"][0]["message"]["content"])

Das obige Script orientiert sich an einem Beispiel von OpenAI.

Bevor du das Script laufen lässt, stelle zunächst sicher, dass du die benötigten Bibliotheken installierst hast. Trage außerdem deinen API-Key von OpenAI ein. Dann benötigst du noch den Pfad zum Bild, das du analysieren und beschreiben lassen möchtest. Trage diesen hinter image_path = ein. Falls dein Bild im gleichen Verzeichnis liegt, wie dein Script – Python dieses jedoch nicht findet, kann die Zeile ganz oben unterhalb der Moduleinbindungen hilfreich sein. Damit setzt du das Verzeichnis auf jenes, in dem dein Script gespeichert ist.

Weiter unten im Script findest du den Prompt, der deine Anweisung bzw. Frage zum Bild enthält. Hier Was ist auf dem Bild zu sehen? Je nachdem, was du genau vorhast oder wissen möchtest, kannst du diesen Prompt natürlich anpassen. Wenn du also zum Beispiel wissen möchtest, welche Farben die Blumen auf einer Wiese haben, passe den Prompt entsprechend an.

Damit die Beschreibung von ChatGPT nicht zu ausschweifend und kostspielig wird, kannst du mit max_tokens noch eine maximale Anzahl an verwendeten Tokens festlegen.

Ein Video analysieren

Was mit einem einzelnen Bild geht, funktioniert auch mit einem Video – da ein solches nichts anderes ist als aneinander gereihte Fotos. Und so stellst du ein Video ChatGPT auch bereit, nämlich Frame für Frame.

Das folgende Script öffnet eine lokal gespeicherte Video-Datei und zerlegt es in einzelne Frames. Anschließend erfolgt die Abfrage bei der API, in der du diese Einzelbilder bereitstellst und ebenfalls deinen Prompt (also deine Frage) mitsendest:

import cv2 #Nicht installiert? -> pip install opencv-python
import base64
import time
from openai import OpenAI
import os

#Fall nötig: Das Verzeichnis auf das setzen, in dem das Script liegt.
os.chdir(os.path.dirname(os.path.realpath(__file__)))

client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "DEIN API-KEY VON OPENAI"))

video = cv2.VideoCapture("DEIN VIDEO.mp4")

base64Frames = []
while video.isOpened():
    success, frame = video.read()
    if not success:
        break
    _, buffer = cv2.imencode(".jpg", frame)
    base64Frames.append(base64.b64encode(buffer).decode("utf-8"))

video.release()
print(len(base64Frames), "frames read.")

PROMPT_MESSAGES = [
    {
        "role": "user",
        "content": [
            "Dies sind Frames eines Videos. Beschreibe, was darin zu sehen ist.",
            *map(lambda x: {"image": x, "resize": 768}, base64Frames[0::50]),
        ],
    },
]
params = {
    "model": "gpt-4-turbo",
    "messages": PROMPT_MESSAGES,
    "max_tokens": 400,
}

result = client.chat.completions.create(**params)
print(result.choices[0].message.content)

Wie du siehst, ist das Script etwas anders aufgebaut als das erste. Hier sendest du deine Abfrage nicht per Request, sondern nutzt die Funktion client. Hinterlege wieder einen API-Key und in der Zeile video = cv2.VideoCapture(„DEIN VIDEO.mp4“) den Dateinamen des Videos, das du analysieren lasse möchtest. Falls es nicht im gleichen Ordner wie dein Script liegt, achte bitte auf den korrekten Pfad.

Im Bereich PROMPT_MESSAGE = [ findest du den Prompt, den du mitsendest. Diesen kannst du wieder an deine Wünsche anpassen. Ebenso gibt es wieder die Einstellung max_tokens – hier kann allerdings eine größere Zahl nötig sein als bei dem einzelnen Bild von vorhin. Wenn die erlaubte Anzahl an Tokens zu gering ist, erhältst du möglicherweise keine vollständige Beschreibung des Videos.

Mit dem Raspberry Pi ein Foto aufnehmen und es beschreiben lassen

Bisher hast du bereits vorhandene Bilder oder Videos verwendet. Du kannst aber natürlich auch ein Foto mit der Kamera am Raspberry Pi aufnehmen und dieses von ChatGPT beschreiben lassen. Diesmal gibst du die Analyse jedoch nicht einfach nur in der Konsole aus, sondern lässt sie dir vorlesen.

Damit baust du dir ein Gerät, das du verwenden könntest, um zum Beispiel dir unbekannte Gegenstände, Gemälde oder Gebäude zu identifizieren. Auch für Menschen mit eingeschränktem oder überhaupt keinem Sehvermögen könnte das eine interessante und hilfreiche Anwendung sein.

Raspberry Pi mit Kamera

Das Projekt aufbauen

Ausgelöst wird die Kamera und die anschließende Analyse durch einen gedrückten Button am Raspberry Pi. Schließe diesen wie folgt an. Einen Pulldown- oder Pullup-Widerstand benötigst du hierbei nicht.

Button am Raspberry Pi

Wie du die Kamera anschließt, habe ich in diesem Projekt zur Objekterkennung am Raspberry Pi beschrieben. Allerdings verwende ich darin noch die Option Legacy Camera, die du nun eigentlich nicht mehr benötigst. Leider war das Thema „Kamera am Raspberry Pi“ eine Zeitlang etwas komplex, weswegen es schwierig ist, eine Patentlösung anzubieten, die für die meisten Versionen von Pi und zugehörigem Betriebssystem passt.

Möglicherweise wirst du hier etwas herumprobieren müssen. Im Folgenden verwende ich das Betriebssystem Debian Bookworm (64 Bit) und die Bibliothek Picamera2. Ob deine Kamera korrekt angeschlossen ist und funktioniert, kannst du übrigens im Terminal schnell testen. Führe dafür einfach den folgenden Befehl aus:

libcamera-still -o test.jpg

Wenn alles funktioniert, erscheint ein Vorschaufenster mit dem aktuellen Kamerabild. In diesem Fall steht dem folgenden Python-Script dann nichts mehr im Wege.

Das Python-Script

Das folgende Script erweitert die hier vorangegangen Programme ein wenig. Zunächst wird die Bildbeschreibung erst ausgeführt, sobald der angeschlossene Button gedrückt wurde. Dann wird die Kamera gestartet, ein Foto aufgenommen und dieses dann an ChatGPT übertragen. Nachdem die Antwort vorliegt, wird diese in Sprache umgewandelt und vorgelesen.

Mehr zum Thema „Text in Sprache umwandeln“ findest du übrigens in diesem Tutorial bei uns. Und noch ein Hinweis: Möglicherweise fragst du dich, was mit deinen aufgenommen Fotos passiert, nachdem du sie an ChatGPT übertragen hast. Laut Angaben von OpenAI werden diese nicht für das Training der KI verwendet. Falls du hier jedoch sichergehen möchtest, solltest du auf die Verwendung dieser Fotos vielleicht lieber verzichten.

Hier das Script:

import cv2 #Nicht installiert? -> pip install opencv-python
import base64
from openai import OpenAI #pip install openai
import os
from picamera2 import Picamera2 #pip install picamera2
import RPi.GPIO as GPIO
import pygame #pip install pygame
import time
from pathlib import Path

GPIO.setmode(GPIO.BOARD)
buttonPin = 16
GPIO.setup(buttonPin, GPIO.IN, pull_up_down=GPIO.PUD_UP)

# Dateiname des aufgenommenen Fotos
image_file = 'image.jpg'

def main():

    # Ein Foto machen
    print("Ich nehme ein Foto auf.")
    picam2 = Picamera2()
    camera_config = picam2.create_still_configuration(main={"size": (1920, 1080)}, lores={"size": (640, 480)}, display="lores")
    picam2.configure(camera_config)
    picam2.start()
    time.sleep(2)
    picam2.capture_file(image_file)
 
    client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "DEIN OPENAI API-KEY"))

    image = cv2.imread(image_file)
    _, buffer = cv2.imencode(".jpg", image)
    base64Image = base64.b64encode(buffer).decode("utf-8")

    print("Ich beschreibe das Foto.")

    PROMPT_MESSAGES = [
        {
            "role": "user",
            "content": [
                "Dies ist ein Foto. Beschreibe, was darauf zu sehen ist.",
                {"image": base64Image, "resize": 768},
            ],
        },
    ]
    params = {
        "model": "gpt-4-turbo",
        "messages": PROMPT_MESSAGES,
        "max_tokens": 200,
    }

    result = client.chat.completions.create(**params)
    description = result.choices[0].message.content
    print(description)

    #Vertonung
    speech_file_path = Path(__file__).parent / "vision.mp3"
    response = client.audio.speech.create(
    model="tts-1",
    voice="alloy",
    input=description
    )
    response.stream_to_file(speech_file_path)
    
    pygame.init()
    pygame.mixer.init()
    pygame.mixer.music.load(speech_file_path)
    pygame.mixer.music.play()
    while pygame.mixer.music.get_busy():
        pass
    pygame.quit()

if __name__ == "__main__":
    while True:
        buttonState = GPIO.input(buttonPin)
        if buttonState == GPIO.LOW: 
            main()
        else:
            print(".")

Wie du siehst, kommen hier noch ein paar weitere Bibliotheken ins Spiel. Wie du sie mit Pip im Terminal installiert, steht als Kommentar jeweils dahinter.

Sobald du alles vorbereitet hast, starte das Script (Kopfhörer bzw. Lautsprecher nicht vergessen). Nachdem du den Button gedrückt hast, sollte das Foto übertragen werden und dir die Bildbeschreibung vorgelesen werden. Hab ein bisschen Geduld, so richtig schnell wird das leider nicht funktionieren – aber es sollte meistens deutlich unter einer Minute dauern.

]]>
Deine persönlichen Radio-Nachrichten auf dem Raspberry Pi https://polluxlabs.net/raspberry-pi-projekte/deine-persoenlichen-radio-nachrichten-auf-dem-raspberry-pi/ Fri, 15 Mar 2024 09:12:31 +0000 https://polluxlabs.net/?p=16173 Deine persönlichen Radio-Nachrichten auf dem Raspberry Pi Weiterlesen »

]]>
Aktuelle Nachrichten selbst zu lesen ist nicht immer möglich – manchmal sind sie vorgelesen gerade praktischer. Nur informiert dich das Radio üblicherweise nur zur vollen Stunde. Dieses Raspberry Pi Projekt schafft Abhilfe: Du besorgst dir die aktuellen Nachrichten von tagesschau.de, fasst sie mit ChatGPT radiotauglich zusammen und lässt sie dir vorlesen.

Mit diesem Projekt lernst du, wie du in Python mit Web Scraping Inhalte von Webseiten herunterlädst, wie du ChatGPT per API verwendest und Texte vertonen lassen kannst. Außerdem erfährst du, wie du per Python-Script MP3s erzeugst und diese auf deinem Raspberry Pi abspielst.

Aufbau des Raspberry Pis

Deine eigene Nachrichtensendung soll auf Knopfdruck starten – hierfür benötigst du einen Button, den du an die Pins des Raspberry Pis anschließt. Orientiere dich hierbei an folgender Skizze:

Button am Raspberry Pi

Einen Pullup- bzw. Pulldown-Widerstand benötigst du hier nicht, das erledigt dein Raspberry Pi intern. Neben dem Button benötigst du noch einen Lautsprecher, über den du die vertonten Nachrichten ausgeben kannst. Hierfür eignen sich z.B. Modelle, die du per 3,5mm Klinkenstecker direkt an den Audioausgang anschließen kannst.

Der API-Key von OpenAI

Falls du noch keinen Account bei OpenAI und auch noch keinen API-Key hast, musst du beides noch schnell einrichten, bevor du ChatGPT und die Funktion zum Vertonen von Texten nutzen kannst. In diesem Tutorial erfährst du, wie.

Im folgenden Python-Script kannst du dann deinen API-Key eintragen, um die Features von OpenAI verwenden zu können.

Das Python-Script

Kommen wir zum Code des Projekts. Hier führst du mehrere Funktionen aus – um die aktuellen Nachrichten von tagesschau.de zu laden, um sie zu kürzen und „radiotauglich“ zu machen, und um sie vorlesen zu lassen.

Hier zunächst das vollständige Script:

import requests
from bs4 import BeautifulSoup
from openai import OpenAI
from pathlib import Path
import pygame
import RPi.GPIO as GPIO

GPIO.setmode(GPIO.BOARD)
buttonPin = 16
GPIO.setup(buttonPin, GPIO.IN, pull_up_down=GPIO.PUD_UP)

def main():
    client = OpenAI(
      api_key="DEIN API-KEY",
    )

    url = 'https://www.tagesschau.de/'

    def getNews():
        nonlocal url
        response = requests.get(url)
        soup = BeautifulSoup(response.text, 'html.parser')
        links = soup.find_all('a', class_='teaser__link')
        for link in links:
            link['href'] = url + link['href']
        return links[:5]

    def getArticle(link):
        response = requests.get(link.get('href'))
        soup = BeautifulSoup(response.text, 'html.parser')
        headline = soup.find('meta', property='og:title')['content'] + '\n'
        paragraphs = soup.find_all('p')
        text = ""
        for paragraph in paragraphs:
            if paragraph.find_parent('div', class_='teaser-absatz__teaserinfo') is None and paragraph.find_parent('div', class_='teaser-xs__teaserinfo') is None:
                text += paragraph.get_text() + '\n'
        return headline, text

    def get_summary(text):
        completion = client.chat.completions.create(
        model="gpt-3.5-turbo", 
        messages=[
        {"role": "system", "content": "Du schreibst Nachrichten fürs Radio."},
        {"role": "user", "content": "Fasse den folgenden Text zusammen: {}".format(text)}]
        )
        summary_text = headline + completion.choices[0].message.content
        return summary_text

    def speech(summary_text):
        speech_file_path = Path(__file__).parent / "news.mp3"
        response = client.audio.speech.create(
        model="tts-1",
        voice="alloy",
        input=summary_text
        )
        response.stream_to_file(speech_file_path)
        pygame.init()
        pygame.mixer.init()
        pygame.mixer.music.load(speech_file_path)
        pygame.mixer.music.play()
        while pygame.mixer.music.get_busy():
            pass
        pygame.quit()

    news_links = getNews()
    for link in news_links:
        headline, text = getArticle(link)
        summary_text = get_summary(text)
        speech(summary_text)

if __name__ == "__main__":
    while True:
        buttonState = GPIO.input(buttonPin)
        if buttonState == GPIO.LOW: 
            main()
        else:
            print(".")

So funktioniert das Script

Sobald du den Button gedrückt hast, wird die Funktion main() aufgerufen. Den Code hierfür findest du ganz am Ende des Scripts. Innerhalb von main() werden mehrere verschachtelte Funktionen definiert, die jeweils für einen bestimmten Teil des Prozesses verantwortlich sind.

Die Funktion getNews() sendet eine GET-Anfrage an eine vordefinierte URL (in unserem Fall ‚https://www.tagesschau.de/‘), analysiert die HTML-Antwort, um alle Links mit der Klasse teaser__link zu finden, und gibt die ersten fünf dieser Links zurück.

Die Funktion getArticle(link) nimmt einen dieser Links als Argument, sendet eine GET-Anfrage an die URL des Links und analysiert ebenso die HTML-Antwort, um die Überschrift und den Text des Artikels zu extrahieren. Dazu sucht sie den meta-Tag mit der Eigenschaft og:title für die Überschrift und alle p-Tags für den Text. Anschließend werden die Überschrift und der Text zurückgegeben.

Die Funktion get_summary(text) nimmt den Text als Argument und verwendet ChatGPT, um eine Zusammenfassung des Textes zu erstellen. Anschließend werden die Überschrift des Artikels und die generierte Zusammenfassung miteinander verknüpft und zurückgegeben.

Die Funktion speech(summary_text) nimmt den zusammengefassten Text als Argument und verwendet die OpenAI-API, um den Text in Sprache umzuwandeln. Anschließend wird der vertonte Text in einer Datei gespeichert und mit pygame abgespielt.

Die benötigten Module

Du benötigst du folgenden Module bzw. Bibliotheken, damit das Script läuft:

import requests
from bs4 import BeautifulSoup
from openai import OpenAI
from pathlib import Path
import pygame
import RPi.GPIO as GPIO

Falls du BeautifulSoup, OpenAI und PyGame noch nicht auf deinem System installiert sind, hole das mit den folgenden Befehlen im Terminal nach:

pip install beautifulsoup4
pip install openai
pip install pygame

Die Bibliothek Beautiful Soup verwendest du fürs Web Scraping. Das bedeutet, dass du hiermit die Texte einer beliebigen Webseite auslesen und in deinem Script weiterverwenden kannst. Mit dem Modul von OpenAI greifst du auf deren Services zu und PyGame verwendest du für die Tonausgabe. Die anderen Module sollten bereits verfügbar sein.

Nachdem du eine Button an deinem Raspberry Pi angebracht und die benötigten Module installiert hast, trage deinen API-Key ein und starte das Python-Script. Nach wenigen Sekunden sollte deine persönlichen Radio-Nachrichten starten.

Möchtest du doch lieber „echtes“ Radio hören? In diesem Tutorial lernst du, wie du dir ein ESP32 Internetradio baust.

]]>
Mit Ollama Sprachmodelle lokal nutzen https://polluxlabs.net/raspberry-pi-projekte/mit-ollama-sprachmodelle-lokal-nutzen/ Sat, 02 Mar 2024 20:54:03 +0000 https://polluxlabs.net/?p=16091 Mit Ollama Sprachmodelle lokal nutzen Weiterlesen »

]]>
Hier auf Pollux Labs konntest du bereits darüber lesen, wie du z.B. die API von OpenAI nutzt, um mit ChatGPT zu interagieren. Aber das geht auch lokal auf deinem eigenen Rechner – zwar nicht mit ChatGPT, dafür jedoch mit anderen Sprachmodellen wie Mistral, Gemma, Llama2 und vielen anderen. Hierfür nutzt du das Tool Ollama. In diesem Tutorial erfährst du, wie du es installierst, einrichtest und mit Python mit dem Modell deiner Wahl interagierst.

Ollama installieren

Wenn du einen Mac oder Windows benutzt, musst du Ollama erst hier auf der offziellen Webseite herunterladen.

Ollama herunterladen

Falls du Linux verwendest, gib im Terminal den folgenden Befehl ein:

curl -fsSL https://ollama.com/install.sh | sh

Entpacke nach dem Download die ZIP-Datei (Mac) und starte das Programm oder starte direkt die .exe (Windows). Anschließend führt dich ein Wizard durch die nächsten Schritte, damit du Ollama im Terminal verwenden kannst. Am Ende erhältst du den Befehl für einen ersten Test.

Ollama-Wizard

Kopiere den Befehl und gib in im Terminal bzw. der Konsole ein. Anschließend wird das Sprachmodell Llama2 heruntergeladen. Dieses Modell stammt vom Facebook-Konzert Meta. Wie du auf dem Bild unten siehst, ist das mit 3,8 Gigabyte nicht gerade klein – achte also auf genügend Speicherplatz.

Download von Llama2 im Terminal

Ollama im Terminal verwenden

Um mit deinem ersten Sprachmodell (Llama2) loszulegen, kannst du direkt im selben Fenster bleiben. Du erhältst nach der erfolgreichen Installation eine Eingabeaufforderung, über die du deine erste Frage stellen kannst – so wie du es vermutlich bereits von ChatGPT kennst. Nach wenigen Sekunden erhältst du dann die Antwort ebenfalls im Terminal:

Llama2 über Ollama im Terminal

Das funktioniert also schon einmal ganz gut. Das Beispiel oben ist auf Englisch – du kannst deine Fragen jedoch auch ebenso auf Deutsch stellen. Die Antwort erhältst du von Llama2 jedoch wiederum auf Englisch. Um Antworten auf Deutsch zu erhalten füge deinem Prompt noch eine entsprechende Anweisung hinzu.

Wenn du deine Session beenden möchtest, gib einfach den Befehl /bye ein.

Ein anderes Sprachmodell in Ollama installieren

Du bist natürlich nicht auf Llama2 beschränkt. Auf der Ollama-Webseite sowie auf deren GitHub-Seite kannst du alle verfügbaren Sprachmodelle einsehen. Auf letzterer erfährst du auch, wieviel Arbeitsspeicher du für die verschiedenen Modelle haben solltest. Versuche es doch als nächstes einmal mit Mistral, einem frei verfügbaren französischen Modell (das auch Deutsch kann). Gib hierfür im Terminal folgenden Befehl ein, nachdem du deine aktive Session mit /bye beendet hast:

ollama run mistral

Nach der Installation kannst du mit Mistral interagieren, so wie du es vorher mit Llama2 getan hast.

Das Sprachmodell von Mistral ist mit 4,1 GB sogar noch etwas größer als Llama2. Es ist also hilfreich zu wissen, wie du installierte Modelle wieder loswirst. Ganz einfach – Um z.B. Llama2 zu entfernen, gib im Terminal den folgenden Befehl ein:

ollama rm llama2

Falls du vergessen hast, welche Modelle du gerade installiert hast, hilft dir folgender Befehl weiter:

ollama list

Ollama mit Python verwenden

Bis jetzt hast du „nur“ im Terminal mit deinem lokalen Sprachmodell kommuniziert. Du kannst aber hierfür natürlich auch ein Python-Script verwenden, ähnlich wie ich es hier schon einmal für ChatGPT beschrieben habe.

Zunächst musst du hierfür die entsprechende Bibliothek installieren:

pip install ollama

Erstelle nach der erfolgreichen Installation ein leeres Python-Script mit folgendem Inhalt:

import ollama
response = ollama.chat(model='mistral', messages=[
  {
    'role': 'user',
    'content': 'Welche Farben können Bären haben? Antworte auf Deutsch.',
  },
])
print(response['message']['content'])

Im obigen Script ist wieder das Sprachmodell von Mistral hinterlegt. Falls du ein anderes verwendest, trage es in der zweiten Zeile hinter model= ein.

Speiche die Datei nun ab und führe sie aus. Vermutlich wirst du ziemlich lange warten müssen, bis die Antwort erscheint. Das kannst du mit einem Stream verbessern – hierdurch erscheint die lange Antwort Wort für Wort zum Mitlesen. Verwende hierfür den folgenden angepassten Code:

import ollama

stream = ollama.chat(
    model='mistral',
    messages=[{'role': 'user', 'content': 'Welche Farben können Bären haben? Anworte auf Deutsch'}],
    stream=True,
)

for chunk in stream:
  print(chunk['message']['content'], end='', flush=True)

Weitere Rollen verwenden

Ähnlich wie bei ChatGPT kannst du auch in Ollama in deinen Rollen zuteilen. Wenn du also deine Antworten z.B. immer auf Deutsch erhalten möchtest, hilft dir die Rolle system weiter. Dort kannst du die entsprechende Anweisung hinterlegen, sodass die Rolle user nur deine Frage enthält:

messages=[{'role': 'user', 'content': 'Welche Farben können Bären haben?'},
          {'role': 'system', 'content': 'Antworte auf Deutsch'}],

Jetzt kennst du die Grundlagen, um mit Ollama auf deinem eigenen Rechner Sprachmodelle auszuführen und in deine Projekte einzubinden. Es gibt natürlich noch viel mehr zu entdecken: Die verschiedenen Modelle besitzen alle unterschiedliche Fähigkeiten – hier lohnt sich ein intensiver Blick, besonders da die Entwicklung natürlich nicht stehen bleibt.

]]>
Wie knapp verfehlt uns heute ein Asteroid? https://polluxlabs.net/python-tutorials-und-projekte/wie-knapp-verfehlt-uns-heute-ein-asteroid/ Wed, 21 Feb 2024 10:11:38 +0000 https://polluxlabs.net/?p=16025 Wie knapp verfehlt uns heute ein Asteroid? Weiterlesen »

]]>
Es ist erstaunlich, wie viele Asteroiden täglich an der Erde vorbeifliegen. Wobei „an der Erde vorbei“ in der Regel mehrere Millionen Kilometer bedeutet und diese Asteroiden also keine Gefahr für uns bedeuten. Die NASA stellt eine API zur Verfügung, mit deren Hilfe du die nächsten Vorbeiflüge von Asteroiden für ein bestimmtes Datum einsehen kannst.

Hieraus lässt sich ein kleines Projekt bauen: Mit einem Python-Script ermittelst du den Passanten, der heute am knappsten an uns vorbeisaust und gibst seinen Namen sowie die Entfernung in der Konsole aus – oder lässt dich z.B. per E-Mail informieren.

Am 21. Februar 2024 sah es folgendermaßen aus:

Heute fliegt der Asteroid (2024 CL5) mit 1826990 Kilometern Entfernung am knappsten an der Erde vorbei.

Gute 1,8 Millionen Kilometer – klingt viel, ist es aber nach kosmischen Maßstäben eigentlich nicht. Aber immerhin war der Asteroid bei seinem Vorbeiflug immer noch ungefährt 5 Mal so weit von uns entfernt wie unser eigener Mond.

Das Python-Script

Du benötigst nicht viel Code für dieses Projekt. Zentral sind die API, bei der du die aktuellen Daten beziehst sowie das aktuelle Datum, das du in dieser API-Abfrage verwendest.

___STEADY_PAYWALL___

Die NASA betreibt eine Übersicht über ihre APIs – dort findest du unter Asteroids – NeoWs alle Informationen, die du benötigst. Wenn du dieses Projekt regelmäßig einsetzen möchtest, registriere deinen eigenen API-Key bei der NASA. Das kannst du auf der eben verlinkten Webseite machen. Falls du die API nur einmal ausprobieren möchtest, reicht auch der DEMO_KEY.

Hier zunächst das vollständige Python-Script:

import requests
from datetime import datetime

def find_asteroids(json_data):
    nearest_asteroid = None
    nearest_miss_distance = float('inf')
    
    for date in json_data["near_earth_objects"]:
        for asteroid in json_data["near_earth_objects"][date]:
            miss_distance = float(asteroid["close_approach_data"][0]["miss_distance"]["kilometers"])
            if miss_distance < nearest_miss_distance:
                nearest_asteroid = asteroid
                nearest_miss_distance = miss_distance
    
    return nearest_asteroid

# Aktuelles Datum erhalten und im passenden Format formatieren
current_date = datetime.now().strftime('%Y-%m-%d')

# API-URL für die NASA NEO-Daten
api_url = f"https://api.nasa.gov/neo/rest/v1/feed?start_date={current_date}&end_date={current_date}&detailed=false&api_key=DEMO_KEY"


# Anfrage an die API senden und JSON-Daten abrufen
response = requests.get(api_url)
json_data = response.json()

# Finde den Asteroiden mit dem knappsten Vorbeiflug
nearest_asteroid = find_asteroids(json_data)

print("Heute fliegt der Asteroid", nearest_asteroid["name"], "mit", int(float(nearest_asteroid["close_approach_data"][0]["miss_distance"]["kilometers"])), "Kilometern Entfernung am knappsten an der Erde vorbei.")

Hier findest du die URL der API, an die du deine Abfrage sendest:

api_url = f"https://api.nasa.gov/neo/rest/v1/feed?start_date={current_date}&end_date={current_date}&detailed=false&api_key=DEMO_KEY"

Dort eingebaut siehst du das aktuelle Datum als {current_date} – dieses Datum ermittelst und formatierst du wie folgt:

current_date = datetime.now().strftime('%Y-%m-%d')

Mit Hilfe der Bibliothek datetime ermittelst du das heutige Datum und bringst es mit dem Befehl strftime in das Format, das die API der NASA erwartet.

Die Antwort erhältst du im JSON-Format. Für den 21.2.24 sieht diese z.B. so aus. In diesen Daten suchst du nun mit der Funktion find_asteroids(json_data) nach dem Asteroiden, der der Erde am nächsten kommt. Sobald dieser gefunden ist, gibst du seinen Namen und die Entfernung des Vorbeiflugs über einen Print-Befehl aus.

print("Heute fliegt der Asteroid", nearest_asteroid["name"], "mit", int(float(nearest_asteroid["close_approach_data"][0]["miss_distance"]["kilometers"])), "Kilometern Entfernung am knappsten an der Erde vorbei.")

Mehr Informationen über die Asteroiden

In den Daten stecken noch viel mehr Informationen, als jene, die du bisher verwendest hast. Z.B. der geschätzte Durchmesser im Key estimated_diameter sowie seine Geschwindigkeit. Oder auch, ob der besagte Asteroid als gefährlich eingestuft wird – im Key is_potentially_hazardous_asteroid. Was diese Einstufung bedeutet, erfährst du in dieser Erklärung.

Falls du dich also für dieses Thema interessierst, sind deinem Tatendrang wenige Grenzen gesetzt. So könntest du z.B. grafisch darstellen, wie nah ein Asteroid uns in Bezug zu anderen Himmelskörpern kommt. Viel Spaß beim Experimentieren!

]]>
Zeige die Position der ISS auf einer Weltkarte an https://polluxlabs.net/raspberry-pi-projekte/zeige-die-position-der-iss-auf-einer-weltkarte-an/ Sun, 04 Feb 2024 14:35:45 +0000 https://polluxlabs.net/?p=15721 Zeige die Position der ISS auf einer Weltkarte an Weiterlesen »

]]>
Wo befindet sich die ISS (International Space Station) gerade? Diese Frage beantwortest du mit diesem Projekt spielend leicht. Mit Hilfe eines Python-Scripts und einer API ermittelst du die aktuellen Koordinaten der Raumstation und legst sie über eine Weltkarte. Diese Karte wird in einem Browser geöffnet und alle 10 Sekunden aktualisiert. So kannst du die aktuelle Position der ISS verfolgen und sehen, über welchem Ort sie sich gerade befindet.

Zum Einsatz kommt in diesem Projekt ein Raspberry Pi. Das Browser-Fenster wird darauf im sogenannten Kiosk-Modus (also im Vollbild) geöffnet. So kannst du mit einem kleinen Display deine eigene ISS-Installation aufbauen.

Die Position der ISS auf einem Display

Der Aufbau des Projekts

Im Prinzip benötigst du nur Python für dieses Projekt. Du kannst das untenstehende Script auch einfach auf deinem PC oder Mac ausführen. Ich lasse es jedoch auf einem Raspberry Pi laufen und verwende statt eines großen Monitors ein kleines 7“ Touch-Display.

Mit diesem Script ermittelst du die Position der ISS

Das folgende Python-Script funktioniert so: Von einer API beziehst du die aktuellen Koordinaten, über denen die ISS gerade fliegt. Anschließend erstellst du mit Hilfe der Bibliothek Folium eine Weltkarte und ein Icon, dass die Position der Raumstation anzeigt. Diese Karte speicherst du als HTML-Datei und öffnest diese wiederum im Chrome-Browser. Diese Webseite (also die Weltkarte) wird alle 10 Sekunden aktualisiert, abgespeichert und im Browser aktualisiert.

Installiere die nötigen Bibliotheken

Zunächst zu den Python-Bibliotheken, die du für dieses Projekt benötigst. Installiere diese im Terminal mit den folgenden Befehlen:

pip install requests
pip install folium
pip install Selenium
sudo apt-get install chromium-chromedriver

Das vollständige Script

Nach der Installation der Bibliotheken, kannst du das folgende Script ausführen. Wie es funktioniert, schauen wir uns gleich an.

import requests
import folium
from datetime import datetime
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


def get_iss_position():
    response = requests.get("http://api.open-notify.org/iss-now.json")
    data = response.json()
    if response.status_code == 200:
        timestamp = datetime.utcfromtimestamp(data['timestamp']).strftime('%Y-%m-%d %H:%M:%S')
        latitude = float(data['iss_position']['latitude'])
        longitude = float(data['iss_position']['longitude'])
        return timestamp, latitude, longitude
    else:
        return None

def main():
    chrome_options = Options()
    chrome_options.add_argument('--kiosk')
    service = webdriver.ChromeService(executable_path = '/usr/bin/chromedriver')
    driver = webdriver.Chrome(service=service, options=chrome_options)
    driver.get(f"file:///home/pi/Desktop/iss_map.html") # Passe den Pfad bei Bedarf an
    
    while True:
        iss_position = get_iss_position()

        if iss_position:
            timestamp, latitude, longitude = iss_position
            print(f"ISS Position at {timestamp}: Latitude {latitude}, Longitude {longitude}")

            iss_map = folium.Map(location=[latitude,longitude], zoom_start=4)
            folium.Marker([latitude, longitude], popup=f"ISS at {timestamp}", icon=folium.Icon(color='red')).add_to(iss_map)

            # Speichere die Karte als HTML
            iss_map.save("/home/pi/Desktop/iss_map.html") # Passe den Pfad an deinen eigenen an
            print("Map saved as iss_map.html")         
            
        else:
            print("Failed to retrieve ISS position.")
        time.sleep(10)
        driver.refresh()

if __name__ == "__main__":
    main()

So funktioniert das Script

Zunächst importierst du die benötigten Bibliotheken bzw. die notwendigen Funktionen:

import requests
import folium
from datetime import datetime
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

Dann folgt eine Funktion, mit der du die aktuelle Position der ISS ermittelst:

def get_iss_position():
    response = requests.get("http://api.open-notify.org/iss-now.json")
    data = response.json()
    if response.status_code == 200:
        timestamp = datetime.utcfromtimestamp(data['timestamp']).strftime('%Y-%m-%d %H:%M:%S')
        latitude = float(data['iss_position']['latitude'])
        longitude = float(data['iss_position']['longitude'])
        return timestamp, latitude, longitude
    else:
        return None

Hier kommt die API von open-notify.org ins Spiel. Diese liefert dir nach ihrem Aufruf die aktuellen Koordinaten der ISS als JSON. Diese Koordinaten sowie die aktuelle Uhrzeit speicherst du in den Variablen latitude, longitude und timestamp.

In der folgenden Funktion main() konfigurierst du zunächst Selenium, die Bibliothek, mit der du die Weltkarte im Browser aufrufst. In der letzten der folgenden Zeilen steckt der Pfad zur HTML-Datei, die die Weltkarte enthält – hierzu gleich mehr.

chrome_options = Options()
chrome_options.add_argument('--kiosk')
service = webdriver.ChromeService(executable_path = '/usr/bin/chromedriver')
driver = webdriver.Chrome(service=service, options=chrome_options)
driver.get(f"file:///home/pi/Desktop/iss_map.html") # Passe den Pfad bei Bedarf an

Zunächst zum Loop, der dafür sorgt, dass du alle 10 Sekunden die aktuelle Position der ISS erhältst.

while True:
    iss_position = get_iss_position()

    if iss_position:
        timestamp, latitude, longitude = iss_position
        print(f"ISS Position at {timestamp}: Latitude {latitude}, Longitude {longitude}")

        iss_map = folium.Map(location=[latitude,longitude], zoom_start=4)
        folium.Marker([latitude, longitude], popup=f"ISS at {timestamp}", icon=folium.Icon(color='red')).add_to(iss_map)

        # Speichere die Karte als HTML
        iss_map.save("/home/pi/Desktop/iss_map.html") # Passe den Pfad an deinen eigenen an
        print("Map saved as iss_map.html")         

Hier rufst du zunächst die Funktion get_iss_position() auf, um die aktuelle Position zu ermitteln. Falls du von dieser Funktion Werte zurück erhältst (der Aufruf der API also geklappt hat), gibst du diese mit print aus.

Wichtiger ist jedoch die Weltkarte, die du gleich darauf mit Hilfe der Bibliothek Folium zeichnest. Lass uns auf diese Zeile kurz genauer schauen:

iss_map = folium.Map(location=[latitude,longitude], zoom_start=4)

Die Parameter location=[latitude,longitude] sorgen dafür, dass die Position der ISS auf der Weltkarte zentral angezeigt wird. Die Nadel, die die Raumstation symbolisiert ist also immer im Zentrum zu sehen. Die Welt dreht sich dann quasi unter der ISS weg. Du kannst auch die Einstellung location=[0,0] verwenden – dann bleibt die Welt stehen und die ISS-Nadel bewegt sich.

Das führt uns gleich zum Zoom-Level. Im Script oben ist dieser auf 4 eingestellt – ein mittlerer Detailgrad. Je kleiner der Wert, desto mehr zoomst du von der Erde weg. Um genau zu sehen, worüber die ISS gerade fliegt, erhöhe die Zahl in dieser Einstellung.

Zuletzt speicherst du die Karte als HTML-Datei ab. Wenn du hier einen anderen Pfad als den obigen angibst, achte darauf, dass du diesen auch an der Stelle weiter oben im Script entsprechend anpasst.

Damit wären wir fast durch – fehlt nur noch die Angabe, wie oft die Karte aktualisiert werden soll und der Befehl zum Refresh des Browsers:

time.sleep(10)
driver.refresh()

Hier sind 10 Sekunden eingestellt. Du kannst diesen Wert natürlich beliebig anpassen – beachte jedoch, dass eine zu häufige Aktualisierung zu Problemen mit dem Browser und auch zu einer vorübergehenden Sperrung deiner API-Aufrufe führen kann.

Wie geht es weiter?

Du könntest als Nächstes den Weg der ISS nachzeichnen – also die vergangenen Positionen der ISS auf die Karte übertragen und so den Flug der Raumstation also geschwungene Linie zeigen. Auch hier kann dir die Bibliothek Folium helfen.

]]>