Smart Home – Pollux Labs https://polluxlabs.net Arduino, ESP32 & ESP8266 | Projekte & Tutorials Tue, 09 Sep 2025 10:14:30 +0000 de hourly 1 https://wordpress.org/?v=6.8.2 https://polluxlabs.net/wp-content/uploads/2020/05/cropped-pollux-labs-p-32x32.png Smart Home – Pollux Labs https://polluxlabs.net 32 32 RFID-Karten mit dem Raspberry Pi 5 und RC522 auslesen https://polluxlabs.net/raspberry-pi-projekte/rfid-karten-mit-dem-raspberry-pi-5-und-rc522-auslesen/ Tue, 01 Jul 2025 07:22:58 +0000 https://polluxlabs.net/?p=19458 RFID-Karten mit dem Raspberry Pi 5 und RC522 auslesen Weiterlesen »

]]>
Dieses Tutorial führt dich durch den Prozess, einen RC522 RFID-Reader über die SPI-Schnittstelle mit einem Raspberry Pi 5 zu verbinden und mit einer minimalistischen Python-Bibliothek die eindeutige ID (UID) von RFID-Karten auszulesen. Viele bestehende Anleitungen für den RC522 nutzen die RPi.GPIO-Bibliothek, die auf dem Raspberry Pi 5 nicht mehr ohne Weiteres funktioniert. Dieses Tutorial löst dieses Problem, indem es eine schlanke Python-Bibliothek nutzt, die auf der modernen und kompatiblen gpiod-Bibliothek aufbaut.

✨ Kernfunktionen der Bibliothek

  • Schlankes Design: Alle notwendigen Funktionen sind in einer einzigen, leichtgewichtigen Datei zusammengefasst.
  • Einfache Integration: Die Bibliothek kann einfach durch Kopieren der Datei in dein Projekt eingebunden werden.
  • Raspberry Pi 5 Kompatibel: Nutzt die moderne gpiod-Bibliothek anstelle des veralteten RPi.GPIO.
  • SPI-Fokus: Optimiert für eine stabile und schnelle SPI-Kommunikation.

Hardware: Voraussetzungen und Verkabelung

Zuerst verbinden wir den RC522-Reader mit dem Raspberry Pi.

Benötigte Hardware

  • Raspberry Pi 5
  • RC522 RFID-Reader-Modul
  • RFID-Karten oder -Tags (z.B. MIFARE Classic 1K)
  • Jumper-Kabel (Dupont-Kabel)

Verkabelungsplan (SPI)

Verbinde das RC522-Modul wie in der folgenden Tabelle beschrieben mit den GPIO-Pins deines Raspberry Pi.

RC522 PinPi 5 Pin (Physisch)GPIO (BCM)Beschreibung
SDA/SSPin 24GPIO 8Chip Select (CS)
SCKPin 23GPIO 11SPI Clock
MOSIPin 19GPIO 10Master Out -> Slave In
MISOPin 21GPIO 9Master In <- Slave Out
RSTPin 15GPIO 22Reset
GNDPin 6, 9, etc.Ground (Masse)
VCCPin 1 oder 173.3V Stromversorgung

Wichtiger Hinweis: Schließe das RC522-Modul ausschließlich an einen 3.3V-Pin an. Die Verwendung eines 5V-Pins kann das Modul dauerhaft beschädigen!

Software: System einrichten

Bevor wir den Code ausführen können, müssen die SPI-Schnittstelle aktiviert und die notwendigen Bibliotheken installiert werden.

a) SPI-Schnittstelle aktivieren

Falls noch nicht geschehen, aktiviere die SPI-Schnittstelle auf deinem Raspberry Pi.

  1. Öffne ein Terminal und gib sudo raspi-config ein.
  2. Navigiere zu 3 Interface Options -> I4 SPI.
  3. Bestätige die Frage, ob die SPI-Schnittstelle aktiviert werden soll, mit „Ja“ oder „Yes“.
  4. Beende das Konfigurationstool. Ein Neustart kann erforderlich sein.

b) Systemabhängigkeiten installieren

Installiere die Python-Bibliotheken spidev für die SPI-Kommunikation und libgpiod für die GPIO-Steuerung.

sudo apt update
sudo apt install python3-spidev python3-libgpiod -y

Bibliothek und Beispiel-Code

Das gesamte Projekt besteht aus nur zwei Dateien: der Bibliothek selbst und einem Beispielskript, das die Anwendung demonstriert. Du findest den Code hier, kannst ihn dir aber auch aus unserem GitHub-Repository herunterladen.

a) Die Bibliotheksdatei: rc522_spi_library.py

Dies ist die eigentliche Bibliothek. Sie ist in sich geschlossen und enthält alle Klassen, Konstanten und Funktionen, um mit dem RC522 zu kommunizieren. Kopiere den folgenden Code und speichere ihn in einer Datei mit dem Namen rc522_spi_library.py.

# -*- coding: utf-8 -*-
#
# A lean Python library for the RC522 RFID reader on the Raspberry Pi 5 via SPI.
# Combines the necessary classes and constants for easy integration.
#
# Pollux Labs
# polluxlabs.net
#

import time
import logging

try:
    import gpiod
    import spidev
except ImportError:
    print("Important Note: The hardware libraries 'gpiod' and 'spidev' could not be imported.")
    print("This library is intended for use on a Raspberry Pi with the SPI interface enabled.")
    gpiod = None
    spidev = None

# --- Constants ---
# From `constants.py`

class RC522Registers:
    COMMAND_REG = 0x01
    COM_IRQ_REG = 0x04
    DIV_IRQ_REG = 0x05
    ERROR_REG = 0x06
    STATUS2_REG = 0x08
    FIFO_DATA_REG = 0x09
    FIFO_LEVEL_REG = 0x0A
    CONTROL_REG = 0x0C
    BIT_FRAMING_REG = 0x0D
    TX_CONTROL_REG = 0x14
    CRC_RESULT_REG_MSB = 0x21
    CRC_RESULT_REG_LSB = 0x22
    VERSION_REG = 0x37
    T_MODE_REG = 0x2A
    T_PRESCALER_REG = 0x2B
    T_RELOAD_REG_H = 0x2C
    T_RELOAD_REG_L = 0x2D
    MODE_REG = 0x11
    TX_AUTO_REG = 0x15

class RC522Commands:
    IDLE = 0x00
    CALC_CRC = 0x03
    TRANSCEIVE = 0x0C
    MF_AUTHENT = 0x0E
    SOFT_RESET = 0x0F

class MifareCommands:
    REQUEST_A = 0x26
    ANTICOLL_1 = 0x93
    SELECT_1 = 0x93
    HALT = 0x50
    READ = 0x30
    AUTH_A = 0x60

class StatusCodes:
    OK = 0
    ERROR = 1
    TIMEOUT = 3
    AUTH_ERROR = 5

DEFAULT_KEY = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]

# --- Exceptions ---
# From `exceptions.py`

class RC522Error(Exception):
    """Base exception for RC522 operations."""
    pass

class RC522CommunicationError(RC522Error):
    """Exception for communication errors with the RC522."""
    pass

# --- Main Class ---
# Combined and simplified logic from `rc522_reader.py`

class RC522SPILibrary:
    """
    A lean and standalone Python library for the RC522 RFID reader
    on the Raspberry Pi 5, focusing on SPI communication.
    """

    def __init__(self, spi_bus=0, spi_device=0, rst_pin=22, debug=False):
        """
        Initializes the reader.

        Args:
            spi_bus (int): The SPI bus (default: 0).
            spi_device (int): The SPI device (default: 0 for CE0).
            rst_pin (int): The GPIO pin for the reset (BCM numbering).
            debug (bool): Enables detailed log output.
        """
        self.logger = logging.getLogger(__name__)
        if debug:
            self.logger.setLevel(logging.DEBUG)
        
        if not spidev or not gpiod:
            raise RC522CommunicationError("The hardware libraries 'spidev' and 'gpiod' are not available.")

        self.spi = spidev.SpiDev()
        self.spi.open(spi_bus, spi_device)
        self.spi.max_speed_hz = 1000000  # 1 MHz
        self.spi.mode = 0

        # GPIO setup for the reset pin using gpiod
        try:
            # Chip 'gpiochip4' is for physical pins on the Pi 5.
            # For Pi 4, this might be 'gpiochip0'.
            self.gpio_chip = gpiod.Chip('gpiochip4')
            self.rst_line = self.gpio_chip.get_line(rst_pin)
            self.rst_line.request(consumer="RC522_RST", type=gpiod.LINE_REQ_DIR_OUT)
        except Exception as e:
            raise RC522CommunicationError(f"Error initializing GPIO pin via gpiod: {e}")

        self._initialized = False
        self.initialize()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.cleanup()

    def _write_register(self, reg, value):
        self.spi.xfer2([reg << 1 & 0x7E, value])

    def _read_register(self, reg):
        return self.spi.xfer2([(reg << 1 & 0x7E) | 0x80, 0])[1]

    def _set_bit_mask(self, reg, mask):
        current = self._read_register(reg)
        self._write_register(reg, current | mask)

    def _clear_bit_mask(self, reg, mask):
        current = self._read_register(reg)
        self._write_register(reg, current & (~mask))

    def _reset(self):
        """Performs a hardware reset of the RC522."""
        self.rst_line.set_value(0)
        time.sleep(0.05)
        self.rst_line.set_value(1)
        time.sleep(0.05)

    def initialize(self):
        """Initializes the RC522 chip."""
        self._reset()
        self._write_register(RC522Registers.COMMAND_REG, RC522Commands.SOFT_RESET)
        time.sleep(0.05)

        self._write_register(RC522Registers.T_MODE_REG, 0x8D)
        self._write_register(RC522Registers.T_PRESCALER_REG, 0x3E)
        self._write_register(RC522Registers.T_RELOAD_REG_L, 30)
        self._write_register(RC522Registers.T_RELOAD_REG_H, 0)
        self._write_register(RC522Registers.TX_AUTO_REG, 0x40)
        self._write_register(RC522Registers.MODE_REG, 0x3D)
        self.antenna_on()
        self._initialized = True
        self.logger.info("RC522 initialized successfully.")

    def antenna_on(self):
        if not (self._read_register(RC522Registers.TX_CONTROL_REG) & 0x03):
            self._set_bit_mask(RC522Registers.TX_CONTROL_REG, 0x03)

    def cleanup(self):
        """Resets the RC522 and releases resources."""
        if self._initialized:
            self._reset()
        if hasattr(self, 'rst_line') and self.rst_line:
            self.rst_line.release()
        if hasattr(self, 'gpio_chip') and self.gpio_chip:
            self.gpio_chip.close()
        self.spi.close()
        self.logger.info("RC522 resources have been released.")

    def _communicate_with_card(self, command, send_data, timeout=0.1):
        """Internal method for card communication."""
        irq_en = 0x77
        wait_irq = 0x30
        
        self._write_register(RC522Registers.COMMAND_REG, RC522Commands.IDLE)
        self._write_register(RC522Registers.COM_IRQ_REG, 0x7F)
        self._set_bit_mask(RC522Registers.FIFO_LEVEL_REG, 0x80)

        for byte in send_data:
            self._write_register(RC522Registers.FIFO_DATA_REG, byte)

        self._write_register(RC522Registers.COMMAND_REG, command)
        
        if command == RC522Commands.TRANSCEIVE:
            self._set_bit_mask(RC522Registers.BIT_FRAMING_REG, 0x80)

        start_time = time.time()
        while time.time() - start_time < timeout:
            n = self._read_register(RC522Registers.COM_IRQ_REG)
            if n & wait_irq:
                break
        
        self._clear_bit_mask(RC522Registers.BIT_FRAMING_REG, 0x80)

        if time.time() - start_time >= timeout:
            return StatusCodes.TIMEOUT, [], 0

        if self._read_register(RC522Registers.ERROR_REG) & 0x1B:
            return StatusCodes.ERROR, [], 0
            
        status = StatusCodes.OK
        back_data = []
        back_len = 0

        if n & 0x01:
            status = StatusCodes.ERROR

        if command == RC522Commands.TRANSCEIVE:
            fifo_size = self._read_register(RC522Registers.FIFO_LEVEL_REG)
            last_bits = self._read_register(RC522Registers.CONTROL_REG) & 0x07
            if last_bits != 0:
                back_len = (fifo_size - 1) * 8 + last_bits
            else:
                back_len = fifo_size * 8

            if fifo_size == 0:
                fifo_size = 1

            if fifo_size > 16:
                fifo_size = 16

            for _ in range(fifo_size):
                back_data.append(self._read_register(RC522Registers.FIFO_DATA_REG))

        return status, back_data, back_len

    def request(self):
        """
        Scans for cards in the antenna field.
        """
        self._write_register(RC522Registers.BIT_FRAMING_REG, 0x07)
        status, back_data, _ = self._communicate_with_card(RC522Commands.TRANSCEIVE, [MifareCommands.REQUEST_A])
        if status != StatusCodes.OK or len(back_data) != 2:
            return StatusCodes.ERROR, None
        return status, back_data

    def anticoll(self):
        """
        Performs an anti-collision procedure to get a card's UID.
        """
        self._write_register(RC522Registers.BIT_FRAMING_REG, 0x00)
        status, back_data, _ = self._communicate_with_card(RC522Commands.TRANSCEIVE, [MifareCommands.ANTICOLL_1, 0x20])
        
        if status == StatusCodes.OK and len(back_data) == 5:
            # Checksum of UID
            checksum = 0
            for i in range(4):
                checksum ^= back_data[i]
            if checksum != back_data[4]:
                return StatusCodes.ERROR, None
            return StatusCodes.OK, back_data[:4]
            
        return StatusCodes.ERROR, None

