This commit is contained in:
chk
2026-06-19 06:43:56 +02:00
parent 83cef32a37
commit 8deb7bb8a6
3 changed files with 326 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
// EmergencyStopButton.ino
// ESP32 WiFi Light Sleep, wacht per GPIO-Interrupt auf Knopfdruck auf
// und sendet sofort einen API-Call. Ziel: <250ms von Knopfdruck bis API.
#include <WiFi.h>
#include <HTTPClient.h>
#include "esp_wifi.h"
#include "esp_sleep.h"
#include "driver/gpio.h"
// ── Konfiguration ────────────────────────────────────────────────────────────
#define BUTTON_PIN 9 // GPIO-Pin des Tasters (gegen GND)
#define WIFI_SSID "DEIN_SSID"
#define WIFI_PASSWORD "DEIN_PASSWORT"
#define API_URL "https://deine-api.example.com/emergency-stop"
#define API_TOKEN "DEIN_API_TOKEN"
// ─────────────────────────────────────────────────────────────────────────────
void connectWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("WiFi verbinden");
while (WiFi.status() != WL_CONNECTED) {
delay(100);
Serial.print(".");
}
Serial.printf("\nVerbunden. IP: %s\n", WiFi.localIP().toString().c_str());
}
void sendEmergencyStop() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi nicht verbunden API-Call abgebrochen");
return;
}
HTTPClient http;
http.begin(API_URL);
http.addHeader("Content-Type", "application/json");
http.addHeader("Authorization", "Bearer " API_TOKEN);
http.setTimeout(3000);
String body = "{\"event\":\"emergency_stop\",\"device\":\"esp32-estop\"}";
int httpCode = http.POST(body);
if (httpCode > 0) {
Serial.printf("API Response: %d\n", httpCode);
} else {
Serial.printf("HTTP Fehler: %s\n", http.errorToString(httpCode).c_str());
}
http.end();
}
void enterLightSleep() {
// Wakeup bei LOW-Pegel (Taster gegen GND, Pull-Up aktiv)
gpio_wakeup_enable((gpio_num_t)BUTTON_PIN, GPIO_INTR_LOW_LEVEL);
esp_sleep_enable_gpio_wakeup();
esp_light_sleep_start();
// Ab hier läuft der Code weiter, sobald der Taster gedrückt wird
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
connectWiFi();
// DTIM=10: ESP32 wacht alle ~1000ms kurz für Beacon auf → ~0.51 mA
esp_wifi_set_ps(WIFI_PS_MAX_MODEM);
Serial.println("Bereit. Warte auf Knopfdruck...");
}
void loop() {
enterLightSleep();
// Wakeup → Taster prüfen (Low-aktiv wegen Pull-Up)
if (digitalRead(BUTTON_PIN) == LOW) {
unsigned long t0 = millis();
sendEmergencyStop();
Serial.printf("Latenz API-Call: %lu ms\n", millis() - t0);
// Warten bis Taster losgelassen (Entprellung)
while (digitalRead(BUTTON_PIN) == LOW) {
delay(10);
}
delay(50);
}
}

Binary file not shown.

View File

