/* * ============================================================================= * firmware.ino - ESP32-C3 + VL53L5CX (8x8 / 4x4 Time-of-Flight Sensor) * ============================================================================= * * Was macht diese Firmware? * 1) Versucht sich mit dem gespeicherten WLAN zu verbinden (STA-Modus). * Wird das WLAN nicht gefunden / schlägt die Verbindung fehl, baut der * ESP32 selbst einen Access Point auf (Fallback-AP). Beim Booten wird * ausführlich über die serielle Schnittstelle geloggt (WLAN-Scan, * gewählter Modus, IP, Sensor-Status, ...). * 2) Stellt eine Landing-Page bereit, die die zuletzt gelesenen VL53L5CX * Punkte als Array anzeigt (kleines Grid + rohes JSON-Array). Die Seite * bekommt neue Frames per WebSocket gepusht (Fallback: HTTP-Polling). * 3) Stellt die Punkte zusätzlich über eine JSON-API bereit: GET /api/points * 4) Stellt einen WebSocket-Server bereit (Port 81). Jeder verbundene * Client bekommt jeden neuen Frame sofort nach dem Auslesen gepusht. * 5) Config-Page (GET/POST /config): WLAN-SSID/Passwort, Sensor-Frequenz * und Sensor-Auflösung (8x8 / 4x4) sind dort ohne Neu-Flashen änderbar. * Die Werte werden persistent im Flash (Preferences/NVS) gespeichert. * 6) Der Sensor wird NUR dann aktiv ausgelesen, wenn mindestens ein * HTTP- oder WebSocket-Client aktiv ist ("jemand ist auf der WebPage"). * Ist niemand da, wird das Ranging gestoppt (spart Strom / I2C-Traffic). * 7) Debug-Seite GET /i2c: scannt den I2C-Bus (1..126) und zeigt alle * antwortenden Geraete an (z.B. OLED 0x3C, VL53L5CX 0x29). Knopf, um die * Sensor-Initialisierung manuell neu auszuloesen (ohne Neustart). * * Benötigte Bibliotheken (Arduino Library Manager): * - "SparkFun VL53L5CX" von SparkFun Electronics * - "WebSockets" von Markus Sattler (Links2004) * - ESP32 Board-Paket von Espressif (Board: "ESP32C3 Dev Module") * (WiFi.h, WebServer.h, Wire.h, ESPmDNS.h, Preferences.h sind im ESP32-Core enthalten) * * Erst-Inbetriebnahme ("Provisioning"): * Wenn das in DEFAULT_WIFI_SSID/-PASSWORD hinterlegte Netz nicht erreichbar * ist, startet der ESP32 einen eigenen Access Point (siehe info.md). Darin * einfach http://192.168.4.1/config öffnen und das echte WLAN eintragen - * kein erneutes Flashen nötig. * * Verkabelung: siehe info.md im selben Verzeichnis (inkl. ASCII-Schaubild). * ============================================================================= */ #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KONFIGURATION - Compile-Time Defaults (nur fuer den allerersten Boot, bevor // jemals etwas über die /config-Seite gespeichert wurde). Danach gewinnen // immer die in den Preferences/NVS gespeicherten Werte - siehe loadConfig(). // ---------------------------------------------------------------------------- static const char *DEFAULT_WIFI_SSID = "ZyXEL7A9E80"; static const char *DEFAULT_WIFI_PASSWORD = "geheim"; static const int DEFAULT_SENSOR_HZ = 10; // 8x8 erlaubt bis 15 Hz, 4x4 bis 60 Hz static const int DEFAULT_SENSOR_RESOLUTION = 64; // 64 = 8x8 Zonen, 16 = 4x4 Zonen // Fallback Access-Point, falls kein bekanntes WLAN gefunden wird static const char *AP_SSID = "VL53L5CX-ESP32"; static const char *AP_PASSWORD = "tofsensor"; // mind. 8 Zeichen (WPA2) oder "" fuer offenes Netz // Hostname fuer mDNS (nur im STA-Modus nutzbar): http://vl53l5cx.local static const char *MDNS_NAME = "vl53l5cx"; // Wie lange (ms) versuchen wir, das gespeicherte WLAN zu finden, bevor wir // auf den eigenen Access Point umschalten? static const unsigned long WIFI_CONNECT_TIMEOUT_MS = 10000; // I2C Pins - werden vom ESP32-C3 0.42" OLED-Board intern bereits fuer das // eingebaute OLED-Display benutzt (SSD1306 @ 0x3C). Laut AliExpress-Listing // und unabhaengigen Quellen liegen diese Pins bei GPIO8 (SDA) / GPIO9 (SCL) // (vorher fälschlich auf 5/6 gesetzt - siehe Git-Historie). Da dieses Board // nur EINEN I2C-Bus hat, haengt der externe VL53L5CX-Sensor (Adresse 0x29) // einfach an den GLEICHEN Pins - kein Konflikt, da unterschiedliche // I2C-Adressen. Siehe info.md. #define SDA_PIN 8 #define SCL_PIN 9 // Wie lange (ms) nach der letzten Anfrage gilt "jemand ist noch auf der // WebPage"? In dieser Zeit bleibt das Sensor-Ranging aktiv. static const unsigned long ACTIVE_TIMEOUT_MS = 5000; // Port des WebSocket-Servers, der neue Frames an verbundene Clients pusht static const uint16_t WS_PORT = 81; // ---------------------------------------------------------------------------- // Globale Variablen // ---------------------------------------------------------------------------- Preferences prefs; WebServer server(80); WebSocketsServer webSocket(WS_PORT); SparkFun_VL53L5CX myImager; VL53L5CX_ResultsData measurementData; // Eingebautes 0.42" OLED (SSD1306, 72x40 Pixel sichtbar), haengt am selben // I2C-Bus wie der VL53L5CX (siehe SDA_PIN/SCL_PIN oben). U8G2_SSD1306_72X40_ER_F_HW_I2C u8g2(U8G2_R0, /* reset= */ U8X8_PIN_NONE); unsigned long lastDisplayMillis = 0; // Laufzeit-Konfiguration (aus Preferences/NVS geladen, ueber /config aenderbar) String cfgSsid; String cfgPassword; int cfgHz; int cfgResolution; unsigned long sampleIntervalMs; bool sensorFound = false; // Sensor beim Start erfolgreich initialisiert? bool sensorRanging = false; // Laeuft das Ranging aktuell? bool apMode = false; // true = eigener Access Point, false = WLAN-Client unsigned long lastClientMillis = 0; // letzter HTTP-Zugriff auf "/" oder "/api/points" unsigned long lastSampleMillis = 0; float latestDistanceMm[64]; int latestStatus[64]; unsigned long latestTimestamp = 0; bool haveData = false; String wifiModeStr = "unbekannt"; String ipAddrStr = "-"; // ---------------------------------------------------------------------------- // Konfiguration laden / speichern (Flash, ueberlebt Neustarts) // ---------------------------------------------------------------------------- void loadConfig() { prefs.begin("cfg", true); // read-only cfgSsid = prefs.getString("ssid", DEFAULT_WIFI_SSID); cfgPassword = prefs.getString("pass", DEFAULT_WIFI_PASSWORD); cfgHz = prefs.getInt("hz", DEFAULT_SENSOR_HZ); cfgResolution = prefs.getInt("res", DEFAULT_SENSOR_RESOLUTION); prefs.end(); if (cfgResolution != 16 && cfgResolution != 64) cfgResolution = DEFAULT_SENSOR_RESOLUTION; int maxHz = (cfgResolution == 64) ? 15 : 60; if (cfgHz < 1) cfgHz = 1; if (cfgHz > maxHz) cfgHz = maxHz; sampleIntervalMs = 1000 / cfgHz; Serial.println("--- Gespeicherte Konfiguration (NVS) ---"); Serial.printf(" WLAN SSID : %s\n", cfgSsid.c_str()); Serial.printf(" WLAN Passwort : %s (%u Zeichen)\n", cfgPassword.length() ? "********" : "(leer)", (unsigned)cfgPassword.length()); Serial.printf(" Sensor Aufloes.: %d Zonen (%s)\n", cfgResolution, cfgResolution == 64 ? "8x8" : "4x4"); Serial.printf(" Sensor Frequenz: %d Hz\n", cfgHz); } void saveConfig(const String &ssid, const String &pass, int hz, int res) { prefs.begin("cfg", false); prefs.putString("ssid", ssid); prefs.putString("pass", pass); prefs.putInt("hz", hz); prefs.putInt("res", res); prefs.end(); } // ---------------------------------------------------------------------------- // WLAN: erst gespeichertes Netz versuchen, sonst eigenen AP aufbauen // Loggt ausfuehrlich: gefundene Netze, gewaehlter Modus, IP, MAC, ... // ---------------------------------------------------------------------------- void connectWiFi() { Serial.println("\n--- WLAN-Scan ---"); WiFi.mode(WIFI_STA); int n = WiFi.scanNetworks(); if (n <= 0) { Serial.println(" Keine WLAN-Netzwerke in Reichweite gefunden."); } else { bool foundConfigured = false; for (int i = 0; i < n; i++) { bool isTarget = WiFi.SSID(i) == cfgSsid; if (isTarget) foundConfigured = true; Serial.printf(" %s %-32s RSSI=%4d dBm Kanal=%2d %s\n", isTarget ? "[ZIEL]" : " ", WiFi.SSID(i).c_str(), WiFi.RSSI(i), WiFi.channel(i), WiFi.encryptionType(i) == WIFI_AUTH_OPEN ? "(offen)" : "(verschluesselt)"); } Serial.printf(" Konfiguriertes Netz \"%s\" %s gefunden.\n", cfgSsid.c_str(), foundConfigured ? "wurde" : "wurde NICHT"); } WiFi.scanDelete(); Serial.printf("\nVerbinde mit WLAN \"%s\" ...\n", cfgSsid.c_str()); WiFi.begin(cfgSsid.c_str(), cfgPassword.c_str()); unsigned long start = millis(); while (WiFi.status() != WL_CONNECTED && millis() - start < WIFI_CONNECT_TIMEOUT_MS) { delay(250); Serial.print("."); } Serial.println(); if (WiFi.status() == WL_CONNECTED) { apMode = false; wifiModeStr = "STA (" + cfgSsid + ")"; ipAddrStr = WiFi.localIP().toString(); Serial.println("--- WLAN verbunden ---"); Serial.printf(" Modus : Station (STA)\n"); Serial.printf(" SSID : %s\n", cfgSsid.c_str()); Serial.printf(" IP-Adresse : %s\n", ipAddrStr.c_str()); Serial.printf(" MAC-Adresse : %s\n", WiFi.macAddress().c_str()); Serial.printf(" RSSI : %d dBm\n", WiFi.RSSI()); Serial.printf(" Kanal : %d\n", WiFi.channel()); if (MDNS.begin(MDNS_NAME)) { Serial.printf(" mDNS : http://%s.local\n", MDNS_NAME); } else { Serial.println(" mDNS : Start fehlgeschlagen"); } } else { Serial.println("--- WLAN-Verbindung fehlgeschlagen ---"); Serial.printf(" Letzter Status-Code: %d\n", (int)WiFi.status()); Serial.println(" -> baue eigenen Access Point auf."); WiFi.mode(WIFI_AP); WiFi.softAP(AP_SSID, AP_PASSWORD); apMode = true; wifiModeStr = "AP (" + String(AP_SSID) + ")"; ipAddrStr = WiFi.softAPIP().toString(); Serial.println("--- Access Point gestartet ---"); Serial.printf(" Modus : Access Point (AP)\n"); Serial.printf(" SSID : %s\n", AP_SSID); Serial.printf(" Passwort : %s\n", AP_PASSWORD); Serial.printf(" IP-Adresse : %s\n", ipAddrStr.c_str()); Serial.printf(" MAC-Adresse : %s\n", WiFi.softAPmacAddress().c_str()); Serial.println(" -> Bitte mit diesem WLAN verbinden und http://192.168.4.1/config oeffnen,"); Serial.println(" um das echte WLAN einzutragen (kein Neu-Flashen noetig)."); } } // ---------------------------------------------------------------------------- // I2C-Bus + OLED initialisieren (muss vor WiFi-Verbindung & Sensor-Setup // laufen, damit das Display von Anfang an benutzbar ist) // ---------------------------------------------------------------------------- void setupI2CAndDisplay() { Wire.begin(SDA_PIN, SCL_PIN); Wire.setClock(400000); // Standard Fast-Mode I2C; auf 1000000 erhoehbar wenn Verkabelung kurz/stabil ist u8g2.begin(); u8g2.setFont(u8g2_font_5x7_tr); u8g2.clearBuffer(); u8g2.drawStr(0, 7, "Starte..."); u8g2.sendBuffer(); Serial.printf("I2C gestartet: SDA=GPIO%d, SCL=GPIO%d (OLED + VL53L5CX teilen sich den Bus)\n", SDA_PIN, SCL_PIN); } // ---------------------------------------------------------------------------- // Zeigt WLAN-Modus, IP-Adresse und Sensor-Status auf dem 0.42" OLED an // ---------------------------------------------------------------------------- void updateDisplay() { u8g2.clearBuffer(); u8g2.setFont(u8g2_font_5x7_tr); u8g2.drawStr(0, 7, apMode ? "WLAN: AP" : "WLAN: STA"); u8g2.drawStr(0, 16, ipAddrStr.c_str()); u8g2.drawStr(0, 25, sensorFound ? (sensorRanging ? "Sensor: aktiv" : "Sensor: idle") : "Sensor: ---"); u8g2.sendBuffer(); } // ---------------------------------------------------------------------------- // VL53L5CX Sensor initialisieren (Ranging wird hier NOCH NICHT gestartet, // das passiert erst dynamisch, wenn ein Client aktiv ist) // I2C-Bus wurde bereits in setupI2CAndDisplay() gestartet. // ---------------------------------------------------------------------------- bool setupSensor() { Serial.println("\n--- VL53L5CX Initialisierung ---"); Serial.printf(" I2C Pins : SDA=GPIO%d, SCL=GPIO%d\n", SDA_PIN, SCL_PIN); if (!myImager.begin()) { Serial.println(" FEHLER: Sensor nicht gefunden! Verkabelung pruefen (siehe info.md)."); return false; } myImager.setResolution(cfgResolution); myImager.setRangingFrequency(cfgHz); Serial.printf(" Aufloesung : %d Zonen (%s)\n", cfgResolution, cfgResolution == 64 ? "8x8" : "4x4"); Serial.printf(" Frequenz : %d Hz\n", cfgHz); Serial.println(" Sensor bereit (Ranging startet erst, wenn ein Client aktiv ist)."); return true; } // ---------------------------------------------------------------------------- // Hilfsfunktion: merkt sich, dass gerade ein HTTP-Client zugegriffen hat // ---------------------------------------------------------------------------- void markClientActive() { lastClientMillis = millis(); } // ---------------------------------------------------------------------------- // WebSocket Events // ---------------------------------------------------------------------------- void onWsEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) { switch (type) { case WStype_CONNECTED: Serial.printf("[WS] Client #%u verbunden von %s\n", num, webSocket.remoteIP(num).toString().c_str()); markClientActive(); break; case WStype_DISCONNECTED: Serial.printf("[WS] Client #%u getrennt\n", num); break; default: break; // eingehende Nachrichten werden aktuell nicht ausgewertet } } // ---------------------------------------------------------------------------- // JSON-Antwort fuer /api/points + WebSocket-Push zusammenbauen // (ohne externe JSON-Bibliothek) // ---------------------------------------------------------------------------- String buildPointsJson() { bool httpActive = (millis() - lastClientMillis) < ACTIVE_TIMEOUT_MS; size_t wsClients = webSocket.connectedClients(); bool active = httpActive || (wsClients > 0); int rows = (cfgResolution == 64) ? 8 : 4; int cols = rows; int count = rows * cols; String j = "{"; j += "\"active\":"; j += (active ? "true" : "false"); j += ","; j += "\"ranging\":"; j += (sensorRanging ? "true" : "false"); j += ","; j += "\"sensorFound\":"; j += (sensorFound ? "true" : "false"); j += ","; j += "\"hz\":"; j += cfgHz; j += ","; j += "\"resolution\":"; j += cfgResolution; j += ","; j += "\"rows\":"; j += rows; j += ","; j += "\"cols\":"; j += cols; j += ","; j += "\"wsClients\":"; j += (int)wsClients; j += ","; j += "\"timestampMs\":"; j += latestTimestamp; j += ","; j += "\"wifiMode\":\""; j += wifiModeStr; j += "\","; j += "\"ip\":\""; j += ipAddrStr; j += "\","; j += "\"distanceMm\":["; for (int i = 0; i < count; i++) { j += haveData ? String((int)latestDistanceMm[i]) : "0"; if (i < count - 1) j += ","; } j += "],"; j += "\"status\":["; for (int i = 0; i < count; i++) { j += haveData ? String(latestStatus[i]) : "0"; if (i < count - 1) j += ","; } j += "]"; j += "}"; return j; } // ---------------------------------------------------------------------------- // HTTP Handler: /api/points -> liefert die Punkte als JSON // ---------------------------------------------------------------------------- void handleApiPoints() { markClientActive(); server.send(200, "application/json", buildPointsJson()); } // ---------------------------------------------------------------------------- // HTTP Handler: / -> Landing Page (HTML, zeigt Punkte als Grid + als Array) // Bezieht neue Frames per WebSocket (Push), Fallback: HTTP-Polling. // ---------------------------------------------------------------------------- const char INDEX_HTML_TEMPLATE[] = R"rawliteral( VL53L5CX Live Punkte

