Pollux Labs https://polluxlabs.net Arduino, ESP32 & ESP8266 | Projekte & Tutorials Thu, 25 Sep 2025 11:20:57 +0000 de hourly 1 https://wordpress.org/?v=6.8.3 https://polluxlabs.net/wp-content/uploads/2020/05/cropped-pollux-labs-p-32x32.png Pollux Labs https://polluxlabs.net 32 32 Mit einem Durchflussmesser den Wasserverbrauch messen https://polluxlabs.net/arduino-projekte/mit-einem-durchflussmesser-den-wasserverbrauch-messen/ Thu, 25 Sep 2025 11:19:44 +0000 https://polluxlabs.net/?p=19918 Mit einem Durchflussmesser den Wasserverbrauch messen Weiterlesen »

]]>
In diesem Projekt erfährst du, wie du mit einem Arduino, einem Durchflussmesser und einem OLED-Display einen einfachen, aber effektiven Wasserzähler baust. Der Zähler misst die verbrauchte Wassermenge in Echtzeit und visualisiert sie direkt auf dem Display.

Benötigte Bauteile

Diese Bauteile brauchst du für dieses Projekt:

  • Mikrocontroller: Zum Beispiel ein Arduino Nano oder Arduino UNO
  • Durchflussmesser: Ein Turbinen-Durchflussmesser (z.B. YF-S201C*)
  • Display: Ein 0,91“ OLED-Display (SSD1306-Chip)
  • Breadboard und Jumper-Kabel
  • Messbecher: Für die Kalibrierung, idealerweise 500 ml oder 1 Liter

Aufbau des Wasserzählers

Der Aufbau ist kompakt und nutzt die I²C-Pins des OLED-Displays und deines Arduinos, um die Verkabelung zu vereinfachen. Der Durchflussmesser benötigt (neben 5V und Erde) nur eine Verbindung. Auf der folgenden Skizze verwende ich einen Arduino Nano, du kannst aber auch einen UNO oder jeden anderen Arduino oder ESP32/8266 verwenden.

Anschluss des Durchflussmessers am Arduino

Hier noch einmal die Verbindungen im Detail:

Verbindung des Durchflussmessers:

  • VCC: An den 5V-Pin des Arduinos
  • GND: An einen GND-Pin des Arduinos
  • Signal: An den digitalen Pin D2 des Arduinos. Dieser Pin ist für Interrupts geeignet, was eine präzise Messung der Impulse ermöglicht.

Verbindung des OLED-Displays:

  • VCC: An den 3.3V-Pin des Arduinos
  • GND:An einen GND-Pin des Arduinos
  • SDA (Daten): An den SDA-Pin des Arduino. Beim Nano und UNO ist das Pin A4.
  • SCL (Takt): An den SCL-Pin des Arduinos. Beim Nano und UNO ist das Pin A5.

Die benötigten Bibliotheken

Für dieses Projekt benötigst du zwei Bibliotheken für das OLED-Display. Suche hierfür im Bibliotheksverwalter nach Adafruit SSD1306. Bei der Installation wirst du gefragt, ob du auch die Bibliothek Adafruit GFX Library installieren möchtest – bestätige das bitte mit Ja.

Für den Durchflussmesser benötigst du in der Regel keine Bibliothek – seine Impulse verarbeitest du selbst im Sketch.

Der Sketch für den Wasserzähler

Der folgende Code liest die Impulse des Durchflussmessers und zeigt die gemessene Wassermenge auf dem OLED-Display an. Im Folgenden gehe ich von einer Wassermenge von 2,25ml pro Impuls aus. Ich empfehle dir jedoch, deinen Sensor selbst zu kalibrieren.

//Wasserverbrauch messen mit einem Durchflussmesser
//polluxlabs.net

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// Deklariere die Display-Parameter für ein 0,91'' Display
#define SCREEN_WIDTH 128 // OLED-Breite in Pixel
#define SCREEN_HEIGHT 32 // OLED-Höhe in Pixel

// Deklariere ein OLED-Display Objekt
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

// Pin für den Durchflussmesser
const int sensorPin = 2;

// Variable für die Impulszählung. `volatile` ist wichtig für Interrupts.
volatile long impulse_count = 0;

// Die kalibrierte Konstante für deinen Sensor (2.25 ml/impuls)
const float ML_PER_IMPULSE = 2.25;

