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!

]]>
MQTT (Teil4): Sichere Kommunikation zwischen Geräten https://polluxlabs.net/esp8266-projekte/mqtt-teil4-sichere-kommunikation-zwischen-geraeten/ Thu, 10 Oct 2024 08:42:42 +0000 https://polluxlabs.net/?p=17423 MQTT (Teil4): Sichere Kommunikation zwischen Geräten Weiterlesen »

]]>
In den letzten Teilen der MQTT-Reihe hast du schrittweise die Kommunikation zwischen zwei ESP8266 eingerichtet und Sensordaten ausgetauscht. Ein wichtiger Baustein fehlt allerdings noch: die sichere Kommunikation. Wenn du nicht sicherstellst, dass der Austausch von Daten privat bleibt, könnte sich jemand in dein System einschleusen und einiges Chaos stiften.

In diesem Tutorial lernst du, wie du eine Authentifizierung integrierst, sodass deine ESP8266 einen Benutzernamen und ein Passwort benötigen, um mit dem MQTT-Broker kommunizieren zu können. Das bietet zwar immer noch keine 100% Sicherheit (falls es diese überhaupt gibt), aber wenn wir davon ausgehen, dass du keine kritischen Anwendungen mit MQTT aufbaust, sollte diese Authentifizierung schon ein großer Schritt in die richtige Richtung sein.

Konfiguration des MQTT-Brokers

Zunächst müssen wir den Mosquitto-Broker auf dem Raspberry Pi konfigurieren, um die Authentifizierung zu aktivieren.

Schritt 1: Erstellen der Passwortdatei

Hierfür verwendest du den Befehl mosquitto_passwd, um eine Passwortdatei zu erstellen und Benutzer hinzuzufügen. Gib im Terminal den folgenden Befehl ein – ersetze hierbei <username> mit einem Benutzernamen deiner Wahl:

sudo mosquitto_passwd -c /etc/mosquitto/passwd <username>

Anschließend wirst du aufgefordert, ein Passwort einzugeben. Dieses sollte natürlich möglichst sicher sein – Tipps für sichere Passwörter findest du z.B. auf der Webseite des BSI. Dieses Passwort wirst du später noch in den Sketches der ESP8266 benötigen.

Falls du noch einen weiteren Benutzer anlegen möchtest, ohne die Passwortdatei erneut zu erstellen, verwenden den folgenden Befehl, in dem du <username> und <password> entsprechend ersetzt:

sudo mosquitto_passwd -b /etc/mosquitto/passwd <username> <password>

Schritt 2: Konfiguration von Mosquitto

Nun widmest du dich der Konfigurationsdatei von Mosquitto, die du mit dem folgenden Befehl öffnest:

sudo nano /etc/mosquitto/mosquitto.conf

Füge am Ende der Datei die folgenden Zeilen hinzu:

allow_anonymous false
password_file /etc/mosquitto/passwd

Mit diesen beiden Zeilen deaktivierst du den anonymen Zugriff und gibst den Pfad zur Passwortdatei an.

Schritt 3: Neustart des Mosquitto-Dienstes

Sollte dein MQTT-Broker noch laufen, starte ihn mit dem folgenden Befehl neu, um deine Änderungen zu übernehmen:

sudo systemctl restart mosquitto

Konfiguration der ESP8266-Clients

Nun kommen die beiden ESP8266 dran. Hier änderst du deine Sketches, sodass sie sich mit Benutzernamen und Passwort beim Broker authentifizieren. Entscheiden sind die folgenden Änderungen. Zunächst die Integration des Benutzernamen und zugehörigen Passworts, dass du anfangs erstellt hast – hinterlege diese Informationen am Anfang des Sketchs (hinter den Infos zu WLAN und IP-Adresse des Raspberry Pis):

const char* mqtt_user = "Dein_MQTT_Benutzername";
const char* mqtt_password = "Dein_MQTT_Passwort";

Diese beiden Konstanten verwendest du anschließend, wenn der ESP8266 sich mit dem Broker verbinden möchte. Die Funktion reconnect() aus den vorherigen Teilen sieht dann folgendermaßen aus:

void reconnect() {
  while (!client.connected()) {
    Serial.print("Verbinde mit MQTT...");
    if (client.connect("ESP8266Client", mqtt_user, mqtt_password)) {
      Serial.println("verbunden");
      client.subscribe("esp8266/light");
    } else {
      Serial.print("Fehler, rc=");
      Serial.print(client.state());
      Serial.println(" Nächster Versuch in 5 Sekunden");
      delay(5000);
    }
  }
}

