diff --git a/auto_recover.py b/auto_recover.py index f11af72..6762622 100644 --- a/auto_recover.py +++ b/auto_recover.py @@ -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) diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..9f30c8b --- /dev/null +++ b/config.example.json @@ -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 +} diff --git a/esp/README.md b/esp/README.md new file mode 100644 index 0000000..31f4aa2 --- /dev/null +++ b/esp/README.md @@ -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:///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. diff --git a/esp/skrift_mosfet.ino b/esp/skrift_mosfet.ino new file mode 100644 index 0000000..764e7a4 --- /dev/null +++ b/esp/skrift_mosfet.ino @@ -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= + * 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 + #include + WebServer server(80); +#else + #include + #include + 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); + } +}