VL53L5CX — ESP32-C3 Live Punkte

⚙ Konfiguration (WLAN / Sensor) | 🔎 I2C Uebersicht

Lade Status ...

Rohdaten (Array, distanceMm)

-
)rawliteral"; void handleRoot() { markClientActive(); String page = String(INDEX_HTML_TEMPLATE); page.replace("__POLL_INTERVAL_MS__", String(sampleIntervalMs)); page.replace("__WS_PORT__", String(WS_PORT)); server.send(200, "text/html", page); } // ---------------------------------------------------------------------------- // HTTP Handler: /config -> WLAN- und Sensor-Einstellungen aendern // ---------------------------------------------------------------------------- const char CONFIG_HTML_TEMPLATE[] = R"rawliteral( VL53L5CX Konfiguration

⚙ VL53L5CX Konfiguration

← zurueck zur Live-Ansicht | 🔎 I2C Uebersicht

Aktuell: __WIFIMODE__ | IP __IP__
)rawliteral"; void handleConfigGet() { markClientActive(); String page = String(CONFIG_HTML_TEMPLATE); page.replace("__SSID__", cfgSsid); page.replace("__SEL64__", cfgResolution == 64 ? "selected" : ""); page.replace("__SEL16__", cfgResolution == 16 ? "selected" : ""); page.replace("__HZ__", String(cfgHz)); page.replace("__MAXHZ__", String(cfgResolution == 64 ? 15 : 60)); page.replace("__WIFIMODE__", wifiModeStr); page.replace("__IP__", ipAddrStr); server.send(200, "text/html", page); } void handleConfigPost() { String newSsid = server.arg("ssid"); String newPass = server.arg("pass"); // leer = Passwort unveraendert int newRes = server.arg("res").toInt(); int newHz = server.arg("hz").toInt(); if (newRes != 16 && newRes != 64) newRes = cfgResolution; int maxHz = (newRes == 64) ? 15 : 60; if (newHz < 1) newHz = 1; if (newHz > maxHz) newHz = maxHz; if (newSsid.length() == 0) newSsid = cfgSsid; if (newPass.length() == 0) newPass = cfgPassword; saveConfig(newSsid, newPass, newHz, newRes); Serial.println("\n--- Neue Konfiguration gespeichert (/config) ---"); Serial.printf(" SSID: %s | Aufloesung: %d | Frequenz: %d Hz\n", newSsid.c_str(), newRes, newHz); Serial.println(" Geraet startet in 1 Sekunde neu..."); server.send(200, "text/html", "" "