b) Die Beispieldatei: example.py

Dieses Skript importiert die soeben erstellte Bibliothek und zeigt, wie man eine Kartenerkennung und das Auslesen der UID in einer einfachen Schleife implementiert. Speichere diesen Code im selben Verzeichnis wie die Bibliotheksdatei unter dem Namen example.py.

# -*- coding: utf-8 -*-
# Pollux Labs
# polluxlabs.net

import time
import logging
# Importiere die zuvor erstellte Bibliothek
from rc522_spi_library import RC522SPILibrary, StatusCodes

# Konfiguriere das Logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def main():
    """
    Allgemeines Beispiel zum Auslesen der UID von einer beliebigen RFID-Karte.
    """
    print("Starte den RFID-Kartenleser...")
    print("Halte eine beliebige RFID-Karte vor den Reader.")
    print("Drücke STRG+C zum Beenden.")

    reader = None
    try:
        # Initialisiere die Bibliothek.
        # RST-Pin 22 (BCM) entspricht dem physischen Pin 15.
        reader = RC522SPILibrary(rst_pin=22)
        
        # Speichert die UID der zuletzt gesehenen Karte, um ständige Wiederholungen zu vermeiden
        last_uid = None

        while True:
            # 1. Suche nach einer Karte im Feld
            status, _ = reader.request()

            if status == StatusCodes.OK:
                # 2. Wenn eine Karte im Feld ist, hole ihre UID (Anti-Kollision)
                status, uid = reader.anticoll()
                
                if status == StatusCodes.OK:
                    # Reagiere nur, wenn es eine neue Karte ist
                    if uid != last_uid:
                        last_uid = uid
                        
                        # Konvertiere die UID in ein lesbares Format
                        uid_str = ":".join([f"{i:02X}" for i in uid])
                        
                        print("\n================================")
                        print(f"Karte erkannt!")
                        print(f"  UID: {uid_str}")
                        print("================================")
                        print("INFO: Du kannst diese UID nun in deinem eigenen Code verwenden.")
            else:
                # 3. Wenn keine Karte mehr im Feld ist, setze `last_uid` zurück
                if last_uid is not None:
                    print("\nKarte entfernt. Der Reader ist bereit für die nächste Karte.")
                    last_uid = None

            # Kurze Pause zur Reduzierung der CPU-Last
            time.sleep(0.1)

    except Exception as e:
        logging.error(f"Ein unerwarteter Fehler ist aufgetreten: {e}")
    except KeyboardInterrupt:
        print("\nProgramm wird beendet.")
    finally:
        # Stelle sicher, dass die Ressourcen am Ende freigegeben werden
        if reader:
            reader.cleanup()
            print("RC522-Ressourcen erfolgreich freigegeben.")

if __name__ == '__main__':
    main()

Anwendung und Test

Jetzt ist alles bereit, um den RFID-Reader zu testen.

  1. Navigiere in deinem Terminal in das Verzeichnis, in dem du die beiden Python-Dateien (rc522_spi_library.py und example.py) gespeichert hast.
  2. Führe das Beispiel-Skript aus:Bashpython3 example.py
  3. Das Programm startet und fordert dich auf, eine Karte an den Reader zu halten.
  4. Wenn du eine RFID-Karte in die Nähe des Lesegeräts hältst, sollte deren UID auf dem Bildschirm erscheinen.

Beispiel-Ausgabe:

Starte den RFID-Kartenleser...
Halte eine beliebige RFID-Karte vor den Reader.
Drücke STRG+C zum Beenden.

================================
Karte erkannt!
  UID: 4A:F3:8B:1E
================================
INFO: Du kannst diese UID nun in deinem eigenen Code verwenden.

Diese UID kannst du nun kopieren und in deinen eigenen Projekten für Zugangskontrollen, zur Identifikation von Objekten oder zum Auslösen von Aktionen verwenden.

]]>
So baust du einen ESP32 Webserver https://polluxlabs.net/esp8266-projekte/so-baust-du-einen-esp32-webserver/ Tue, 24 Jun 2025 08:55:13 +0000 https://polluxlabs.net/?p=19423 So baust du einen ESP32 Webserver Weiterlesen »

]]>
In diesem Projekt baust du dir deinen eigenen ESP32 Webserver. Hierbei arbeitest du dich schrittweise voran: vom einfachen „Hello World“ über die Steuerung einer LED bis zur Abfrage der aktuellen Temperatur. All dies stellt dein Microcontroller auf einer Webseite zur Verfügung, die du von einem Gerät deiner Wahl aufrufen kannst. Los geht’s!

Warum ein Webserver auf dem ESP32?

Der ESP32 ist ein echtes Kraftpaket. Mit seinem integrierten WLAN-Modul ist er wie geschaffen für Projekte im „Internet der Dinge“ (IoT). Ein eigener kleiner Webserver auf dem ESP32 erlaubt es euch, von jedem Gerät in eurem WLAN-Netzwerk – sei es euer Smartphone, Tablet oder Laptop – auf den Mikrocontroller zuzugreifen und ihn zu steuern. Ihr könnt Zustände abfragen, Aktionen auslösen und Daten visualisieren, und das alles über einen einfachen Webbrowser.

Teil 1: „Hallo Welt!“ – Dein erster ESP32 Webserver

Wie bei jedem guten Programmier-Tutorial starten wir auch hier mit den absoluten Grundlagen. Unser erstes Ziel: eine Webseite zu erstellen, die uns im Browser ein freundliches „Hallo Welt von deinem ESP32!“ anzeigt.

Was du hierfür brauchst:

Der Sketch

Da du für diesen ersten Test deines ESP32 Webservers außer dem Microcontroller keine weitere Hardware brauchst, kann es direkt mit dem Code losgehen. Kopiere den folgenden Sketch, trage im oberen Bereich des Sketchs deine WLAN-Zugangsdaten ein und lade ihn auf deinen ESP32:

//ESP32 Webserver
//polluxlabs.net

#include <WiFi.h>

// Deine WLAN-Zugangsdaten
const char* ssid = "DEIN_WLAN_NAME";
const char* password = "DEIN_WLAN_PASSWORT";

// Wir erstellen ein Server-Objekt auf Port 80 (Standard für HTTP)
WiFiServer server(80);

void setup() {
  Serial.begin(115200);
  while (!Serial) { ; }

  // Mit dem WLAN verbinden
  Serial.println();
  Serial.print("Verbinde mit ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WLAN verbunden.");
  Serial.println("IP-Adresse: ");
  Serial.println(WiFi.localIP());

  server.begin();
}

void loop() {
  WiFiClient client = server.available();   // Auf neue Clients warten

  if (client) {                             // Wenn sich ein Client verbindet...
    Serial.println("Neuer Client verbunden.");
    String currentLine = "";                // String, um die ankommenden Daten vom Client zu speichern
    while (client.connected()) {            // solange der Client verbunden ist...
      if (client.available()) {             // wenn der Client Daten sendet...
        char c = client.read();             // ...lese ein Byte
        Serial.write(c);
        if (c == '\n') {                    // wenn das Byte ein Zeilenumbruch ist...
          // Eine leere Zeile vom Client bedeutet das Ende der HTTP-Anfrage
          if (currentLine.length() == 0) {
            // HTTP-Header senden
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println();

            // Die eigentliche Webseite
            client.print("<h1>Hallo Welt von deinem ESP32!</h1>");
            
            // Die HTTP-Antwort endet mit einer leeren Zeile
            client.println();
            break;
          } else {    
            currentLine = "";
          }
        } else if (c != '\r') {  
          currentLine += c;      
        }
      }
    }
    // Verbindung schließen
    client.stop();
    Serial.println("Client getrennt.");
  }
}

Was passiert hier?

  1. setup(): Der ESP32 verbindet sich mit dem von dir angegebenen WLAN. Sobald die Verbindung steht, gibt er seine IP-Adresse im Seriellen Monitor aus. Diese Adresse brauchen wir gleich! Anschließend wird der Webserver gestartet.
  2. loop(): Die Schleife wartet auf eingehende Verbindungen. Sobald du die IP-Adresse in einem Browser öffnest, wird der Browser zu einem „Client“. Der ESP32 erkennt das, sendet einen simplen HTML-Code (<h1>Hallo Welt...</h1>) zurück und schließt die Verbindung wieder.

Kopiere dir die IP-Adresse deines Webservers und öffne sie in einem Browser deiner Wahl. Du solltest nun die folgende (sehr einfache) Webseite sehen:

Webseite mit Hallo Welt vom ESP32 Webserver

Teil 2: Licht an! – Steuerung einer LED über den Webserver

Nachdem die Grundlagen sitzen, wollen wir nun etwas Handfestes steuern. Eine LED ist dafür das perfekte Beispiel. Wir erweitern unseren Code so, dass auf unserer Webseite zwei simple Schaltflächen erscheinen, mit denen wir eine LED ein- und ausschalten können.

Was du zusätzlich brauchst:

  • Eine LED
  • Ein 220-Ohm-Widerstand
  • Ein Breadboard und ein paar Jumperkabel

Der Aufbau auf dem Breadboard

Verbinde die Anode (das lange Beinchen) der LED über den Widerstand mit einem GPIO-Pin des ESP32 (z.B. am Arduino ESP32 der Pin D2). Die Kathode (das kurze Beinchen) verbindest du mit GND.

Aufbau des ESP32 Webservers mit LED

Der Sketch

Du baust auf deinen ersten Sketch auf. Die größten Änderungen finden in der loop()-Funktion statt, wo wir die Anfrage des Browsers auswerten müssen.

___STEADY_PAYWALL___

//ESP32 Webserver mit LED
//polluxlabs.net

#include <WiFi.h>

// Deine WLAN-Zugangsdaten
const char* ssid = "DEIN_WLAN_NAME";
const char* password = "DEIN_WLAN_PASSWORT";

WiFiServer server(80);

// Pin für die LED definieren
const int ledPin = D2; //Am Arduino ESP32 Pin D2
String ledStatus = "aus";

void setup() {
  Serial.begin(115200);
  while (!Serial) { ; }

  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);

  // ... (WLAN-Verbindungscode aus Teil 1)
  // Mit dem WLAN verbinden
  Serial.println();
  Serial.print("Verbinde mit ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WLAN verbunden.");
  Serial.println("IP-Adresse: ");
  Serial.println(WiFi.localIP());

  server.begin();
}

void loop() {
  WiFiClient client = server.available();

  if (client) {
    Serial.println("Neuer Client verbunden.");
    String header = ""; // Speichert den Header der Anfrage
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        header += c;
        if (c == '\n') {
          if (header.indexOf("GET /led/an") >= 0) {
            digitalWrite(ledPin, HIGH);
            ledStatus = "an";
          } else if (header.indexOf("GET /led/aus") >= 0) {
            digitalWrite(ledPin, LOW);
            ledStatus = "aus";
          }
          
          if (header.endsWith("\r\n\r\n")) {
            // HTTP-Header senden
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println();

            // Die Webseite mit Schaltflächen
            client.print("<!DOCTYPE html><html><head><title>ESP32 LED Steuerung</title></head><body>");
            client.print("<h1>LED Steuerung</h1>");
            client.print("<p>Der Zustand der LED ist: <strong>" + ledStatus + "</strong></p>");
            client.print("<a href=\"/led/an\"><button>LED AN</button></a>");
            client.print("<a href=\"/led/aus\"><button>LED AUS</button></a>");
            client.print("</body></html>");
            
            client.println();
            break;
          }
        }
      }
    }
    client.stop();
    Serial.println("Client getrennt.");
  }
}

