Pollux Labs https://polluxlabs.net Arduino, ESP32 & ESP8266 | Projekte & Tutorials Thu, 12 Dec 2024 10:58:56 +0000 de hourly 1 https://wordpress.org/?v=6.6.2 https://polluxlabs.net/wp-content/uploads/2020/05/cropped-pollux-labs-p-32x32.png Pollux Labs https://polluxlabs.net 32 32 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.

]]>
OTA Updates für den ESP32 – Aktualisierungen aus der Ferne https://polluxlabs.net/arduino-tutorials/ota-updates-fuer-den-esp32/ Mon, 11 Nov 2024 08:30:57 +0000 https://polluxlabs.net/?p=17845 OTA Updates für den ESP32 – Aktualisierungen aus der Ferne Weiterlesen »

]]>
Der ESP32 unterstützt OTA (Over-the-Air), mit dem du den Sketches drahtlos aktualisieren kannst. OTA ist besonders hilfreich, wenn der Mikrocontroller schwer zugänglich ist oder du Änderungen ohne physische Verbindung übertragen möchtest. In diesem Tutorial erfährst du, wie du OTA einrichtest und lernst Schritt für Schritt ein Beispielprojekt kennen: Eine blinkende LED, deren Blinkfrequenz per OTA-Update verändert wird.

Die Bibliothek ArduinoOTA

Falls du die ESP32-Boards über den Boardverwalter der Arduino-IDE hinzugefügt hast, ist die Bibliothek ArduinoOTA normalerweise schon dabei. Die Bibliothek ermöglicht dir eine unkomplizierte Integration der OTA-Funktionalität.

Sollte die Bibliothek dennoch fehlen, kannst du sie über den Bibliotheksverwalter installieren. Gehe dazu in der Arduino-IDE zu Sketch > Bibliothek einbinden > Bibliotheken verwalten und suche nach “ArduinoOTA”. Stelle sicher, dass die neueste Version installiert ist, damit du alle aktuellen Features und Sicherheitsverbesserungen nutzen kannst.

Die Bibliothek ArduinoOTA installieren

Schritt 1: Der erste Sketch für eine blinkende LED

Du startest mit einem einfachen Sketch, der eine LED am ESP32 im Sekundentakt blinken lässt. Die LED ist dabei über einen passenden Vorwiderstand an den GPIO-Pin 2 des ESP32 angeschlossen. Dieser Sketch dient als Grundlage für das OTA-Update.

#include <WiFi.h>
#include <ArduinoOTA.h>

const char* ssid = "Dein_WLAN-NETZWERK";          // Ersetze durch deinen WLAN-Namen
const char* password = "Dein_WLAN_Passwort";  // Ersetze durch dein WLAN-Passwort

#define LED_PIN 2 // GPIO-Pin der LED

unsigned long previousMillis = 0;
const long interval = 1000; // Blinkintervall in Millisekunden (1 Sekunde)

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);

  // WLAN-Verbindung herstellen
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nVerbunden mit WiFi");

  // OTA-Setup
  ArduinoOTA.setHostname("esp32_led_ota");
  ArduinoOTA.setPassword("Dein_Sicheres_Passwort"); // Setze hier ein starkes Passwort für OTA-Updates, um unbefugten Zugriff zu verhindern
  ArduinoOTA.begin();
}

void loop() {
  ArduinoOTA.handle(); // Nach OTA-Updates suchen

  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    digitalWrite(LED_PIN, !digitalRead(LED_PIN)); // Zustand der LED umschalten
  }
}

So funktioniert der Sketch

  1. Bibliotheken einbinden: Der Sketch beginnt mit dem Einbinden der notwendigen Bibliotheken WiFi.h und ArduinoOTA.h. Erstere ermöglicht die Verbindung des ESP32 mit einem WLAN-Netzwerk, während ArduinoOTA.h die OTA-Funktionalität bereitstellt. Diese Bibliotheken sind notwendig, um die gewünschten Netzwerk- und Update-Funktionen auf dem ESP32 zu implementieren.
  2. Netzwerk-Konfiguration: Die Variablen ssid und password speichern die Zugangsdaten für dein WLAN. Diese werden verwendet, um den ESP32 mit deinem Netzwerk zu verbinden.
  3. GPIO-Pin-Definition: #define LED_PIN 2 definiert den Pin, an den die LED angeschlossen ist. In diesem Fall nutzt du den GPIO-Pin 2 des ESP32.
  4. WLAN-Verbindung herstellen: Im setup()-Teil des Codes wird die Verbindung zum WLAN hergestellt. Mit WiFi.begin(ssid, password) verbindet sich der ESP32 mit dem Netzwerk. Die Schleife while (WiFi.status() != WL_CONNECTED) sorgt dafür, dass das Programm wartet, bis die Verbindung hergestellt ist.
  5. OTA-Setup: Im setup()-Teil wird auch die OTA-Funktionalität initialisiert. Mit ArduinoOTA.setHostname("esp32_led_ota") wird der Name des ESP32 im Netzwerk festgelegt. Dieser Name erleichtert es, das Gerät im Netzwerk zu identifizieren, besonders wenn du mehrere ESP32-Geräte verwendest. Ein (möglichst sicheres) Passwort hinterlegst du mit Hilfe der Funktion ArduinoOTA.setPassword("Mein_OTA_Passwort"), damit nur du Updates durchführen kannst. ArduinoOTA.begin() startet den OTA-Service, damit der ESP32 auf eingehende Updates wartet.
  6. LED-Blinken: Die Funktion loop() enthält den Code, der die LED im Sekundentakt blinken lässt. Mit der Funktion millis() wird überprüft, ob der festgelegte Intervall (1000 Millisekunden) vergangen ist. Wenn dies der Fall ist, wird der Zustand der LED umgeschaltet mit digitalWrite(LED_PIN, !digitalRead(LED_PIN)).
  7. OTA-Handler: In der loop()-Funktion wird ArduinoOTA.handle() aufgerufen, um kontinuierlich nach OTA-Updates zu suchen. Dies ermöglicht es, jederzeit ein Update zu empfangen, während das Hauptprogramm weiterläuft.

Schritt 2: OTA-Update zur Änderung des intervalls

Nun nimmst du eine Änderung am Code vor, um die Blinkfrequenz der LED auf 500 ms zu reduzieren. Diese Änderung überträgst du drahtlos per OTA auf den ESP32.

Ändere im obigen Sketch den Wert des Intervalls von 1000 auf 500:

const long interval = 500; // Blinkintervall in Millisekunden (500 ms)

Update via OTA durchführen

Nun folgt der Upload des aktualisierten Sketchs. Sofern dein ESP32 mit deinem WLAN-Netzwerk verbunden ist, sollte er in der Arduino-IDE als Netzwerkport sichtbar sein. Gehe hierfür zu Werkzeuge > Port und wähle den ESP32 (esp32_led_ota) aus.

Der ESP32 im Netzwerk finden für das Update per OTA

OTA-Update hochladen: Lade den neuen Sketch (mit 500 ms Blinkintervall) über den Netzwerkport hoch. Klicke hierfür einfach wie gewohnt auf den Upload-Button – so wie du es auch machst, wenn dein ESP32 über ein USB-Kabel verbunden ist. Achte darauf, dass in deinem Update wieder die WLAN-Zugangsdaten und dein Passwort hinterlegt sind.

Die Arduino-IDE fordert dich auf, das OTA-Passwort einzugeben. Gib das definierte Passwort (“Mein_OTA_Passwort”) ein und das Update wird drahtlos übertragen.

Hinweis: Solltest du Updates mit verschiedenen Passwörtern machen, kann es sein, dass die Arduino IDE ein falsches Passwort automatisch verwenden möchte. Schließe in diesem Fall die IDE und öffne sie erneut – dann wirst du wieder nach dem Passwort für deinen ESP32 gefragt.

Nachdem das Update drahtlos auf deinen ESP32 übertragen wurde, sollte die LED nun im neuen Halbsekundentakt aufleuchten. Und das war es auch schon – du kennst nun eine Möglichkeit, Programme zu aktualisieren, ohne den Microcontroller irgendwo ausbauen und an deinen Computer anschließen zu müssen. Weitere Informationen zum Thema findest du bei Espressif.

]]>
UX-Design für Maker: So baust du benutzerfreundliche Projekte https://polluxlabs.net/arduino-tutorials/ux-design-fur-maker/ Tue, 05 Nov 2024 19:15:12 +0000 https://polluxlabs.net/?p=17071 UX-Design für Maker: So baust du benutzerfreundliche Projekte Weiterlesen »

]]>
Als Maker sind wir oft so fasziniert von unseren Projekten, dass wir einen entscheidenden Aspekt übersehen: die Benutzerfreundlichkeit. Wir löten begeistert Bauteile zusammen, programmieren Mikrocontroller und konstruieren komplexe Mechanismen. Doch wenn unser Projekt fertig ist, stellen wir fest, dass nur wir selbst es richtig bedienen können. Freunde und Familie, denen wir unsere neueste Kreation vorführen wollen, stehen ratlos davor.

Hier kommt UX-Design ins Spiel. UX steht für “User Experience”, also Nutzererfahrung. Es geht darum, wie Menschen mit unserem Produkt interagieren und welche Erfahrungen sie dabei machen. Gutes UX-Design sorgt dafür, dass die Bedienung intuitiv, effizient und angenehm ist.

Don Norman, einer der Pioniere des UX-Designs, hat in seinem Buch “The Design of Everyday Things” grundlegende Prinzipien formuliert, die auch für uns Maker Gold wert sind. In diesem Artikel werden wir diese Prinzipien auf die Welt der DIY-Elektronik und Maker-Projekte übertragen.

Warum ist UX-Design für Maker wichtig?

Zum einen macht es unsere Projekte zugänglicher für andere. Ein gut designtes Gerät kann von jedem genutzt werden, nicht nur vom Erfinder. Zum anderen verbessert es auch unsere eigene Nutzererfahrung. Wer hat sich nicht schon einmal über seine eigenen, unübersichtlich angeordneten Buttons und Regler geärgert?