Passe deine Sketches für die ESP8266 nun an und teste die Verbindung. Funktioniert alles wie vorher? Dann hast du ein MQTT-System jetzt erfolgreich abgesichert!

]]>
MQTT (Teil 3): Daten senden und empfangen https://polluxlabs.net/esp8266-projekte/mqtt-teil-3-daten-senden-und-empfangen/ Wed, 09 Oct 2024 09:29:58 +0000 https://polluxlabs.net/?p=17331 MQTT (Teil 3): Daten senden und empfangen Weiterlesen »

]]>
Im letzten Teil hast du den MQTT-Broker kennengelernt und erfahren, wie du ihn mit Mosquitto auf einem Raspberry Pi einrichtest. Nun wird es Zeit für etwas Kommunikation. In diesem Tutorials lernst du, wie du Daten von einem ESP8266 an den Broker sendest. Ein zweiter ESP8266 hat diese Daten abonniert und reagiert entsprechend. Konkret: Du sendest die aktuelle Lichtstärke und wenn es dunkel wird, geht woanders das Licht an.

Für diese Lektion brauchst du folgende Teile:

  • 2x ESP8266-Modul
  • 1x Lichtsensor plus 10kΩ Widerstand
  • 1x LED plus passendem Vorwiderstand
  • Breadboards und Kabel

MQTT-Bibliotheken für Arduino

Um MQTT mit dem ESP8266 zu nutzen, verwendest du die Bibliothek PubSubClient von Nick O’Leary. Sie ist speziell für die Verwendung mit der Arduino-IDE und ESP8266 optimiert.

Installation der PubSubClient-Bibliothek:

  1. Öffne die Arduino-IDE.
  2. Gehe zu Sketch > Bibliothek einbinden > Bibliotheken verwalten (oder klicke auf den entsprechenden Button im Menü links)
  3. Suche nach „PubSubClient“ und installiere die neueste Version.
PubSubClient in der Arduino IDE installieren

Daten an den MQTT-Broker senden

Lass uns einen ersten Sketch erstellen, der eine Verbindung zum MQTT-Broker herstellt und eine Nachricht published.

//Nachrichten an den MQTT-Broker senden

#include <ESP8266WiFi.h>
#include <PubSubClient.h>

const char* ssid = "DEIN_WLAN_SSID";
const char* password = "DEIN_WLAN_PASSWORT";
const char* mqtt_server = "IP_ADRESSE_DES_BROKERS";

WiFiClient espClient;
PubSubClient client(espClient);

void setup() {
  Serial.begin(115200);
  setup_wifi();
  client.setServer(mqtt_server, 1883);
}