Was ist neu?

  • Wir lesen den kompletten „Header“ der Anfrage vom Browser.
  • Wir suchen im Header nach bestimmten URLs, die wir selbst definieren: /led/an und /led/aus.
  • Je nachdem, welche URL aufgerufen wird (indem ihr auf den entsprechenden Button klickt), schalten wir die LED mit digitalWrite() ein oder aus.
  • Die Webseite selbst enthält nun klickbare Links (<a>-Tags), die genau diese URLs aufrufen.

Lade den neuen Code hoch und rufe die IP-Adresse auf. Du kannst jetzt die LED über die Webseite steuern:

Webseite des ESP32 Webservs mit LED-Steuerung

Teil 3: Temperaturanzeige mit einem DHT11

Jetzt wird es richtig spannend! Wir lesen einen Sensor aus und zeigen die Daten live auf unserer Webseite an. Dafür nutzen wir den beliebten und günstigen DHT11 Temperatur- und Feuchtigkeitssensor. Auf Pollux Labs findest du jedoch auch Anleitungen für den Anschluss anderer Temperatursensoren wie den DHT22 oder den BMP180.

Was du zusätzlich brauchst:

  • Einen DHT11-Sensor
  • Die Adafruit DHT Sensor Library und die Adafruit Unified Sensor Library. Beide findest du im Bibliotheksmanager der Arduino IDE.

Der Aufbau auf dem Breadboard

Der DHT11 hat meistens vier Pins, von denen jedoch nur drei verwendet werden: VCC (oder +), DATA und GND. Orientiere dich beim Aufbau an folgender Skizze. Die LED kannst du auf dem Breadboard lassen, im nächsten Teil kombinieren wir die LED-Steuerung und die Temperaturmessung für deinen ESP32 Webserver.

  • Verbinde VCC mit dem 3.3V-Pin des ESP32.
  • Verbinde GND mit der Erde.
  • Verbinde den DATA-Pin mit einem digitalen Pin, zum Beispiel Pin D4.
Aufbau des ESP32 Webservsers mit LED und DHT11

Der Sketch

Hier der Sketch, mit dem dein ESP32 die Temperatur misst und auf der Webseite anzeigt.

//ESP32 Webserver mit Temperaturmessung
//polluxlabs.net

#include <WiFi.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>

// Deine WLAN-Zugangsdaten
const char* ssid = "DEIN_WLAN_NAME";
const char* password = "DEIN_WLAN_PASSWORT";

WiFiServer server(80);

// DHT Sensor Konfiguration
#define DHTPIN D4       // Pin, an dem der DHT11 angeschlossen ist
#define DHTTYPE DHT11  // Sensortyp
DHT dht(DHTPIN, DHTTYPE);

void setup() {
  Serial.begin(115200);
  while (!Serial) { ; }
  
  // ... (WLAN-Verbindungscode aus Teil 1)
  // Mit dem WLAN verbinden
  Serial.println();
  Serial.print("Verbinde mit ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WLAN verbunden.");
  Serial.println("IP-Adresse: ");
  Serial.println(WiFi.localIP());

  dht.begin();
  server.begin();
}

void loop() {
  WiFiClient client = server.available();

  if (client) {
    Serial.println("Neuer Client verbunden.");
    String header = "";
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        header += c;
        if (c == '\n' && header.endsWith("\r\n\r\n")) {
          // Temperatur auslesen
          float temperatur = dht.readTemperature();

          // Prüfen, ob das Auslesen erfolgreich war
          if (isnan(temperatur)) {
            Serial.println("Fehler beim Auslesen des DHT-Sensors!");
            temperatur = 0; // Standardwert bei Fehler
          }

          // HTTP-Header senden
          client.println("HTTP/1.1 200 OK");
          client.println("Content-type:text/html; charset=UTF-8"); // Wichtig für Umlaute wie °
          client.println();

          // Die Webseite mit Temperaturanzeige
          client.print("<!DOCTYPE html><html><head><title>ESP32 Wetterstation</title>");
          client.print("<meta http-equiv='refresh' content='10'>"); // Seite alle 10s neu laden
          client.print("</head><body>");
          client.print("<h1>ESP32 Mini-Wetterstation</h1>");
          client.print("<p>Aktuelle Temperatur: <strong>" + String(temperatur) + " &deg;C</strong></p>");
          client.print("</body></html>");
          
          client.println();
          break;
        }
      }
    }
    client.stop();
    Serial.println("Client getrennt.");
  }
}

Das ist neu

  • Wir binden die DHT.h-Bibliothek ein und initialisieren den Sensor.
  • In der loop() lesen wir bei jeder Anfrage die Temperatur mit dht.readTemperature() aus.
  • Wir haben einen kleinen meta-Tag in unser HTML eingefügt, der den Browser anweist, die Seite alle 10 Sekunden automatisch neu zu laden. So siehst du immer die aktuellen Werte!
  • Die Temperatur wird dann als Teil der Webseite an den Browser gesendet.

Teil 4: LED-Steuerung und Temperatur zusammen

Wir haben gelernt, wie man eine LED schaltet und wie man Sensordaten anzeigt. Jetzt bringen wir beides zusammen! Unser Ziel ist eine einzige Webseite, auf der wir die aktuelle Raumtemperatur sehen und gleichzeitig eine LED ein- und ausschalten können. Das ist wie eine kleine, feine Smart-Home-Zentrale.

Auf dem Breadboard musst du hierfür nichts verändern – du wirst nur den Sketch anpassen:

//ESP32 Webserver mit LED-Steuerung und Temperaturmessung
//polluxlabs.net

#include <WiFi.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>

// Deine WLAN-Zugangsdaten
const char* ssid = "DEIN_WLAN_NAME";
const char* password = "DEIN_WLAN_PASSWORT";

WiFiServer server(80);

// --- Konfiguration für die LED ---
const int ledPin = 2;
String ledStatus = "aus";

// --- Konfiguration für den DHT Sensor ---
#define DHTPIN 4       // Pin, an dem der DHT11 angeschlossen ist
#define DHTTYPE DHT11  // Sensortyp
DHT dht(DHTPIN, DHTTYPE);

void setup() {
  Serial.begin(115200);
  while (!Serial) { ; }
  
  // --- Initialisierung für die LED ---
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);

  // --- Initialisierung für den DHT Sensor ---
  dht.begin();
  
  // Mit dem WLAN verbinden (Code aus den vorherigen Teilen)
  Serial.println();
  Serial.print("Verbinde mit ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WLAN verbunden.");
  Serial.println("IP-Adresse: ");
  Serial.println(WiFi.localIP());

  server.begin();
}

void loop() {
  WiFiClient client = server.available();

  if (client) {
    Serial.println("Neuer Client verbunden.");
    String header = "";
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        header += c;

        // Wenn die Anfrage des Browsers komplett empfangen wurde
        if (c == '\n' && header.endsWith("\r\n\r\n")) {

          // --- URL auswerten für die LED-Steuerung ---
          if (header.indexOf("GET /led/an") >= 0) {
            digitalWrite(ledPin, HIGH);
            ledStatus = "an";
          } else if (header.indexOf("GET /led/aus") >= 0) {
            digitalWrite(ledPin, LOW);
            ledStatus = "aus";
          }

          // --- Temperatur vom DHT-Sensor auslesen ---
          float temperatur = dht.readTemperature();
          if (isnan(temperatur)) {
            Serial.println("Fehler beim Auslesen des DHT-Sensors!");
            temperatur = 0; // Standardwert bei Fehler
          }

          // --- HTTP-Antwort & kombinierte Webseite senden ---
          client.println("HTTP/1.1 200 OK");
          client.println("Content-type:text/html; charset=UTF-8");
          // Wichtiger Hinweis: Die folgende Zeile sorgt für einen Auto-Refresh.
          // Das ist super für die Temperatur, setzt aber auch den angezeigten LED-Status
          // alle 10 Sekunden auf den echten Zustand zurück.
          client.println("Refresh: 10");
          client.println(); // Leere Zeile nach den Headern

          // Die eigentliche Webseite
          client.print("<!DOCTYPE html><html><head><title>ESP32 Kombi-Steuerung</title>");
          client.print("<style> body { font-family: sans-serif; } button { padding: 10px; font-size: 16px; } </style>");
          client.print("</head><body>");
          
          client.print("<h1>ESP32 Kombi-Steuerung</h1>");
          
          // Abschnitt für den Sensor
          client.print("<h2>Mini-Wetterstation</h2>");
          client.print("<p>Aktuelle Temperatur: <strong>" + String(temperatur) + " &deg;C</strong></p>");

          // Abschnitt für die LED
          client.print("<h2>LED Steuerung</h2>");
          client.print("<p>Der Zustand der LED ist: <strong>" + ledStatus + "</strong></p>");
          client.print("<a href=\"/led/an\"><button>LED AN</button></a>");
          client.print("<a href=\"/led/aus\"><button>LED AUS</button></a>");

          client.print("</body></html>");
          
          client.println();
          break; // Schleife verlassen, da die Antwort gesendet wurde
        }
      }
    }
    client.stop();
    Serial.println("Client getrennt.");
  }
}

Was ist neu?

  1. Variablen zusammengeführt: Wir haben die Deklarationen für den ledPin und den ledStatus ganz oben zu denen für den DHT-Sensor hinzugefügt.
  2. setup() erweitert: Im setup() wird jetzt nicht nur der Sensor, sondern auch der LED-Pin initialisiert.
  3. Der loop(): Das ist der wichtigste Teil. Bevor die Webseite zusammengebaut wird, passiert jetzt beides:
    • Zuerst wird wie in Teil 2 geprüft, ob die aufgerufene URL /led/an oder /led/aus enthält, und die LED wird entsprechend geschaltet.
    • Danach wird wie in Teil 3 die Temperatur vom Sensor ausgelesen.
  4. HTML kombiniert: Der HTML-Code, der an den Browser gesendet wird, enthält jetzt einfach beide Abschnitte – die Temperaturanzeige und die Schaltflächen für die LED. Es wurde auch ein paar simple <h2>-Überschriften zur Strukturierung und ein winziges bisschen CSS für die Buttons hinzugefügt.

Wenn du diesen Code nun hochlädst und die IP-Adresse im Browser aufrufst, siehst du eine Webseite, die dir die Temperatur anzeigt und dir gleichzeitig erlaubt, das Licht ein- und auszuschalten.

Teil 5: Der Feinschliff – Eine moderne und responsive Oberfläche

Dein ESP32 Webserver funktioniert also – nur die Webseite könnte etwas moderner sein. Hier kommt etwas CSS (Cascading Style Sheets) zum Einsatz. Die Schaltung bleibt hingegen genau dieselbe wie in Teil 4.

Der finale Code mit schicker Oberfläche: Lade diesen Code auf deinen ESP32. Das Design der Webseite passiert in den langen client.print()-Zeilen, die den HTML- und CSS-Code enthalten.

//ESP32 Webserver mit LED-Steuerung und Temperaturmessung//polluxlabs.net

#include <WiFi.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>

// Deine WLAN-Zugangsdaten
const char* ssid = "DEIN_WLAN_NAME";
const char* password = "DEIN_WLAN_PASSWORT";

WiFiServer server(80);

// --- Konfiguration für die LED ---
const int ledPin = 2;

// --- Konfiguration für den DHT Sensor ---
#define DHTPIN 4       // Pin, an dem der DHT11 angeschlossen ist
#define DHTTYPE DHT11  // Sensortyp
DHT dht(DHTPIN, DHTTYPE);

void setup() {
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);
  dht.begin();
  
  // Mit dem WLAN verbinden
  Serial.println();
  Serial.print("Verbinde mit ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WLAN verbunden.");
  Serial.println("IP-Adresse: ");
  Serial.println(WiFi.localIP());

  server.begin();
}