Gutes UX-Design muss dabei nicht bedeuten, dass wir Kompromisse bei der Funktionalität eingehen. Im Gegenteil: Oft führt die Auseinandersetzung mit UX-Fragen zu cleveren Lösungen, die unser Projekt noch verbessern.

Grundprinzipien des UX-Designs nach Don Norman

In seinem wegweisenden Werk The Design of Everyday Things* hat Don Norman einige fundamentale Prinzipien des UX-Designs formuliert, die für uns Maker von großem Wert sind. In diesem Beitrag soll es um drei der wichtigsten Konzepte gehen:

  1. Gulf of Execution und Gulf of Evaluation:Der Gulf of Execution beschreibt die Kluft zwischen den Zielen des Nutzers und den Möglichkeiten des Systems. Der Gulf of Evaluation ist die Kluft zwischen dem, was das System tut, und dem Verständnis des Nutzers davon.
  2. Affordances und Signifiers: Affordances sind die möglichen Interaktionen zwischen einem Objekt und dem Nutzer. Ein Drehregler “bietet” das Drehen an. Signifiers sind die sichtbaren Hinweise auf diese Möglichkeiten.
  3. Feedback:Jede Aktion des Nutzers sollte eine wahrnehmbare Reaktion hervorrufen.

Diese Prinzipien bilden das Fundament für benutzerfreundliches Design. Als Maker können wir sie nutzen, um Projekte zu bauen, die nicht nur funktionieren, sondern auch Freude bei der Benutzung bereiten. In den folgenden Abschnitten werden wir uns ansehen, wie wir diese Konzepte konkret in unseren DIY-Projekten umsetzen können.

Gulf of Execution und Evaluation

Zwei zentrale Konzepte in Don Normans Theorie, die für uns Maker besonders relevant sind, sind der “Gulf of Execution” und der “Gulf of Evaluation”. Diese beschreiben die Herausforderungen, mit denen Nutzer bei der Interaktion mit unseren Projekten konfrontiert werden.

Gulf of Execution und Gulf of Evaluation im UX-Design

Der “Gulf of Execution” ist die Kluft zwischen dem, was der Nutzer tun möchte, und den Möglichkeiten, die unser Gerät bietet. Stelle dir einen selbstgebauten Synthesizer vor: Der Nutzer möchte einen bestimmten Sound erzeugen, aber wie? Sind die Regler logisch angeordnet? Sind die Funktionen klar beschriftet? Je größer diese Kluft, desto frustrierender die Erfahrung.

Der “Gulf of Evaluation” hingegen beschreibt die Schwierigkeit, den aktuellen Zustand des Systems zu verstehen. Hat unser Arduino die Eingabe verarbeitet? Wurde die Einstellung gespeichert? Läuft das Programm überhaupt? Ohne klares Feedback tappt der Nutzer im Dunkeln.

Als Maker ist es unsere Aufgabe, Brücken über diese Klüfte zu bauen:

  1. Überbrücken des Gulf of Execution:
    • Verwenden eindeutige Beschriftungen und Symbole
    • Gruppiere zusammengehörige Funktionen
    • Benutze vertraute Metaphern, zum Biespiel ein Zahnrad-Symbol für Einstellungen
  2. Überbrücken des Gulf of Evaluation:
    • Integriere Status-LEDs für wichtige Funktionen
    • Verwende Displays zur Anzeige komplexer Informationen
    • Gibt akustisches Feedback bei wichtigen Ereignissen

Ein Beispiel: Bei einem Smart-Home-Projekt könnte ein Touch-Display die aktuelle Raumtemperatur anzeigen (Evaluation) und gleichzeitig als Schieberegler zur Temperatureinstellung dienen (Execution). Eine kurze Vibration bestätigt die Eingabe, während eine animierte Kurve den Temperaturverlauf visualisiert.

Indem wir diese Konzepte berücksichtigen, machen wir unsere Projekte nicht nur funktional, sondern auch intuitiv bedienbar. Das Ziel ist es, dass der Nutzer mühelos von der Idee zur Ausführung und dann zum Verständnis des Ergebnisses gelangt.

Affordances und Signifiers

Affordances und Signifiers sind zentrale Konzepte, die uns helfen, die Interaktionen zwischen Nutzer und Objekt zu verbessern. Affordances beschreiben, welche Aktionen ein Objekt anbietet. Ein einfacher Drehregler an einem Verstärker bietet beispielsweise die Affordance, gedreht zu werden, um die Lautstärke zu verändern. Das Design eines Objekts sollte klar machen, welche Interaktionen möglich sind.

Signifiers sind die visuellen Hinweise, die dem Nutzer zeigen, wie er interagieren soll. Sie können physisch (z.B. eine Markierung auf einem Schalter) oder digital (z.B. ein Symbol auf einem Touchscreen) sein. Signifiers können oft auch dazu beitragen, Missverständnisse zu vermeiden. Wenn ein Knopf gedrückt werden soll, könnte er leicht hervorgehoben und beschriftet sein, um die Funktion deutlich zu machen.

Als Maker können wir Affordances und Signifiers gezielt einsetzen, um unsere Projekte benutzerfreundlicher zu gestalten. Zum Beispiel könnten wir:

  • Einen Drehregler mit einer sichtbaren Markierung versehen, um zu verdeutlichen, in welche Richtung gedreht werden kann.
  • Bei einem selbstgebauten Bedienfeld alle Tasten so gestalten, dass deren Funktionen klar erkennbar sind (z.B. durch eindeutige Symbole).
  • LED-Streifen als Signifiers nutzen, um den Nutzer zu bestimmten Bereichen des Geräts zu leiten, besonders bei komplexeren Projekten.

Ein weiteres Beispiel sind Touch-Interfaces: Wenn wir ein Touchscreen-Display nutzen, sollten wir darauf achten, dass die zu drückenden Bereiche klar markiert und ausreichend groß sind, um eine präzise Eingabe zu ermöglichen. Das Verwenden von Signifiers sorgt dafür, dass der Nutzer die beabsichtigte Interaktion schnell erkennt und umsetzen kann.

Indem wir Affordances und Signifiers bewusst einsetzen, machen wir die Benutzung unserer Projekte intuitiver und senken die Einstiegshürde für neue Nutzer.

Feedback

Feedback ist ein weiterer essenzieller Aspekt des UX-Designs. Jede Aktion, die ein Nutzer durchführt, sollte eine wahrnehmbare Reaktion hervorrufen. Ohne Feedback ist es für den Nutzer schwer zu erkennen, ob seine Interaktion erfolgreich war oder ob ein Fehler aufgetreten ist – siehe den erwähnten Gulf of Evaluation.

Als Maker können wir viele verschiedene Formen von Feedback einsetzen:

  • Visuelles Feedback: LED-Anzeigen, die aufleuchten, wenn eine Funktion aktiviert wird, oder ein Display, das den aktuellen Status zeigt. Zum Beispiel kann eine grüne LED anzeigen, dass ein System betriebsbereit ist, während eine rote LED einen Fehler signalisiert.
  • Akustisches Feedback: Ein Piepton kann darauf hinweisen, dass eine Eingabe erfolgt ist oder ein Fehler aufgetreten ist. Ein sanfter Signalton kann bestätigen, dass eine Einstellung erfolgreich vorgenommen wurde.
  • Haptisches Feedback: Vibrationen bei der Berührung eines Touchscreens können dem Nutzer zusätzliche Bestätigung bieten, dass eine Eingabe registriert wurde.

Ein Beispiel für gutes Feedback bei einem selbstgebauten Code-Schloss: Wenn der Nutzer den richtigen Code eingibt, leuchtet eine grüne LED auf. Bei einem falschen Code hingegen leuchtet eine rote LED.

Code-Schloss Arduino

Feedback hilft nicht nur dem Nutzer, die Funktionalität zu verstehen, sondern trägt auch dazu bei, Vertrauen in das System aufzubauen. Es reduziert Unsicherheit und sorgt dafür, dass der Nutzer immer weiß, was gerade passiert. Besonders bei komplexeren Projekten ist es wichtig, dass der Nutzer sofort Rückmeldung erhält, ob seine Eingaben erfolgreich waren oder nicht.

Indem wir in unseren Projekten stets Feedback einplanen, sorgen wir dafür, dass Nutzer mit unseren Kreationen besser interagieren können und die Bedienung angenehmer und effizienter wird.

Fazit

UX-Design ist ein unverzichtbarer Bestandteil des Entwicklungsprozesses, der unsere Projekte von rein technischen Spielereien zu benutzerfreundlichen und inspirierenden Kreationen macht. Indem wir die Prinzipien von Don Norman – wie den Gulf of Execution und Evaluation, Affordances und Signifiers sowie Feedback – in unsere DIY-Projekte integrieren, können wir die Benutzerfreundlichkeit erheblich steigern.

Unsere Projekte sollen nicht nur funktionieren, sondern auch von anderen Menschen genutzt und geschätzt werden können. Gutes UX-Design ermöglicht es uns, Barrieren abzubauen, die Bedienung zu vereinfachen und das Nutzererlebnis zu verbessern. Letztendlich schaffen wir damit Geräte, die nicht nur uns selbst stolz machen, sondern auch andere begeistern und inspirieren.

Also, denke bei deinen nächsten Projekten nicht nur an die Technik, sondern auch an die Menschen, die sie nutzen werden. Denn die beste Technologie ist die, die Freude bringt und von allen verstanden wird.

]]>
Snake spielen auf dem Arduino (und dem ESP32) https://polluxlabs.net/arduino-projekte/snake-spielen-auf-dem-arduino-und-dem-esp32/ Sun, 27 Oct 2024 20:51:07 +0000 https://polluxlabs.net/?p=17469 Snake spielen auf dem Arduino (und dem ESP32) Weiterlesen »

]]>
Der Spieleklassiker Snake begleitet uns schon lange und auf ganz unterschiedlichen Geräten – auf dem PC, auf Taschenrechnern von Texas Instruments oder auch auf dem ehrwürdigen Nokia 3210. Dank des einfachen Prinzips ist das Spiel fast überall dort umzusetzen, wo etwas Prozessorleistung vorhanden ist. Also warum nicht auch auf einem Arduino?