void setup_wifi() {
  delay(10);
  Serial.println();
  Serial.print("Verbinde mit ");
  Serial.println(ssid);

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

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

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

void reconnect() {
  while (!client.connected()) {
    Serial.print("Verbinde mit MQTT...");
    if (client.connect("ESP8266Client")) {
      Serial.println("verbunden");
    } else {
      Serial.print("Fehler, rc=");
      Serial.print(client.state());
      Serial.println(" Nächster Versuch in 5 Sekunden");
      delay(5000);
    }
  }
}

void loop() {
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  client.publish("esp8266/test", "Hallo vom ESP8266!");
  delay(5000);
}

Ersetze DEIN_WLAN_SSID, DEIN_WLAN_PASSWORT und IP_ADRESSE_DES_BROKERS durch deine eigenen Werte. Falls du die IP deines Raspberry Pis (also des laufenden Brokers) nicht kennst, gib im Terminal Folgendes ein:

hostname -I

Im Terminal siehst du dann die IP-Adresse, zum Beispiel 192.168.0.143

Das Sketch stellt eine Verbindung zum WLAN und dem MQTT-Broker her. In der loop()-Funktion wird alle 5 Sekunden eine Nachricht an das Topic esp8266/test gesendet.

Lade das Sketch auf deinen ESP8266 hoch und öffne den Seriellen Monitor. Du solltest sehen, wie der ESP8266 eine Verbindung herstellt und Nachrichten sendet.

Auf dem Raspberry Pi kannst du die Nachrichten mit mosquitto_sub empfangen. Gibt hierfür den folgenden Befehl in einem neuen Terminal-Fenster (während Mosquitto aktiv ist) ein:

mosquitto_sub -t esp8266/test

So weit, so gut. Als Nächstes testest du die andere Richtung und sendest du Nachrichten vom MQTT-Broker zum ESP8266

Daten vom MQTT-Broker empfangen

In diesem Test installierst du an deinem ESP8266 eine LED, die du vom Terminal (also vom Broker) aus an- und ausschaltest. Verbinde die LED wie folgt:

LED am ESP8266

Nun lade den folgenden Sketch auf deinen ESP8266 – hinterlege jedoch zunächst wieder dein Netzwerk, Passwort und die IP-Adresse des Brokers:

//Nachrichten vom MQTT-Broker empfangen

#include <ESP8266WiFi.h>
#include <PubSubClient.h>

const char* ssid = "DEIN_WLAN_SSID";
const char* password = "DEIN_WLAN_PASSWORT";
const char* mqtt_server = "IP_ADRESSE_DES_BROKERS";

WiFiClient espClient;
PubSubClient client(espClient);

const int ledPin = D5;

void setup() {
  pinMode(ledPin, OUTPUT);
  Serial.begin(115200);
  setup_wifi();
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
}

void setup_wifi() {
  delay(10);
  Serial.println();
  Serial.print("Verbinde mit ");
  Serial.println(ssid);

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

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

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

void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Nachricht empfangen [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();

  if ((char)payload[0] == '1') {
    digitalWrite(ledPin, HIGH);  // LED einschalten
  } else {
    digitalWrite(ledPin, LOW);  // LED ausschalten
  }
}

void reconnect() {
  while (!client.connected()) {
    Serial.print("Verbinde mit MQTT...");
    if (client.connect("ESP8266Client")) {
      Serial.println("verbunden");
      client.subscribe("esp8266/led");
    } else {
      Serial.print("Fehler, rc=");
      Serial.print(client.state());
      Serial.println(" Nächster Versuch in 5 Sekunden");
      delay(5000);
    }
  }
}

void loop() {
  if (!client.connected()) {
    reconnect();
  }
  client.loop();
}

So funktioniert der Sketch

Interessant in diesem Sketch sind die folgenden zwei Funktionen:

  • Die callback()-Funktion wird aufgerufen, wenn eine Nachricht auf einem abonnierten Topic eintrifft. Wir überprüfen den Inhalt der Nachricht und schalten die LED entsprechend ein oder aus.
  • In der reconnect()-Funktion abonnieren wir das Topic „esp8266/led“, um Steuerbefehle für die LED zu empfangen.

Nachdem du den Sketch auf den ESP8266 geladen hast, kannst du die Funktion wieder im Terminal testen. Steuere dort die LED mit einem der folgenden Befehle. Ersetze IP_ADRESSE_DES_BROKERS hierbei mit der tatsächlichen IP-Adresse deines Raspberry Pi:

mosquitto_pub -h IP_ADRESSE_DES_BROKERS -t "esp8266/led" -m "1"  # LED einschalten
mosquitto_pub -h IP_ADRESSE_DES_BROKERS -t "esp8266/led" -m "0"  # LED ausschalten

Funktioniert? Dann wird es Zeit für den letzten Teil: Hier lässt du zwei ESP8266 miteinander sprechen.

Kommunikation zwischen zwei ESP8266

Bisher hast du das Terminal verwendet, um den MQTT-Broker zu bedienen, also Nachrichten an einen ESP8266 zu senden oder von dort zu empfangen. Aber eigentlich sollte der Broker seine Arbeit im Hintergrund erledigen und nur ein Postbote sein, von dem du nicht viel mitbekommst. Nun sollen also zwei ESP8266 miteinander kommunizieren: Am ersten ist ein Lichtsensor installiert, der die Lichtstärke misst. Fällt diese unter einen bestimmten Wert, sendet er eine entsprechende Nachricht an den Broker. Ein zweiter ESP8266 hat diese Nachrichten abonniert und schaltet bei Bedarf eine LED ein.

Die LED hast du ja bereits installiert. Am anderen ESP8266 verbindest du den Lichtsensor wie folgt:

Lichtsensor am ESP8266

Der Sketch für den Sender

Für den Sender der Nachrichten, also den ESP8266 mit dem angeschlossenen Lichtsensor benötigst du folgenden Sketch:

//Lichtstärke senden

#include <ESP8266WiFi.h>
#include <PubSubClient.h>

const char* ssid = "DEIN_WLAN_SSID";
const char* password = "DEIN_WLAN_PASSWORT";
const char* mqtt_server = "IP_ADRESSE_DES_BROKERS";

WiFiClient espClient;
PubSubClient client(espClient);

const int lightSensorPin = A0;
const int lightThreshold = 500;

void setup() {
  Serial.begin(115200);
  setup_wifi();
  client.setServer(mqtt_server, 1883);
}

void setup_wifi() {
  delay(10);
  Serial.println();
  Serial.print("Verbinde mit ");
  Serial.println(ssid);

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

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

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

void reconnect() {
  while (!client.connected()) {
    Serial.print("Verbinde mit MQTT...");
    if (client.connect("ESP8266Client_Publisher")) {
      Serial.println("verbunden");
    } else {
      Serial.print("Fehler, rc=");
      Serial.print(client.state());
      Serial.println(" Nächster Versuch in 5 Sekunden");
      delay(5000);
    }
  }
}

void loop() {
  if (client.connected()) {
    client.loop();

    int lightValue = analogRead(lightSensorPin);
    Serial.print("Lichtwert: ");
    Serial.println(lightValue);

    if (lightValue < lightThreshold) {
      client.publish("esp8266/light", "1");  // LED einschalten
      Serial.println("MQTT Message Sent: 1");
    } else {
      client.publish("esp8266/light", "0");  // LED ausschalten
      Serial.println("MQTT Message Sent: 0");
    }

    delay(5000);
  } else {
    reconnect();
  }
}

Oben im Sketch hast du (neben den obligatorischen WLAN- und Broker-Daten) die Möglichkeit, in der Konstanten lightThreshold den Schwellenwert für den Lichtsensor einzustellen. Nach dem Upload siehst du den aktuellen Wert im Seriellen Monitor. Nutze diese Informationen, um einen passenden Schwellenwert für deine Anwendung zu definieren.

Der Sender misst alle 5 Sekunden den Lichtwert und sendet dann die passende Nachricht per MQTT.

Der Sketch für den Empfänger

Nun soll dein zweiter ESP8266 mit der angeschlossenen LED auf diese Nachrichten reagieren. Lade hierfür den folgenden Sketch hoch:

//Lichtstärke empfangen

#include <ESP8266WiFi.h>
#include <PubSubClient.h>

const char* ssid = "DEIN_WLAN_SSID";
const char* password = "DEIN_WLAN_PASSWORT";
const char* mqtt_server = "IP_ADRESSE_DES_BROKERS";

WiFiClient espClient;
PubSubClient client(espClient);

const int ledPin = D5;

void setup() {
  pinMode(ledPin, OUTPUT);
  Serial.begin(115200);
  setup_wifi();
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
}

void setup_wifi() {
  delay(10);
  Serial.println();
  Serial.print("Verbinde mit ");
  Serial.println(ssid);

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

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

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

void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Nachricht empfangen [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();

  if ((char)payload[0] == '1') {
    digitalWrite(ledPin, HIGH);  // LED einschalten
  } else {
    digitalWrite(ledPin, LOW);  // LED ausschalten
  }
}

void reconnect() {
  while (!client.connected()) {
    Serial.print("Verbinde mit MQTT...");
    if (client.connect("ESP8266Client_Receiver")) {
      Serial.println("verbunden");
      client.subscribe("esp8266/light");
    } else {
      Serial.print("Fehler, rc=");
      Serial.print(client.state());
      Serial.println(" Nächster Versuch in 5 Sekunden");
      delay(5000);
    }
  }
}

void loop() {
  if (client.connected()) {
    client.loop();
  } else {
    reconnect();
  }
}

Dieser Sketch unterscheidet sich nur leicht von jenem, mit dem du die LED per Terminal gesteuert hast. Eine Sache ist jedoch wichtig, wenn du zwei ESP8266 miteinander über den MQTT-Broker kommunizieren lässt: Sie müssen unterschiedliche Namen haben.

Den Namen, mit dem sich dein ESP8266 beim Broker anmeldet, definierst du in der folgenden Funktion (recht weit unten im Sketch):

client.connect("ESP8266Client_Receiver")

Und das war es schon, mehr benötigst du für eine einfache Kommunikation zweier ESP8266 nicht. Teste nun alle drei Geräte (Broker, Sender und Empfänger) und schaue, ob die LED angeht, wenn das Licht ausgeht.

]]>
MQTT (Teil 2): Ein MQTT-Broker auf dem Raspberry Pi https://polluxlabs.net/raspberry-pi-projekte/mqtt-teil-2-der-mqtt-broker-auf-dem-raspberry-pi/ Mon, 07 Oct 2024 08:04:10 +0000 https://polluxlabs.net/?p=17322 MQTT (Teil 2): Ein MQTT-Broker auf dem Raspberry Pi Weiterlesen »

]]>
Im vorherigen Teil hast du die Grundlagen von MQTT kennengelernt. Nun tauchen wir tiefer ein und beschäftigen uns mit dem zentralen Element jeder MQTT-Anwendung: dem Broker. In diesem Tutorial erfährst du, wie du mit Mosquitto einen MQTT-Broker auf deinem Raspberry Pi einrichtest.