void setup() {
  // Startet die serielle Kommunikation zur Fehlersuche
  Serial.begin(9600);

  // Initialisiert das OLED-Display
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("SSD1306-Zuweisung fehlgeschlagen"));
    for (;;); // Endlosschleife, wenn das Display nicht gefunden wird
  }
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  
  // Initialisiert den Sensor-Pin als Eingang
  pinMode(sensorPin, INPUT);
  
  // Richtet einen Interrupt ein, der die Funktion count_impulses bei jedem Impuls aufruft
  attachInterrupt(digitalPinToInterrupt(sensorPin), count_impulses, RISING);
}

void loop() {
  // Eine Kopie des Zählers erstellen, um race conditions zu vermeiden
  long current_impulses = impulse_count; 

  // Berechnung des Wasserverbrauchs
  float water_volume_ml = current_impulses * ML_PER_IMPULSE;

  // Löscht den Display-Inhalt
  display.clearDisplay();

  // Zeigt den Titel an
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println("Wasserverbrauch:");

  // Zeigt den Wert an
  display.setTextSize(2);
  display.setCursor(0, 16);
  display.print(water_volume_ml);
  display.print(" ml");

  // Aktualisiert das Display
  display.display();

  // Kleine Pause, um das Display nicht zu überfordern
  delay(100);
}

// Interrupt-Service-Routine (ISR)
// Diese Funktion wird automatisch bei jedem Impuls aufgerufen
void count_impulses() {
  impulse_count++;
}

Kalibrierung des Sensors

Der Wert in der Zeile const float ML_PER_IMPULSE = 2.25; ist entscheidend für die Genauigkeit deiner Messung. Je nachdem, welches Modell du verwendest oder auch, wenn dein Sensor nicht präzise misst, kann dieser Wert abweichen. Um die richtige Wassermenge pro Impuls zu ermitteln, gehe wie folgt vor:

  1. Vorbereitung: Halte einen Messbecher mit bekanntem Volumen (z. B. 500 ml) bereit. Lade den obenstehenden Sketch auf deinen Arduino.
  2. Nullstellung: Starte den Sketch neu, indem du den Reset-Knopf auf deinem Arduino drückst. Das Display sollte 0.00 ml anzeigen.
  3. Messung: Lasse genau 500 ml Wasser konstant schnell durch den Sensor in den Messbecher laufen.
  4. Wert ablesen: Warte, bis die Messung auf dem Display nicht mehr steigt. Notiere den angezeigten Wert (z.B. 650 ml).
  5. Neuer Menge: Berechne die korrekte Menge Wasser pro Impuls: 2,25ml x (500ml / 650ml) = 1,73ml/Impuls
  6. Code anpassen: Ändere die Zeile im Sketch zu const float ML_PER_IMPULSE = 1.73;.
  7. Testen: Lade den Sketch erneut hoch und wiederhole den Test mit 500 ml Wasser. Der angezeigte Wert sollte nun sehr nah an 500 ml liegen.

Um das Ergebnis genauer zu machen, kannst du auch mehrere Messungen durchführen und einen Mittelwert finden. Es kann durchaus sein, dass z.B. die Fließgeschwindigkeit des Wassers Auswirkungen auf die Zahl der gemessenen Impulse hat.

So funktioniert der Sketch

Lass uns schnell einen Blick auf die wichtigsten Teile des Sketchs werfen:

1. Die Setup-Funktion

Der Befehl attachInterrupt() ist hier das Herzstück. Anstatt im Loop ständig zu prüfen, ob ein Impuls vom Sensor kommt, richtest du hier einen sogenannten Hardware-Interrupt ein. Das bedeutet, dass der Arduino-Chip sofort seine aktuelle Aufgabe unterbricht und direkt zu der kleinen Funktion count_impulses() springt, sobald er ein Signal am Pin D2 empfängt. Das macht die Messung extrem reaktionsschnell und präzise.

2. Der Loop: Die Hauptlogik