void loop() {
  WiFiClient client = server.available();

  if (client) {
    String header = "";
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        header += c;

        if (c == '\n' && header.endsWith("\r\n\r\n")) {
          // --- URL auswerten für die LED-Steuerung ---
          if (header.indexOf("GET /led/an") >= 0) {
            digitalWrite(ledPin, HIGH);
          } else if (header.indexOf("GET /led/aus") >= 0) {
            digitalWrite(ledPin, LOW);
          }

          // --- Aktuellen Status von LED und Sensor auslesen ---
          String ledStatus = digitalRead(ledPin) ? "an" : "aus";
          float temperatur = dht.readTemperature();

          if (isnan(temperatur)) {
            Serial.println("Fehler beim Auslesen des DHT-Sensors!");
            temperatur = 0.0;
          }

          // --- HTTP-Antwort & moderne Webseite senden ---
          client.println("HTTP/1.1 200 OK");
          client.println("Content-type:text/html; charset=UTF-8");
          client.println();

          client.print("<!DOCTYPE html>");
          client.print("<html lang='de'>");
          client.print("<head>");
          client.print("<meta charset='UTF-8'>");
          // Diese Zeile ist ENTSCHEIDEND für die korrekte Darstellung auf Smartphones!
          client.print("<meta name='viewport' content='width=device-width, initial-scale=1.0'>");
          client.print("<title>Pollux Labs - ESP32 Steuerung</title>");
          client.print("<meta http-equiv='refresh' content='10'>");
          // --- HIER BEGINNT DAS CSS STYLING ---
          client.print("<style>");
          client.print("body{background-color:#1e1e1e;color:#e0e0e0;font-family:system-ui,sans-serif;text-align:center;margin:0;padding:20px;}");
          client.print(".container{max-width:800px;margin:auto;display:flex;flex-wrap:wrap;justify-content:center;gap:20px;}");
          client.print(".card{background-color:#2a2a2a;border-radius:15px;padding:20px;box-shadow:0 4px 8px rgba(0,0,0,0.2);flex-basis:300px;flex-grow:1;}");
          client.print("h1{color:#00aaff;}");
          client.print("h2{border-bottom:2px solid #00aaff;padding-bottom:10px;margin-top:0;}");
          client.print(".sensor-value{font-size:3.5rem;font-weight:bold;color:#fff;}");
          client.print(".unit{font-size:1.5rem;color:#00aaff;}");
          client.print(".led-status{font-size:1.2rem;margin-bottom:20px;}");
          client.print(".buttons{display:flex;gap:15px;justify-content:center;}");
          client.print("a.button{text-decoration:none;color:#fff;padding:15px 30px;border-radius:10px;font-weight:bold;transition:transform 0.2s ease;}");
          client.print(".on-button{background-color:#28a745;}"); // Grün
          client.print(".off-button{background-color:#dc3545;}"); // Rot
          client.print("a.button:active{transform:scale(0.95);}");
          client.print("footer{color:#555;margin-top:40px;}");
          client.print("</style>");
          // --- HIER ENDET DAS CSS STYLING ---
          
          client.print("</head>");
          client.print("<body>");
          
          client.print("<h1>Pollux Labs ESP32 Steuerung</h1>");
          client.print("<div class='container'>");
          
          // Karte für den Temperatursensor
          client.print("<div class='card'>");
          client.print("<h2>Temperatur</h2>");
          client.print("<p class='sensor-value'>" + String(temperatur, 1) + "<span class='unit'>&deg;C</span></p>");
          client.print("</div>");
          
          // Karte für die LED-Steuerung
          client.print("<div class='card'>");
          client.print("<h2>Beleuchtung</h2>");
          client.print("<p class='led-status'>Status: <strong>" + ledStatus + "</strong></p>");
          client.print("<div class='buttons'>");
          client.print("<a href='/led/an' class='button on-button'>EINSCHALTEN</a>");
          client.print("<a href='/led/aus' class='button off-button'>AUSSCHALTEN</a>");
          client.print("</div>");
          client.print("</div>");
          
          client.print("</div>"); // Ende .container
          client.print("<footer>Diese Seite wird alle 10 Sekunden automatisch aktualisiert.</footer>");
          client.print("</body></html>");
          
          client.println();
          break;
        }
      }
    }
    client.stop();
  }
}

Das Ergebnis

Wenn du jetzt die IP-Adresse deines ESP32 aufrufst, wirst du mit einer modernen Oberfläche begrüßt, die in etwa so aussieht:

Webseite des ESP32 Webservers

Was ist neu und warum?

  • <meta name='viewport'...>: Diese eine Zeile im <head> ist der Schlüssel für responsives Design. Sie sagt dem Smartphone-Browser: „Hey, betrachte die Webseite nicht als winzige Desktop-Seite, sondern passe die Breite an den Bildschirm an und starte ohne Zoom.“
  • .card: Jede Funktion hat ihre eigene „Karte“. Das schafft eine tolle optische Trennung. Durch display:flex im .container ordnen sich die Karten auf großen Bildschirmen nebeneinander an und auf schmalen Bildschirmen automatisch untereinander.
  • .sensor-value: Wir geben der reinen Temperaturzahl eine eigene Klasse, um sie mit font-size riesig und fett darzustellen – so sticht der wichtigste Wert sofort ins Auge.
  • Farbige Buttons: Die Buttons sind jetzt nicht nur größer und rund, sondern auch farbig. Grün für „AN“, Rot für „AUS“. Das ist intuitiv und sieht professionell aus.

Fühl dich frei, mit den Farbwerten (#1e1e1e, #00aaff, etc.) im CSS-Teil zu experimentieren und der Seite deinen ganz persönlichen Anstrich zu geben. Viel Spaß mit deinem neuen, schicken Web-Interface!

Fazit

Herzlichen Glückwunsch! Du hast dich vom einfachen „Hallo Welt“ über die LED-Steuerung bis hin zur Anzeige von Live-Sensordaten hochgearbeitet. Du hast jetzt eine solide Grundlage, um deine eigenen, viel komplexeren IoT-Projekte zu realisieren. Stell dir vor, du könntest die Heizung fernsteuern, die Rollläden automatisieren oder eine E-Mail erhalten, wenn die Pflanzen Wasser braucht – all das ist mit dem Wissen, das du heute gelernt hast, nicht weit entfernt.

]]>
Arduino RFID: Der Einstieg mit dem RC522-Reader https://polluxlabs.net/arduino-tutorials/arduino-rfid-der-einstieg-mit-dem-rc522-reader/ Mon, 23 Jun 2025 08:57:01 +0000 https://polluxlabs.net/?p=19411 Arduino RFID: Der Einstieg mit dem RC522-Reader Weiterlesen »

]]>
In diesem Tutorial tauchen wir in eine faszinierende Technologie ein, die uns im Alltag ständig begegnet: RFID. Ob beim kontaktlosen Bezahlen, bei der Zeiterfassung oder in modernen Schließsystemen – die kleinen Chips sind überall. Aber hast du dich schon mal gefragt, wie das eigentlich funktioniert? Und noch besser: Wie du es selbst für deine eigenen Projekte nutzen kannst?

Du wirst einen RFID-Reader an einen Arduino anschließen und die einzigartige ID von RFID-Tags und -Karten auslesen. Also, schnapp dir deinen Arduino und lass uns loslegen!

Was ist RFID eigentlich?

RFID steht für „Radio-Frequency Identification“, also die Identifizierung mithilfe von Radiowellen. Ein RFID-System besteht immer aus zwei Teilen:

  1. Einem Transponder (der Tag oder die Karte): Dieser enthält einen winzigen Chip mit einer Antenne. Auf dem Chip ist eine eindeutige Kennung (UID – Unique Identifier) gespeichert.
  2. Einem Lesegerät (der Reader): Dieses erzeugt ein elektromagnetisches Feld. Kommt ein Transponder in die Nähe dieses Feldes, wird er mit Energie versorgt und sendet seine Informationen zurück an das Lesegerät.

Das Schöne daran ist, dass dies alles kontaktlos und ohne Sichtverbindung funktioniert.

Für dieses Arduino RFID Tutorial brauchst du

  • Ein Arduino-Board (im Folgenden ein Arduino Uno)
  • Ein RFID-RC522 Modul (meistens kommen hier schon eine Karte und ein Schlüsselanhänger-Tag mit)
  • Ein Breadboard & Kabel

Schritt 1: Die Verkabelung

Das Herzstück ist das RC522-Modul. Es kommuniziert mit dem Arduino über eine Schnittstelle namens SPI (Serial Peripheral Interface).

Wichtiger Hinweis: Das RC522-Modul arbeitet mit 3.3V! Schließe es auf keinen Fall an den 5V-Pin deines Arduinos an, da es sonst beschädigt werden könnte.

Verbinde die Pins des RC522-Moduls wie folgt mit deinem Arduino Uno:

RC522 PinArduino Uno PinBeschreibung
3.3V3.3VSpannungsversorgung
RSTPin 9Reset-Pin
GNDGNDMasse
MISOPin 12Master In, Slave Out (SPI)
MOSIPin 11Master Out, Slave In (SPI)
SCKPin 13Serial Clock (SPI)
SDAPin 10Slave Select (SPI)
IRQ(wird nicht verbunden)

Nimm dir für die Verkabelung einen Moment Zeit und prüfe alles doppelt. Eine saubere Verkabelung ist die halbe Miete!

Schritt 2: Die Bibliothek installieren

Damit dein Arduino versteht, wie er mit dem RFID-Modul sprechen soll, benötigst du eine passende Bibliothek. Für das RC522-Modul gibt es eine sehr beliebte und gut gepflegte Bibliothek von „GitHubCommunity“.

So installierst du sie ganz einfach über die Arduino IDE:

  1. Öffne deine Arduino IDE.
  2. Gehe zum Menüpunkt Werkzeuge > Bibliotheken verwalten….
  3. Gib in das Suchfeld MFRC522 ein.
  4. Suche nach der Bibliothek „MFRC522 by GithubCommunity“ und klicke auf Installieren.

Das war’s schon! Jetzt kennt dein Arduino die Befehle, um mit dem RFID-Modul zu kommunizieren.

Schritt 3: Einen Tag auslesen

Jetzt wird es spannend! Hier kommt der Sketch, um die einzigartige ID eines Tags auszulesen. Diese UID ist quasi der Personalausweis des Chips, den du in deinen Projekten verwenden kannst.

Öffne die Arduino IDE und füge den folgenden Code ein:

// Arduino RFID
//polluxlabs.net

#include <SPI.h>
#include <MFRC522.h>

#define RST_PIN 9          // Reset-Pin, den wir an Pin 9 angeschlossen haben
#define SS_PIN 10          // Slave-Select-Pin, den wir an Pin 10 angeschlossen haben

// Erstellen einer Instanz des MFRC522-Objekts
MFRC522 mfrc522(SS_PIN, RST_PIN);  

void setup() {
  Serial.begin(9600);   // Starten der seriellen Kommunikation für die Ausgabe am PC
  while (!Serial);      // Warten, bis der serielle Monitor bereit ist
  SPI.begin();          // Initialisieren der SPI-Kommunikation
  mfrc522.PCD_Init();   // Initialisieren des RFID-Moduls
  Serial.println("Halte einen RFID-Tag an das Lesegerät...");
}

void loop() {
  // Suche nach neuen Karten
  if ( ! mfrc522.PICC_IsNewCardPresent()) {
    return;
  }

  // Wähle eine der Karten aus
  if ( ! mfrc522.PICC_ReadCardSerial()) {
    return;
  }

  // Zeige die UID im Seriellen Monitor an
  Serial.print("UID des Tags: ");
  String content= "";
  for (byte i = 0; i < mfrc522.uid.size; i++) {
     Serial.print(mfrc522.uid.uidByte[i] < 0x10 ? " 0" : " ");
     Serial.print(mfrc522.uid.uidByte[i], HEX);
     content.concat(String(mfrc522.uid.uidByte[i] < 0x10 ? " 0" : " "));
     content.concat(String(mfrc522.uid.uidByte[i], HEX));
  }
  Serial.println();
  Serial.println("------------");
  
  // Halte die Kommunikation an, damit derselbe Tag nicht ständig neu gelesen wird
  mfrc522.PICC_HaltA();
}

Was macht dieser Code?

  • #include: Wir binden die benötigten Bibliotheken für SPI und das RFID-Modul ein.
  • #define: Wir legen fest, welche Arduino-Pins wir für RST und SS verwenden.
  • setup(): Hier wird die serielle Kommunikation gestartet und das RFID-Modul initialisiert.
  • loop(): Die Hauptschleife wartet darauf, dass ein neuer Tag (PICC_IsNewCardPresent()) in Reichweite kommt. Wenn einer erkannt wird, wird seine UID ausgelesen (PICC_ReadCardSerial()) und anschließend in einer lesbaren Form (Hexadezimal) auf dem Seriellen Monitor ausgegeben.

Lade den Sketch auf deinen Arduino hoch und halte nun eine RFID-Karte oder den Schlüsselanhänger an dein Modul. Voilà! Du solltest die UID des Tags auf dem Monitor sehen.