Was ist ein MQTT-Broker?

Ein MQTT-Broker ist das Herzstück jeder MQTT-basierten Kommunikation. Er fungiert als zentrale Vermittlungsinstanz zwischen den Geräten, die Nachrichten senden (Publisher) und denen, die Nachrichten empfangen (Subscriber). Stell dir den Broker als eine Art intelligenten Postboten vor, der Nachrichten entgegennimmt und sie an die richtigen Empfänger verteilt.

Die Aufgaben eines MQTT-Brokers

Ein MQTT-Broker hat folgende Hauptaufgaben:

  1. Verbindungen annehmen: Der Broker nimmt Verbindungen von MQTT-Clients (Publisher und Subscriber) entgegen.
  2. Nachrichten empfangen: Er nimmt Nachrichten von Publishern entgegen.
  3. Nachrichten verteilen: Der Broker leitet die empfangenen Nachrichten an die entsprechenden Subscriber weiter.
  4. Sicherheit gewährleisten: Er kümmert sich um die Authentifizierung und Autorisierung von Clients.
  5. Quality of Service (QoS) sicherstellen: Der Broker sorgt dafür, dass die vereinbarten QoS-Level eingehalten werden.
  6. Retained Messages verwalten: Er speichert die letzten Nachrichten für bestimmte Topics.
  7. Last Will and Testament (LWT) verwalten: Der Broker sendet LWT-Nachrichten, wenn sich Clients unerwartet abmelden.