Dieser Teil wird immer wieder wiederholt.

  • Impulse auslesen: long current_impulses = impulse_count; kopiert den Wert des Volatile-Zählers. Das ist eine Schutzmaßnahme. Da der Zähler impulse_count jederzeit von der Interrupt-Funktion geändert werden könnte, sichern wir den Wert, bevor wir damit rechnen, um Fehler zu vermeiden.
  • Berechnung: float water_volume_ml = current_impulses * ML_PER_IMPULSE; ist der Kern des Projekts. Hier wird die Anzahl der Impulse mit dem Kalibrierungsfaktor multipliziert, um das tatsächliche Wasservolumen in Millilitern zu erhalten.
  • Display-Update: Die folgenden Befehle, wie display.clearDisplay(), display.setCursor() und display.print(), kümmern sich darum, die berechneten Daten auf dem OLED-Display darzustellen. Der Befehl display.display() sendet am Ende die gesammelten Daten an das Display, um die Anzeige zu aktualisieren.
  • Pause: delay(100); sorgt für eine kurze Pause. Das ist wichtig, um zu verhindern, dass das Display zu schnell aktualisiert wird und um die CPU-Ressourcen des Arduinos zu schonen.

3. Die Funktion count_impulses(): Der Interrupt

Dies ist die kleine, aber sehr wichtige Funktion, die durch den Interrupt-Befehl ausgelöst wird. Sie wird nicht vom Loop aufgerufen, sondern springt automatisch in Aktion, sobald der Sensor einen Impuls sendet. Ihre einzige Aufgabe ist es, den Zähler impulse_count um eins zu erhöhen.

Wie geht es weiter?

Du kannst du nun den Wasserverbrauch messen und auf einem Display anzeigen. Eine interessante Weiterentwicklung könnte z.B. ein LED-Ring sein, der anzeigt, wie sehr du dich beim Händewaschen einem festgelegten Maximalverbrauch näherst. Oder du misst in deinem Garten den Wasserverbrauch und speicherst die Daten z.B. bei Adafruit IO in der Cloud.

]]>
Arduino UKW-Radio – Musik hören ohne Internet https://polluxlabs.net/arduino-projekte/arduino-ukw-radio/ Tue, 23 Sep 2025 10:21:59 +0000 https://polluxlabs.net/?p=19891 Arduino UKW-Radio – Musik hören ohne Internet Weiterlesen »

]]>
Wer hat nicht schon einmal davon geträumt, sein eigenes Radio zu bauen? Bei Pollux Labs findest du ein Internetradio, aber solange es die klassische UKW noch gibt, spricht auch nichts gegen einen passenden Empfänger. In diesem Projekt lernst du, wie du aus wenigen, günstigen Bauteilen ein UKW-Radio mit Sendersuchlauf erstellen kannst. Das Herzstück des Projekts ist ein Arduino Uno, der als Gehirn des Radios dient. Dazu kommt ein spezielles Radio-Modul (Si470x), das den Empfang übernimmt, und einen LM386-Verstärker, um den Sound auf einen Lautsprecher zu bringen.

Dieses Projekt ist perfekt für Einsteiger geeignet, die erste Erfahrungen mit dem Arduino, I²C-Kommunikation und einfachen Audioschaltungen sammeln möchten. Am Ende hast du nicht nur ein cooles, selbstgebautes Gadget, sondern auch ein tieferes Verständnis dafür, wie diese Komponenten zusammenspielen. Los geht’s!

Diese Bauteile brauchst du

Für dieses Projekt benötigst du die folgenden Bauteile:

  • Mikrocontroller: Arduino UNO*
  • Radio-Modul: Si4703*
  • Verstärker: LM386 Audioverstärker-IC*
  • Audio-Ausgabe: 1x kleiner Lautsprecher (0,5 Watt mit 8 Ohm)
  • Bedienelemente: 2x Push Buttons
  • Verkabelung: Breadboard und diverse Jumper-Kabel
  • Antenne: Ein ca. 75 cm langes Stück Draht

Der Aufbau auf dem Breadboard

Orientiere dich beim Aufbau des UKW-Radios an der folgenden Skizze.

Aufbau des Arduino UKW-Radios auf dem Breadboard

___STEADY_PAYWALL___

Da auf dem Breadboard ziemlich viel los ist, hier noch einmal alle Verbindungen in einer Tabelle.