In diesem Projekt baust du dir zunächst eine Snake-Version auf einem Arduino UNO R4. Als Display verwendest du hierbei ein OLED-Display mit 128×64 Pixeln. Steuern wirst du das Spiel mit einem kleinen Joystick. Da das Spiel zwar auf dem Arduino UNO läuft, für geschickte Spieler jedoch vielleicht etwas zu langsam ist, kommt später noch ein ESP32 zum Zug. Dieser verfügt über mehr Leistung und sorgt für ein flüssigeres Spielvergnügen.

Für dieses Projekt benötigst du:

Snake auf dem Arduino UNO

Zunächst also die Version auf dem Arduino UNO. Hier eignet sich der “neue” UNO R4, da dieser deutlich mehr Leistung als sein Vorgänger R3 besitzt. Ob du die WiFi- oder die Minima-Version verwendest, ist für dieses Projekt egal. Falls du jedoch noch überlegst, dir einen R4 zuzulegen, empfehle ich dir auf jeden Fall erstere, da der R4 WiFi nur ein paar Euro mehr kostet und du damit mit deinen Projekten ins Internet kannst.

Nun aber zum Anschluss der Bauteile: Verbinde das OLED-Display und den den Joystick folgendermaßen mit dem Arduino UNO:

Anschluss von OLED-Display und Joystick am Arduino UNO für Snake

Je nachdem, welchen Joystick du verwendest, kann die Beschriftung der Pins variieren. So kann zum Beispiel statt VERT auch VRy zu lesen sein. Mehr Infos hierüber findest du im Tutorial zum Arduino Joystick. Wenn du alles verkabelt hast, kann es direkt mit dem Sketch weitergehen.

Die benötigten Bibliotheken

Damit du am Arduino dein OLED-Display verwenden kannst, benötigst du zwei Bibliotheken. Öffne in der Arduino IDE den Bibliotheksverwalter, suche und installiere dort diese beiden Bibliotheken von Adafruit:

Adafruit SSD1306
Adafruit GFX

Im Sketch wirst du gleich noch eine dritte Bibliothek sehen, Wire.h – diese ist jedoch bereits vorinstalliert, sodass du dich um diese nicht kümmern musst.

Der gesamte Sketch

Hier nun der vollständige Sketch für Snake. Kopiere dir den folgenden Code, erstelle einen neuen Sketch und lade ihn auf deinen Arduino hoch.

// Snake spielen auf dem Arduino
// Pollux Labs

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

// OLED Display Konfiguration
#define SCREEN_WIDTH 128  // Breite des OLED-Displays
#define SCREEN_HEIGHT 64  // Höhe des OLED-Displays
#define OLED_RESET    -1  // OLED-Reset-Pin (nicht verwendet)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Joystick Konfiguration
#define VRX_PIN A0  // Joystick X-Achsen-Pin (analoger Pin A0 des Arduino Uno)
#define VRY_PIN A1  // Joystick Y-Achsen-Pin (analoger Pin A1 des Arduino Uno)
#define SW_PIN  7   // Joystick Taster-Pin (digitaler Pin 7 des Arduino Uno)

// Eigenschaften der Schlange
#define SNAKE_SIZE 4  // Größe jedes Segments der Schlange
#define MAX_SNAKE_LENGTH 50  // Maximale Länge der Schlange (reduziert für Arduino Uno, um Speicher zu sparen)
int snakeX[MAX_SNAKE_LENGTH], snakeY[MAX_SNAKE_LENGTH];  // Arrays zur Speicherung der Segmentpositionen der Schlange
int snakeLength = 5;  // Anfangslänge der Schlange
int directionX = 1, directionY = 0;  // Anfangsrichtung der Bewegung (nach rechts)