Populäre MQTT-Broker

Es gibt eine Vielzahl von MQTT-Broker-Implementierungen, sowohl Open Source als auch kommerzielle Lösungen. Hier sind einige der beliebtesten:

  • Mosquitto: Ein leichtgewichtiger Open-Source-Broker, ideal für Einsteiger und kleinere Projekte.
  • HiveMQ: Ein skalierbarer kommerzieller Broker mit vielen Zusatzfunktionen für unternehmenskritische IoT-Anwendungen.
  • VerneMQ: Ein Open-Source-Broker.
  • AWS IoT Core: Ein vollständig verwalteter Cloud-Broker von Amazon Web Services.

In diesem Kurs konzentrieren wir uns auf Mosquitto, da es sich leicht installieren und konfigurieren lässt und für die meisten Maker-Projekte völlig ausreicht.

Mosquitto auf einem Raspberry Pi einrichten

Lass uns nun Schritt für Schritt einen Mosquitto-Broker auf deinem Raspberry Pi einrichten. Wenn du keinen Raspberry Pi zur Hand hast, kannst du Mosquitto auch auf deinem PC oder Mac installieren. Die Schritte sind ähnlich, aber die genauen Befehle können je nach Betriebssystem variieren.

Schritt 1: Installation von Mosquitto

Öffne ein Terminal auf deinem Raspberry Pi und führe die folgenden Befehle aus:

sudo apt update
sudo apt install mosquitto mosquitto-clients

Der erste Befehl aktualisiert die Paketlisten deines Systems. Der zweite Befehl installiert den Mosquitto-Broker und die Mosquitto-Clients. Die Clients sind nützliche Kommandozeilen-Tools, die wir später zum Testen verwenden werden.

Schritt 2: Mosquitto-Dienst starten

Nach der Installation müssen wir den Mosquitto-Dienst starten:

sudo systemctl start mosquitto

Schritt 3: Status überprüfen

Um zu überprüfen, ob Mosquitto erfolgreich gestartet wurde, verwenden wir den folgenden Befehl:

sudo systemctl status mosquitto

Du solltest eine Ausgabe ähnlich der folgenden sehen:

Laufender MQTT-Broker auf einem Raspberry Pi

