Anlegen vom Raum-Scanner
This commit is contained in:
806
firmware/firmware.ino
Normal file
806
firmware/firmware.ino
Normal 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 — ESP32-C3 Live Punkte</h1>
|
||||
<div id="apbanner" style="display:none;">
|
||||
⚠ Kein bekanntes WLAN gefunden – ESP32 läuft im eigenen Access-Point-Modus.
|
||||
<a href="/config">Jetzt WLAN konfigurieren</a>
|
||||
</div>
|
||||
<p><a href="/config">⚙ Konfiguration (WLAN / Sensor)</a> | <a href="/i2c">🔎 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>⚙ VL53L5CX Konfiguration</h1>
|
||||
<p><a href="/">← zurueck zur Live-Ansicht</a> | <a href="/i2c">🔎 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×8 (64 Zonen, max. 15 Hz)</option>
|
||||
<option value="16" __SEL16__>4×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 & 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>🔎 I2C Uebersicht</h1>
|
||||
<p><a href="/">← 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? -> 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.</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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user