Bauteil (Von)Pin am BauteilVerbindung zuAnmerkung
Si4703 Radio-Modul3.3V / VCC3.3V am ArduinoWichtig: Nicht an 5V anschliessen!
GNDGND am ArduinoGemeinsame Masse
SDIO (SDA)A4 am ArduinoI²C Datenleitung
SCLK (SCL)A5 am ArduinoI²C Taktleitung
RSTD2 am ArduinoReset-Leitung
LOUT (Left Out)Input / + am LM386Audio-Signal zum Verstärker
ANTca. 75cm DrahtDie Antenne, ins Leere hängend
LM386 VerstärkerVS5V am ArduinoStrom für den Verstärker
GNDGND am ArduinoGemeinsame Masse
+INPUTLOUT vom RadioDas Audiosignal geht hier rein
-INPUTGNDDient als Referenz für +INPUT
VOUTZum Lautsprecher mit 100uF KondensatorVerstärkter Audio-Ausgang
Taster 1Ein PinD13 am ArduinoSendersuche aufwärts (Seek Up)
Anderer PinGND am Arduino
Taster 2Ein PinD12 am ArduinoSendersuche abwärts (Seek Down)
Anderer PinGND am Arduino

So funktioniert das Radio

Ohne Antenne kein Radioempfang, klar. Hierfür reicht bereits ein langes, isoliertes Kabel (ca. 75 – 100cm) aus der Werkstatt. Du kannst die Antenne direkt an das Radio-Modul löten oder es in dein Breadboard stecken.

Mit den beiden Buttons startest du die Sendersuche: Der rechte sucht aufwärts nach Frequenzen mit Empfang, der linke Button entsprechend abwärts.

Und noch einige Worte zum Lautsprecher: Diesen schließt du nicht direkt an den Ausgang des LM386 an, sondern mit einem dazwischengeschalteten Elektrolyt-Kondensator (Elko) mit 100uF. Der Kondensator wirkt wie ein Filter: Er blockiert Gleichspannung und lässt nur das Audiosignal zum Lautsprecher durch. Man nennt ihn auch Koppelkondensator. Achte beim Einbau des Elkos unbedingt auf die Polarität – Plus kommt an den LM386 und Minus (gekennzeichnet durch einen Streifen) vor den Anschluss des Lautsprechers.

Ein genauerer Blick: Der LM386 Audioverstärker

Das Audiosignal, das direkt aus dem Radio-Modul kommt, ist viel zu schwach, um einen Lautsprecher anzutreiben. Hier kommt der LM386 ins Spiel. Er ist ein klassischer und sehr robuster Audioverstärker-Chip, perfekt für Hobby-Projekte. Seine Aufgabe ist es, das leise Signal vom LOUT-Pin des Radios aufzunehmen und es so zu verstärken, dass wir es deutlich hören können.

Da der Chip selbst keine Beschriftung hat, ist ein Pinout-Diagramm unerlässlich, um die Anschlüsse zu identifizieren. Achte auf die Kerbe an einer der kurzen Seiten des Chips – damit identifizierst du die linke und rechte Seite des LM386.

Die wichtigsten Anschlüsse für dieses Projekt:

  • Pin 6 (Vs) & Pin 4 (GND): Hier wird der Verstärker mit Strom versorgt (5V und Masse).
  • Pin 3 (+INPUT): An diesen „nicht-invertierenden“ Eingang legen wir unser Audiosignal vom Radio an.
  • Pin 2 (-INPUT): Der „invertierende“ Eingang dient als Referenz und wird einfach mit Masse (GND) verbunden.
  • Pin 5 (Vout): Hier kommt das verstärkte Signal für den Lautsprecher heraus.

Die übrigen Pins (Gain, Bypass) benötigen wir für eine einfache Schaltung nicht, sie könnten aber für eine höhere Lautstärke oder bessere Rauschunterdrückung genutzt werden.

Noch ein Hinweis: Das Radio-Modul gibt es auch mit einer Kopfhörer-Buchse. Bei dieser Version kannst du dir die Verstärkung über den LM386 und sogar die Antenne sparen. Sowohl der Ton als auch der Empfang kommt dann über die Kopfhörer bzw. deren Kabel.

Die passende Bibliothek für das Si403 Radio-Modul

Damit dein Arduino mit dem Radio-Modul sprechen kann, benötigt er eine passende Bibliothek. Diese findest du im Bibliotheksmanager deiner Arduino IDE. Suche dort einfach nach SI470x und installiere die aktuelle Version der Bibliothek PU2CLR SI470X. Das X steht hier für die verschiedenen Varianten des Modul – neben dem 4703 gibt es auch die Modelle 4701 und 4702.

