AutoRecover: bei Recover-Fenster ESP-MOSFET triggern („Stromzufuhr aktivieren")

Erkennt AutoRecover das Plotter-Recover-Fenster, ruft es jetzt zuerst
einen ESP auf (HTTP /trigger) und klickt danach „Recover". Der ESP
schaltet einen MOSFET und aktiviert damit die Stromzufuhr. Konfiguration
über config.json (esp_url/esp_path/esp_key/esp_timeout) bzw. Env-Vars;
ohne esp_url unverändertes Verhalten.

Neu: esp/skrift_mosfet.ino (ESP8266/ESP32-Firmware, WLAN + HTTP-Server,
Endpunkte /trigger /on /off, getimter Puls oder Latch) + esp/README.md
(Verdrahtung/Flashen) + config.example.json.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-09-21 20:20:36 +02:00
parent f75957c15e
commit 16d603b19f
4 changed files with 271 additions and 0 deletions

View File

@@ -47,6 +47,61 @@ def _lade_directus_cfg():
_DX_URL, _DX_TOKEN, _DX_MID = _lade_directus_cfg()
def _lade_esp_cfg():
"""ESP-Zugang (MOSFET-Trigger) aus config.json bzw. Env-Variablen.
Erwartete Schlüssel in der config.json (neben diesem Skript oder beim Agenten):
"esp_url": "http://192.168.2.50" Basis-URL des ESP (Pflicht, sonst aus)
"esp_path": "/trigger" Endpunkt (Default /trigger)
"esp_key": "geheim" optionaler Schutz-Schlüssel (?key=)
"esp_timeout": 5 Sekunden (Default 5)
"""
url = path = key = None
timeout = 5
for pfad in (os.path.join(_HIER, "config.json"),
os.path.join(_HIER, "..", "skrift-agent", "config.json")):
try:
with open(pfad, "r", encoding="utf-8") as f:
c = json.load(f)
if c.get("esp_url"):
url = c.get("esp_url")
path = c.get("esp_path")
key = c.get("esp_key")
timeout = c.get("esp_timeout", 5)
break
except Exception:
pass
url = url or os.environ.get("SKRIFT_ESP_URL")
path = path or os.environ.get("SKRIFT_ESP_PATH") or "/trigger"
key = key or os.environ.get("SKRIFT_ESP_KEY") or ""
try:
timeout = float(timeout)
except Exception:
timeout = 5.0
return (url.rstrip("/") if url else None), path, key, timeout
_ESP_URL, _ESP_PATH, _ESP_KEY, _ESP_TIMEOUT = _lade_esp_cfg()
def esp_trigger():
"""Sagt dem ESP „Stromzufuhr aktivieren" (schaltet den MOSFET). Nie fatal.
Wird bei jedem erkannten Recover-Fenster aufgerufen. Ohne konfigurierte
esp_url passiert nichts (AutoRecover läuft normal weiter).
"""
if not (requests and _ESP_URL):
return
try:
params = {"key": _ESP_KEY} if _ESP_KEY else None
r = requests.get(_ESP_URL + _ESP_PATH, params=params, timeout=_ESP_TIMEOUT)
print(f" → ESP getriggert ({_ESP_URL}{_ESP_PATH}): HTTP {r.status_code}")
dx_log("info", f"ESP-Stromzufuhr aktiviert (HTTP {r.status_code}).")
except Exception as e:
print(f" → ESP-Trigger fehlgeschlagen: {e}")
dx_log("warn", f"ESP-Trigger fehlgeschlagen: {e}")
def dx_log(level, message):
"""Meldet ein Ereignis an Directus (agent_logs). Fehler nie fatal."""
if not (requests and _DX_URL and _DX_TOKEN):
@@ -122,6 +177,9 @@ def main():
print(f"[Versuch {versuche}/{MAX_VERSUCHE}] Fehlerfenster gefunden!")
dx_log("error", f"Plotter-Fehlerfenster erkannt (Versuch {versuche}/{MAX_VERSUCHE}): {meldung}")
# ESP zuerst: MOSFET/„Stromzufuhr aktivieren", dann Recover klicken.
esp_trigger()
time.sleep(0.5)
click_recover_button(hwnd)

12
config.example.json Normal file
View File

@@ -0,0 +1,12 @@
{
"_hinweis": "Kopie als config.json speichern und Werte anpassen. Directus-Felder sind optional (nur fürs Logging); ohne esp_url wird kein ESP getriggert.",
"directus_url": "https://directus.example.de",
"directus_token": "",
"machine_id": 1,
"esp_url": "http://192.168.2.50",
"esp_path": "/trigger",
"esp_key": "",
"esp_timeout": 5
}

65
esp/README.md Normal file
View File

@@ -0,0 +1,65 @@
# Skrift ESP MOSFET-Trigger
Kleiner WLAN-„Schalter": Wenn am Plotter das **Recover-Fenster** erscheint, ruft
AutoRecover den ESP auf (`/trigger`), der einen **MOSFET** schaltet und damit die
**Stromzufuhr aktiviert**.
## Hardware
- ESP8266 (NodeMCU / Wemos D1 mini) **oder** ESP32.
- N-Kanal-Logic-Level-MOSFET als **Low-Side-Schalter** (z. B. IRLZ44N, oder ein
fertiges AO3400-/IRF520-Modul).
### Verdrahtung (Low-Side)
```
ESP GPIO (MOSFET_PIN) ──[100–220 Ω]── Gate
Gate ──[100 kΩ]── GND (Pulldown: MOSFET beim Booten sicher AUS)
Last(+) ── Versorgung+
Last(−) ── Drain
Source ── GND
GND(ESP) ── GND(Versorgung) (gemeinsame Masse ist Pflicht!)
```
`ACTIVE_HIGH = true`: GPIO **HIGH = MOSFET leitet = Strom an**.
Bei einem fertigen Modul mit „VCC/GND/SIG": SIG an `MOSFET_PIN`, GND gemeinsam.
## Firmware flashen
1. Arduino IDE, passendes Board wählen (ESP8266 **oder** ESP32 – der Sketch
erkennt beides automatisch).
2. In `skrift_mosfet.ino` oben anpassen:
- `WIFI_SSID` / `WIFI_PASS`
- `MOSFET_PIN` (ESP8266: `5` = D1; ESP32: z. B. `23`)
- `PULSE_MS` – wie lange „Strom an" bleibt (Default 3000 ms; `0` = bleibt an bis `/off`)
- optional `AUTH_TOKEN` (dann in der config.json `esp_key` gleich setzen)
- optional feste IP (`USE_STATIC_IP = true`) – **empfohlen**, damit AutoRecover
eine stabile Adresse hat.
3. Hochladen, seriellen Monitor (115200) öffnen → zeigt die IP.
## Endpunkte (HTTP GET, Port 80)
| Pfad | Wirkung |
|------------|-----------------------------------------------------|
| `/` | Statusseite (Text) |
| `/trigger` | MOSFET aktivieren (Puls `PULSE_MS`; `0` = dauerhaft) |
| `/on` | MOSFET dauerhaft AN |
| `/off` | MOSFET AUS |
Test im Browser: `http://<ESP-IP>/trigger`
## AutoRecover verbinden
In `Produktion/AutoRecover/config.json` (siehe `config.example.json`):
```json
{
"esp_url": "http://192.168.2.50",
"esp_path": "/trigger",
"esp_key": "",
"esp_timeout": 5
}
```
Ab dann triggert AutoRecover bei **jedem** erkannten Recover-Fenster zuerst den
ESP und klickt danach „Recover". Ohne `esp_url` bleibt alles beim Alten.

136
esp/skrift_mosfet.ino Normal file
View File

@@ -0,0 +1,136 @@
/*
* Skrift – ESP MOSFET-Trigger („Stromzufuhr aktivieren")
* ===========================================================================
* Kleiner WLAN-Empfänger für einen N-Kanal-MOSFET (Low-Side-Schalter).
* AutoRecover ruft bei jedem Plotter-Recover-Fenster den Endpunkt /trigger auf;
* der ESP schaltet dann den MOSFET (GPIO) und aktiviert damit die Stromzufuhr.
*
* Läuft auf ESP8266 (NodeMCU/Wemos D1 mini) UND ESP32 – die passenden Bibliotheken
* werden über die Board-Auswahl in der Arduino IDE automatisch gewählt.
*
* Endpunkte (HTTP GET, Port 80):
* / Statusseite (Text) – Pin-Zustand, Uptime, IP
* /trigger MOSFET aktivieren (Puls von PULSE_MS ms; 0 = dauerhaft an)
* /on MOSFET dauerhaft AN
* /off MOSFET AUS
* Optionaler Schutz: ist AUTH_TOKEN gesetzt, muss jeder Aufruf ?key=<token>
* mitschicken (muss zu esp_key in der AutoRecover-config.json passen).
*
* Verdrahtung (Low-Side, N-Kanal-Logic-Level-MOSFET, z. B. IRLZ44N / AO3400-Modul):
* ESP GPIO (MOSFET_PIN) ─► Gate (ggf. 100–220 Ω in Reihe)
* Gate ─► 100 kΩ ─► GND (Pulldown, damit der MOSFET beim Booten sicher AUS ist)
* Last(+) an Versorgung+, Last(−) ─► Drain ; Source ─► GND (gemeinsame Masse!)
* GND von ESP und Last-Versorgung verbinden.
* ACTIVE_HIGH=true: GPIO HIGH = MOSFET leitet = „Strom an".
*/
#if defined(ESP32)
#include <WiFi.h>
#include <WebServer.h>
WebServer server(80);
#else
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
ESP8266WebServer server(80);
#endif
// ── Konfiguration ──────────────────────────────────────────────────────────
const char* WIFI_SSID = "DEIN_WLAN";
const char* WIFI_PASS = "DEIN_WLAN_PASSWORT";
// GPIO, an dem das MOSFET-Gate hängt. ESP8266: 5 = D1 (Wemos/NodeMCU). ESP32: z. B. 23.
const int MOSFET_PIN = 5;
const bool ACTIVE_HIGH = true; // true: HIGH = MOSFET an ("Strom an")
const unsigned long PULSE_MS = 3000; // /trigger: Dauer der Aktivierung (0 = latch, bleibt an)
// Optionaler Schutz. Leer = kein Schutz. Muss zu esp_key in der config.json passen.
const char* AUTH_TOKEN = "";
// Optionale feste IP (empfohlen, damit AutoRecover eine stabile Adresse hat).
// Auf true setzen und Werte anpassen; sonst DHCP.
const bool USE_STATIC_IP = false;
IPAddress IP_ADDR (192, 168, 2, 50);
IPAddress IP_GATE (192, 168, 2, 1);
IPAddress IP_MASK (255, 255, 255, 0);
IPAddress IP_DNS (192, 168, 2, 1);
// ── Zustand ────────────────────────────────────────────────────────────────
bool mosfetAn = false;
bool pulsAktiv = false;
unsigned long pulsBis = 0;
void mosfetSchreiben(bool an) {
mosfetAn = an;
digitalWrite(MOSFET_PIN, (an == ACTIVE_HIGH) ? HIGH : LOW);
}
bool autorisiert() {
if (strlen(AUTH_TOKEN) == 0) return true;
return server.hasArg("key") && server.arg("key") == AUTH_TOKEN;
}
void sendeStatus(const char* hinweis) {
String s = String(hinweis) + "\n";
s += "mosfet="; s += (mosfetAn ? "an" : "aus"); s += "\n";
s += "puls="; s += (pulsAktiv ? "aktiv" : "-"); s += "\n";
s += "ip="; s += WiFi.localIP().toString(); s += "\n";
s += "uptime_s="; s += String(millis() / 1000); s += "\n";
server.send(200, "text/plain; charset=utf-8", s);
}
void handleRoot() { sendeStatus("Skrift ESP MOSFET-Trigger"); }
void handleTrigger() {
if (!autorisiert()) { server.send(403, "text/plain", "verboten\n"); return; }
mosfetSchreiben(true);
if (PULSE_MS > 0) { // getimter Puls
pulsAktiv = true;
pulsBis = millis() + PULSE_MS;
sendeStatus("OK trigger (puls)");
} else { // dauerhaft an (latch)
pulsAktiv = false;
sendeStatus("OK trigger (latch)");
}
}
void handleOn() { if (!autorisiert()) { server.send(403, "text/plain", "verboten\n"); return; } pulsAktiv = false; mosfetSchreiben(true); sendeStatus("OK on"); }
void handleOff() { if (!autorisiert()) { server.send(403, "text/plain", "verboten\n"); return; } pulsAktiv = false; mosfetSchreiben(false); sendeStatus("OK off"); }
void setup() {
Serial.begin(115200);
pinMode(MOSFET_PIN, OUTPUT);
mosfetSchreiben(false); // sicher AUS beim Start
if (USE_STATIC_IP) WiFi.config(IP_ADDR, IP_GATE, IP_MASK, IP_DNS);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
Serial.print("Verbinde WLAN");
while (WiFi.status() != WL_CONNECTED) { delay(400); Serial.print("."); }
Serial.println();
Serial.print("Verbunden. IP: "); Serial.println(WiFi.localIP());
server.on("/", handleRoot);
server.on("/trigger", handleTrigger);
server.on("/on", handleOn);
server.on("/off", handleOff);
server.onNotFound([]() { server.send(404, "text/plain", "nicht gefunden\n"); });
server.begin();
Serial.println("HTTP-Server auf Port 80 gestartet.");
}
void loop() {
server.handleClient();
// WLAN bei Bedarf neu verbinden (Plotter läuft lange).
static unsigned long letzteWlanPruefung = 0;
if (millis() - letzteWlanPruefung > 10000) {
letzteWlanPruefung = millis();
if (WiFi.status() != WL_CONNECTED) WiFi.begin(WIFI_SSID, WIFI_PASS);
}
// Getimten Puls beenden.
if (pulsAktiv && (long)(millis() - pulsBis) >= 0) {
pulsAktiv = false;
mosfetSchreiben(false);
}
}