Wie geht es weiter?

Glückwunsch! Du hast soeben deinen eigenen RFID-Reader in Betrieb genommen und gelernt, wie man die ID eines Tags ausliest. Allein damit sind die Möglichkeiten schon riesig:

  • Baue ein smartes Türschloss: Speichere die UID deines Tags im Code. Wird diese UID erkannt, steuert der Arduino ein Servo an, um ein Schloss zu öffnen.
  • Erstelle eine Musikbox: Weise verschiedenen UIDs im Code unterschiedliche Lieder zu. Je nachdem, welchen Tag du an den Reader hältst, wird eine andere Melodie abgespielt.
  • Ein personalisiertes Begrüßungssystem: Der Arduino erkennt deine UID und begrüßt dich mit Namen auf einem LCD-Display.

Experimentiere mit dem Code und entwickle deine eigenen Ideen.

]]>
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

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!

]]>
Die Zimmer lüften mit dem Arduino https://polluxlabs.net/arduino-projekte/die-zimmer-lueften-mit-dem-arduino/ Thu, 05 Dec 2024 09:22:06 +0000 https://polluxlabs.net/?p=18059 Die Zimmer lüften mit dem Arduino Weiterlesen »

]]>
Gehörst du zu den Leuten, die gerne die Zimmer lüften, aber dann vergessen, dass das Fenster offen ist? Mir passiert das regelmäßig – bis jetzt. In diesem Projekt überwacht ein Arduino Nano die Temperatur, sobald du das Fenster zum Lüften öffnest. Fällt die Temperatur im Raum um 0,5°C schlägt er Alarm. So kannst du sicher sein, dass der Raum oder sogar die ganze Wohnung nicht unnötig auskühlt.

Diese Bauteile benötigst du:

  • Arduino Nano
  • Temperatursensor DHT22
  • Piezo-Summer
  • Button
  • 2x 10kΩ Widerstände
  • Breadboard & Kabel

So funktioniert die Temperaturüberwachung

Wenn du das Fenster öffnest, drückst du einen Button am Arduino – damit wird der Startwert der Temperatur festgelegt und die Messung beginnt. Während nun (mal abgesehen vom Sommer) kühlere Luft ins Zimmer gelangt, fällt die Temperatur darin kontinuierlich. Sobald die Zimmertemperatur um 0,5°C gefallen ist, schlägt der Piezo-Summer Alarm und ruft dich ans geöffnete Fenster, um es zu schließen. Du drückst den Button erneut: der Alarm hört auf und der Arduino wird wieder in den Wartezustand zurückgesetzt – bis zum nächsten Lüften.

Natürlich kannst du den Wert von 0,5°C ändern und so einstellen, dass er zu deinen Wünschen, zur Positionierung des Arduinos im Zimmer etc. passt.

Aufgebauter Arduino Temperaturwächter

So baust du das Projekt zusammen

Orientiere dich beim Aufbau an der folgenden Skizze. Ich habe für dieses Projekt wegen seiner kompakten Größe einen Arduino Nano verwendet. Du kannst aber natürlich auch ein anderes Board verwenden, das du gerade zur Hand hast – also zum Beispiel einen Arduino UNO. Auch muss es nicht unbedingt der Sensor DHT22 sein – in diesem Tutorial lernst du, wie du den „kleinen Bruder“ DHT11 am Arduino anschließt und verwendest.

Aufbau des Projekts mit dem Arduino Nano und weiteren Bauteilen

Wenn du alles verkabelt hast, kann es mit dem Sketch weitergehen.

Der Sketch für das Projekt

Bevor du den folgenden Sketch kopierst und auf deinen Arduino hochlädst, benötigst du noch die passenden Bibliotheken für den Sensor DHT22 – falls du in der Arduino IDE noch nie ein Projekt mit diesem Sensor umgesetzt hast.

Öffne in diesem Fall den Bibliotheksmanager in der IDE und suche nach DHT sensor library und klicke auf Installieren. In einem Dialogfenster wirst du daraufhin gefragt, ob du die Bibliothek Adafruit Unified Sensor gleich mitinstallieren möchtest. Bestätige das mit einem Klick auf Ja.

Nun zum vollständigen Sketch:

___STEADY_PAYWALL___

#include <DHT.h>

// Pins
#define DHTPIN 7       // DHT22 an Pin D7
#define BUZZER_PIN 4   // Piezo-Summer an Pin D4
#define BUTTON_PIN 6   // Taster an Pin D6
#define LED_PIN 13     // Interne LED an Pin D13

// DHT Sensor
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

// Variablen
float startTemperature = 0.0;
bool measuring = false;
bool alarmActive = false;

void setup() {
  Serial.begin(9600);
  dht.begin();
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP); // Interner Pull-Up-Widerstand aktivieren
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(BUZZER_PIN, LOW);
  digitalWrite(LED_PIN, LOW); // LED initial ausgeschaltet
}

void loop() {
  // Taster prüfen (wechseln zwischen Messung und Leerlauf)
  if (digitalRead(BUTTON_PIN) == LOW) { // Taster gedrückt
    delay(50); // Entprellung
    // Sicherstellen, dass der Taster weiterhin gedrückt ist
    if (digitalRead(BUTTON_PIN) == LOW) {
      if (!measuring) {
        // Messung starten
        measuring = true;
        alarmActive = false; // sicherstellen, dass der Alarm deaktiviert ist
        startTemperature = dht.readTemperature();
        if (isnan(startTemperature)) {
          Serial.println("Fehler beim Lesen des DHT22!");
          startTemperature = 0.0; // Standardwert
        }
        Serial.print("Messung gestartet. Starttemperatur: ");
        Serial.println(startTemperature);
        digitalWrite(LED_PIN, HIGH); // LED einschalten
      } else {
        // In Leerlauf wechseln: Alarm und Messung stoppen
        noTone(BUZZER_PIN); // Alarmton stoppen
        digitalWrite(LED_PIN, LOW); // LED ausschalten
        alarmActive = false; 
        measuring = false;
        Serial.println("Alarm ausgeschaltet und Leerlauf aktiviert.");
      }
      // Warten bis der Button losgelassen wird
      while (digitalRead(BUTTON_PIN) == LOW);
      delay(50); // Entprellzeit nach Loslassen
    }
  }
  
  // Messlogik, wenn aktiv
  if (measuring) {
    float currentTemperature = dht.readTemperature();
    if (isnan(currentTemperature)) {
      Serial.println("Fehler beim Lesen des DHT22!");
      delay(2000);
      return;
    }

    // Temperaturänderung prüfen
    if (currentTemperature <= startTemperature - 0.5) {
      alarmActive = true; // Alarm auslösen
    }

    // Alarm ausführen
    if (alarmActive) {
      tone(BUZZER_PIN, 1000); // Alarmton
      Serial.println("Alarm: Temperatur ist gesunken!");
    } else {
      noTone(BUZZER_PIN); // Alarmton deaktivieren
    }

    // Debug-Ausgabe
    Serial.print("Aktuelle Temperatur: ");
    Serial.println(currentTemperature);

    delay(2000); // Messung alle 2 Sekunden
  }
}

So funktioniert der sketch

Lass uns einen genaueren Blick auf den Sketch werfen. Zunächst bindest du die Bibliothek für den Temperatursensor ein und legst die Anschlüsse der Bauteile fest:

#include <DHT.h>

#define DHTPIN 7       // DHT22 an Pin D7
#define BUZZER_PIN 4   // Piezo-Summer an Pin D4
#define BUTTON_PIN 6   // Taster an Pin D6
#define LED_PIN 13     // Interne LED an Pin D13

#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

Weiter geht es mit den benötigten Variablen. In startTemperature wird die Temperatur zu Beginn deiner Messung – also sobald du auf den Button drückst – erfasst. Die beiden Variablen measuring und alarmActive dienen später der Programmsteuerung.

float startTemperature = 0.0;
bool measuring = false;
bool alarmActive = false;

Setup-Funktion

Hier startest du den Seriellen Monitor und initialisierst den Sensor. Außerdem legst du die benötigten pinModes fest und aktivierst für den Button den internen Pullup-Widerstand des Arduinos. Zuletzt stellst du sicher, dass der Piezo-Summer und die interne LED ausgeschaltet sind.

void setup() {

  Serial.begin(9600);
  dht.begin();
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP); // Interner Pull-Up-Widerstand aktivieren
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(BUZZER_PIN, LOW);
  digitalWrite(LED_PIN, LOW); // LED initial ausgeschaltet
}

Loop-Funktion

Das ist der Hauptteil des Programms, der kontinuierlich läuft. Hier wird entschieden, ob die Messung durchgeführt wird oder nicht:

if (digitalRead(BUTTON_PIN) == LOW) {

  // Wurde der Button gedrückt? Dann beginnt die Messung, falls sie nicht bereits läuft.

}

Bei einem Druck auf den Button passiert Folgendes: Wenn die Messung nicht läuft (die Variable measuring ist false), wird sie gestartet. Die Variable measuring wird dann auf true gesetzt.

Wenn die Messung allerdings läuft, wird sie gestoppt. Der Alarm wird deaktiviert und die LED ausgeschaltet. Die Variable measuring wird wieder auf false gesetzt. Der Arduino befindet sich nun sozusagen im Leerlauf.

In der Messung selbst wird alle 2 Sekunden geprüft, ob die aktuelle Temperatur mehr als 0,5°C unterhalb der Starttemperatur liegt. Ist das der Fall, wird der Alarm aktiviert:

// Temperaturänderung prüfen
if (currentTemperature <= startTemperature - 0.5) {
  alarmActive = true; // Alarm auslösen
}

Hierfür wird die Variable alarmActive auf true gesetzt, was dazu führt, dass in der darauffolgenden Abfrage der Piezo-Summer aktiviert wird:

if (alarmActive) {
  tone(BUZZER_PIN, 1000); // Alarmton
  Serial.println("Alarm: Temperatur ist gesunken!");
} else {
  noTone(BUZZER_PIN); // Alarmton deaktivieren
}

Hier wird geprüft, ob alarmActive == true ist (in einer verkürzten Schreibweise) und der Piezo entsprechend aktiviert oder deaktiviert.

Wie geht es weiter?

Du hast nun einen Arduino, der laut Alarm schlägt, sobald die Temperatur entsprechend deiner Einstellung gefallen ist. Statt einen Piezo-Summers kannst du aber natürlich auch eine andere Benachrichtigung wählen. Wie wäre es z.B. mit einer Push Notification auf deinem Smartphone? Hierfür benötigst du allerdings einen Microcontroller mit WLAN, wie z.B. den ESP8266 oder ESP32.

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

]]>
ESP32 Internetradio https://polluxlabs.net/esp8266-projekte/esp32-internetradio/ Wed, 07 Aug 2024 19:01:34 +0000 https://polluxlabs.net/?p=16902 ESP32 Internetradio Weiterlesen »

]]>
Radio übers Internet zu hören, ist heute natürlich nichts Besonderes mehr – mit einem selbstgebauten ESP32 Internetradio allerdings schon! In diesem Tutorial baust du dir deinen eigenen Empfänger, mit dem du deine Lieblingssender hören kannst. Das Projekt ist mehrstufig aufgebaut – du erweiterst dein Internetradio nach und nach um weitere Bauteile und Funktionen, bis du ein vollwertiges Gerät mit Senderwahl, Display und Lautstärkeregelung hast.

Höre dir vorab die Projekt-Vorstellung an:

Diese Bauteile benötigst du:

Update: In diesem Tutorial lernst du zunächst, wie du ein Radio baust, das „nur“ einen Sender empfängt und abspielt. Weiter unten findest du die Erweiterung für ein ESP32 Internetradio mit Senderwahl und Display.

Aufbau des ESP32 Internetradios

Zunächst, wie angekündigt, die einfachste Schaltung für dein Internetradio. Hierbei benötigst du nur den ESP32-S3 Zero (z.B. von Waveshare), das Verstärkermodul und einen Lautsprecher. Orientiere dich beim Aufbau an folgender Skizze:

ESP32 Internetradio v1

Hier noch einmal als Tabelle:

VerstärkermodulESP32-S3
VIN5V
GNDGND
GAIN
DINGPIO 2
BCLKGPIO 3
LRCGPIO 4

Kurzer Exkurs zum Gain