Der Sketch für das Arduino UKW-Radio

Nun zur Software für deinen Radioempfänger. Kopiere den folgenden Sketch und lade ihn auf deinen Arduino:

/*
  UKW-Radio mit Arduino Uno, Si470x und LM386
*/

#include <Arduino.h>
#include <Wire.h>      // Für die I2C-Kommunikation
#include <SI470X.h>    // Die Bibliothek für das Radio-Modul

// Pin-Definitionen
const int RESET_PIN = 2;       // Pin für den Reset des Radio-Moduls
const int SEEK_UP_PIN = 13;    // Taster für Sendersuche aufwärts
const int SEEK_DOWN_PIN = 12;  // Taster für Sendersuche abwärts

// Radio-Objekt erstellen
SI470X radio;

// --- Funktion zur Ausgabe der aktuellen Sender-Informationen ---
void printRadioStatus() {
  // Aktuelle Daten vom Radio-Chip holen
  float currentFrequency = radio.getFrequency() / 100.0; // Frequenz von kHz in MHz umrechnen
  int currentRSSI = radio.getRssi();

  // --- AUSGABE IM SERIELLEN MONITOR ---
  Serial.println("--------------------");
  Serial.print("Aktueller Sender: ");
  Serial.print(currentFrequency, 2); // Zeigt zwei Nachkommastellen an
  Serial.println(" MHz");
  Serial.print("Signalstaerke: ");
  Serial.print(currentRSSI);
  Serial.println(" dBuV");
  Serial.println("--------------------");
}

void setup() {
  // Starte die serielle Kommunikation für die Ausgabe am Computer
  Serial.begin(9600);
  Serial.println("\n\nArduino UKW-Radio wird gestartet...");

  // Konfiguriere die Taster-Pins als Eingänge mit internem Pull-up-Widerstand.
  pinMode(SEEK_UP_PIN, INPUT_PULLUP);
  pinMode(SEEK_DOWN_PIN, INPUT_PULLUP);

  // Initialisiere die I2C-Kommunikation und das Radio-Modul
  Wire.begin();
  radio.setup(RESET_PIN, A4); // A4 ist der SDA-Pin beim Arduino Uno
  Serial.println("Si470x Radio initialisiert.");
  
  // Setze einen Schwellenwert für den Sendersuchlauf.
  radio.setSeekThreshold(45); 
  Serial.println("Sendersuchlauf-Schwelle auf 45 dBuV gesetzt.");
  
  // Stelle eine mittlere Lautstärke ein (0-15)
  radio.setVolume(5);
  Serial.println("Lautstaerke auf 5 gesetzt.");
  
  // Suche automatisch den ersten verfügbaren Sender nach dem Start
  Serial.println("Suche ersten Sender...");
  radio.seek(0, 1); // Mode 0 = am Bandende weitersuchen, Direction 1 = aufwärts
  
  // Gib den Status des ersten Senders aus
  printRadioStatus();
  
  Serial.println("Setup abgeschlossen. Radio ist bereit.");
}

void loop() {
  // Prüfe, ob der "Suchen Aufwärts"-Taster gedrückt wurde
  if (digitalRead(SEEK_UP_PIN) == LOW) {
    Serial.println("Taster 'Seek Up' gedrueckt -> Suche naechsten Sender...");
    radio.seek(0, 1);
    delay(200); // Entprellung des Tasters
    printRadioStatus();
  }

  // Prüfe, ob der "Suchen Abwärts"-Taster gedrückt wurde
  if (digitalRead(SEEK_DOWN_PIN) == LOW) {
    Serial.println("Taster 'Seek Down' gedrueckt -> Suche vorigen Sender...");
    radio.seek(0, 0); // Direction 0 = abwärts
    delay(200);
    printRadioStatus();
  }

  // Kurze Pause, damit der Loop nicht überlastet wird
  delay(50);
}

So funktioniert der Sketch