Gespeichert!

Das Geraet startet jetzt neu und verbindet sich neu...

" ""); delay(1000); ESP.restart(); } void handleNotFound() { server.send(404, "text/plain", "404 - Not found"); } // ---------------------------------------------------------------------------- // I2C-Scan: testet alle Adressen 1..126 mit einem leeren Schreibversuch. // Laeuft komplett synchron innerhalb des HTTP-Handlers (Arduino loop() ist // single-threaded), stoert daher nichts am laufenden Sensor-Ranging. // ---------------------------------------------------------------------------- String knownI2CDeviceName(uint8_t addr) { switch (addr) { case 0x3C: return "OLED-Display (SSD1306)"; case 0x29: return "VL53L5CX (Standard-Adresse)"; default: return "unbekannt"; } } int scanI2C(uint8_t *foundAddresses, int maxCount) { int count = 0; for (uint8_t addr = 1; addr < 127 && count < maxCount; addr++) { Wire.beginTransmission(addr); uint8_t err = Wire.endTransmission(); if (err == 0) { foundAddresses[count++] = addr; } } return count; } // ---------------------------------------------------------------------------- // HTTP Handler: /i2c -> I2C-Bus-Uebersicht (Debug-Seite) // ---------------------------------------------------------------------------- const char I2C_HTML_TEMPLATE[] = R"rawliteral( I2C Uebersicht

🔎 I2C Uebersicht

← zurueck zur Live-Ansicht | Konfiguration

Gefundene Geraete (SDA=GPIO__SDA__, SCL=GPIO__SCL__)

__ROWS__
AdresseBekannt als

VL53L5CX Status

Initialisiert: __SENSORTXT__

Tipp: Nur die OLED-Adresse 0x3C gefunden? -> Verkabelung/Versorgung des VL53L5CX pruefen (3V3, GND, SDA, SCL, LPN auf 3V3). Adresse 0x29 da, aber Sensor trotzdem nicht initialisiert? -> eventuell Bibliotheks-/Timing-Problem, nicht Verkabelung.
)rawliteral"; void handleI2CGet() { markClientActive(); uint8_t found[127]; int count = scanI2C(found, 127); String rows = ""; if (count == 0) { rows = "Keine Geraete gefunden!"; } else { for (int i = 0; i < count; i++) { char hex[8]; snprintf(hex, sizeof(hex), "0x%02X", found[i]); rows += "" + String(hex) + "" + knownI2CDeviceName(found[i]) + ""; } } String page = String(I2C_HTML_TEMPLATE); page.replace("__ROWS__", rows); page.replace("__SDA__", String(SDA_PIN)); page.replace("__SCL__", String(SCL_PIN)); page.replace("__SENSORCLASS__", sensorFound ? "ok" : "warn"); page.replace("__SENSORTXT__", sensorFound ? "ja" : "nein"); server.send(200, "text/html", page); } void handleSensorRetryPost() { Serial.println("\n--- Manueller Retry: VL53L5CX Initialisierung (/i2c/retry-sensor) ---"); if (sensorRanging) { myImager.stopRanging(); sensorRanging = false; } sensorFound = setupSensor(); updateDisplay(); server.sendHeader("Location", "/i2c"); server.send(303); } // ---------------------------------------------------------------------------- // Webserver-Routen registrieren // ---------------------------------------------------------------------------- void setupWebServer() { server.on("/", HTTP_GET, handleRoot); server.on("/api/points", HTTP_GET, handleApiPoints); server.on("/config", HTTP_GET, handleConfigGet); server.on("/config", HTTP_POST, handleConfigPost); server.on("/i2c", HTTP_GET, handleI2CGet); server.on("/i2c/retry-sensor", HTTP_POST, handleSensorRetryPost); server.onNotFound(handleNotFound); server.begin(); Serial.println("Webserver gestartet auf Port 80 (/, /api/points, /config, /i2c)."); } // ---------------------------------------------------------------------------- // setup() / loop() // ---------------------------------------------------------------------------- void setup() { Serial.begin(115200); delay(300); Serial.println("\n\n=== VL53L5CX ESP32-C3 Firmware startet ==="); Serial.printf("Chip : %s Rev %u, %u Kern(e), %u MHz\n", ESP.getChipModel(), (unsigned)ESP.getChipRevision(), (unsigned)ESP.getChipCores(), (unsigned)ESP.getCpuFreqMHz()); Serial.printf("Flash : %u KB\n", (unsigned)(ESP.getFlashChipSize() / 1024)); Serial.printf("Freier Heap : %u Bytes\n", (unsigned)ESP.getFreeHeap()); setupI2CAndDisplay(); loadConfig(); connectWiFi(); updateDisplay(); sensorFound = setupSensor(); updateDisplay(); setupWebServer(); webSocket.begin(); webSocket.onEvent(onWsEvent); Serial.printf("WebSocket gestartet: ws://%s:%u/\n", ipAddrStr.c_str(), WS_PORT); Serial.println("=== Setup abgeschlossen ===\n"); } void loop() { server.handleClient(); webSocket.loop(); // OLED periodisch aktualisieren (z.B. damit "Sensor: aktiv/idle" live mitgeht) if (millis() - lastDisplayMillis >= 1000) { lastDisplayMillis = millis(); updateDisplay(); } if (!sensorFound) return; bool clientActive = (millis() - lastClientMillis < ACTIVE_TIMEOUT_MS) || (webSocket.connectedClients() > 0); // Ranging dynamisch starten/stoppen, je nachdem ob jemand auf der WebPage ist if (clientActive && !sensorRanging) { myImager.startRanging(); sensorRanging = true; lastSampleMillis = 0; // sofort beim naechsten loop()-Durchlauf neu lesen Serial.println("Client aktiv -> Sensor-Ranging gestartet."); } else if (!clientActive && sensorRanging) { myImager.stopRanging(); sensorRanging = false; Serial.println("Kein Client mehr aktiv -> Sensor-Ranging gestoppt."); } // Daten mit der konfigurierten Frequenz auslesen (nicht-blockierend) und // sofort per WebSocket an alle verbundenen Clients pushen if (sensorRanging && (millis() - lastSampleMillis >= sampleIntervalMs)) { lastSampleMillis = millis(); if (myImager.isDataReady()) { if (myImager.getRangingData(&measurementData)) { for (int i = 0; i < cfgResolution; i++) { latestDistanceMm[i] = measurementData.distance_mm[i]; latestStatus[i] = measurementData.target_status[i]; } latestTimestamp = millis(); haveData = true; if (webSocket.connectedClients() > 0) { String json = buildPointsJson(); // broadcastTXT() will nicht-const String&, kein Temporary webSocket.broadcastTXT(json); } } } } }