Wenn du Active: active (running) siehst, läuft dein Mosquitto-Broker erfolgreich!

Mosquitto testen

Mosquitto bringt zwei nützliche Kommandozeilen-Tools mit:

  • mosquitto_pub: Zum Veröffentlichen von Nachrichten an ein Topic
  • mosquitto_sub: Zum Abonnieren von Topics und Anzeigen empfangener Nachrichten

Lass uns diese Tools nutzen, um unseren Broker zu testen.

Schritt 1: Ein Topic abonnieren

Öffne ein neues Terminal-Fenster und gib den folgenden Befehl ein:

mosquitto_sub -t test

Dieser Befehl abonniert das Topic „test“. Der Terminal bleibt nun offen und wartet auf eingehende Nachrichten.

Schritt 2: Eine Nachricht veröffentlichen

Öffne ein weiteres Terminal-Fenster und veröffentliche eine Nachricht mit folgendem Befehl:

mosquitto_pub -t test -m "Hallo MQTT!"

Dieser Befehl veröffentlicht die Nachricht „Hallo MQTT!“ zum Topic „test“.

Schritt 3: Ergebnis überprüfen

Im ersten Terminal-Fenster, wo du mosquitto_sub ausgeführt hast, solltest du nun die Nachricht „Hallo MQTT!“ sehen. Falls ja, hast du deinen ersten MQTT-Broker erfolgreich in Betrieb genommen und getestet.

Test des MQTT-Brokers im Terminal

Fazit und Ausblick

In diesem Tutorial hast du gelernt, was ein MQTT-Broker ist, welche Aufgaben er hat und wie du einen eigenen Mosquitto-Broker auf einem Raspberry Pi einrichtest. Du hast auch gelernt, wie du den Broker mit den Mosquitto-Kommandozeilen-Tools testen kannst.

Ein laufender MQTT-Broker ist die Grundlage für viele spannende IoT-Projekte. Im nächsten Teil wirst du lernen, wie du verschiedene Geräte wie den ESP8266 als MQTT-Clients einrichtest, um Daten zu senden und zu empfangen. Du wirst sehen, wie einfach es ist, mit MQTT Sensordaten zu übertragen, Geräte zu steuern und IoT-Systeme aufzubauen.

Bis dahin kannst du gerne weiter mit deinem Mosquitto-Broker experimentieren:

  • Versuche, Nachrichten zu verschiedenen Topics zu senden und zu empfangen.
  • Schau dir die Dokumentation von Mosquitto an, um mehr über die Konfigurationsmöglichkeiten zu erfahren.
  • Überlege dir, welche Anwendungen in deinem Haushalt oder deiner Umgebung von einer MQTT-basierten Kommunikation profitieren könnten.

Viel Spaß beim Experimentieren mit deinem neuen MQTT-Broker!

]]>
MQTT (Teil 1): Einführung https://polluxlabs.net/esp8266-projekte/teil-1-einfuehrung-in-mqtt/ Sat, 05 Oct 2024 10:42:42 +0000 https://polluxlabs.net/?p=17307 MQTT (Teil 1): Einführung Weiterlesen »

]]>
Diese Tutorial-Reihe dreht sich ganz um MQTT – dem Nachrichtenprotokoll, das sich hervorragend dafür eignet, Daten zwischen Geräten auszutauschen. Stell dir vor, du möchtest die Temperatur in deinem Gewächshaus überwachen. Dazu installierst du Temperatursensoren, die ihre Messwerte per MQTT an einen zentralen Broker senden. Der Broker verteilt die Daten dann an alle „interessierten“ Empfänger, wie einen Mikrocontroller, der die Bewässerung steuert.

In dieser Einführung klären wir zunächst, worum es sich bei MQTT überhaupt handelt und wie es grundlegend funktioniert.

Was ist MQTT?

MQTT steht für „Message Queuing Telemetry Transport“. Es handelt sich um ein leichtgewichtiges, offenes Nachrichtenprotokoll, das sich ideal für die M2M-Kommunikation (Machine-to-Machine) eignet. MQTT ermöglicht es Geräten, auf einfache und effiziente Weise Daten auszutauschen.

Das Protokoll wurde 1999 von Andy Stanford-Clark (IBM) und Arlen Nipper (Cirrus Link) entwickelt. Heute hat sich MQTT zu einem Standard im IoT-Bereich entwickelt.