Auch wenn der Code auf den ersten Blick komplex aussieht, lässt er sich in drei logische Blöcke unterteilen. Schauen wir uns die wichtigsten Befehle genauer an.

  • Der setup()-Block: Die Vorbereitung Dieser Teil wird nur einmal beim Start des Arduinos ausgeführt.
    • Serial.begin(9600);: Startet die Kommunikation mit dem Computer, damit wir uns Nachrichten im Seriellen Monitor ansehen können.
    • pinMode(..., INPUT_PULLUP);: Konfiguriert die Pins für die Buttons zur Sendersuche. Der Arduino aktiviert einen internen Widerstand, der dafür sorgt, dass der Pin ein klares HIGH-Signal hat, solange der Taster nicht gedrückt wird. Das macht die Schaltung stabiler.
    • radio.setup(RESET_PIN, A4);: Weckt das Radio-Modul auf und stellt die I²C-Kommunikation her.
    • radio.setSeekThreshold(45);: Setzt den „Qualitätsfilter“ für den Suchlauf. Nur Sender mit einer Signalstärke von mindestens 45 dBuV werden berücksichtigt. Falls du zu wenige Sender empfängst, wähle einen niedrigeren Wert.
    • radio.setVolume(5);: Stellt die Grundlautstärke ein. Auch diese kannst du bei Bedarf anpassen.
    • radio.seek(0, 1);: Startet den ersten automatischen Suchlauf, um direkt nach dem Einschalten einen Sender zu finden.
  • Der loop()-Block: Die Endlosschleife Dieser Code wird immer wieder ausgeführt, solange das Radio eingeschaltet ist.
    • Der Arduino prüft ständig, ob die Buttons für die Sendersuche gedrückt wurden.
    • Wenn einer der Buttons gedrückt wird, wird der Befehl zum Suchen an das Radio gesendet.
  • Die printRadioStatus()-Funktion: Jedes Mal, wenn sie aufgerufen wird, holt sie sich die aktuelle Frequenz (radio.getFrequency()) und Signalstärke (radio.getRssi()) vom Modul und gibt sie sauber formatiert im Seriellen Monitor aus.

So erweiterst du das Arduino Radio

Der bisherige Aufbau ist vielleicht nur der Anfang: Du kannst ein OLED-Display anschließen und darauf die aktuelle Frequenz anzeigen. Eine sehr sinnvolle Erweiterung ist ein Potentiometer, mit dem du die Lautstärke regeln kannst. Und zuletzt bietet das Si403 Radio-Modul auch RDS (Radio Data System) – damit kannst du weitere Informationen empfangen, sofern sie die Sender zur Verfügung stellen bzw. über ihre Frequenz mitsenden.

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

]]>
So verwendest du einen Arduino Joystick https://polluxlabs.net/arduino-tutorials/arduino-joystick-erste-schritte/ Wed, 27 Nov 2024 10:41:40 +0000 https://polluxlabs.net/?p=18031 So verwendest du einen Arduino Joystick Weiterlesen »

]]>
Das Joystick-Modul ist ein vielseitiges Eingabegerät, das häufig in Maker-Projekten verwendet wird. In diesem Artikel erfährst du, wie du einen Arduino Joystick an einen Arduino UNO anschließt, wie er funktioniert und wie du ihn in deinem nächsten Projekt einsetzen kannst.

So funktioniert der Joystick

Das Joystick-Modul verwendet Potentiometer für die x- und y-Achse. Wenn der Joystick bewegt wird, ändert sich der Widerstand der Potentiometer, was zu einer Änderung der Ausgangsspannung führt. Diese Spannung wird vom Arduino ausgelesen und in einen Zahlenwert zwischen 0 und 1023 umgewandelt.

Mit diesen Werte kannst du dann weiterarbeiten: Die Höhe des Werts zeigt dir an, in welche Richtung der Joystick bewegt wurde, also nach oben, unten, links oder rechts. Neben den Potis für die x- und y-Achse findest du meistens noch einen Button, den du betätigst, indem den Stick drückst. Auch das kannst du natürlich in deinem Sketch auslesen und bei Bedarf in deinem Projekt verwenden.

Und so schließt du ihn an

Ein typischer Arduino Joystick hat fünf Pins. Die Stromversorgung und die beiden Anschlüsse für die x- und y-Achse sind hierbei obligatorisch. Der Pin SW ist mit dem Button verbunden – wenn in deinem Projekt später keinen Button benötigst, brauchst du ihn nicht mit deinem Microcontroller verbinden. Im Folgenden verwenden wir jedoch alle fünf Anschlüsse:

Anschlussdiagramm des Arduino Joysticks am Arduino UNO R4

