Anlegen vom Raum-Scanner

This commit is contained in:
chk
2026-06-16 10:06:05 +02:00
commit 792022d1fb
1836 changed files with 773869 additions and 0 deletions

806
firmware/firmware.ino Normal file
View File

@@ -0,0 +1,806 @@
/*
* =============================================================================
* 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 <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Wire.h>
#include <Preferences.h>
#include <WebSocketsServer.h>
#include <SparkFun_VL53L5CX_Library.h>
#include <U8g2lib.h>
// ----------------------------------------------------------------------------
// 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(
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VL53L5CX Live Punkte</title>
<style>
body { font-family: monospace, sans-serif; background:#111; color:#eee; margin:0; padding:16px; }
h1 { font-size:1.2rem; margin-bottom:4px; }
a { color:#6cf; }
#meta { font-size:0.85rem; color:#9c9; margin-bottom:16px; }
#apbanner { background:#642; color:#fc6; padding:8px; border-radius:6px; margin-bottom:12px; }
#grid { display:grid; gap:2px; margin-bottom:16px; }
.cell {
display:flex; align-items:center; justify-content:center;
width:38px; height:38px;
font-size:0.6rem; border-radius:3px; color:#000;
background:#333;
}
pre#raw {
background:#000; color:#0f0; padding:10px; border-radius:6px;
max-height:300px; overflow:auto; font-size:0.75rem;
}
.ok { color:#9f9; }
.warn { color:#fc6; }
</style>
</head>
<body>
<h1>VL53L5CX &mdash; ESP32-C3 Live Punkte</h1>
<div id="apbanner" style="display:none;">
&#9888; Kein bekanntes WLAN gefunden &ndash; ESP32 läuft im eigenen Access-Point-Modus.
<a href="/config">Jetzt WLAN konfigurieren</a>
</div>
<p><a href="/config">&#9881; Konfiguration (WLAN / Sensor)</a> | <a href="/i2c">&#128270; I2C Uebersicht</a></p>
<div id="meta">Lade Status ...</div>
<div id="grid"></div>
<h2 style="font-size:1rem;">Rohdaten (Array, distanceMm)</h2>
<pre id="raw">-</pre>
<script>
const POLL_MS = __POLL_INTERVAL_MS__;
const WS_PORT = __WS_PORT__;
const grid = document.getElementById('grid');
const metaEl = document.getElementById('meta');
let cells = [];
let currentCellCount = 0;
let usingWs = false;
let ws;
function ensureGrid(rows, cols) {
const total = rows * cols;
if (total === currentCellCount) return;
grid.innerHTML = '';
grid.style.gridTemplateColumns = `repeat(${cols}, 38px)`;
grid.style.gridTemplateRows = `repeat(${rows}, 38px)`;
cells = [];
for (let i = 0; i < total; i++) {
const c = document.createElement('div');
c.className = 'cell';
grid.appendChild(c);
cells.push(c);
}
currentCellCount = total;
}
function colorForDistance(mm) {
if (!mm || mm <= 0) return '#333';
const clamped = Math.max(200, Math.min(4000, mm));
const t = (clamped - 200) / (4000 - 200); // 0 = nah, 1 = weit
const r = Math.round(255 * (1 - t));
const g = Math.round(255 * t);
return `rgb(${r},${g},80)`;
}
function renderData(data) {
document.getElementById('apbanner').style.display =
data.wifiMode && data.wifiMode.startsWith('AP') ? 'block' : 'none';
ensureGrid(data.rows, data.cols);
metaEl.innerHTML =
`Quelle: ${usingWs ? 'WebSocket (Push)' : 'HTTP-Polling'} | ` +
`WLAN: ${data.wifiMode} | IP: ${data.ip} | Aufl.: ${data.rows}x${data.cols} @ ${data.hz} Hz | ` +
`WS-Clients: ${data.wsClients} | ` +
(data.sensorFound
? `<span class="${data.ranging ? 'ok' : 'warn'}">${data.ranging ? 'Sensor liest aktiv' : 'Sensor pausiert (kein Client)'}</span>`
: '<span class="warn">Sensor NICHT gefunden!</span>');
for (let i = 0; i < data.distanceMm.length; i++) {
const mm = data.distanceMm[i];
cells[i].style.background = colorForDistance(mm);
cells[i].textContent = mm;
}
document.getElementById('raw').textContent = JSON.stringify(data.distanceMm);
}
function connectWs() {
const url = `ws://${location.hostname}:${WS_PORT}/`;
ws = new WebSocket(url);
ws.onopen = () => { usingWs = true; console.log('WS verbunden:', url); };
ws.onmessage = (evt) => { try { renderData(JSON.parse(evt.data)); } catch (e) {} };
ws.onclose = () => { usingWs = false; setTimeout(connectWs, 2000); };
ws.onerror = () => { ws.close(); };
}
async function pollFallback() {
if (!usingWs) {
try {
const res = await fetch('/api/points');
renderData(await res.json());
} catch (e) {
metaEl.innerHTML = '<span class="warn">Verbindung zum ESP32 verloren...</span>';
}
}
setTimeout(pollFallback, POLL_MS);
}
connectWs();
pollFallback();
</script>
</body>
</html>
)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(
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VL53L5CX Konfiguration</title>
<style>
body { font-family: monospace, sans-serif; background:#111; color:#eee; margin:0; padding:16px; max-width:480px; }
a { color:#6cf; }
h1 { font-size:1.2rem; }
label { display:block; margin-bottom:14px; font-size:0.9rem; }
input, select { width:100%; padding:6px; margin-top:4px; background:#222; color:#eee; border:1px solid #444; border-radius:4px; box-sizing:border-box; }
button { padding:10px 16px; background:#2a6; color:#fff; border:none; border-radius:6px; font-size:1rem; cursor:pointer; }
button:hover { background:#3b7; }
#status { font-size:0.85rem; color:#9c9; margin-top:16px; }
</style>
</head>
<body>
<h1>&#9881; VL53L5CX Konfiguration</h1>
<p><a href="/">&larr; zurueck zur Live-Ansicht</a> | <a href="/i2c">&#128270; I2C Uebersicht</a></p>
<form method="POST" action="/config">
<label>WLAN SSID
<input type="text" name="ssid" value="__SSID__" maxlength="32">
</label>
<label>WLAN Passwort (leer lassen = unveraendert)
<input type="password" name="pass" value="" maxlength="64" placeholder="unveraendert">
</label>
<label>Sensor Aufloesung
<select name="res" id="res" onchange="adjustHz()">
<option value="64" __SEL64__>8&times;8 (64 Zonen, max. 15 Hz)</option>
<option value="16" __SEL16__>4&times;4 (16 Zonen, max. 60 Hz)</option>
</select>
</label>
<label>Sensor Frequenz (Hz)
<input type="number" name="hz" id="hz" value="__HZ__" min="1" max="__MAXHZ__">
</label>
<button type="submit">Speichern &amp; Neustart</button>
</form>
<div id="status">Aktuell: __WIFIMODE__ | IP __IP__</div>
<script>
function adjustHz(){
const res = document.getElementById('res').value;
const hz = document.getElementById('hz');
hz.max = (res === '64') ? 15 : 60;
if (parseInt(hz.value, 10) > parseInt(hz.max, 10)) hz.value = hz.max;
}
</script>
</body>
</html>
)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",
"<html><body style='font-family:monospace;background:#111;color:#eee;padding:16px;'>"
"<h1>Gespeichert!</h1><p>Das Geraet startet jetzt neu und verbindet sich neu...</p>"
"</body></html>");
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(
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>I2C Uebersicht</title>
<style>
body { font-family: monospace, sans-serif; background:#111; color:#eee; margin:0; padding:16px; max-width:480px; }
a { color:#6cf; }
h1 { font-size:1.2rem; }
table { width:100%; border-collapse:collapse; margin:12px 0; }
th, td { text-align:left; padding:6px 8px; border-bottom:1px solid #333; font-size:0.9rem; }
.ok { color:#9f9; }
.warn { color:#fc6; }
button { padding:8px 14px; background:#2a6; color:#fff; border:none; border-radius:6px; font-size:0.9rem; cursor:pointer; margin-right:8px; }
button:hover { background:#3b7; }
#status { font-size:0.85rem; color:#9c9; margin-top:16px; }
</style>
</head>
<body>
<h1>&#128270; I2C Uebersicht</h1>
<p><a href="/">&larr; zurueck zur Live-Ansicht</a> | <a href="/config">Konfiguration</a></p>
<h2 style="font-size:1rem;">Gefundene Geraete (SDA=GPIO__SDA__, SCL=GPIO__SCL__)</h2>
<table>
<tr><th>Adresse</th><th>Bekannt als</th></tr>
__ROWS__
</table>
<h2 style="font-size:1rem;">VL53L5CX Status</h2>
<p>Initialisiert: <span class="__SENSORCLASS__">__SENSORTXT__</span></p>
<form method="POST" action="/i2c/retry-sensor" style="display:inline;">
<button type="submit">Sensor erneut initialisieren</button>
</form>
<a href="/i2c"><button type="button">Neu scannen</button></a>
<div id="status">Tipp: Nur die OLED-Adresse 0x3C gefunden? -&gt; Verkabelung/Versorgung des
VL53L5CX pruefen (3V3, GND, SDA, SCL, LPN auf 3V3). Adresse 0x29 da, aber Sensor trotzdem
nicht initialisiert? -&gt; eventuell Bibliotheks-/Timing-Problem, nicht Verkabelung.</div>
</body>
</html>
)rawliteral";
void handleI2CGet() {
markClientActive();
uint8_t found[127];
int count = scanI2C(found, 127);
String rows = "";
if (count == 0) {
rows = "<tr><td colspan=\"2\" class=\"warn\">Keine Geraete gefunden!</td></tr>";
} else {
for (int i = 0; i < count; i++) {
char hex[8];
snprintf(hex, sizeof(hex), "0x%02X", found[i]);
rows += "<tr><td>" + String(hex) + "</td><td>" + knownI2CDeviceName(found[i]) + "</td></tr>";
}
}
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);
}
}
}
}
}