Über den Gain-Pin am Verstärkermodul kannst du die Lautstärke steuern – unterschiedlich stark, je nachdem, wie du ihn anschließt:

  • 15dB mit einem 100kΩ Widerstand zwischen GAIN und GND
  • 12dB wenn GAIN direkt an GND angeschlossen ist
  • 9dB wenn GAIN überhaupt nicht verbunden ist (wie in der Skizze oben)
  • 6dB wenn GAIN mit Vin verbunden ist
  • 3dB mit einem 100kΩ Widerstand zwischen GAIN und Vin

Für deinen Lautsprecher hat das Verstärkermodul eine Buchse, in der du mit einer Schraube die Kabel fixieren kannst. Aufgebaut könnte dein Radio so aussehen:

Foto des ersten ESP32 Internetradios

Der Sketch für das Radio

Zentral für dein Internetradio ist eine Bibliothek, die du über den Bibliotheksverwalter der Arduino IDE installieren kannst. Suche dort nach ESP32-audioI2S und installiere die aktuelle Version.

Wichtiger Hinweis: Die folgenden Sketches benötigen die Bibliothek in der Version 3.4.1 – aber der Version 3.4.2 werden Infos (z.B. zum aktuell gespielten Song) anders bereitgestellt, was ich noch nicht berücksichtigt habe. Wähle bitte im Bibliotheksmanager die Version 3.4.1 aus.

Kopiere nun den folgenden Sketch, ergänze die Zugangsdaten für dein WLAN-Netz und lade ihn auf deinen ESP32 hoch. Solltest du hierbei Probleme mit dem ESP32-S3 Zero haben, prüfe, ob du das richtige Board ausgewählt hast. Funktionieren sollte es mit ESP32 S3 Dev Module. Schaue auch nach, ob du im Menü Tools den Eintrag USB CDC On Boot: „Enabled“ siehst. Falls dieser auf Disabled steht, ändere ihn entsprechend.

Falls du noch nie einen ESP32 mit der Arduino IDE programmiert hast, wirf zunächst einen Blick dieses Tutorial.

//ESP32 Internetradio
//https://polluxlabs.net

#include "Arduino.h"
#include "WiFi.h"
#include "Audio.h"

// Verbindungen ESP32 <-> Verstärkermodul
#define I2S_DOUT  2
#define I2S_BCLK  3
#define I2S_LRC   4

Audio audio;

// WLAN Zugangsdaten
String ssid =    "DEIN NETZWERK";
String password = "DEIN PASSWORT";

void setup() {

  Serial.begin(115200);

  WiFi.disconnect();
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid.c_str(), password.c_str());

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  Serial.println("");

  audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);

  // Lautstärke (0-100)
  audio.setVolume(10);

  // Verbindung zum gewünschten Stream, z.B. Byte.fm
  audio.connecttohost("http://www.byte.fm/stream/bytefm.m3u");

}

void loop()

{
  audio.loop();

}

// Print der Senderinfos

void audio_info(const char *info) {
  Serial.print("info        "); Serial.println(info);
}
void audio_id3data(const char *info) { //id3 metadata
  Serial.print("id3data     "); Serial.println(info);
}
void audio_eof_mp3(const char *info) { //end of file
  Serial.print("eof_mp3     "); Serial.println(info);
}
void audio_showstation(const char *info) {
  Serial.print("station     "); Serial.println(info);
}
void audio_showstreaminfo(const char *info) {
  Serial.print("streaminfo  "); Serial.println(info);
}
void audio_showstreamtitle(const char *info) {
  Serial.print("streamtitle "); Serial.println(info);
}
void audio_bitrate(const char *info) {
  Serial.print("bitrate     "); Serial.println(info);
}
void audio_commercial(const char *info) { //duration in sec
  Serial.print("commercial  "); Serial.println(info);
}
void audio_icyurl(const char *info) { //homepage
  Serial.print("icyurl      "); Serial.println(info);
}
void audio_lasthost(const char *info) { //stream URL played
  Serial.print("lasthost    "); Serial.println(info);
}
void audio_eof_speech(const char *info) {
  Serial.print("eof_speech  "); Serial.println(info);
}

Wenn alles funktioniert, solltest du nach dem Start des ESP32 Internetradios den Sender Byte.fm hören – ein mitgliederfinanzierter Sender aus Hamburg, der abseits des Mainstreams Programmradio macht – ohne Werbung.

Hinweis zum Speicherplatz: Falls du beim Upload eine Fehlermeldung erhältst, dass der Sketch den verfügbaren Speicherplatz deines ESP32 überschreitet, wirf bitte kurz einen Blick auf diesen Hinweis.

So funktioniert der Sketch

Zunächst bindest du wie immer einige Bibliotheken ein. Die ersten beiden sind hierbei bereits in deiner Arduino IDE verfügbar, die Bibliothek Audio.h hast du wie oben beschrieben heruntergeladen und in der IDE eingebunden.

Anschließend definierst du, an welchen Pins das Verstärkermodul am ESP32 angeschlossene ist. Diese kannst du natürlich frei wählen, vergiss nur nicht, sie entsprechend im Sketch zu hinterlegen:

#define I2S_DOUT  2
#define I2S_BCLK  3
#define I2S_LRC   4

Anschließend erstellst du ein Objekt der Audio-Bibliothek und hinterlegst die Zugangsdaten zu deinem WLAN-Netzwerk. In der Setup-Funktion startest du dann den Seriellen Monitor und verbindest den ESP32 mit dem WLAN. Wichtig sind die beiden Zeilen

audio.setVolume(10);
audio.connecttohost("http://www.byte.fm/stream/bytefm.m3u");

In der ersten stellst du die Lautstärke des Streams ein – mit einer Zahl zwischen 0 und 100. Anschließend hinterlegst du die Adresse des Streams, hier also jener von Byte.fm. Wenn du einen anderen Stream abspielen möchtest, wirst du meist recht einfach über eine Suche fündig.

Im Loop gibt es nur eine Funktion: Das Abspielen des Streams. Zuletzt folgen noch einige sogenannte Callback-Funktionen, die aufgerufen werden, wenn ich bestimmtes Ereignis auftritt oder um Informationen auszugeben. Hier sind das z.B. Infos zum Sender oder zum aktuell gespielten Titel, die dann in deinem Seriellen Monitor erscheinen.

erweiterung der Stromversorgung

Aktuell bezieht dein Internetradio bzw. dein ESP32 den Strom von deinem Computer oder vielleicht auch einer Powerbank. In diesem Abschnitt passen wir die Stromversorgung etwas an – entweder mit einem Akku oder zumindest mit dem dazugehörigen Lademodul. An diesem Modul wirst du später ein Poti mit integriertem Schalter anschließen, um damit deinen ESP32 und damit das Radio ein- und ausschalten zu können.

Einen Akku anschließen

Um dein ESP32 Internetradio unabhänging von Kabeln zur Stromversorgung zu machen, kannst du einen Akku samt Lademodul installieren. Ein wichtiger Hinweis jedoch vorab: Falls du keine Erfahrung mit Akkus für Arduino, ESP32 und Co. hast, bleibe bitte doch beim Kabel zu einer externen Stromquelle. Beachte auch auf jeden Fall die Sicherheitshinweise des Herstellers, da bei unsachgemäßem Gebrauch Feuer und sogar Explosionen drohen – für etwaige Schäden übernehme ich keine Gewähr und Haftung.

Falls du jedoch erfahren genug bist, orientiere dich beim Aufbau des ESP32 Internetradios an der folgenden Skizze:

ESP32 Internetradio mit Akku

Mit diesem Lademodul kannst du den Akku laden während das Radio läuft. Möchtest du den Akku laden ohne Musik zu hören, installiere noch einen Schalter zwischen dem Lademodul und dem ESP32 – dazu später mehr. Am Sketch ändert sich durch diese Erweiterung nichts.

Wie du oben siehst, ist die Stromversorgung auf 5 Volt umgezogen, da diese Spannung zu dem von mir verwendeten Lautsprecher passt. Je nachdem, welchen Widerstand (4 oder 8Ω) und welche Leistung dein Lautsprecher hat, verwende entweder 3,3V vom ESP32 oder (wie oben) 5V vom Lademodul. Hier eine Übersicht von Adafruit:

  • 5V into 4Ω – 3W max
  • 5V into 4Ω – 2.5W max
  • 3.3V into 4Ω – 1.3W max
  • 3.3V into 4Ω – 1.0W max
  • 5V into 8Ω – 1.8W max
  • 5V into 8Ω – 1.4W max
  • 3.3V into 8Ω – 0.8W max
  • 3.3V into 8Ω – 0.6W max

Nur das Lademodul verwenden

Du wirst dich vielleicht fragen, wozu es gut sein soll, nur das Lademodul für die Stromversorgung zu nutzen – du könntest das benötigte USB-Kabel ja auch weiterhin direkt im ESP32 unterbringen. Das stimmt – aber die abschließende Erweiterung soll ja ein Poti zum Ein- und Ausschalten sowie zur Lautstärkeregelung sein. Damit das am ESP32-S3 funktioniert benötigst du ein Verbindung, die du mit dem Schalter des Potis unterbrechen kannst – und das wird jene zwischen Lademodul und ESP32 sein.

Das in der obigen und in der folgenden Skizze verwendete Lademodul hat für Plus und Minus je zwei Anschlüsse. Einen (das mittlere Paar) für den Akku und einen (das äußere) für die dauerhafte Speisung eines Geräts, in unserem Fall also den ESP32.

Verbinde deinen ESP32 also wie folgt mit dem Lademodul:

ESP32 Internetradio nur Lademodul

Wenn du nun das Lademodul über USB mit Strom versorgst, erhält auch dein ESP32 hierüber Energie und das Internetradio springt an.

Fehlt noch der letzte Schritt – das Potentiometer.

Das Potentiometer installieren

Hierfür benötigst du kein „Standard“-Poti mit seinen drei Anschlüssen, sondern eines mit fünf Polen. Die äußersten zwei sind hierbei mit einem Schalter auf der Unterseite des Potis verbunden und können einen Stromkreis unterbrechen. In der Praxis drehst du das Poti nach rechts, bis ein Klicken des Schalters ertönt – nun fließt Strom (in unserem Fall springt der ESP32 an).

Drehst du nun weiter, übernimmt das Poti seine übliche Funktion und regelt den Widerstand, den du als Wert im ESP32 auslesen kannst. Orientiere dich beim Aufbau an der folgenden Skizze, wobei dort die oberen zwei Anschlüsse am Poti in der Realität oft die äußeren sind.

ESP32 Internetradio mit Poti

Wie du siehst, führt der Pluspol des Lademoduls zum Poti und von dort aus wieder zurück aufs Breadboard zu einem Kabel, das mit dem ESP32 verbunden ist. Sobald du das Poti anschaltest, fließt also Strom vom Lademodul zum ESP32.

Erweiterung des Sketchs

Nun ist das Poti zwar installiert und der Ein- und Ausschaltmechanismus funktioniert auch schon. Was jedoch deinem ESP32 Internetradio noch fehlt, ist die Lautstärkeregelung. Hierfür muss der bestehende Sketch um eine Funktion erweitert werden, die den aktuellen Wert des Potis ausliest und auf die Lautstärke des Radio-Streams „mappt“.

Das erreichst du mit dem folgenden Code:

void loop() {
  audio.loop();
  
  int volumeValue = analogRead(VOLUME_PIN);
  int volume = map(volumeValue, 0, 4095, 0, 21);
  audio.setVolume(volume);

Hier liest du das Poti aus, und verwendest die Funktion map(), um dessen Werte von 0 bis 4095 auf eine Lautstärke von 0 bis 21 zu „mappen“. Je weiter du also das Poti aufdrehst, desto lauter wird das Radio – bis ganz rechts der Wert 21 erreicht ist. Hier kannst du auch eine höhere Zahl eintragen, die zu deinem Lautsprecher passt – wie du oben gelesen hast, reicht die Spanne bis 100. Zuletzt übergibst du den gefunden Wert in der Variable volume an die Funktion audio.setVolume().

Das Problem an dieser Methode ist jedoch, dass die Funktion audio.loop(), also der Stream, immer wieder kurz unterbrochen wird. Dies führt zu unangenehmen Rucklern. Deshalb benötigen wir eine etwas ausgefeiltere Methode, um die Lautstärke (mehr oder weniger) unterbrechungsfrei zu gestalten.

Zum einen liest du nur noch alle 500ms den Werte des Potis aus und führst eine Anpassung in audio.setVolume() nur dann aus, wenn sich etwas geändert, du also am Poti gedreht hast. Außerdem glättest du die die Messwerte des Potis rechnerisch, was zu einer etwas sanfteren Anpassung führt.

Wenn du bereits das Poti installiert hast, lade den folgenden Sketch auf deinen ESP32:

//ESP32 Internetradio
//https://polluxlabs.net

#include "Arduino.h"
#include "WiFi.h"
#include "Audio.h"

#define I2S_DOUT 2
#define I2S_BCLK 3
#define I2S_LRC 4
#define VOLUME_PIN 5

Audio audio;

const char* ssid = "DEIN NETZWERK";
const char* password = "DEIN PASSWORT";

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; // Check alle 500ms

void setup() {
  Serial.begin(115200);
  pinMode(VOLUME_PIN, INPUT);

  WiFi.disconnect();
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("\nWiFi connected");
  Serial.println("IP address: " + WiFi.localIP().toString());

  audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
  audio.setVolume(10);
  audio.connecttohost("http://www.byte.fm/stream/bytefm.m3u");

  // Erste Auswertung des Potis
  for (int i = 0; i < SAMPLES; i++) {
    volumeReadings[i] = 0;
  }
}

void loop() {
  audio.loop();
  
  unsigned long currentMillis = millis();
  if (currentMillis - lastVolumeCheck >= VOLUME_CHECK_INTERVAL) {
    lastVolumeCheck = currentMillis;
    
    // Letzten Wert entfernen
    total = total - volumeReadings[readIndex];
    // Werte des Potis auslesen
    volumeReadings[readIndex] = analogRead(VOLUME_PIN);
    // Wert hinzufügen
    total = total + volumeReadings[readIndex];
    // Zum nächsten Wert im Array wechseln
    readIndex = (readIndex + 1) % SAMPLES;

    // Durchschnitt berechnen
    average = total / SAMPLES;
    
    // Geglätteten Wert auf die Lautstärke mappen
    int volume = map(average, 0, 4095, 0, 31);
    
    // NUR wenn am Poti gedreht wurde, die Lautstärke anpassen
    static int lastVolume = -1;
    if (volume != lastVolume) {
      audio.setVolume(volume);
      lastVolume = volume;
      Serial.println("Volume set to: " + String(volume));
    }
  }
}

void audio_info(const char *info) {
  Serial.print("info        "); Serial.println(info);
}
void audio_id3data(const char *info) { //id3 metadata
  Serial.print("id3data     "); Serial.println(info);
}
void audio_eof_mp3(const char *info) { //end of file
  Serial.print("eof_mp3     "); Serial.println(info);
}
void audio_showstation(const char *info) {
  Serial.print("station     "); Serial.println(info);
}
void audio_showstreaminfo(const char *info) {
  Serial.print("streaminfo  "); Serial.println(info);
}
void audio_showstreamtitle(const char *info) {
  Serial.print("streamtitle "); Serial.println(info);
}
void audio_bitrate(const char *info) {
  Serial.print("bitrate     "); Serial.println(info);
}
void audio_commercial(const char *info) { //duration in sec
  Serial.print("commercial  "); Serial.println(info);
}
void audio_icyurl(const char *info) { //homepage
  Serial.print("icyurl      "); Serial.println(info);
}
void audio_lasthost(const char *info) { //stream URL played
  Serial.print("lasthost    "); Serial.println(info);
}
void audio_eof_speech(const char *info) {
  Serial.print("eof_speech  "); Serial.println(info);
}

Nun solltest du dein ESP32 Internetradio mit dem Poti einschalten und kurz darauf – unterbrechungsfrei – Byte.fm hören können.

Umzug auf eine Lochplatine und in das passende Gehäuse

Bis jetzt hast du die Komponenten deines ESP32 Internetradios auf einem Breadboard montiert. Wenn du es jedoch dauerhaft nutzen und es auch optisch etwas aufwerten möchtest, benötigst du ein Gehäuse – und vermutlich auch eine platzsparendere Methode für die Bauteile.

Als Gehäuse bietet sich eine alte Kassettenhülle an, in der du die Technik unterbringen kannst. Mit einer Säge oder Feile kannst du darin Löcher für das Potentiometer und das Ladekabel (falls du keinen Akku benutzt) bohren. Den Lautsprecher kannst du an der Außenseite oder innen anbringen – ganz wie es seine Maße dir ermöglichen.

So könnte anschließend dein ESP32 Internetradio aussehen:

Das Anbringen der Bauteile auf einer Lochplatine erfordert etwas Planung und durchaus fortgeschrittene Kenntnisse im Löten. Überlege dir vorher, wie du den ESP32, den Verstärker etc. anbringen möchtest, sodass du Platz hast für das Poti oder um auf die USB-Buchse zugreifen zu können. Auch die Platzierung des Lautsprechers spielt eine Rolle.

Eine gute Anleitung für das Löten auf einer Lochplatine findest du hier.

Und das war es mit dem „Einsender-Radio“. Aber was, wenn gerade Werbung kommt oder ein Song, den du nicht mehr hören kannst? Dann muss ein Radio mit Senderwahl her. Wie du dein aktuelles Radio damit erweiterst, erfährst du im folgenden Teil dieses Tutorials.

ESP32 Internetradio mit Senderwahl

Damit du an deinem Radio verschiedene Sender einstellen kannst, benötigst du eine geeignete Eingabemöglichkeit. Hierfür eignet sich ein Rotary Encoder* (oder auch Drehgeber). Dieser rastet, anders als ein stufenloses Poti, an festen Positionen ein und sendet einen entsprechenden Impuls an deinen ESP32. Mit jeder Drehung wechselst du dann zum nächsten Radiosender.

Auf deinem Breadboard ist hierfür nur ein kleiner Umbau nötig:

ESP32 Internetradio mit Senderwahl

Wie du siehst, wird der Rotary Encoder (rechts unten) auch vom Lademodul mit Strom versorgt. Die beiden Pins CLK und DT schließt du an die Pins 12 und 13 an.

Die bibliothek für den Rotary Encoder

Damit du die Signale des Rotary Encoders unkompliziert verarbeiten kannst, eignet sich die Bibliothek AiEsp32RotaryEncoder, die du einfach über den Bibliotheksmanager deiner Arduino IDE installieren kannst:

AiEsp32RotaryEncoder in der IDE installieren

Finde deine Lieblingssender

Als nächstes benötigst du eine Liste von Sendern, die dein ESP32 Internetradio abspielen können soll. Hierfür benötigst du die entsprechenden URLs der Streams. Die URL des Senders Byte.fm hast du ja bereits oben kennengelernt. Zusätzliche oder andere Adressen findest du oft ganz einfach über eine Suchmaschine. Suche hierfür zum Beispiel nach dem [Sendername] + Stream URL.

Ich habe in meinem Radio die folgenden vier Sender hinterlegt (als Array, das du gleich im vollständigen Sketch wiedersehen wirst):

const char* stations[] = {
    "http://www.byte.fm/stream/320.m3u", //Byte.fm
    "https://st01.sslstream.dlf.de/dlf/01/128/mp3/stream.mp3", //Deutschlandfunk
    "https://frontend.streamonkey.net/fho-schwarzwaldradiolive/mp3-stream.m3u", //Schwarzwaldradio
    "https://kexp-mp3-128.streamguys1.com/kexp128.mp3" //KEXP
};

Der vollständige Sketch des ESP32 Internetradios mit Senderwahl

Und hier nun der gesamte Sketch zum Herauskopieren und Anpassen:

//ESP32 Internetradio
//https://polluxlabs.net

#include "Arduino.h"
#include "WiFi.h"
#include "Audio.h"
#include "AiEsp32RotaryEncoder.h"

#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 14
#define ROTARY_ENCODER_STEPS 4

AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(ROTARY_ENCODER_A_PIN, ROTARY_ENCODER_B_PIN, ROTARY_ENCODER_BUTTON_PIN, -1, ROTARY_ENCODER_STEPS);

Audio audio;

// WiFi credentials
const char* ssid = "DEIN NETZWERK";
const char* password = "DEIN PASSWORT";

// Radio stations
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"
};
const int NUM_STATIONS = sizeof(stations) / sizeof(stations[0]);
int currentStation = 0;

// Volume control variables
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; // Check every 500ms

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

void setup() {
    Serial.begin(115200);
    pinMode(VOLUME_PIN, INPUT);

    // Rotary Encoder setup
    rotaryEncoder.begin();
    rotaryEncoder.setup(readEncoderISR);
    rotaryEncoder.setBoundaries(0, NUM_STATIONS - 1, true); // circular behavior
    rotaryEncoder.setAcceleration(0); // no acceleration

    WiFi.disconnect();
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }

    Serial.println("\nWiFi connected");
    Serial.println("IP address: " + WiFi.localIP().toString());

    audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
    audio.setVolume(10);
    connectToStation(currentStation);

    // Initialize volume readings
    for (int i = 0; i < SAMPLES; i++) {
        volumeReadings[i] = 0;
    }
}

void loop() {
    audio.loop();
    checkVolumeControl();
    checkStationChange();
}

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;
            Serial.println("Volume set to: " + String(volume));
        }
    }
}

void checkStationChange() {
    if (rotaryEncoder.encoderChanged()) {
        int newStation = rotaryEncoder.readEncoder();
        if (newStation != currentStation) {
            currentStation = newStation;
            Serial.println("Changing to station: " + String(currentStation));
            connectToStation(currentStation);
        }
    }
    
    if (rotaryEncoder.isEncoderButtonClicked()) {
        Serial.println("Encoder button clicked");
        // You can add functionality here for when the encoder button is pressed
    }
}

void connectToStation(int stationIndex) {
    audio.stopSong();
    audio.connecttohost(stations[stationIndex]);
    Serial.println("Connected to: " + String(stations[stationIndex]));
}

// Audio status functions
void audio_info(const char *info) {
    Serial.print("info        "); Serial.println(info);
}
void audio_id3data(const char *info) {
    Serial.print("id3data     "); Serial.println(info);
}
void audio_eof_mp3(const char *info) {
    Serial.print("eof_mp3     "); Serial.println(info);
}
void audio_showstation(const char *info) {
    Serial.print("station     "); Serial.println(info);
}
void audio_showstreaminfo(const char *info) {
    Serial.print("streaminfo  "); Serial.println(info);
}
void audio_showstreamtitle(const char *info) {
    Serial.print("streamtitle "); Serial.println(info);
}
void audio_bitrate(const char *info) {
    Serial.print("bitrate     "); Serial.println(info);
}
void audio_commercial(const char *info) {
    Serial.print("commercial  "); Serial.println(info);
}
void audio_icyurl(const char *info) {
    Serial.print("icyurl      "); Serial.println(info);
}
void audio_lasthost(const char *info) {
    Serial.print("lasthost    "); Serial.println(info);
}
void audio_eof_speech(const char *info) {
    Serial.print("eof_speech  "); Serial.println(info);
}

Passe vor dem Upload zunächst wieder die Zugangsdaten für dein WLAN an und trage deine Lieblingssender ein. Wenn du den Sketch auf deinen ESP32 hochgeladen und gestartet hast, solltest du mit dem Rotary Encoder die einzelnen Sender auswählen können. Bei meinen Tests war es beim ersten Umschalten nötig, zweimal zu drehen. Anschließend reichte eine Drehung in die nächste Stellung.

Noch ein Hinweis: Im Sketch oben, ist auch der Button des Rotary Encoders (auf dem Modul oft SW benannt) an Pin 14 hinterlegt. Falls du deinen Drehgeber auch drücken kannst, hast du die Möglichkeit, darüber eine weitere Funktion deiner Wahl in deinem ESP32 Internetradio zu implementieren.

Auf deinem Breadboard könnte das Radio dann so aussehen:

ESP32 Internetradio mit Rotary Encoder auf dem Breadboard

Wie geht es weiter? Zwar erkennst du deine Radiosender vielleicht an der Musik, die dort läuft – ein Display wäre aber sicherlich hilfreich. Darüber könntest du nicht nur den Sendernamen, sondern auch den Song anzeigen, der gerade gespielt wird. Hierfür hilfreiche Funktionen findest du bereits im Sketch oben: void audio_showstreaminfo() zeigt dir den Sendernamen und void audio_showstreamtitle() den aktuellen Song oder den Namen der aktuellen Sendung.

Das Radio um ein OLED-Display erweitern

Nun also zum letzten Baustein deines ESP32 Internetradios – ein kleines Display, auf dem du den abgespielten Sender sowie den aktuellen Musiktitel ablesen kannst. Hierfür eignet sich ein OLED-Display mit 128×32 Pixeln*. Schließe dieses wie folgt an:

ESP32 Internetradio mit Display