___STEADY_PAYWALL___

Beachte, dass die Beschriftung der Pins je nach Modell variieren kann: Meistens findest du jedoch VRx für die horizontale x-Achse (HOR) und VRy für die vertikale y-Achse (VER).

Ein erster Test mit dem ARduino Joystick

Nun, wo du den Joystick angeschlossen hast, wird es Zeit für einen ersten Test. Hierbei liest du einfach die Bewegungen des Sticks und den Button aus und zeigst deine Aktionen entsprechend im Seriellen Monitor an.

Kopiere den folgenden Sketch und lade ihn auf deinen Arduino:

const int joyPinX = A0; // Pin für die x-Achse des Joysticks
const int joyPinY = A1; // Pin für die y-Achse des Joysticks
const int joyPinSW = A2; // Pin für den Button des Joysticks

void setup() {
  pinMode(joyPinSW, INPUT_PULLUP); // Button-Pin als Eingang mit internem Pull-up-Widerstand
  Serial.begin(9600); // Serielle Kommunikation starten
}

void loop() {
  int xValue = analogRead(joyPinX); // x-Achse auslesen
  int yValue = analogRead(joyPinY); // y-Achse auslesen
  int swValue = analogRead(joyPinSW); // Button auslesen

  // Richtung ermitteln
  String direction = "Mitte";
  if (xValue > 600) {
    direction = "Rechts";
  } else if (xValue < 400) {
    direction = "Links";
  }
  if (yValue > 600) {
    direction = "Unten";
  } else if (yValue < 400) {
    direction = "Oben";
  }

  // Werte im seriellen Monitor anzeigen
  Serial.print("Richtung: ");
  Serial.print(direction);
  Serial.print(" | X: ");
  Serial.print(xValue);
  Serial.print(" | Y: ");
  Serial.print(yValue);
  Serial.print(" | Button: ");
  Serial.print(swValue);
  if (swValue < 20) {
    Serial.println(" | Gedrückt!");
  } else {
    Serial.println(" | ");
  }

  delay(100); // Kurze Pause
}

Teste nun deinen Joystick – siehst du im Seriellen Monitor die Richtungen und einen Hinweis, wenn du den Button drückst? Falls du dich wunderst, dass die ermittelten Richtungen nicht mit deinen Absichten übereinstimmen: Dann positioniere deinen Arduino Joystick bitte wie auf diesem Foto. 😉

Arduino Joystick richtig ausrichten

So funktioniert der Sketch

Zunächst legst du die Pins fest, an denen dein Joystick mit dem Arduino UNO verbunden ist:

const int joyPinX = A0; // Pin für die x-Achse des Joysticks
const int joyPinY = A1; // Pin für die y-Achse des Joysticks
const int joyPinSW = A2; // Pin für den Button des Joysticks

Im Setup legst du fest, dass der Button des Arduino Joysticks den internen Pullup-Widerstand nutzt, damit du seine Werte zuverlässig auslesen kannst. Außerdem startest du den Seriellen Monitor:

void setup() {
  pinMode(joyPinSW, INPUT_PULLUP); // Button-Pin als Eingang mit internem Pull-up-Widerstand
  Serial.begin(9600); // Serielle Kommunikation starten
}

Der Loop beginnt damit, dass dein Arduino die aktuellen Werte des Joysticks einliest und basierend darauf die gedrückte Richtung ermittelt:

void loop() {
  int xValue = analogRead(joyPinX); // x-Achse auslesen
  int yValue = analogRead(joyPinY); // y-Achse auslesen
  int swValue = analogRead(joyPinSW); // Button auslesen

  // Richtung ermitteln
  String direction = "Mitte";
  if (xValue > 600) {
    direction = "Rechts";
  } else if (xValue < 400) {
    direction = "Links";
  }
  if (yValue > 600) {
    direction = "Unten";
  } else if (yValue < 400) {
    direction = "Oben";
  }

Wie eingangs erwähnt sind im Arduino Joystick zwei Potentiometer verbaut, für jede Achse eines. Am Beispiel der x-Achse bedeutet das: Ein Drücken nach links bis zum Anschlag erzeugt den Wert 0. Diese Werte steigen an, bis sie in der Ruheposition des Sticks im Bereich von 500 liegen. Dein Drücken nach rechts lässt die Werte weiter steigen, bis sie am Anschlag den vollen Wert 1023 erreichen.

Fehlen noch zwei Dinge: Die Ausgabe der Richtungen im Seriellen Monitor und der Check, ob du den Button des Sticks gedrückt hast:

  // Werte im seriellen Monitor anzeigen
  Serial.print("Richtung: ");
  Serial.print(direction);
  Serial.print(" | X: ");
  Serial.print(xValue);
  Serial.print(" | Y: ");
  Serial.print(yValue);
  Serial.print(" | Button: ");
  Serial.print(swValue);
  if (swValue < 20) {
    Serial.println(" | Gedrückt!");
  } else {
    Serial.println(" | ");
  }

  delay(100); // Kurze Pause
}