MQTT bietet einige Vorteile, die es für IoT-Anwendungen besonders attraktiv machen:

  • Leichtgewichtig: MQTT kommt mit minimaler Netzwerk-Bandbreite und geringen Hardwareanforderungen aus.
  • Publish/Subscribe-Modell: MQTT entkoppelt Sender (Publisher) und Empfänger (Subscriber) von Nachrichten. Geräte kommunizieren nicht direkt miteinander, sondern über einen Broker.
  • Skalierbarkeit: Das Protokoll ist hervorragend für große Netzwerke mit vielen Geräten geeignet.
  • Zuverlässigkeit: MQTT bietet drei Quality of Service (QoS) Levels, um unterschiedliche Anforderungen zu erfüllen.
  • Flexibilität: MQTT lässt sich leicht in bestehende Systeme integrieren und ist mit vielen Programmiersprachen und Plattformen kompatibel.

Wie funktioniert MQTT?

Um die Funktionsweise von MQTT zu verstehen, ist wie so oft ein Schaubild ganz hilfreich:

Schaubild: So funktioniert MQTT

Dieses Schaubild zeigt ein mögliches Smart-Home-Szenario. Lass uns die einzelnen Elemente durchgehen:

  1. Broker: Im Zentrum des Bildes siehst du einen grünen Kreis mit der Beschriftung „Broker“. Der Broker ist das Herzstück des MQTT-Systems und fungiert als zentraler Vermittler zwischen allen Geräten.
  2. Publisher (Sender): Auf der linken Seite sind drei blaue Kreise mit Thermometer-Symbolen zu sehen. Diese stellen die Publisher dar – in diesem Fall Temperatursensoren an verschiedenen Orten:
    • Ein Sensor im Haus (home/temperature) misst 19°C
    • Ein Sensor in der Garage (garage/temperature) misst 15°C
    • Ein Sensor im Garten (garden/temperature) misst -4°C
  3. Subscriber (Empfänger): Auf der rechten Seite befinden sich zwei rote Kreise mit Chip-Symbolen. Diese repräsentieren die Subscriber – Geräte, die Temperaturinformationen empfangen und verarbeiten:
    • Ein Gerät, das sich für die Haustemperatur interessiert und z.B. die Temperatur regelt.
    • Ein Gerät, das die Gartentemperatur überwacht und z.B. die Rollläden herunterlässt, um das Haus besser zu isolieren.
  4. Publish-Vorgang: Die schwarzen Pfeile von den Publishern zum Broker zeigen den Publish-Vorgang. Jeder Sensor sendet seine Messwerte mit einem spezifischen Topic (z.B. „home/temperature“) an den Broker.
  5. Subscribe-Vorgang: Die grauen Pfeile vom Broker zu den Subscribern zeigen den Subscribe-Vorgang. Die Subscriber teilen dem Broker mit, für welche Topics sie sich interessieren (z.B. „home/temperature“ oder „garden/temperature“).
  6. Nachrichtenverteilung: Die schwarzen Pfeile vom Broker zu den Subscribern zeigen, wie der Broker die empfangenen Nachrichten an die interessierten Subscriber weiterleitet.

Das Besondere an MQTT ist die Entkopplung von Sendern und Empfängern. Die Temperatursensoren (Publisher) wissen nicht, wer ihre Daten empfängt. Die Empfangsgeräte (Subscriber) wissen nicht, woher die Daten genau kommen. Alles läuft über den zentralen Broker.

Dieses System ermöglicht eine flexible und effiziente Kommunikation in IoT-Netzwerken. Neue Geräte können einfach hinzugefügt werden, indem sie sich beim Broker für die relevanten Topics registrieren, ohne dass andere Geräte angepasst werden müssen.

Topics und Topic-Hierarchie

In MQTT werden Nachrichten immer zu einem bestimmten „Topic“ (Thema) veröffentlicht. Topics sind Zeichenketten, die hierarchisch aufgebaut sein können, ähnlich wie Verzeichnispfade.

Ein paar Beispiele für Topics hast du bereits im Schaubild kennengelernt:

  • home/temperature
  • garage/temperature
  • garden/temperature

Topics werden mit Schrägstrichen (/) strukturiert. Das ermöglicht eine übersichtliche Ordnung und erleichtert das Abonnieren verwandter Themen.