Da sich auf deinem Breadboard (und auf der Skizze oben) mittlerweile eine Menge Kabel tummeln, hier noch mal die Anschlüsse im Detail:

OLED-DisplayESP32-S3 Zero
VCC (3,3V)3,3V
GNDGND
SDA8
SCK/SCL9

Hardwareseitig war es das schon – kommen wir zum Sketch. Hier musst du vor dem Upload allerdings noch eine Kleinigkeit in der Arduino IDE einstellen. Und zwar würde der folgende Sketch den standardmäßig vorgesehenen Speicherplatz auf deinem ESP32 sprengen, wenn du nicht vorab etwas mehr freigeben würdest. Das geht glücklicherweise ganz einfach:

Mehr Speicherplatz auf dem ESP32 bereitstellen

Klicke im Menü Werkzeuge (Tools) auf den Menüpunkt Partition Scheme und wähle die Einstellung Huge APP. Damit hast du nun statt den üblichen 1,2 MB ganze 3 MB zur Verfügung.

Partition Scheme ändern in Huge APP

Nun, wo du genug Speicherplatz auf dem ESP32 hast, kann es mit dem Upload des Sketchs weitergehen. Ein Hinweis noch vorab: Dein ESP32 hat mit all der angeschlossenen Peripherie ganz schön zu tun, weswegen es passieren kann, dass er nicht zuverlässig bootet. Um das zu verhindern, werden im folgenden Sketch das Display, die Verbindung zum WLAN und der Verstärker schrittweise initialisiert. Die Setup-Funktion hingegen ist nun aufgeräumter.

Damit dein Display den richtigen Sendernamen zum jeweiligen Stream anzeigen kann, benötigst du ein Array mit den Namen. Trage dort die Sendernamen in der gleichen Reihenfolge wie im Stream-Array ein:

const char* stationNames[] = {
    "Byte.fm",
    "Deutschlandfunk",
    "Schwarzwaldradio",
    "KEXP",
    "Psychedelic Jukebox"
};

Hier nun der vollständige Sketch:

//ESP32 Internetradio
//https://polluxlabs.net

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

#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 14
#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

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 NETZWERK";
const char* password = "DEIN PASSWORT";

// Radio stations
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;

char streamTitle[64] = "";  // Buffer to store the current stream title

// Volume control variables
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; // Check every 500ms

// Flags für verzögerte Initialisierung
bool isWiFiConnected = false;
bool isDisplayInitialized = false;
bool isAudioInitialized = false;

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

String replaceSpecialChars(String input) {
    input.replace("ä", "ae");
    input.replace("ö", "oe");
    input.replace("ü", "ue");
    input.replace("Ä", "AE");
    input.replace("Ö", "OE");
    input.replace("Ü", "UE");
    input.replace("ß", "ss");
    return input;
}

void setup() {
    delay(1000);  // Kurze Pause für stabilen Start
    Serial.begin(115200);
    while(!Serial) { delay(10); }  // Warte auf Serial-Verbindung
    Serial.println(F("ESP32-S3 Internet Radio startet..."));
    
    pinMode(VOLUME_PIN, INPUT);

    rotaryEncoder.begin();
    rotaryEncoder.setup(readEncoderISR);
    rotaryEncoder.setBoundaries(0, NUM_STATIONS - 1, true);
    rotaryEncoder.setAcceleration(0);

    Serial.println(F("Initialisiere I2C..."));
    Wire.begin(I2C_SDA, I2C_SCL);

    // Initialisiere Volumen-Readings
    for (int i = 0; i < SAMPLES; i++) {
        volumeReadings[i] = 0;
    }
}

void loop() {
    static unsigned long lastInitAttempt = 0;
    const unsigned long initInterval = 5000;  // 5 Sekunden zwischen Initialisierungsversuchen

    // 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
    if (isDisplayInitialized && isWiFiConnected && isAudioInitialized) {
        audio.loop();
        checkEncoder();
        checkVolumeControl();
    }

    yield();  // Watchdog füttern
    delay(10);  // Kurze Pause für Stabilität
}

void initializeDisplay() {
    Serial.println(F("Initialisiere OLED Display..."));
    if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
        Serial.println(F("SSD1306 Initialisierung fehlgeschlagen"));
        return;
    }
    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(0,0);
    display.println(F("Initialisierung..."));
    display.display();
    isDisplayInitialized = true;
    Serial.println(F("OLED Display initialisiert"));
}

void connectToWiFi() {
    Serial.println(F("Verbinde mit 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 verbunden"));
        isWiFiConnected = true;
        if (isDisplayInitialized) {
            display.clearDisplay();
            display.setCursor(0,0);
            display.println(F("WiFi verbunden"));
            display.display();
        }
    } else {
        Serial.println(F("\nWiFi-Verbindung fehlgeschlagen"));
    }
}

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

void checkEncoder() {
    if (rotaryEncoder.encoderChanged()) {
        currentStation = rotaryEncoder.readEncoder();
        connectToStation(currentStation);
    }
    
    if (rotaryEncoder.isEncoderButtonClicked()) {
        Serial.println(F("Encoder-Taste gedrückt"));
        // Hier könnte eine Aktion für den Tastendruck implementiert werden
    }
}

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;
            Serial.println("Lautstärke eingestellt auf: " + String(volume));
            updateDisplay();
        }
    }
}

void updateDisplay() {
    if (!isDisplayInitialized) return;
    display.clearDisplay();
    display.setCursor(0,0);
    display.println(replaceSpecialChars(String(stationNames[currentStation])));
    display.println();
    display.println(replaceSpecialChars(String(streamTitle)));
    display.display();
    Serial.println(F("Display aktualisiert"));
}

// Audio callback functions
void audio_info(const char *info) { 
    Serial.print("info        "); Serial.println(info);
}
void audio_id3data(const char *info) {
    Serial.print("id3data     "); Serial.println(info);
}
void audio_eof_mp3(const char *info) {
    Serial.print("eof_mp3     "); Serial.println(info);
}
void audio_showstation(const char *info) {
    Serial.print("station     "); Serial.println(info);
}
void audio_showstreaminfo(const char *info) {
    Serial.print("streaminfo  "); Serial.println(info);
}
void audio_showstreamtitle(const char *info) {
    Serial.print("streamtitle: "); Serial.println(info);
    strncpy(streamTitle, info, sizeof(streamTitle) - 1);
    streamTitle[sizeof(streamTitle) - 1] = '\0'; // Ensure null-termination
    updateDisplay();
}
void audio_bitrate(const char *info) {
    Serial.print("bitrate     "); Serial.println(info);
}
void audio_commercial(const char *info) {
    Serial.print("commercial  "); Serial.println(info);
}
void audio_icyurl(const char *info) {
    Serial.print("icyurl      "); Serial.println(info);
}
void audio_lasthost(const char *info) {
    Serial.print("lasthost    "); Serial.println(info);
}
void audio_eof_speech(const char *info) {
    Serial.print("eof_speech  "); Serial.println(info);
}

Nach dem Upload sollte dein ESP32 Internetradio starten (was nun etwas länger dauert) und den ersten Sender in deiner Liste spielen. Auf dem Display erscheint der von dir hinterlegte Sendername und – sofern verfügbar – der Name des Songs oder der Sendung, der oder die gerade läuft.

Und das war es an dieser Stelle. Viel Spaß beim Tüfteln! 🙂

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

]]>
Text To Speech mit Python – So vertonst du Texte https://polluxlabs.net/raspberry-pi-projekte/text-to-speech-mit-python-so-vertonst-du-texte/ Mon, 13 Nov 2023 12:54:54 +0000 https://polluxlabs.net/?p=15278 Text To Speech mit Python – So vertonst du Texte Weiterlesen »

]]>
In diesem Tutorial lernst du zwei Methoden kennen, mit denen du in Python Text in gesprochene Sprache umwandeln kannst. Mit Text to Speech kannst in deinen Projekten zum Beispiel eine Sprachausgabe umsetzen.

Die erste Möglichkeit ist die Python-Bibliothek gTTS, mit der du kostenlos Texte vertonen lassen kannst. Dieses Modul habe ich im Projekt ChatGPT im Telefon eingesetzt. Als zweite Methode lernst du eine API-Funktion von openAI kennen. Diese Variante ist kostenpflichtig – allerdings hört sich das Ergebnis hierfür auch weit besser an.

Text to Speech mit gTTS (Google Text-to-Speech)

Wenn dein Projekt keine astreine Aussprache erfordert, ist die Bibliothek gTTS eine gute Wahl. Die Qualität ist nicht schlecht, allerdings hakt es bei der Aussprache oft bei Abkürzungen oder die Betonung von Satzteilen kommt durch das ein oder andere Komma durcheinander. Dafür kannst du mit diesem Python-Modul den Google-Service kostenlos verwenden – was sicherlich ein gutes Argument für einen Test ist.

Installiere zunächst die Bibliothek mit dem Befehl

pip install gtts

Um die Sprachausgabe zu testen, reichen drei Zeilen Python-Code:

from gtts import gTTS

tts = gTTS('Hello, world. This is a first test.')
tts.save('hello.mp3')

Nachdem du das kleine Script ausgeführt hast, öffne die Datei hello.mp3 und lausche dem Ergebnis. Bist du zufrieden?

Du kannst übrigens auch deutsche Texte vertonen lassen. Füge hierfür den Parameter lang=’de‘ hinzu:

tts = gTTS('Hallo, das ist einer erster Test.', lang='de')

Das war im Prinzip schon alles. Wenn du wissen möchtest, wie du die MP3 direkt mit deinem Python-Script abspielen kannst, wirf einen Blick in das oben verlinkte Projekt. Weitere Infos über gTTS erhältst du hier.

Text To Speech mit openAI

Wenn dir eine gute Sprachqualität ein paar Cent wert ist, ist OpenAI einen Versuch wert. Neben dem allseits bekannten ChatGPT findest du dort auch eine API-Funktion, mit der du Text vertonen lassen kannst. Die Integration in dein Python-Script ist dabei ähnlich einfach wie mit gTTS. Allerdings kostet der Service derzeit (November 2023) 0,015 € je 1.000 Zeichen – was ein recht überschaubarer Preis ist. Die aktuelle Preisliste findest du hier unter Audio models.

Wenn du noch nicht mit OpenAI experimentiert hast, erfährst du in diesem Tutorial, wie du dort ein Konto und einen API-Key erstellst.

Nachdem du die Bibliothek openai (mit pip install openai) installiert hast, binde sie in deinem Python-Script ein. Zusätzlich benötigst du noch das Modul pathlib der Bibliothek Path, das aber bereits vorinstalliert ist.

from pathlib import Path
from openai import OpenAI

Anschließend hinterlegst du deine API-Key von OpenAI:

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

Und schon kann es mit der Vertonung losgehen. Du gibst zunächst an, wie die erzeugte MP3 heißen soll – hier speech.mp3:

speech_file_path = Path(__file__).parent / "speech.mp3"

Anschließend legst du ein paar Parameter fest: Zunächst das Modell – hier hast du die Wahl zwischen tts-1 und tts-1-hd. Letzteres hat eine etwas höhere Qualität und ist auch teurer. Für die allermeisten Anwendungen dürfte das einfachere Modell jedoch ausreichen. Der Parameter voice gibt vor, welche Stimme verwendet werden soll. Derzeit gibt es sechs Stimmen, die du hier probehören kannst. Dort findest du auch aktuelle Informationen und Updates zu Text to Speech mit OpenAI.

Zuletzt fehlt nur noch der Text, den du vertonen lassen möchtest und der Befehl zum Speichern der MP3:

response = client.audio.speech.create(
  model="tts-1",
  voice="alloy",
  input="Das Pferd frisst keinen Gurkensalat."
)

response.stream_to_file(speech_file_path)

Und das war schon alles. Führe das Script aus – sobald es fertig ist, findest du im gleichen Ordner die Datei speech.mp3 mit deiner Sprachausgabe. Hier nun das gesamte Script:

from pathlib import Path
from openai import OpenAI

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

speech_file_path = Path(__file__).parent / "speech.mp3"
response = client.audio.speech.create(
  model="tts-1",
  voice="alloy",
  input="Das Pferd frisst keinen Gurkensalat."
)

response.stream_to_file(speech_file_path)

Mit den oben beschriebenen Bibliotheken und Services hast du nun zwei Methoden zur Hand, wie du in deinem Projekt Text to Speech anwenden kannst. Ob dir eine kostenlose Vertonung reicht, oder du etwas qualitativ hochwertigeres benötigst, hängt natürlich vom Einsatzgebiet ab.

]]>