Eine Besonderheit des Arduino Joysticks ist, dass du an einem Analog-Pin ermitteln musst, ob der Button gedrückt wurde. Der ungedrückte Button liefert an seinem Pin SW immer einen Wert von ungefähr 500. Erst wenn du ihn betätigst fällt dieser Wert auf Null. Diesen Moment fängst du im Code oben mit einer If-Abfrage ein und gibst entsprechend ein Gedrückt! im Seriellen Monitor aus.

Analog-Stick und diagonale Bewegungen

Wie du im Seriellen Monitor erkennst, ermittelt dein Arduino Werte, die von der Intensität deiner Bewegungen abhängen. Das heißt konkret, dass zum Beispiel der Wert bei einer Bewegung nach rechts immer weiter ansteigt, bis er ab einer Größe von 600 als „nach rechts gedrückt“ gilt. Damit kannst du aber auch feine Bewegungen erkennen, ähnlich wie bei einem Analog-Stick eines herkömmlichen Controllers für die Playstation, Xbox oder Switch.

Falls du also für dein Projekt sowohl schnelle als auch langsame Bewegungen erkennen möchtest, setze bei den Werten mehrere Schwellenwerte: Der Wert 600 könnte dann ein gemächliches Schleichen sein, der Maximalwert von 1023 (also der Anschlag des Joysticks nach rechts) dann die volle Geschwindigkeit.

Auch diagonale Möglichkeiten sind möglich, denn diese sind nichts anderes als eine Kombination von x- und y-Werten. Ersetze in deinem Sketch den Teil Richtung ermitteln durch folgenden Code:

  // Richtung ermitteln
  String direction = "Mitte";
  if (xValue > 600 && yValue > 600) {
    direction = "Rechts-Unten";
  } else if (xValue > 600 && yValue < 400) {
    direction = "Rechts-Oben";
  } else if (xValue < 400 && yValue > 600) {
    direction = "Links-Unten";
  } else if (xValue < 400 && yValue < 400) {
    direction = "Links-Oben";
  } else if (xValue > 600) {
    direction = "Rechts";
  } else if (xValue < 400) {
    direction = "Links";
  } else if (yValue > 600) {
    direction = "Unten";
  } else if (yValue < 400) {
    direction = "Oben";
  }

Hier wird zum Beispiel eine Bewegung nach links oben erkannt, wenn sowohl der x-Wert als auch der y-Wert unter 400 liegt. Entsprechend behandelst du die anderen drei diagonalen Richtungen. Das ermöglicht dir weitere Möglichkeiten für deine Steuerung – mit etwas Fleißarbeit sind sogar noch weitere Abstufungen möglich.

Wie geht es weiter?

Jetzt wo du weißt, wie du deinen Arduino Joystick anschließt und verwendest, möchtest du bestimmt ein passendes Projekt bauen. In diesem Tutorial lernst du, wie du den Spieleklassiker Snake auf einem Arduino und ESP32 umsetzt.

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

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

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

Gmail für den Versand der E-Mails einrichten

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

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

Die benötigten Python-Bibliotheken

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

pip install numpy scipy

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

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

___STEADY_PAYWALL___

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

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

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

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

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

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

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

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

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

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

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

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

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

So funktioniert das Script

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

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

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

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

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

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

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

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

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

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

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

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

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

Das Script für den Empfänger

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

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

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

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

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

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

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

    return binary_data

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

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

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

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

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

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

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

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

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

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

So funktioniert das Script

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

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

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

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

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

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

       return binary_data

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

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

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

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

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

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

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

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

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

Wie geht es weiter?

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

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

]]>