Inhaltsverzeichnis
WWW: ESP32 - IR-Schnittstelle auslesen
ESP32
Warum B und nicht A?
- A: GPIO15 ist ein Strapping-Pin (beeinflusst den Boot), GPIO34 ist nur Eingang.
- Als Lesekopf-RX geht 34, aber A ist insgesamt unflexibler.
- B: 14, 33, 27 und 25 sind unkritisch.
- GPIO14 gibt beim Booten kurz ein PWM-Signal aus, was hier nicht stört.
Anschlüsse
| A | B | ||||
|---|---|---|---|---|---|
| 1 | 15 | PWM | 1 | 14 | |
| 2 | 34 | analog | 2 | 33 | TX (zum Adapter-Tx) |
| 3 | 22 | SCL | 3 | 22 | |
| 4 | 3.3 V | + | 4 | 3.3 V | + |
| 5 | GND | - | 5 | GND | - |
| 6 | 21 | SDA | 6 | 21 | |
| 7 | 26 | interupt | 7 | 27 | RX (vom Adapter-Rx) |
| 8 | 32 | PWM | 8 | 25 |
Lesekopf
- Versorgung: 3,3–5 V, der Logikpegel entspricht Vcc. Mit 3,3 V am ESP32 passt das, am Nano gehen 5 V.
- Pegel: Der Adapter invertiert intern so, dass am Rx-Pin normale UART-Polarität anliegt (Ruhe = HIGH).
- In der Software ist keine Invertierung nötig.
- Maximale Baudrate: 9600. Für den Stromzähler (SML, 9600) reicht das genau.
- Beim Wärmezähler später muss das Protokoll mit ≤ 9600 Baud laufen.
- Anschlussreihenfolge: Die Platine ist von links nach rechts mit GND, Tx, Rx, Vcc beschriftet.
- Der RJ22-Stecker hat die Reihenfolge Vcc, Tx, Rx, GND.
- Beim Verdrahten also nach der Beschriftung gehen, nicht nach der Reihenfolge.
- Halterung: Der Adapter hält magnetisch.
- In der Kompatibilitätsliste steht nur der Iskra MT691, der E320 ist dort nicht aufgeführt.
- Teste also, ob der Magnet am E320 hält und der Kopf exakt über der optischen Schnittstelle sitzt, notfalls mit etwas Klebeband.
Zähler
Stromzähler auslesen (E320, SML)
Verdrahtung (Variante B)
| Lesekopf | ESP32 |
|---|---|
| Vcc | 3,3 V |
| GND | GND |
| Rx (Daten vom Zähler) | GPIO27 (RX) |
| Tx (Senden zum Zähler) | GPIO33 (TX) |
| OLED SDA | GPIO21 |
| OLED SCL | GPIO22 |
Im Sketch:
Serial1.begin(9600, SERIAL_8N1, 27, 33); // RX=27, TX=33
Stromzähler E320 (SML)
Daten
- Der E320 sendet jede Sekunde ein SML-Telegramm mit 9600 Baud, 8N1. Der Lesekopf muss nicht senden.
- Ohne PIN-Freischaltung kommen nur Bezug und Einspeisung in ganzen kWh (Telegramm ca. 252 Bytes). Mit freigeschaltetem erweitertem Datensatz kommt zusätzlich die Wirkleistung und die Telegramme sind länger.
| OBIS | Bedeutung | Einheit im Telegramm |
|---|---|---|
| 1.8.0 | Bezug (Zählerstand) | Wh, Skalierer beachten |
| 2.8.0 | Einspeisung (Zählerstand) | Wh, Skalierer beachten |
| 16.7.0 | Momentanleistung (negativ bei Einspeisung) | W |
PIN-Freischaltung (einmalig) Die PIN erhält man vom Messstellenbetreiber. Sie gehört nicht ins Wiki und nicht in den Quellcode.
- Lesekopf abnehmen (er deckt die optische Taste ab).
- Mit Handy-Taschenlampe kurz auf die optische Taste leuchten: Displaytest, dann Abfrage „PIn“.
- Erneut leuchten, die Anzeige beginnt bei 0. Jeder kurze Puls (unter 2 s) zählt die Ziffer um 1 hoch. Nach ca. 3 s ohne Puls wird die Ziffer übernommen und die nächste beginnt.
- Nach der PIN mit kurzen Pulsen durch die Liste blättern bis „inF“. Dort langer Puls (ca. 5 s), bis „inF on“ erscheint. Damit sendet die Schnittstelle den erweiterten Datensatz.
- Weiter zu „PIn on“, wieder ca. 5 s leuchten, bis „PIn oFF“ erscheint. Sonst verlangt der Zähler nach einem Stromausfall die PIN neu.
- Nach 120 s ohne Bedienung kehrt der Zähler zur Standardanzeige zurück. Dann PIN neu eingeben.
Bei LED-Handylicht erkennt der Sensor die Pulse manchmal schlecht. Hilfreich ist eine saubere Lichtquelle, direkt an der Taste, zwischen den Pulsen wirklich wegnehmen. Alternativ die IR-Diode des Lesekopfs über den ESP32 schalten, solange die BOOT-Taste gedrückt ist (Sketch unten). Das hat bei uns funktioniert.
const int PIN_LED = 33; // Adapter-Tx: LOW = IR-LED an const int PIN_TASTE = 0; // BOOT-Taste (gedrückt = LOW) void setup() { pinMode(PIN_LED, OUTPUT); digitalWrite(PIN_LED, HIGH); // LED aus pinMode(PIN_TASTE, INPUT_PULLUP); } void loop() { digitalWrite(PIN_LED, digitalRead(PIN_TASTE) == LOW ? LOW : HIGH); }
Sketch
- Lesen: SML-Telegramm erkennen (Start 1B 1B 1B 1B 01 01 01 01, Ende 1B 1B 1B 1B 1A), danach OBIS-Werte suchen.
- Senden: alle 20 s per HTTP-POST an emonCMS in einem eigenen Task, damit das Lesen nicht blockiert.
- OLED: Bezug, Einspeisung, Leistung, WLAN-Status, Telegrammzähler, HTTP-Code.
- Flag
VERBINDUNGSTEST:truesendet nur einen Testwert (1 Versuch),falsedie Zählerdaten. - Bibliothek:
U8g2(Arduino-Bibliotheksverwalter). Board: ESP32.
/******************************************************************************************** * Stromzähler E320 (SML) -> OLED + EmonCMS ESP32, statische IP, selbstheilend * * 2026-11-07 * Features: * - NUR HIER ÄNDERN: SENSOR_ID, NODE, VERBINDUNGSTEST, Netzwerk-Auswahl (Bereich A/B/C) * - VERBINDUNGSTEST true = nur Testwert senden, 1 Versuch, Port-Test (80/443/8080) * VERBINDUNGSTEST false = Zählerdaten (B_kWh, E_kWh, P_kWh) werden gesendet * - Statische IP (kein DHCP), robuster WLAN-Start mit 3 Versuchen + Funk-Reset * - Lesen (SML, 9600 Baud) in loop(), Senden in eigenem Task (blockiert das Lesen nicht) * - Server ist nach dem Start erst nach ca. 5 min erreichbar: * • in den ersten 10 min keine Funk-Resets * • Boot-Watchdog erst nach 15 min * - Auto-Neustart, falls: * • beim Boot 15 min lang kein erfolgreicher Upload (nur im Normalbetrieb mit Zählerdaten) * • WLAN im Betrieb > 2 min am Stück weg * * Pinbelegung ESP32: Lesekopf Rx -> GPIO27 (RX), Lesekopf Tx -> GPIO33 (TX), * Vcc -> 3,3 V, GND, OLED SDA = GPIO21, SCL = GPIO22 ********************************************************************************************/ #include <WiFi.h> #include <HTTPClient.h> #include <WiFiClient.h> #include <U8g2lib.h> /**************************************** NUR HIER ÄNDERUNGEN VORNEHMEN ******************************/ // // 1) Geräte-Identität #define SENSOR_ID 241 // letzte IP-Stelle: 192.168.1.SENSOR_ID const char* NODE = "241-ESP"; // Node-Name in EmonCMS // // 2) Verbindungstest #define VERBINDUNGSTEST false // true = nur Testwert senden, false = Zählerdaten senden // // 3) Netzwerk-Auswahl siehe weiter unten (Bereich A/B/C) // /**************************************** AB HIER KEINE ÄNDERUNGEN VORNEHMEN *****************************************************/ IPAddress ip(192, 168, 1, SENSOR_ID); // feste IP aus SENSOR_ID IPAddress SERVER_IP(192, 168, 1, 2); // EmonCMS-Server (gilt für alle drei Netzwerkprofile) const int httpPort = 80; const char* EMON_PATH = "/emoncms/input/post.json"; // ============================================================ // NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen // ============================================================ #define NETZ_A_MOBIL false // SAS-ARDUINO-mobil (persönliches Testnetz) #define NETZ_B_TEST true // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key) #define NETZ_C_LIVE false // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key) #if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1 #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!" #endif // ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) ------------- // Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN! #if NETZ_A_MOBIL const char* ssid = "SAS-ARDUINO-mobil"; const char* password = "..."; const char* apikey = "..."; #endif // ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb ------------------------- #if NETZ_B_TEST const char* ssid = "SAS-ARDUINO"; const char* password = "..."; const char* apikey = "..."; #endif // ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb -------------------------- #if NETZ_C_LIVE const char* ssid = "SAS-ARDUINO"; const char* password = "..."; const char* apikey = "..."; #endif IPAddress gateway(192, 168, 1, 1); IPAddress subnet (255, 255, 255, 0); IPAddress dns (0, 0, 0, 0); /*** Messintervall *************************************************************************/ const int Schreibpause = NETZ_A_MOBIL ? 10000 : 20000; // A (mobil/Test) = 10 s, B/C (Schulnetz) = 20 s /*** Hardware ******************************************************************************/ const int PIN_RX = 27; // Adapter-Rx (Daten vom Zähler) const int PIN_TX = 33; // Adapter-Tx (wird nicht benutzt) U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); // SDA=21, SCL=22 /*** Selbstheilungs-Parameter **************************************************************/ const uint32_t BOOT_UPLOAD_DEADLINE_MS = 900000; // 15 min: Server ist erst nach ca. 5 min erreichbar const uint32_t NO_RESET_AFTER_BOOT_MS = 600000; // 10 min: in dieser Zeit keine Funk-Resets const uint32_t WIFI_DOWN_REBOOT_MS = 120000; // 2 min am Stück offline -> Neustart const uint8_t WIFI_BOOT_TRIES = 3; /*** Zustände ******************************************************************************/ static volatile bool g_lastSendFailed = false; static volatile bool g_uploadedOnce = false; static volatile uint32_t g_wifiDownSince = 0; static volatile int g_httpCode = 0; static uint32_t g_bootMs = 0; struct Messwerte { double bezug, einsp, leistung; bool gueltig; uint32_t nr; }; Messwerte g_mw = {0, 0, 0, false, 0}; portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; Messwerte holeMesswerte() { Messwerte c; portENTER_CRITICAL(&mux); c = g_mw; portEXIT_CRITICAL(&mux); return c; } /*** SML-Parser ****************************************************************************/ const uint8_t OBIS_BEZUG[] = {0x77,0x07,0x01,0x00,0x01,0x08,0x00,0xFF}; const uint8_t OBIS_EINSPEIS[] = {0x77,0x07,0x01,0x00,0x02,0x08,0x00,0xFF}; const uint8_t OBIS_LEISTUNG[] = {0x77,0x07,0x01,0x00,0x10,0x07,0x00,0xFF}; const uint8_t START[8] = {0x1B,0x1B,0x1B,0x1B,0x01,0x01,0x01,0x01}; const uint8_t ENDM[5] = {0x1B,0x1B,0x1B,0x1B,0x1A}; uint8_t buf[1024]; int n = 0; int rest = -1; bool imFrame = false; bool finde(const uint8_t *obis, double &out) { for (int i = 0; i < n - 30; i++) { if (memcmp(buf + i, obis, 8) != 0) continue; for (int j = i + 8; j < i + 24 && j < n - 2; j++) { if (buf[j] == 0x52 && (buf[j - 1] == 0x1E || buf[j - 1] == 0x1B)) { int8_t scaler = (int8_t)buf[j + 1]; uint8_t typ = buf[j + 2]; int len = (typ & 0x0F) - 1; if (len < 1 || len > 8 || j + 3 + len > n) return false; int64_t v = (buf[j + 3] & 0x80) ? -1 : 0; for (int k = 0; k < len; k++) v = (v << 8) | buf[j + 3 + k]; out = (double)v; for (int s = scaler; s < 0; s++) out /= 10.0; for (int s = scaler; s > 0; s--) out *= 10.0; return true; } } } return false; } void anzeigen() { Messwerte m = holeMesswerte(); u8g2.firstPage(); do { u8g2.setFont(u8g2_font_6x12_tf); u8g2.setCursor(0, 12); u8g2.printf("Bezug: %.1f kWh", m.bezug); u8g2.setCursor(0, 28); u8g2.printf("Einsp: %.1f kWh", m.einsp); u8g2.setCursor(0, 44); u8g2.printf("Leist: %.0f W", m.leistung); u8g2.setCursor(0, 60); u8g2.printf("%s #%lu H%d%s", WiFi.status() == WL_CONNECTED ? "WLAN" : "w/o", (unsigned long)m.nr, (int)g_httpCode, VERBINDUNGSTEST ? " TEST" : ""); } while (u8g2.nextPage()); } void auswerten() { double v; bool ok = false; Messwerte c = holeMesswerte(); if (finde(OBIS_BEZUG, v)) { c.bezug = v / 1000.0; ok = true; } if (finde(OBIS_EINSPEIS, v)) { c.einsp = v / 1000.0; } if (finde(OBIS_LEISTUNG, v)) { c.leistung = v; } if (ok) c.gueltig = true; c.nr++; portENTER_CRITICAL(&mux); g_mw = c; portEXIT_CRITICAL(&mux); Serial.printf("#%lu (%d Bytes) Bezug %.3f | Einsp %.3f | %.0f W\n", (unsigned long)c.nr, n, c.bezug, c.einsp, c.leistung); anzeigen(); } void byteEmpfangen(uint8_t b) { if (n < (int)sizeof(buf)) buf[n++] = b; else { n = 0; rest = -1; imFrame = false; return; } if (n >= 8 && memcmp(buf + n - 8, START, 8) == 0) { // Telegrammanfang memmove(buf, buf + n - 8, 8); n = 8; rest = -1; imFrame = true; return; } if (!imFrame) { if (n > 16) { memmove(buf, buf + n - 8, 8); n = 8; } return; } if (rest > 0) { // 3 Bytes nach dem Endmarker if (--rest == 0) { auswerten(); n = 0; rest = -1; imFrame = false; } return; } if (n >= 5 && memcmp(buf + n - 5, ENDM, 5) == 0) rest = 3; } /*** WLAN-Helfer ***************************************************************************/ bool ensureWifi(uint32_t timeoutMs = 15000) { if (WiFi.status() == WL_CONNECTED) { g_wifiDownSince = 0; return true; } Serial.print("[WIFI] Verbinde"); uint32_t start = millis(); while (WiFi.status() != WL_CONNECTED && millis() - start < timeoutMs) { delay(250); Serial.print("."); } if (WiFi.status() == WL_CONNECTED) { Serial.printf("\n[WIFI] OK: %s\n", WiFi.localIP().toString().c_str()); g_wifiDownSince = 0; return true; } Serial.println("\n[WIFI] Timeout"); if (!g_wifiDownSince) g_wifiDownSince = millis(); return false; } void funkNeustart() { WiFi.disconnect(true); delay(200); WiFi.mode(WIFI_OFF); delay(200); WiFi.mode(WIFI_STA); delay(200); WiFi.config(ip, gateway, subnet, dns); WiFi.begin(ssid, password); } bool wifiBootConnect() { WiFi.persistent(false); WiFi.mode(WIFI_STA); WiFi.setAutoReconnect(true); WiFi.setSleep(false); WiFi.setTxPower(WIFI_POWER_19_5dBm); for (uint8_t attempt = 1; attempt <= WIFI_BOOT_TRIES; ++attempt) { Serial.printf("[BOOT] WLAN-Versuch %u/%u\n", attempt, WIFI_BOOT_TRIES); funkNeustart(); if (ensureWifi(20000)) { Serial.printf("[WIFI] OK: %s RSSI:%d dBm\n", WiFi.localIP().toString().c_str(), WiFi.RSSI()); return true; } int backoff = 1000 + random(0, 2000); Serial.printf("[BOOT] fehlgeschlagen -> Backoff %d ms\n", backoff); delay(backoff); } return false; } bool netSoftReset(uint32_t connectTimeoutMs = 15000) { Serial.println("[NET] Soft-Reset: Funk aus/an"); funkNeustart(); return ensureWifi(connectTimeoutMs); } bool tcpPreflight(uint16_t port = 80, uint16_t graceMs = 50) { WiFiClient poke; if (!poke.connect(SERVER_IP, port)) return false; delay(graceMs); poke.stop(); return true; } // Nur im VERBINDUNGSTEST: Ports des Servers und des Gateways prüfen void portTest() { Serial.printf("[TEST] Eigene IP %s, Gateway %s, Subnetz %s, RSSI %d dBm\n", WiFi.localIP().toString().c_str(), WiFi.gatewayIP().toString().c_str(), WiFi.subnetMask().toString().c_str(), WiFi.RSSI()); const uint16_t ports[] = {80, 443, 8080}; for (uint16_t p : ports) { WiFiClient c; bool ok = c.connect(SERVER_IP, p, 3000); Serial.printf("[TEST] Server %s Port %u : %s\n", SERVER_IP.toString().c_str(), p, ok ? "OFFEN" : "zu / keine Antwort"); c.stop(); } WiFiClient g; bool ok = g.connect(gateway, 80, 3000); Serial.printf("[TEST] Gateway %s Port 80 : %s\n", gateway.toString().c_str(), ok ? "OFFEN" : "zu / keine Antwort"); g.stop(); } // Nur im VERBINDUNGSTEST: misst, nach wie vielen Sekunden der Server nach dem Start antwortet void warteBisServerAntwortet() { uint32_t t0 = millis(); while (millis() - t0 < 600000) { // max. 10 min WiFiClient g, s; bool gw = g.connect(gateway, 80, 2000); g.stop(); bool sv = s.connect(SERVER_IP, 80, 2000); s.stop(); Serial.printf("[DIAG] %lu s Gateway:%s Server:%s\n", (millis() - t0) / 1000, gw ? "ok" : "-", sv ? "ok" : "-"); if (sv) break; delay(3000); } } /*** SENDEN der Daten **********************************************************************/ void sendData(const Messwerte &m) { if (WiFi.status() != WL_CONNECTED) { Serial.println("[HTTP] offline - Upload uebersprungen"); g_lastSendFailed = true; return; } String path = String(EMON_PATH); String query = "?node=" + String(NODE) + "&apikey=" + String(apikey); String body; if (VERBINDUNGSTEST) { body = "json={\"Test\":" + String(millis() / 1000) + "}"; } else { body = "json={\"B_kWh\":" + String(m.bezug, 3) + ",\"E_kWh\":" + String(m.einsp, 3) + ",\"P_kWh\":" + String(m.leistung, 0) + "}"; } Serial.printf("[HTTP] POST http://%s%s node=%s body=%s\n", SERVER_IP.toString().c_str(), path.c_str(), NODE, body.c_str()); if (g_lastSendFailed && !VERBINDUNGSTEST) { bool ok = tcpPreflight(httpPort, 80); Serial.printf("[NET] Preflight %s\n", ok ? "OK" : "FAIL"); } String hostStr = SERVER_IP.toString(); auto doPostOnce = [&](int attempt) -> int { if (WiFi.status() != WL_CONNECTED) { ensureWifi(8000); if (WiFi.status() != WL_CONNECTED) { Serial.printf("[HTTP] WLAN down (Try %d) - skip\n", attempt); return -10000; } } WiFiClient client; client.setNoDelay(true); HTTPClient http; http.setConnectTimeout(5000); http.setTimeout(15000); // ms http.setReuse(false); if (!http.begin(client, hostStr, httpPort, path + query, false)) { Serial.println("[HTTP] begin() failed"); return -2; } http.addHeader("Content-Type", "application/x-www-form-urlencoded"); http.addHeader("Connection", "close"); int code = http.POST(body); String err = http.errorToString(code); String payload = (code > 0) ? http.getString() : String(); http.end(); g_httpCode = code; Serial.printf("[HTTP] Try %d -> code=%d (%s)\n", attempt, code, err.c_str()); if (payload.length()) { Serial.print("[HTTP] resp: "); Serial.println(payload); } if (code == 200) { Serial.println(VERBINDUNGSTEST ? "Sent: Testwert" : "Sent: Zaehlerdaten"); g_uploadedOnce = true; g_lastSendFailed = false; } return code; }; int code = -9999; for (int attempt = 1; attempt <= 3; ++attempt) { // 3 normale Versuche code = doPostOnce(attempt); if (code == 200) return; if (VERBINDUNGSTEST) return; // im Test nur 1 Versuch delay(250 * attempt); } // Server ist nach dem Start erst nach ca. 5 min erreichbar: in den ersten 10 min keine Funk-Resets if (millis() < NO_RESET_AFTER_BOOT_MS) { g_lastSendFailed = true; return; } if (netSoftReset(15000)) { // Soft-Reset + 4. Versuch code = doPostOnce(4); if (code == 200) return; } Serial.println("[NET] Soft-Reset #2 ..."); // zweiter Soft-Reset + 5. Versuch if (netSoftReset(15000)) { code = doPostOnce(5); if (code == 200) return; } g_lastSendFailed = true; Serial.printf("[HTTP] FAILED after retries | Node=%s\n", NODE); } /*** Sende-Task (läuft getrennt vom Lesen) *************************************************/ void emonTask(void *) { uint32_t zyklus = 0; for (;;) { vTaskDelay(Schreibpause / portTICK_PERIOD_MS); ensureWifi(10000); if ((++zyklus % 18) == 0) Serial.printf("[WIFI] RSSI: %d dBm\n", WiFi.RSSI()); Messwerte m = holeMesswerte(); if (!VERBINDUNGSTEST && !m.gueltig) continue; // im Normalbetrieb erst senden, wenn Daten da sind sendData(m); } } /*** SETUP *********************************************************************************/ void setup() { Serial.begin(115200); delay(200); Serial.println("\n=== Boot: Stromzaehler-Lesekopf startet ==="); Serial.printf("Node = %s, IP = %s, Server = %s%s\n", NODE, ip.toString().c_str(), SERVER_IP.toString().c_str(), EMON_PATH); if (VERBINDUNGSTEST) Serial.println("*** VERBINDUNGSTEST aktiv: es wird nur ein Testwert gesendet ***"); u8g2.begin(); anzeigen(); randomSeed((uint32_t)ESP.getEfuseMac()); if (!wifiBootConnect()) { Serial.println("[BOOT] WLAN nicht erreichbar -> Neustart"); delay(800); ESP.restart(); } if (VERBINDUNGSTEST) { portTest(); warteBisServerAntwortet(); } Serial1.setRxBufferSize(1024); Serial1.begin(9600, SERIAL_8N1, PIN_RX, PIN_TX); g_bootMs = millis(); xTaskCreate(emonTask, "emon", 8192, NULL, 1, NULL); } /*** LOOP: nur Lesen + Watchdogs ***********************************************************/ void loop() { while (Serial1.available()) byteEmpfangen(Serial1.read()); uint32_t now = millis(); if (!VERBINDUNGSTEST && g_mw.gueltig && !g_uploadedOnce && (now - g_bootMs > BOOT_UPLOAD_DEADLINE_MS)) { Serial.println("[WD] Kein erfolgreicher Upload binnen 15 min -> Reboot"); delay(500); ESP.restart(); } if (g_wifiDownSince && (now - g_wifiDownSince > WIFI_DOWN_REBOOT_MS)) { Serial.println("[WD] WLAN > 120 s weg -> Reboot"); delay(500); ESP.restart(); } }
emonCMS
- Node:
241-ESP, Inputs:B_kWh(Bezug),E_kWh(Einspeisung),P_kWh(enthält die Leistung in Watt, nur der Name ist irreführend). - Feeds mit „Log to feed“, Engine PHPFina, Intervall 20 s (muss zum Sendeintervall passen, später nicht änderbar).
- Für Tagesverbrauch auf dem Feed „kWh/d“ oder „Delta“ verwenden.
- Wichtig: Nach dem Start des ESP32 ist der Server ca. 5 Minuten nicht erreichbar (Ursache im Netz noch unklar, nach dieser Zeit klappt alles). Der Sketch berücksichtigt das: in den ersten 10 min keine Funk-Resets, Boot-Watchdog erst nach 15 min.
Fehlersuche
| Symptom | Ursache / Lösung |
|---|---|
Kompilierfehler extended character is not valid in an identifier | Unsichtbare geschützte Leerzeichen durch Kopieren-Schaltfläche. Code mit der Maus markieren statt „Kopieren“ klicken. |
| Keine Bytes im Serial Monitor | Kopf verrutscht, Rx/Tx vertauscht oder falsche GPIOs. Mit Pin-Test prüfen. |
1B 1B 1B 1B 01 01 01 01 kommt, aber nur kWh, keine Leistung | PIN-Freischaltung fehlt (inF on). |
| Anzeige bleibt stehen, Telegramme laufen weiter | Telegrammerkennung per Pause funktioniert nicht, Marker-Erkennung im Sketch nutzen. |
HTTP code=-1 direkt nach dem Start | Server ca. 5 min nicht erreichbar, abwarten. |
HTTP code=200, Antwort ok, aber keine Inputs | Falscher Node oder Write-Key. |
Echo-Test des Lesekopfs (ohne Zähler): ESP32 sendet 100 Bytes 0x55, der Empfänger sieht die eigene IR-Diode und meldet ca. 100 empfangene Bytes. Das zeigt nur, dass Diode und Empfänger arbeiten, nicht die Strecke zum Zähler.
Wärmezähler Sharky 775 (in Arbeit)
- Optische Schnittstelle: ZVEI, M-Bus-Protokoll, 2400 Baud, 8E1.
- Ablauf: 2,2 s lang 0x55 senden (8N1, 480 bis 600 Bytes), dann umschalten auf 8E1,
SND_NKE(AntwortE5), dannREQ_UD2(langes Telegramm). - Der Lesekopf muss hier senden, Tx (GPIO33) ist also angeschlossen.
- Stand: Der Zähler in der Schule hat bisher nicht geantwortet. Echo-Test lief gut. Mögliche Ursachen: Position auf der Linse, Wecksignal oder zu schwache Sendeleistung des Adapters. Alternative: M-Bus-Pegelwandler.