@@ -0,0 +1,237 @@
\documentclass[a4paper,11pt]{article}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage[ngerman]{babel}
\usepackage{geometry}
\usepackage{amsmath}
\usepackage{booktabs}
\usepackage{listings}
\usepackage{xcolor}
\usepackage[unicode=true]{hyperref}
\usepackage{enumitem}
\lstset{
basicstyle=\ttfamily\small,
keywordstyle=\color{blue},
commentstyle=\color{gray},
stringstyle=\color{orange},
backgroundcolor=\color{gray!10},
frame=single,
breaklines=true,
columns=fullflexible
}
\title{\textbf{Emergency Stop Button -- ESP32}\\
\large Technische Dokumentation}
\author{}
\date{Juni 2026}
\begin{document}
\maketitle
\tableofcontents
\newpage
% -----------------------------------------------
\section{Ziel und Anforderungen}
Ein physischer Notaus-Taster soll beim Drücken so schnell wie möglich einen
API-Call absetzen. Der ESP32 befindet sich im Ruhezustand und wird durch den
Tastendruck geweckt.
\begin{itemize}
\item Latenz Knopfdruck $\rightarrow$ API-Call: \textbf{< 250\,ms}
\item Stromversorgung: LiPo 1000\,mAh (kabellos, batteriebetrieben)
\item Möglichst lange Akkulaufzeit (Taster wird selten gedrückt, $\leq$1\,×/h)
\item Einfache, wartungsarme Architektur
\end{itemize}
% -----------------------------------------------
\section{Architekturentscheidung}
\subsection{Bewertete Optionen}
\begin{center}
\begin{tabular}{lllll}
\toprule
Option & Latenz & Ø Strom & Gateway nötig & Komplexität \\
\midrule
Deep Sleep + WiFi (RTC-Memory) & 600--1300\,ms & $\approx$0{,}02\,mA & nein & niedrig \\
\textbf{WiFi Light Sleep (DTIM=10)} & \textbf{150--250\,ms} & \textbf{0{,}5--1\,mA} & \textbf{nein} & \textbf{niedrig} \\
BLE Light Sleep + Gateway & 100--300\,ms & 0{,}5--2\,mA & ja & hoch \\
\bottomrule
\end{tabular}
\end{center}
\subsection{Entscheidung: WiFi Light Sleep}
WiFi Light Sleep erfüllt alle Anforderungen ohne zusätzliche Infrastruktur:
\begin{itemize}
\item Der ESP32 hält die WiFi-Verbindung aufrecht und schläft nur die CPU
\item Ein GPIO-Interrupt weckt den ESP32 sofort beim Tastendruck
\item Der API-Call geht direkt vom ESP32 -- kein Container, kein Gateway
\end{itemize}
\textbf{Warum nicht BLE?} Bluetooth LE wäre minimal sparsamer
($\approx$0{,}5\,mA vs. $\approx$1\,mA), erfordert aber ein dauerhaft laufendes
Gateway-Gerät (Raspberry Pi, PC), das seinerseits Strom verbraucht und eine
Fehlerquelle darstellt.
\textbf{Warum nicht Deep Sleep?} Deep Sleep braucht beim Aufwachen
600--1300\,ms (WiFi-Reconnect), was die Latenzanforderung von 250\,ms
verfehlt.
% -----------------------------------------------
\section{WiFi Light Sleep -- Funktionsprinzip}
Im WiFi Light Sleep (Modem Sleep) schläft die CPU, während der WiFi-Stack
aktiv bleibt. Der Router sendet alle 100\,ms einen Beacon; mit DTIM=10 wacht
der ESP32 automatisch alle $\approx$1000\,ms für 1--2\,ms auf, um gepufferte
Pakete abzuholen. Die Verbindungsassoziation bleibt dabei erhalten.
\subsection{DTIM-Einstellung und Stromverbrauch}
\begin{center}
\begin{tabular}{lll}
\toprule
DTIM & Wakeup-Intervall & Ø Strom \\
\midrule
1 & 100\,ms & 5--10\,mA \\
3 & 300\,ms & 2--4\,mA \\
10 & 1000\,ms & 0{,}5--1\,mA \\
\bottomrule
\end{tabular}
\end{center}
Empfohlen: \texttt{DTIM=10} -- sparsamste Option, Verbindung bleibt stabil.
\subsection{Akkulaufzeit (1000 mAh LiPo)}
Bei $\approx$1\,mA Durchschnittsstrom (DTIM=10, selten gedrückt):
\[
t = \frac{1000~\mathrm{mAh}}{1~\mathrm{mA}} \approx 40~\mathrm{Tage}
\]
% -----------------------------------------------
\section{Latenzbudget}
\begin{center}
\begin{tabular}{ll}
\toprule
Schritt & Zeit \\
\midrule
ESP32 Wakeup aus Light Sleep & 1--5\,ms \\
WiFi-Verbindung prüfen (bereits aktiv) & 0\,ms \\
HTTP-Request aufbauen & 20--50\,ms \\
TLS-Handshake (HTTPS) & 50--150\,ms \\
Server-Antwort & 20--50\,ms \\
\midrule
\textbf{Gesamt} & \textbf{$\approx$100--250\,ms} \\
\bottomrule
\end{tabular}
\end{center}
% -----------------------------------------------
\section{Hardware}
\subsection{Empfohlene Boards}
\begin{center}
\begin{tabular}{llll}
\toprule
Board & Lader & Preis & Bemerkung \\
\midrule
\textbf{FireBeetle ESP32-E} (DFRobot) & integriert & 8--12\,& Low-Power optimiert \\
LOLIN D32 (Wemos) & TP4054 & 5--8\,& günstig, bewährt \\
Adafruit HUZZAH32 Feather & MCP73831 & $\approx$20\,& gute Dokumentation \\
\bottomrule
\end{tabular}
\end{center}
\subsection{Schaltung}
\begin{lstlisting}
ESP32 Taster
GPIO 9 ---- [Taster] ---- GND
(interner Pull-Up aktiv: HIGH = offen, LOW = gedrueckt)
LiPo 1000mAh ---- BAT+ / BAT- des Boards
\end{lstlisting}
Kein weiteres Bauteil nötig -- der interne Pull-Up des ESP32 reicht.
% -----------------------------------------------
\section{Software}
\subsection{Abhängigkeiten (Arduino IDE)}
\begin{itemize}
\item Board-Package: \texttt{esp32} by Espressif (Boards Manager)
\item Bibliotheken: \texttt{WiFi.h}, \texttt{HTTPClient.h} (im esp32-Package enthalten)
\end{itemize}
\subsection{Konfiguration in \texttt{EmergencyStopButton.ino}}
\begin{lstlisting}[language=C]
#define BUTTON_PIN 9
#define WIFI_SSID "DEIN_SSID"
#define WIFI_PASSWORD "DEIN_PASSWORT"
#define API_URL "https://deine-api.example.com/emergency-stop"
#define API_TOKEN "DEIN_API_TOKEN"
\end{lstlisting}
\subsection{Ablauf}
\begin{enumerate}
\item \texttt{setup()}: WiFi verbinden, \texttt{WIFI\_PS\_MAX\_MODEM} aktivieren
\item \texttt{loop()}: \texttt{esp\_light\_sleep\_start()} -- CPU schläft, WiFi aktiv
\item Tastendruck löst GPIO-Interrupt aus -- ESP32 wacht auf
\item \texttt{sendEmergencyStop()} sendet HTTP-POST an API
\item Warten bis Taster losgelassen, zurück zu Schritt 2
\end{enumerate}
\subsection{Kritische API-Funktion}
\begin{lstlisting}[language=C]
void sendEmergencyStop() {
HTTPClient http;
http.begin(API_URL);
http.addHeader("Content-Type", "application/json");
http.addHeader("Authorization", "Bearer " API_TOKEN);
http.setTimeout(3000);
String body = "{\"event\":\"emergency_stop\",\"device\":\"esp32-estop\"}";
int httpCode = http.POST(body);
http.end();
}
\end{lstlisting}
% -----------------------------------------------
\section{Deployment-Hinweise}
\begin{itemize}
\item \textbf{HTTPS}: TLS-Handshake kostet 50--150\,ms. Falls die Latenz
kritisch ist und das Netz vertrauenswürdig, kann HTTP verwendet werden
(dann $\approx$50--100\,ms Gesamt).
\item \textbf{DTIM am Router}: Manche Router ignorieren den DTIM-Wunsch des
Clients. Im Zweifelsfall DTIM=3 am Router einstellen.
\item \textbf{API-Token}: Nicht im Quellcode einchecken -- z.\,B. in eine
separate \texttt{secrets.h} auslagern, die im \texttt{.gitignore} steht.
\item \textbf{Watchdog}: Bei produktivem Einsatz einen Hardware-Watchdog
aktivieren, damit der ESP32 sich bei WiFi-Verlust selbst neu startet.
\end{itemize}
% -----------------------------------------------
\section{Dateien}
\begin{center}
\begin{tabular}{ll}
\toprule
Datei & Beschreibung \\
\midrule
\texttt{EmergencyStopButton.ino} & Arduino-Sketch (Hauptprogramm) \\
\texttt{eStopESP32.tex} & Diese Dokumentation \\
\bottomrule
\end{tabular}
\end{center}
\end{document}