Ein Subscriber kann nicht nur einzelne Topics, sondern auch ganze Topic-Zweige abonnieren. Dazu gibt es zwei Platzhalter:

  • Plus (+): Ersetzt eine einzelne Hierarchieebene. Beispiel: home/+/temperature.
  • Raute (#): Ersetzt beliebig viele Hierarchieebenen. Beispiel: home/#.

Quality of Service (QoS) Levels

Neben den Topics sind Quality of Service Levels ein wichtiger und interessanter Aspekt von MQTT. Unterschieden werden 3 Level, die die Zustellung und Verarbeitung von Nachrichten steuern:

QoS 0 (At most once):

  • Dies ist das einfachste und schnellste Level.
  • Der Sender schickt die Nachricht einmal an den Broker und vergisst sie dann.
  • Der Broker leitet die Nachricht an die Subscriber weiter, ohne eine Bestätigung zu erwarten.
  • Es gibt keine Garantie, dass die Nachricht ankommt.
  • Verwendung: Ideal für häufig gesendete, nicht kritische Daten wie regelmäßige Sensorwerte.
  • Beispiel: Ein Temperatursensor, der alle 5 Sekunden einen Wert sendet. Wenn mal ein Wert verloren geht, ist das nicht schlimm.

QoS 1 (At least once):

  • Der Sender speichert die Nachricht, bis er eine Bestätigung (PUBACK) vom Broker erhält.
  • Wenn keine Bestätigung kommt, sendet er die Nachricht erneut.
  • Der Broker leitet die Nachricht an Subscriber weiter und erwartet ebenfalls eine Bestätigung.
  • Die Nachricht kommt mindestens einmal an, kann aber auch mehrfach ankommen.
  • Verwendung: Gut für wichtige Nachrichten, bei denen Duplikate kein Problem sind.
  • Beispiel: Ein Schalter, der seinen Status meldet. Es ist wichtig, dass die Änderung ankommt, auch wenn sie vielleicht doppelt gemeldet wird.

QoS 2 (Exactly once):

  • Das zuverlässigste, aber auch komplexeste und langsamste Level.
  • Verwendet ein 4-Wege-Handshake zwischen Sender und Empfänger.
  • Garantiert, dass die Nachricht genau einmal ankommt, ohne Verlust oder Duplikate.
  • Verwendung: Für kritische Nachrichten, bei denen jede Nachricht wichtig ist und Duplikate Probleme verursachen würden.
  • Beispiel: Finanztransaktionen oder kritische Steuerungsbefehle in industriellen Anlagen.

Die Wahl des passenden QoS-Levels hängt also von den Anforderungen deiner Anwendung ab. In vielen Fällen reicht bereits der einfachste Level QoS 0 aus.

Retained Messages und Last Will and Testament

Zuletzt noch zwei weitere nützliche MQTT-Funktionen:

Retained Messages

Stell dir einen digitalen Notizzettel vor, der an einer Pinnwand (dem Broker) hängt. Jeder, der vorbeikommt (sich subscribt), kann sofort den letzten Stand sehen, ohne warten zu müssen, bis jemand eine neue Notiz macht.

Beispiel:

  • Du hast eine smarte Glühbirne im Wohnzimmer.
  • Der Lichtschalter sendet eine Retained Message „AN“ oder „AUS“ an das Topic „haus/wohnzimmer/licht“.
  • Wenn du eine neue App auf deinem Smartphone installierst und sie sich mit dem MQTT-Broker verbindet, weiß sie sofort, ob das Licht an oder aus ist, ohne auf die nächste Zustandsänderung warten zu müssen.

Last Will and Testament (LWT)

Stell dir vor, du gehst auf eine Party und sagst dem Gastgeber: „Wenn ich in der nächsten Stunde nicht wiederkomme, sag allen, ich musste plötzlich nach Hause.“ Das ist im Prinzip das LWT.

Beispiel:

  • Du hast einen Temperatursensor im Gewächshaus.
  • Bei der Verbindung zum Broker richtet der Sensor ein LWT ein: „Gewächshaussensor offline“.
  • Wenn der Sensor unerwartet die Verbindung verliert (z.B. wegen eines Stromausfalls), sendet der Broker automatisch diese Nachricht.
  • So wissen alle interessierten Geräte, dass der Sensor nicht mehr aktiv ist, ohne auf ein Timeout warten zu müssen.

Das soll es zunächst mit Teil 1 der Reihe gewesen sein. Der nächste Teil beschäftigt sich mit dem Broker – dort erfährst du, wie du diesen auf einem Raspberry Pi einrichtest und wie das Senden und Abonnieren von Daten in der Praxis aussieht.

]]>