// Eigenschaften des Futters
int foodX = random(2, (SCREEN_WIDTH - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;  // X-Koordinate des Futters (nicht direkt am Rand)
int foodY = random(12 / SNAKE_SIZE, (SCREEN_HEIGHT - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;  // Y-Koordinate des Futters (Spielbereich unterhalb des Rahmens, nicht direkt am Rand)

// Score
int score = 0;

void setup() {
  Serial.begin(9600);  // Initialisiere serielle Kommunikation mit niedrigerer Baudrate für Arduino Uno

  // Initialisiere Display
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {  // Initialisiere das OLED-Display mit der I2C-Adresse 0x3C
    Serial.println(F("SSD1306 allocation failed"));
    for (;;);  // Stoppe, falls die Display-Initialisierung fehlschlägt
  }
  display.clearDisplay();  // Leere den Display-Puffer
  display.display();  // Zeige den geleerten Puffer an

  // Initialisiere Joystick
  pinMode(VRX_PIN, INPUT);  // Setze Joystick X-Achsen-Pin als Eingang
  pinMode(VRY_PIN, INPUT);  // Setze Joystick Y-Achsen-Pin als Eingang
  pinMode(SW_PIN, INPUT_PULLUP);  // Setze Joystick-Taster-Pin als Eingang mit internem Pull-up-Widerstand

  // Initialisiere Position der Schlange
  for (int i = 0; i < snakeLength; i++) {
    snakeX[i] = SCREEN_WIDTH / 2 - (i * SNAKE_SIZE);  // Setze anfängliche X-Koordinaten der Schlangensegmente
    snakeY[i] = SCREEN_HEIGHT / 2;  // Setze anfängliche Y-Koordinate der Schlangensegmente
  }
  Serial.println("Setup abgeschlossen");
}

void loop() {
  // Lese Joystick-Werte
  int xValue = analogRead(VRX_PIN);  // Lese X-Achsen-Wert vom Joystick
  int yValue = analogRead(VRY_PIN);  // Lese Y-Achsen-Wert vom Joystick
  Serial.print("Joystick X: ");
  Serial.print(xValue);
  Serial.print(" Y: ");
  Serial.println(yValue);

  // Setze Richtung basierend auf Joystick-Eingaben, verhindere diagonale Bewegung
  if (xValue < 300 && directionX == 0) {  // Bewegung nach links, wenn derzeit nicht horizontal bewegt wird
    directionX = -1;
    directionY = 0;
    Serial.println("Richtung: Links");
  } else if (xValue > 700 && directionX == 0) {  // Bewegung nach rechts, wenn derzeit nicht horizontal bewegt wird
    directionX = 1;
    directionY = 0;
    Serial.println("Richtung: Rechts");
  } else if (yValue < 300 && directionY == 0) {  // Bewegung nach oben, wenn derzeit nicht vertikal bewegt wird
    directionX = 0;
    directionY = -1;
    Serial.println("Richtung: Oben");
  } else if (yValue > 700 && directionY == 0) {  // Bewegung nach unten, wenn derzeit nicht vertikal bewegt wird
    directionX = 0;
    directionY = 1;
    Serial.println("Richtung: Unten");
  }

  // Aktualisiere Position der Schlange
  for (int i = snakeLength - 1; i > 0; i--) {  // Bewege jedes Segment zur Position des vorherigen Segments
    snakeX[i] = snakeX[i - 1];
    snakeY[i] = snakeY[i - 1];
  }
  snakeX[0] += directionX * SNAKE_SIZE;  // Aktualisiere Kopfposition in X-Richtung
  snakeY[0] += directionY * SNAKE_SIZE;  // Aktualisiere Kopfposition in Y-Richtung
  Serial.print("Schlangenkopf X: ");
  Serial.print(snakeX[0]);
  Serial.print(" Y: ");
  Serial.println(snakeY[0]);

  // Prüfe auf Kollision mit dem Futter
  if (snakeX[0] == foodX && snakeY[0] == foodY) {  // Wenn der Schlangenkopf das Futter erreicht
    if (snakeLength < MAX_SNAKE_LENGTH) {
      snakeLength++;  // Erhöhe die Schlangenlänge
      score++;  // Erhöhe den Score
      Serial.println("Futter gegessen, Schlangenlänge: " + String(snakeLength));
    }
    // Generiere neue Futterposition (nicht direkt am Rand)
    foodX = random(2, (SCREEN_WIDTH - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
    foodY = random(12 / SNAKE_SIZE, (SCREEN_HEIGHT - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
    Serial.print("Neues Futter bei X: ");
    Serial.print(foodX);
    Serial.print(" Y: ");
    Serial.println(foodY);
  }

  // Prüfe auf Kollision mit den Wänden
  if (snakeX[0] < SNAKE_SIZE || snakeX[0] >= SCREEN_WIDTH - SNAKE_SIZE || snakeY[0] < 10 || snakeY[0] >= SCREEN_HEIGHT - SNAKE_SIZE) {
    Serial.println("Kollision mit der Wand, Spiel wird zurückgesetzt");
    resetGame();  // Setze das Spiel zurück, wenn die Schlange die Wand trifft
  }

  // Prüfe auf Kollision mit sich selbst
  for (int i = 1; i < snakeLength; i++) {
    if (snakeX[0] == snakeX[i] && snakeY[0] == snakeY[i]) {  // Wenn der Schlangenkopf mit ihrem eigenen Körper kollidiert
      Serial.println("Kollision mit sich selbst, Spiel wird zurückgesetzt");
      resetGame();  // Setze das Spiel zurück
    }
  }

  // Zeichne alles
  display.clearDisplay();  // Leere den Display-Puffer

  // Zeichne den Score
  display.setTextSize(1);  // Setze Textgröße
  display.setTextColor(SSD1306_WHITE);  // Setze Textfarbe
  display.setCursor(0, 0);  // Setze Cursor für Score
  display.print("Score: ");
  display.print(score);  // Zeige aktuellen Score an

  // Zeichne den Rand um das Spielfeld
  display.drawRect(0, 10, SCREEN_WIDTH, SCREEN_HEIGHT - 10, SSD1306_WHITE);  // Zeichne einen weißen Rahmen um das Spielfeld

  // Zeichne die Schlange
  for (int i = 0; i < snakeLength; i++) {
    display.fillRect(snakeX[i], snakeY[i], SNAKE_SIZE, SNAKE_SIZE, SSD1306_WHITE);  // Zeichne jedes Segment der Schlange
  }

  // Zeichne das Futter
  display.fillRect(foodX, foodY, SNAKE_SIZE, SNAKE_SIZE, SSD1306_WHITE);  // Zeichne das Futter

  display.display();  // Zeige den aktualisierten Puffer an

  delay(150);  // Verzögerung zur Steuerung der Geschwindigkeit der Schlange
}

void resetGame() {
  // Setze Eigenschaften der Schlange zurück
  snakeLength = 5;  // Setze die Schlangenlänge zurück
  directionX = 1;  // Setze die Richtung nach rechts zurück
  directionY = 0;
  score = 0;  // Setze den Score zurück
  for (int i = 0; i < snakeLength; i++) {
    snakeX[i] = SCREEN_WIDTH / 2 - (i * SNAKE_SIZE);  // Setze die Position der Schlange zurück auf die Mitte
    snakeY[i] = SCREEN_HEIGHT / 2;
  }
  // Generiere neue Futterposition (nicht direkt am Rand)
  foodX = random(2, (SCREEN_WIDTH - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
  foodY = random(12 / SNAKE_SIZE, (SCREEN_HEIGHT - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
  Serial.println("Spiel zurückgesetzt");
  Serial.print("Neues Futter bei X: ");
  Serial.print(foodX);
  Serial.print(" Y: ");
  Serial.println(foodY);
}

Nach dem Upload sollte es auf dem Display direkt losgehen: In der Mitte erscheint, die zunächst 5 Pixel lange Schlange und bewegt sich. Ein Stück Futter erscheint zufällig platziert auf dem Spielfeld. Wie du sicherlich weißt, ist dein Ziel, so viel zu fressen wie möglich – ohne mit dem Spielfeldrand oder dir selbst zu kollidieren.

Oben links findest du deine Punktzahl. Für jede Verlängerung deiner Schlange erhöht sich dein Score um 1.

So funktioniert der Sketch

Lass uns ein paar wichtige Stellen des Sketchs anschauen.

Setup

Die setup()-Funktion führt alle notwendigen Initialisierungen durch:

  • Serielle Kommunikation wird gestartet, um Debugging-Informationen auszugeben.
  • Das OLED-Display wird initialisiert. Falls die Initialisierung fehlschlägt, wird eine Fehlermeldung ausgegeben und das Programm angehalten.
void setup() {
  Serial.begin(9600);
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("SSD1306 allocation failed"));
    for (;;);
  }
  display.clearDisplay();
  display.display();
}
  • Der Joystick wird initialisiert, indem seine Pins als Eingänge konfiguriert werden.
  • Die Position der Schlange wird initial in der Mitte des Displays gesetzt.

Loop

Die loop()-Funktion läuft kontinuierlich und behandelt alle Aspekte des Spiels, wie die Steuerung, Bewegung, Kollisionserkennung und die Ausgabe auf dem Display.

Joystick-Steuerung

Der Joystick wird verwendet, um die Bewegungsrichtung der Schlange zu ändern. Die analogen Eingänge des Joysticks liefern Werte, die bestimmen, in welche Richtung sich die Schlange bewegt:

  • Wenn der Joystick nach links bewegt wird, wird die Richtung auf links gesetzt, solange die Schlange sich nicht bereits nach rechts bewegt.
  • Gleiches gilt für die anderen Richtungen.
if (xValue < 300 && directionX == 0) {
  directionX = -1;
  directionY = 0;
  Serial.println("Richtung: Links");
}
Bewegung der Schlange

Die Position der Schlange wird durch eine Schleife aktualisiert, in der jedes Segment der Schlange zur Position des vorherigen Segments bewegt wird. Der Kopf der Schlange wird in die gewählte Richtung verschoben.

for (int i = snakeLength - 1; i > 0; i--) {
  snakeX[i] = snakeX[i - 1];
  snakeY[i] = snakeY[i - 1];
}
snakeX[0] += directionX * SNAKE_SIZE;
snakeY[0] += directionY * SNAKE_SIZE;
Fressen des Futters

Wenn die Schlange das Futter erreicht, wird ihre Länge erhöht, und ein neuer Punktestand wird berechnet. Danach wird das Futter an einer neuen Position generiert, die nicht direkt am Rand liegt, um das Spiel einfacher zu machen.

if (snakeX[0] == foodX && snakeY[0] == foodY) {
  if (snakeLength < MAX_SNAKE_LENGTH) {
    snakeLength++;
    score++;
  }
  foodX = random(2, (SCREEN_WIDTH - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
  foodY = random(12 / SNAKE_SIZE, (SCREEN_HEIGHT - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
}
Kollisionserkennung

Die loop()-Funktion prüft auch, ob die Schlange mit den Rändern des Spielfeldes oder mit sich selbst kollidiert:

  • Wenn die Schlange mit einer Wand kollidiert, wird das Spiel zurückgesetzt.
  • Wenn die Schlange ihren eigenen Körper berührt, wird ebenfalls das Spiel zurückgesetzt.
if (snakeX[0] < SNAKE_SIZE || snakeX[0] >= SCREEN_WIDTH - SNAKE_SIZE || snakeY[0] < 10 || snakeY[0] >= SCREEN_HEIGHT - SNAKE_SIZE) {
  resetGame();
}
Anzeige auf dem OLED-Display

Das OLED-Display wird in jeder Schleife aktualisiert:

  • Der Punktestand wird oben links angezeigt.
  • Ein weißer Rahmen wird um das Spielfeld gezeichnet, um die Grenzen des Spielfeldes anzuzeigen.
  • Die Schlange und das Futter werden auf dem Display gezeichnet.
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print("Score: ");
display.print(score);

display.drawRect(0, 10, SCREEN_WIDTH, SCREEN_HEIGHT - 10, SSD1306_WHITE);

for (int i = 0; i < snakeLength; i++) {
  display.fillRect(snakeX[i], snakeY[i], SNAKE_SIZE, SNAKE_SIZE, SSD1306_WHITE);
}
display.fillRect(foodX, foodY, SNAKE_SIZE, SNAKE_SIZE, SSD1306_WHITE);
display.display();

Das Spiel zurücksetzen

Die resetGame()-Funktion setzt die Schlange zurück auf ihre Ausgangsposition und -länge, wenn eine Kollision erkannt wird. Auch der Punktestand wird zurückgesetzt, und eine neue Position für das Futter wird generiert.

void resetGame() {
  snakeLength = 5;
  directionX = 1;
  directionY = 0;
  score = 0;
  for (int i = 0; i < snakeLength; i++) {
    snakeX[i] = SCREEN_WIDTH / 2 - (i * SNAKE_SIZE);
    snakeY[i] = SCREEN_HEIGHT / 2;
  }
}

Snake auf dem ESP32 spielen

Wenn du an dieser Stelle bereits Snake auf dem Arduino UNO gespielt hast, wirst du sicherlich bemerkt haben, dass das Spiel sehr gemächlich – um nicht zu sagen “ruckelig” – abläuft. Das liegt an der eher begrenzten Leistung, die auch beim Modell R4 nicht ausreichend ist, alle Berechnungen in der Loop-Funktion schnell genug durchzuführen.

Falls du jedoch auch einen ESP32 dein Eigen nennst, kannst du das Spiel (fast) ganz einfach umziehen und ihm so mehr Geschwindigkeit einhauchen. Schließe das OLED-Display und den Joystick folgendermaßen am ESP32 an:

Anschluss von OLED-Display und Joystick am ESP32

Der Sketch

Damit Snake auf dem ESP32 läuft, sind nur ein paar kleinere Adaptionen nötig. So hinterlegst du im Sketch zunächst natürlich andere Pins. Aber auch die maximale Länge der Schlange kannst du hier nach oben setzen – von 50 für den Arduino auf 100 für den ESP32:

#define MAX_SNAKE_LENGTH 100  // Maximale Länge der Schlange

Auch die Schwellenwerte für den Joystick musst du beim ESP32 ändern, weil der dieser eine höhere Auflösung für die analogen Eingänge hat als der Arduino UNO. Der Arduino Uno verwendet eine 10-Bit-Auflösung, was bedeutet, dass die analogen Eingänge Werte von 0 bis 1023 zurückgeben. Der ESP32 hingegen verwendet eine 12-Bit-Auflösung, was einen Bereich von 0 bis 4095 ergibt.

Für eine Bewegung nach links ist das zum Beispiel ein Wert von 1000. Im Sketch für den Arduino UNO lag dieser Wert bei 300:

if (xValue < 1000 && directionX == 0) { //Schwellenwert für den ESP32

Mit einem Delay steuerst du die Geschwindigkeit der Schlange. Da dir auf dem ESP32 mehr Leistung zur Verfügung steht, setzt du diesen Wert hier in Zeile 147 des Sketchs auf 100 (statt 150 für den Arduino UNO).

Hier nun der gesamte Sketch für den ESP32:

// Snake spielen auf dem ESP32
// Pollux Labs

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

// OLED Display Konfiguration
#define SCREEN_WIDTH 128  // Breite des OLED-Displays
#define SCREEN_HEIGHT 64  // Höhe des OLED-Displays
#define OLED_RESET    -1  // OLED-Reset-Pin (nicht verwendet)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Joystick Konfiguration
#define VRX_PIN 34  // Joystick X-Achsen-Pin
#define VRY_PIN 35  // Joystick Y-Achsen-Pin
#define SW_PIN  32  // Joystick Taster-Pin

// Eigenschaften der Schlange
#define SNAKE_SIZE 4  // Größe jedes Segments der Schlange
#define MAX_SNAKE_LENGTH 100  // Maximale Länge der Schlange
int snakeX[MAX_SNAKE_LENGTH], snakeY[MAX_SNAKE_LENGTH];  // Arrays zur Speicherung der Segmentpositionen der Schlange
int snakeLength = 5;  // Anfangslänge der Schlange
int directionX = 1, directionY = 0;  // Anfangsrichtung der Bewegung (nach rechts)

// Eigenschaften des Futters
int foodX = random(2, (SCREEN_WIDTH - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;  // X-Koordinate des Futters (nicht direkt am Rand)
int foodY = random(12 / SNAKE_SIZE, (SCREEN_HEIGHT - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;  // Y-Koordinate des Futters (Spielbereich unterhalb des Rahmens, nicht direkt am Rand)

// Score
int score = 0;

void setup() {
  Serial.begin(115200);  // Initialisiere serielle Kommunikation

  // Initialisiere Display
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {  // Initialisiere das OLED-Display mit der I2C-Adresse 0x3C
    Serial.println(F("SSD1306 allocation failed"));
    for (;;);  // Stoppe, falls die Display-Initialisierung fehlschlägt
  }
  display.clearDisplay();  // Leere den Display-Puffer
  display.display();  // Zeige den geleerten Puffer an

  // Initialisiere Joystick
  pinMode(VRX_PIN, INPUT);  // Setze Joystick X-Achsen-Pin als Eingang
  pinMode(VRY_PIN, INPUT);  // Setze Joystick Y-Achsen-Pin als Eingang
  pinMode(SW_PIN, INPUT_PULLUP);  // Setze Joystick-Taster-Pin als Eingang mit internem Pull-up-Widerstand

  // Initialisiere Position der Schlange
  for (int i = 0; i < snakeLength; i++) {
    snakeX[i] = SCREEN_WIDTH / 2 - (i * SNAKE_SIZE);  // Setze anfängliche X-Koordinaten der Schlangensegmente
    snakeY[i] = SCREEN_HEIGHT / 2;  // Setze anfängliche Y-Koordinate der Schlangensegmente
  }
  Serial.println("Setup abgeschlossen");
}

void loop() {
  // Lese Joystick-Werte
  int xValue = analogRead(VRX_PIN);  // Lese X-Achsen-Wert vom Joystick
  int yValue = analogRead(VRY_PIN);  // Lese Y-Achsen-Wert vom Joystick
  Serial.print("Joystick X: ");
  Serial.print(xValue);
  Serial.print(" Y: ");
  Serial.println(yValue);

  // Setze Richtung basierend auf Joystick-Eingaben, verhindere diagonale Bewegung
  if (xValue < 1000 && directionX == 0) {  // Bewegung nach links, wenn derzeit nicht horizontal bewegt wird
    directionX = -1;
    directionY = 0;
    Serial.println("Richtung: Links");
  } else if (xValue > 3000 && directionX == 0) {  // Bewegung nach rechts, wenn derzeit nicht horizontal bewegt wird
    directionX = 1;
    directionY = 0;
    Serial.println("Richtung: Rechts");
  } else if (yValue < 1000 && directionY == 0) {  // Bewegung nach oben, wenn derzeit nicht vertikal bewegt wird
    directionX = 0;
    directionY = -1;
    Serial.println("Richtung: Oben");
  } else if (yValue > 3000 && directionY == 0) {  // Bewegung nach unten, wenn derzeit nicht vertikal bewegt wird
    directionX = 0;
    directionY = 1;
    Serial.println("Richtung: Unten");
  }

  // Aktualisiere Position der Schlange
  for (int i = snakeLength - 1; i > 0; i--) {  // Bewege jedes Segment zur Position des vorherigen Segments
    snakeX[i] = snakeX[i - 1];
    snakeY[i] = snakeY[i - 1];
  }
  snakeX[0] += directionX * SNAKE_SIZE;  // Aktualisiere Kopfposition in X-Richtung
  snakeY[0] += directionY * SNAKE_SIZE;  // Aktualisiere Kopfposition in Y-Richtung
  Serial.print("Schlangenkopf X: ");
  Serial.print(snakeX[0]);
  Serial.print(" Y: ");
  Serial.println(snakeY[0]);

  // Prüfe auf Kollision mit dem Futter
  if (snakeX[0] == foodX && snakeY[0] == foodY) {  // Wenn der Schlangenkopf das Futter erreicht
    if (snakeLength < MAX_SNAKE_LENGTH) {
      snakeLength++;  // Erhöhe die Schlangenlänge
      score++;  // Erhöhe den Score
      Serial.println("Futter gegessen, Schlangenlänge: " + String(snakeLength));
    }
    // Generiere neue Futterposition (nicht direkt am Rand)
    foodX = random(2, (SCREEN_WIDTH - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
    foodY = random(12 / SNAKE_SIZE, (SCREEN_HEIGHT - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
    Serial.print("Neues Futter bei X: ");
    Serial.print(foodX);
    Serial.print(" Y: ");
    Serial.println(foodY);
  }

  // Prüfe auf Kollision mit den Wänden
  if (snakeX[0] < SNAKE_SIZE || snakeX[0] >= SCREEN_WIDTH - SNAKE_SIZE || snakeY[0] < 10 || snakeY[0] >= SCREEN_HEIGHT - SNAKE_SIZE) {
    Serial.println("Kollision mit der Wand, Spiel wird zurückgesetzt");
    resetGame();  // Setze das Spiel zurück, wenn die Schlange die Wand trifft
  }

  // Prüfe auf Kollision mit sich selbst
  for (int i = 1; i < snakeLength; i++) {
    if (snakeX[0] == snakeX[i] && snakeY[0] == snakeY[i]) {  // Wenn der Schlangenkopf mit ihrem eigenen Körper kollidiert
      Serial.println("Kollision mit sich selbst, Spiel wird zurückgesetzt");
      resetGame();  // Setze das Spiel zurück
    }
  }

  // Zeichne alles
  display.clearDisplay();  // Leere den Display-Puffer

  // Zeichne den Score
  display.setTextSize(1);  // Setze Textgröße
  display.setTextColor(SSD1306_WHITE);  // Setze Textfarbe
  display.setCursor(0, 0);  // Setze Cursor für Score
  display.print("Score: ");
  display.print(score);  // Zeige aktuellen Score an

  // Zeichne den Rand um das Spielfeld
  display.drawRect(0, 10, SCREEN_WIDTH, SCREEN_HEIGHT - 10, SSD1306_WHITE);  // Zeichne einen weißen Rahmen um das Spielfeld

  // Zeichne die Schlange
  for (int i = 0; i < snakeLength; i++) {
    display.fillRect(snakeX[i], snakeY[i], SNAKE_SIZE, SNAKE_SIZE, SSD1306_WHITE);  // Zeichne jedes Segment der Schlange
  }

  // Zeichne das Futter
  display.fillRect(foodX, foodY, SNAKE_SIZE, SNAKE_SIZE, SSD1306_WHITE);  // Zeichne das Futter

  display.display();  // Zeige den aktualisierten Puffer an

  delay(100);  // Verzögerung zur Steuerung der Geschwindigkeit der Schlange
}

void resetGame() {
  // Setze Eigenschaften der Schlange zurück
  snakeLength = 5;  // Setze die Schlangenlänge zurück
  directionX = 1;  // Setze die Richtung nach rechts zurück
  directionY = 0;
  score = 0;  // Setze den Score zurück
  for (int i = 0; i < snakeLength; i++) {
    snakeX[i] = SCREEN_WIDTH / 2 - (i * SNAKE_SIZE);  // Setze die Position der Schlange zurück auf die Mitte
    snakeY[i] = SCREEN_HEIGHT / 2;
  }
  // Generiere neue Futterposition (nicht direkt am Rand)
  foodX = random(2, (SCREEN_WIDTH - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
  foodY = random(12 / SNAKE_SIZE, (SCREEN_HEIGHT - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
  Serial.println("Spiel zurückgesetzt");
  Serial.print("Neues Futter bei X: ");
  Serial.print(foodX);
  Serial.print(" Y: ");
  Serial.println(foodY);
}

Wie geht es weiter?

Du hast nun eine spielbare Version von Snake samt einer Anzeige deiner Punkte. Wie könntest du das Spiel noch erweitern oder verbessern? Eine Idee könnte zum Beispiel ein Highscore sein. Der aktuelle Halter dieses Highscores könnte mit dem Joystick seine Initialen eintragen, die neben der Punktezahl im Dateisystem des ESP32 gespeichert werden und auf dem Display angezeigt werden – ein guter Ansporn für eine weitere Partie Snake!

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

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

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

Eine erste Webseite mit Flask

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

pip install Flask

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

from flask import Flask

app = Flask(__name__)

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

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

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

Ausgabe des Servers im Terminal

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

So funktioniert der code

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

from flask import Flask

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

Die folgenden drei Zeilen

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

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

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

Zuletzt die beiden Zeilen

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

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

Sensordaten empfangen und anzeigen

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

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

Temperatur und Luftdruck auf dem Raspberry Pi Webserver

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

Das Python-Script für den Server

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

//Raspberry Pi Webserver
//polluxlabs.net

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

app = Flask(__name__)

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

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

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

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

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

So funktioniert das Script

Ein kurzer Blick auf die Funktionsweise des Raspberry Pi Webservers:

Daten empfangen

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

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

Daten anzeigen

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

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

Webseite einrichten

Der Server wird hier gestartet:

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

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

Starte den Raspberry Pi WebServer

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

python3 webserver.py 

Anschluss des BMP180 und der Sketch für den ESP8266

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

BMP180 am ESP8266 angeschlossen

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

Doch nun zum Sketch:

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

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

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

Adafruit_BMP085 bmp;

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

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

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

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

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

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

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

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

So funktioniert der Sketch

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

Einbindung der Bibliotheken

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

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

Konfiguration

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

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

Adafruit_BMP085 bmp;

Setup-Funktion

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

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

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

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

Loop-Funktion

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

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

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

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

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

    int httpResponseCode = http.POST(httpRequestData);

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

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

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

Hübsche die Webseite etwas auf

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

Webseite mit Temperatur und Luftdruck

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

//Raspberry Pi Webserver
//polluxlabs.net

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

app = Flask(__name__)

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

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

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

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

    <script>
        let chart;

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

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

        // Initialer Datenabruf
        updateData();

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

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

Wie geht es weiter?

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

]]>
Berechne den nächsten Überflug der ISS https://polluxlabs.net/python-tutorials-und-projekte/berechne-den-naechsten-ueberflug-der-iss/ Sun, 25 Aug 2024 19:17:18 +0000 https://polluxlabs.net/?p=17023 Berechne den nächsten Überflug der ISS Weiterlesen »

]]>
In diesem Tutorial erfährst du, wie den nächsten Überflug der ISS mit Python berechnest. Auf Pollux Labs gibt es bereits zwei Projekte, die sich mit der International Space Station auseinandersetzen: Ein Raspberry Pi Projekt, mit dem du die Flugbahn der ISS auf einer Karte darstellst und ein eine Erweiterung der LEGO ISS, die bei der Passage der Raumstation aufleuchtet.

Letzteres Projekt basierte auf einer API, die die Überflugsdaten für einen beliebigen Standort bereitstellen konnte. Leider wurde der Betrieb dieser API eingestellt, weswegen du diese Berechnung am besten selbst vornimmst – wie, lernst du hier.

Die Koordinaten für deinen Standort ermitteln

Um zu berechnen, wann die ISS über deinem Kopf fliegt (oder zumindest theoretisch sichtbar ist), benötigst du zunächst die Koordinaten für deinen Standort. Hierfür eignet sich z.B. die Website geoplaner.de – trage hier deinen Standort ein und du erhältst umgehend die zugehörigen Koordinaten (zum Herauskopieren eignet sich der gelbe Kasten):

Koordinaten für deinen Standort, um den Überflug der ISS zu berechnen

Im obigen Fall wären das für Karlsruhe circa 49° nördliche Breite und 8,4° östliche Länge. Diese Zahlen benötigst du später im Python-Script für die Berechnung des Überflugs bzw. der Sichtbarkeit der ISS.

Die benötigte Python-Bibliothek

Um den Überflug der ISS zu ermitteln, benötigst du die Bibliothek ephem, die im Terminal wie folgt installierst:

pip install ephem

Diese Bibliothek bietet umfangreiche Funktionen für astronomische Beobachtungen – du kannst damit neben verschiedenen Himmelskörpern wie Planeten, Asteroiden und Kometen auch die Flugbahnen von Satelliten berechnen, dazu gehört im weitesten Sinne auch die ISS. Mehr über dieses großartige Projekt erfährst du auf der offiziellen Webseite.

Die aktuellen TLE-Daten ermitteln

Damit die Bibliothek ephem den Überflug der ISS berechnen kann, benötigt sie neben deinem Standort auch aktuelle Daten der Raumstation. Hierfür kommt ein sogenanntes Two-Line Element zum Einsatz, kurz TLE. Hierbei handelt es sich um ein standardisiertes Format für eine Reihe von Daten eines Flugkörpers in der Umlaufbahn der Erde. Mehr über TLEs erfährst du auf Wikipedia (Englisch).

Um das aktuelle TLE der ISS zu erhalten, eignen sich die beiden Webseiten von ARISS und Celestrak. Auf beiden Seiten findest du die aktuellen Daten für das ISS-Modul Sarja (englisch Zarya). Hier ein Beispiel eines TLE:

ISS (ZARYA)
1 25544U 98067A   24238.20528372  .00032948  00000-0  58135-3 0  9999
2 25544  51.6406 333.3912 0006392 271.4643 173.4301 15.50058100469255

Die beiden Zeilen kannst du von der Webseite kopieren und gleich in dein Script eintragen. Ein wichtiger Hinweis: Das TLE für die Raumstation wird regelmäßig aktualisiert und an die tatsächlichen Begebenheiten der ISS angepasst. Damit deine Berechnung zuverlässige Daten liefert, kopiere am besten die aktuellen Werte in dein Script.

Den nächsten ISS Überflug berechnen

Nun zum Kern des Vorhabens: mit dem folgenden Python-Script berechnest du den nächsten Überflug der ISS:

import ephem

# Koordinaten deines Standorts
latitude = '49'
longitude = '8.4'

# Aktuelles TLE
tle1 = "1 25544U 98067A   24238.20528372  .00032948  00000-0  58135-3 0  9999"
tle2 = "2 25544  51.6406 333.3912 0006392 271.4643 173.4301 15.50058100469255"


# Instanz für deinen Standort erzeugen
observer = ephem.Observer()
observer.lat = latitude
observer.lon = longitude

# Instanz für die ISS erzeugen
iss = ephem.readtle("ISS (ZARYA)", tle1, tle2)

# Höhe deines Standorts (optional) und lokale Zeit einstellen
observer.elevation = 0  # Elevation in meters (optional)
observer.date = ephem.localtime(ephem.now())  # Use current date and time

print("Aktuelle Zeit (lokal):", observer.date)

# Nächsten Überflug berechnen
next_pass = observer.next_pass(iss)

# Daten ausgeben
print("Next pass of the ISS:")
print(f"Date and Time (lokale Zeit): {ephem.localtime(next_pass[0])}")
print(f"Rise (lokale Zeit): {ephem.localtime(next_pass[1])}")
print(f"Maximum Elevation (lokale Zeit): {ephem.localtime(next_pass[2])}")
print(f"Set (lokale Zeit): {ephem.localtime(next_pass[3])}")
print(f"Max Elevation (degrees): {next_pass[4]}")

So funktioniert das Script

Nachdem du die Bibliothek ephem eingebunden hast, hinterlegst du die Koordinaten deines Standorts. Anschließend folgen die Werte des aktuellen TLE. Als nächstes erzeugst du eine Instanz observer, die die Koordinaten deines Standorts enthält sowie eine Instanz für die ISS mit den Werten des TLE.

Zusätzlich zu den Koordinaten kannst du auch die Höhe deines Standorts eintragen. Liegt dieser z.B. 150 Meter über dem Meeresspiegel, trage hier einfach eine 150 ein. In der Zeile darunter stellst du die aktuelle Uhrzeit deiner Zeitzone ein.

Mit all diesen Daten rufst du die Funktion observer.next_pass(iss) auf und speicherst das Ergebnis in der Variablen next_pass. Die Berechnung berücksichtigt also deine Standortdaten in der Instanz observer sowie die TLE-Werte in der Insanz iss.

Zuletzt musst du nur noch die Ergebnisse ausgeben. Hier gibt es allerdings ein kleines Problem: Eigentlich erhältst du nicht nur einen Zeitpunkt, an dem die ISS am Himmel sichtbar ist, sondern auch den Zeitpunkt, an dem sie über den Horizont steigt und wieder dahinter verschwindet. Außerdem berechnet ephem auch den maximalen Winkel der ISS über dem Horizont. Leider gibt es – zumindest bei meinen Tests – nur eine sinnvollen Wert: den Zeitpunkt ihrer maximalen Höhe:

Next pass of the ISS:
Date and Time (lokale Zeit): 2024-08-26 02:50:05.657860
Rise (lokale Zeit): 1900-01-03 17:40:39.115906
Maximum Elevation (lokale Zeit): 2024-08-26 02:54:23.887056
Set (lokale Zeit): 1899-12-31 16:40:30.704498
Max Elevation (degrees): 2024/8/26 00:58:43

Wie du in der obigen Ausgabe siehst, sind die Zeitangaben für Rise und Set (also Aufgang und Untergang) unsinnig. Ebenso der Wert für Max Elevation. Einen guten (und für dein Projekt möglicherweise ausreichenden) Wert findest du jedoch hinter Maximum Elevation (lokale Zeit) – diese Zeitangabe sagt dir, wann der nächste Überflug der ISS an deinem Standort zu sehen ist.

Falls du einen Anhaltspunkt oder eine Lösung für diese Fehler hast, schreib mir gerne an info@polluxlabs.net – ich bin für jeden Hinweis dankbar.

Überprüfung der Ergebnisse

Falls du der Berechnung noch nicht so recht Glauben schenkst, kannst du z.B. eine App auf deinem Smartphone hinzuziehen und dort nachschauen, wo sich die Raumstation gerade befindet und wann du mit dem nächsten Überflug der ISS rechnen kannst. Ich selbst verwende hierfür die kostenlose App SkyView Lite.

Was kannst du damit bauen?

Falls du “nur” den Zeitpunkt für den nächsten Überflug der ISS benötigst, damit du sie am Abend- oder Morgenhimmel beobachten kannst, reicht dir dieses Script vermutlich schon. Falls du jedoch etwas bauen möchtest, könnte die oben verlinkte LEGO ISS interessant für dich sein. Dort habe ich zwar einen ESP8266 verwendet, darauf läuft jedoch kein Python. Stattdessen könntest du einen Raspberry Pi Zero verwenden, der ähnlich klein ist und sich in der aufgebauten Raumstation gut unterbringen lässt.

So könnte das beleuchtete Modell aussehen:

Lego-Set, das leuchtet beim Überflug der ISS

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

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

Höre dir vorab die Projekt-Vorstellung an:

Diese Bauteile benötigst du:

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

Aufbau des ESP32 Internetradios

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

Aufbau des einfachsten ESP32 Internetradios

Hier noch einmal als Tabelle:

VerstärkermodulESP32-S3
VIN3,3V
GNDGND
GAINGND
DINGPIO 2
BCLKGPIO 3
LRCGPIO 4

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

Foto des ersten ESP32 Internetradios

Der Sketch für das Radio

Zentral für dein Internetradio ist eine Bibliothek, die du nicht im Bibliotheksmanager der Arduino IDE findest – dafür jedoch auf GitHub. Lade dir die Bibliothek als ZIP-Datei hier herunter. Erstelle nun in der Arduino IDE einen neuen Sketch und binde die sie im Menü über Sketch > Bibliothek einbinden > ZIP-Datei hinzufügen ein.

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

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

#ESP32 Internetradio
#https://polluxlabs.net

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

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

Audio audio;

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

void setup() {

  Serial.begin(115200);

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

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

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

  audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);

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

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

}

void loop()

{
  audio.loop();

}

// Print der Senderinfos

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

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

So funktioniert der Sketch

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

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

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

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

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

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

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

erweiterung der Stromversorgung

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

Einen Akku anschließen

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

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

ESP32 Internetradio mit Akkubetrieb

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

Nur das Lademodul verwenden

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

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

Verbinde deinen ESP32 also wie folgt mit dem Lademodul:

ESP32 betrieben mit einem Lademodul

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

Fehlt noch der letzte Schritt – das Potentiometer.

Das Potentiometer installieren

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

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

ESP32 Internetradio mit Potentiometer zur Lautstärkeregulierung

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

Erweiterung des Sketchs

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

Das erreichst du mit dem folgenden Code:

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

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

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

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

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

#ESP32 Internetradio
#https://polluxlabs.net

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

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

Audio audio;

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

const int SAMPLES = 5;
int volumeReadings[SAMPLES];
int readIndex = 0;
int total = 0;
int average = 0;
unsigned long lastVolumeCheck = 0;
const unsigned long VOLUME_CHECK_INTERVAL = 500; // Check alle 500ms

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

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

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

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

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

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

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

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

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

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

Umzug auf eine Lochplatine und in das passende Gehäuse

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

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

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

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

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

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

ESP32 Internetradio mit Senderwahl

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

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

ESP32 Internetradio mit Senderwahl - Aufbau

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

Die bibliothek für den Rotary Encoder

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

AiEsp32RotaryEncoder in der IDE installieren

Finde deine Lieblingssender

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

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

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

Der vollständige Sketch des ESP32 Internetradios mit Senderwahl

Und hier nun der gesamte Sketch zum Herauskopieren und Anpassen:

#ESP32 Internetradio
#https://polluxlabs.net

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

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

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

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

Audio audio;

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

// Radio stations
const char* stations[] = {
    "http://www.byte.fm/stream/bytefm.m3u",
    "https://st01.sslstream.dlf.de/dlf/01/128/mp3/stream.mp3",
    "https://frontend.streamonkey.net/fho-schwarzwaldradiolive/mp3-stream.m3u",
    "https://kexp-mp3-128.streamguys1.com/kexp128.mp3"
};
const int NUM_STATIONS = sizeof(stations) / sizeof(stations[0]);
int currentStation = 0;

// Volume control variables
const int SAMPLES = 5;
int volumeReadings[SAMPLES];
int readIndex = 0;
int total = 0;
int average = 0;
unsigned long lastVolumeCheck = 0;
const unsigned long VOLUME_CHECK_INTERVAL = 500; // Check every 500ms

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

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

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

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

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

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

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

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

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

void checkVolumeControl() {
    unsigned long currentMillis = millis();
    if (currentMillis - lastVolumeCheck >= VOLUME_CHECK_INTERVAL) {
        lastVolumeCheck = currentMillis;
        
        total = total - volumeReadings[readIndex];
        volumeReadings[readIndex] = analogRead(VOLUME_PIN);
        total = total + volumeReadings[readIndex];
        readIndex = (readIndex + 1) % SAMPLES;

        average = total / SAMPLES;
        int volume = map(average, 0, 4095, 5, 23);
        
        static int lastVolume = -1;
        if (volume != lastVolume) {
            audio.setVolume(volume);
            lastVolume = volume;
            Serial.println("Volume set to: " + String(volume));
        }
    }
}

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

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

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

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

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

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

ESP32 Internetradio mit Rotary Encoder auf dem Breadboard

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

Das Radio um ein OLED-Display erweitern

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

ESP32 Internetradio mit OLED-Display

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

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

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

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

Partition Scheme ändern in Huge APP

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

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

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

Hier nun der vollständige Sketch:

#ESP32 Internetradio
#https://polluxlabs.net

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

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

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

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

#define I2C_SDA 8
#define I2C_SCL 9

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

Audio audio;

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

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

const int NUM_STATIONS = sizeof(stations) / sizeof(stations[0]);
int currentStation = 0;

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

// Volume control variables
const int SAMPLES = 5;
int volumeReadings[SAMPLES];
int readIndex = 0;
int total = 0;
int average = 0;
unsigned long lastVolumeCheck = 0;
const unsigned long VOLUME_CHECK_INTERVAL = 500; // Check every 500ms

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

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

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

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

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

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

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

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

    // Verzögerte Initialisierung
    if (!isDisplayInitialized && millis() - lastInitAttempt > initInterval) {
        initializeDisplay();
        lastInitAttempt = millis();
    }

    if (!isWiFiConnected && millis() - lastInitAttempt > initInterval) {
        connectToWiFi();
        lastInitAttempt = millis();
    }

    if (isWiFiConnected && !isAudioInitialized && millis() - lastInitAttempt > initInterval) {
        initializeAudio();
        lastInitAttempt = millis();
    }

    // Normale Loop-Funktionalität
    if (isDisplayInitialized && isWiFiConnected && isAudioInitialized) {
        audio.loop();
        checkEncoder();
        checkVolumeControl();
    }

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

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

void connectToWiFi() {
    Serial.println(F("Verbinde mit WiFi..."));
    WiFi.begin(ssid, password);
    int attempts = 0;
    while (WiFi.status() != WL_CONNECTED && attempts < 20) {
        delay(500);
        Serial.print(".");
        attempts++;
    }
    if (WiFi.status() == WL_CONNECTED) {
        Serial.println(F("\nWiFi verbunden"));
        isWiFiConnected = true;
        if (isDisplayInitialized) {
            display.clearDisplay();
            display.setCursor(0,0);
            display.println(F("WiFi verbunden"));
            display.display();
        }
    } else {
        Serial.println(F("\nWiFi-Verbindung fehlgeschlagen"));
    }
}

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

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

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

void checkVolumeControl() {
    unsigned long currentMillis = millis();
    if (currentMillis - lastVolumeCheck >= VOLUME_CHECK_INTERVAL) {
        lastVolumeCheck = currentMillis;
        
        total = total - volumeReadings[readIndex];
        volumeReadings[readIndex] = analogRead(VOLUME_PIN);
        total = total + volumeReadings[readIndex];
        readIndex = (readIndex + 1) % SAMPLES;

        average = total / SAMPLES;
        int volume = map(average, 0, 4095, 5, 23);
        
        static int lastVolume = -1;
        if (volume != lastVolume) {
            audio.setVolume(volume);
            lastVolume = volume;
            Serial.println("Lautstärke eingestellt auf: " + String(volume));
            updateDisplay();
        }
    }
}

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

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

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

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

]]>
Babyphone mit Geräusch- und Bewegungsmelder https://polluxlabs.net/esp8266-projekte/babyphone-mit-display-und-geraeusch-bewegungsmelder/ Sat, 06 Jul 2024 21:42:59 +0000 https://polluxlabs.net/?p=16774 Babyphone mit Geräusch- und Bewegungsmelder Weiterlesen »

]]>
Dieses Projekt dreht sich rund um den Schlaf von Babys und kleinen Kindern: Du baust ein Babyphone, das aus zwei ESP8266 besteht, die mit ESP-NOW miteinander verbunden sind. Am ersten Microcontroller schließt du einen PIR-Sensor, um Bewegungen zu erkennen sowie einen Geräuschsensor an, um die Lautstärke zu ermitteln.

Gesendet werden die Daten an einen zweiten ESP8266, an dem wiederum ein LED-Matrix-Display angeschlossen ist. Hierauf kannst du die aktuelle Lautstärke im Schlafzimmer ermitteln. Außerdem leuchtet das Display auf, wenn eine Bewegung erkannt wurde.

Den ESP8266 mit den Sensoren stellst du neben das Bett der oder des Kleinen und jenen mit dem Display dorthin, wo du dich selbst aufhältst. So hast du das Geschehen im Schlafzimmer immer im Blick.

Du lernst mit diesem Projekt, wie du

  • einen PIR- und einen Geräuschsensor am ESP8266 anschließt,
  • ein LED-Matrix-Display verwendest und
  • zwei ESP8266 per ESP-NOW miteinander verbindest.

Diese Bauteile benötigst du

Der Empfänger

Los geht es mit dem Teil des Babyphones, auf dessen Display du ablesen kannst, was sich im Schlaf- oder Kinderzimmer so tut. Hierfür verwendest du ein 8×8 LED-Matrix-Display. Auf der einen Seite des Displays erstellst du eine Art Equalizer, der dir die gemessene Lautstärke anzeigt. Die andere Seite leuchtet rot auf, sobald eine Bewegung erkannt und vom Sender weitergegeben wurde.

Verbinde das Matrix-Display wie folgt mit dem ESP8266:

DisplayESP8266
GNDGND
VCC3,3V
DIND5
CLDD6
CSD7

Als nächstes musst du die MAC-Adresse des ESP8266 herausfinden, um ESP-NOW für die Kommunikation zwischen Sender und Empfänger nutzen zu können. MAC steht für Media Access Control und ist eine eindeutige Adresse zur Identifikation eines Geräts in einem Netzwerk.

Um diese Adresse herauszufinden, kannst du folgenden kleinen Sketch auf den Empfänger hochladen und die MAC-Adresse im Seriellen Monitor ablesen:

#include <ESP8266WiFi.h>

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  Serial.println(WiFi.macAddress());
}

void loop() {}

Deine Adresse könnte zum Beispiel wie folgt aussehen: EC:FA:BC:6E:EE:F4

Diese Adresse wirst du gleich im Sketch des Senders benötigen, um die Messdaten auf dem Matrix-Display anzuzeigen. Zunächst jedoch der eigentliche Sketch, der die Lautstärke und Bewegungen entgegennimmt und visualisiert:

#include <ESP8266WiFi.h>
#include <espnow.h>
#include <LedControl.h>

// Pins des Displays
const int DIN_PIN = D5;
const int CLK_PIN = D6;
const int CS_PIN = D7;

// LED Matrix Setup
LedControl lc = LedControl(DIN_PIN, CLK_PIN, CS_PIN, 1);

// Struktur der zu empfangenden Daten
struct SensorData {
  bool motionDetected;
  int noiseLevel;
};

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

  // LED matrix initialisieren
  lc.shutdown(0, false);
  lc.setIntensity(0, 8);  // Helligkeit (0-15)
  lc.clearDisplay(0);

  // ESP-NOW initialisieren
  WiFi.mode(WIFI_STA);
  if (esp_now_init() != 0) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  // Funktion für die empfangenen Daten
  esp_now_register_recv_cb(onDataReceived);
}

void loop() {
  // Leer, da du die Funktion onDataReceived verwendest
}

void onDataReceived(uint8_t *mac, uint8_t *incomingData, uint8_t len) {
  SensorData data;
  memcpy(&data, incomingData, sizeof(data));

  // Update des Displays
  updateLEDMatrix(data.motionDetected, data.noiseLevel);

  // Empfangene Daten im Seriellen Monitor ausgeben
  Serial.print("Motion: ");
  Serial.print(data.motionDetected);
  Serial.print(" | Noise: ");
  Serial.println(data.noiseLevel);
}

void updateLEDMatrix(bool motionDetected, int noiseLevel) {
  lc.clearDisplay(0);

  // Bewegung anzeigen
  if (motionDetected) {
    for (int row = 0; row < 4; row++) {
      for (int col = 0; col < 8; col++) {
        lc.setLed(0, row, col, true);
      }
    }
  }

  // Lautstärke anzeigen
  int noiseLEDs = map(noiseLevel, 150, 300, 0, 8);
  for (int row = 4; row < 8; row++) {
    for (int col = 0; col < noiseLEDs; col++) {
      lc.setLed(0, row, col, true);
    }
  }
}

Erklärung des Sketchs

Lass uns einen genaueren Blick auf den Code in diesem Sketch werfen.

Bibliotheken und Definitionen

#include <ESP8266WiFi.h>
#include <espnow.h>
#include <LedControl.h>

const int DIN_PIN = D5;
const int CLK_PIN = D6;
const int CS_PIN = D7;

LedControl lc = LedControl(DIN_PIN, CLK_PIN, CS_PIN, 1);

struct SensorData {
  bool motionDetected;
  int noiseLevel;
};

Der Code verwendet die ESP8266WiFi- und espnow-Bibliotheken für die drahtlose Kommunikation und die LedControl-Bibliothek für die Steuerung der LED-Matrix. Letztere musst du wahrscheinlich noch über den Bibliotheksmanager installieren.

Die Pins für die LED-Matrix werden definiert (DIN, CLK, CS) und ein LedControl-Objekt erstellt, um die LED-Matrix zu steuern. Zuletzt erzeugst du eine Struktur, um die empfangenen Daten in Variablen zu speichern.

Setup-Funktion

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

  lc.shutdown(0, false);
  lc.setIntensity(0, 8);
  lc.clearDisplay(0);

  WiFi.mode(WIFI_STA);
  if (esp_now_init() != 0) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  esp_now_register_recv_cb(onDataReceived);
}
  • Die serielle Kommunikation wird initialisiert.
  • Die LED-Matrix wird eingeschaltet, die Helligkeit auf 8 (von 15) gesetzt und das Display gelöscht.
  • Der ESP8266 wird in den Station-Modus versetzt.
  • ESP-NOW wird initialisiert und ein Fehler wird gemeldet, falls die Initialisierung fehlschlägt.
  • Die Funktion onDataReceived wird als Callback für empfangene Daten registriert.

Hauptschleife

void loop() {
}

Der Loop des Sketchs belibt leer, da die Datenverarbeitung in der folgenden Funktion erfolgt.

Datenempfangs-Callback

void onDataReceived(uint8_t *mac, uint8_t *incomingData, uint8_t len) {
  SensorData data;
  memcpy(&data, incomingData, sizeof(data));

  updateLEDMatrix(data.motionDetected, data.noiseLevel);

  Serial.print("Motion: ");
  Serial.print(data.motionDetected);
  Serial.print(" | Noise: ");
  Serial.println(data.noiseLevel);
}
  • Diese Funktion wird aufgerufen, wenn Daten empfangen werden.
  • Die empfangenen Daten werden in die SensorData-Struktur kopiert.
  • Die LED-Matrix wird mit den empfangenen Daten aktualisiert.
  • Die empfangenen Daten werden im Seriellen Monitor ausgegeben.

LED-Matrix-Aktualisierung

void updateLEDMatrix(bool motionDetected, int noiseLevel) {
  lc.clearDisplay(0);

  if (motionDetected) {
    for (int row = 0; row < 4; row++) {
      for (int col = 0; col < 8; col++) {
        lc.setLed(0, row, col, true);
      }
    }
  }

  int noiseLEDs = map(noiseLevel, 150, 300, 0, 8);
  for (int row = 4; row < 8; row++) {
    for (int col = 0; col < noiseLEDs; col++) {
      lc.setLed(0, row, col, true);
    }
  }
}
  • Diese Funktion aktualisiert die LED-Matrix basierend auf den empfangenen Sensordaten.
  • Zuerst wird das Display gelöscht.
  • Wenn Bewegung erkannt wurde, werden die oberen 4 Reihen der Matrix vollständig beleuchtet.
  • Der Geräuschpegel wird auf die unteren 4 Reihen abgebildet. Die Anzahl der beleuchteten LEDs hängt vom Geräuschpegel ab, wobei Werte zwischen 150 und 300 auf 0 bis 8 LEDs abgebildet werden.

Dieser Code empfängt also kontinuierlich Daten von einem Sender, verarbeitet diese und stellt sie visuell auf einer LED-Matrix dar.

Wichtig ist hier noch die Map-Funktion, die im Code oben Werte zwischen 150 und 300 auf die acht Zeilen des Displays “mappt”. Das sind Werte, die du einerseits direkt am Geräuschsensor einstellst und andererseite experimentell ermitteln musst. Also wie weit nach oben geht der Wert des Sensors bei einer bestimmten Geräuschkulisse? Idealerweise werden alle 8 Zeilen erleuchtet, wenn eine maximal “übliche” Lautstärke zum Beispiel durch Schreien erreicht wird.

Der Sender des Babyphones

Kommen wir zum zweiten Teil des Babyphones – dem Sender samt des Geräusch- und Bewegungsmelders. Dieser ESP8266 ermittelt, ob sich das Kind bewegt oder gar weint oder schreit. Hierfür eignet sich ein PIR-Sensor, der Bewegungen im Dunkeln erkennt und dessen Empfindlichkeit du so einstellen kannst, dass nicht jedes Umdrehen im Bett gleich eine Meldung auslöst. Dazu kommt noch ein einfacher Geräuschsensor zum Einsatz, der die Lautstärke im Raum in eine Zahl zwischen 0 und 1023 darstellt.

Schließe zunächst beide Sensoren wie folgt am ESP8266 an:

Anschluss PIR-Sensor und KY-037 am ESP8266

Vom Geräuschsensor liest du den analogen Ausgang aus, was bedeutet, dass du den analogen Eingang A0 des ESP8266 verwenden musst. Bei der Wahl des Digitalpins hast du freie Wahl – im Folgenden ist das der Pin D1.

Kommen wir gleich zum Sketch:

#include <ESP8266WiFi.h>
#include <espnow.h>

const int PIR_PIN = D1;
const int NOISE_PIN = A0;

//Die MAC-Adresse des Empfängers
uint8_t receiverAddress[] = {0xEC, 0xFA, 0xBC, 0x6E, 0xEE, 0xF4};

struct SensorData {
  bool motionDetected;
  int noiseLevel;
};

void OnDataSent(uint8_t *mac_addr, uint8_t sendStatus) {
  Serial.print("Last Packet Send Status: ");
  if (sendStatus == 0){
    Serial.println("Delivery success");
  }
  else{
    Serial.println("Delivery fail");
  }
}

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

  WiFi.mode(WIFI_STA);
  WiFi.disconnect();

  if (esp_now_init() != 0) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);
  esp_now_register_send_cb(OnDataSent);

  esp_now_add_peer(receiverAddress, ESP_NOW_ROLE_SLAVE, 1, NULL, 0);

  Serial.println("Sender initialized");
}

void loop() {
  SensorData data;
  
  data.motionDetected = digitalRead(PIR_PIN);
  data.noiseLevel = analogRead(NOISE_PIN);

  esp_now_send(receiverAddress, (uint8_t *) &data, sizeof(data));

  Serial.print("Sending - Motion: ");
  Serial.print(data.motionDetected);
  Serial.print(" | Noise: ");
  Serial.println(data.noiseLevel);

  delay(200);
}

Hast du noch die MAC-Adresse des Empfängers zu Hand? Diese trägst du im oberen Bereich des Sketchs ein – ersetze hierfür die obige Adresse durch deine eigene.

Vieles dürfte dir in diesem Sketch schon aus jenem des Empfängers bekannt sein. Im Loop erfasst du die Daten des Sensors aus und sendest sie per ESP-NOW an den ESP8266 des Empfängers. Ganz zum Schluss stellst du mit einem Delay ein, wie häufig die Daten gemessen werden – hier ist ein Wert von 200 Millisekunden praktikabel. Du kannst aber natürlich damit experimentieren.

Den Geräuschsensor einstellen

Eine Sache musst du jedoch an der Hardware des Babyphone noch erledigen und den Sketch des Empfängers entsprechend aktualisieren. Am Geräuschsensor findest du eine kleine Schraube, mit der einstellen kannst, welcher Wert Stille bedeuten soll. Lade den obigen Sketch auf deinen ESP8266 und beobachte im Seriellen Monitor die gemessenen Werte. Drehe nun an der Schraub, bis die Wert sich etwas unter 200 einpendeln.

Mach nun in etwas Entfernung ein lauteres Geräusch (zum Beispiel in der Lautstärke eines weinenden Kindes) und beobachte, wie sich die Messwerte verhalten. Dieser obige Wert wird der Wert, bei dem alle 8 Zeilen des Matrix-Displays leuchten sollen. Passe nun die Zahl 300 im Empfänger-Sketch entsprechend an:

int noiseLEDs = map(noiseLevel, 150, 300, 0, 8);

Und das war es schon. Versorge beide ESP8266 mit Strom, zum Beispiel mit Hilfe von Powerbanks. Anschließend sollten sie eine Verbindung miteinander aufbauen und das Display aufleuchten.

Falls nicht, überprüfe noch einmal die MAC-Adresse, die du eingetragen hast. Stimmt sie mit jener des Empfängers überein?

Weitere Ideen für dein Babyphone

Neben dem Display könntest du natürlich auch noch andere Methoden nutzen, um dich informieren zu lassen. Das könnte ein einfacher Piezo-Summer sein, aber auch eine WhatsApp-Nachricht auf dein Smartphone.

]]>