====== WWW: ESP32 - IR-Schnittstelle auslesen ======
[[www-messdatenerfassung-sas:startseite|Messdatenerfassung]]
[[biebl:3d-druck:strommessstelle|Quelle intern]]
===== ESP32 =====
* [[info:3d-druck:esp32-rj45#infoesp32-messbox_rj45|ESP-32 mit RJ45]]
{{:info:3d-druck:esp32-2x-rj45-Boden-2026-04-10.png?200|}}
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 | |
{{:info:arduino:rj45-belegung.jpeg?600|}}
===== Lesekopf =====
* [[https://samples.ing-dom.de/produkt/ing-dom-smartmeter-ir-adapter/]]
* [[https://github.com/Ing-Dom/Smartmeter-IR-Adapter/blob/main/README.md]]
{{:info:arduino:smartmeter-ir-adapter_v00.11a_pcba_shop.jpg?200|}}
* {{ :info:arduino:smartmeter-ir-adapter.pdf |Quelle: https://samples.ing-dom.de/produkt/ing-dom-smartmeter-ir-adapter/}}
* 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 =====
BILD vom ZÄHLER: {{:www-fuer-alle:messstelle-ir-fenster.png?400|}}
====== 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'': ''true'' sendet nur einen Testwert (1 Versuch), ''false'' die 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
#include
#include
#include
/**************************************** 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'' (Antwort ''E5''), dann ''REQ_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.