// ===================================================================== // WebServerManager.cpp // ===================================================================== #include "WebServerManager.h" #include WebServerManager::WebServerManager(ZoneManager &zoneManager) : _zoneManager(zoneManager), _server(80), _ws("/ws") { } void WebServerManager::onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { if (type == WS_EVT_CONNECT) { Serial.printf("[WS] client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str()); } else if (type == WS_EVT_DISCONNECT) { Serial.printf("[WS] client #%u disconnected\n", client->id()); } } void WebServerManager::setupRoutes() { _ws.onEvent(onWsEvent); _server.addHandler(&_ws); // GET /zones -> current zone definitions + occupancy _server.on("/zones", HTTP_GET, [this](AsyncWebServerRequest *request) { request->send(200, "application/json", _zoneManager.zonesToJson()); }); // POST /updateZones -> replace zone definitions (partial updates // allowed; see ZoneManager::zonesFromJson) _server.on( "/updateZones", HTTP_POST, [](AsyncWebServerRequest *request) {}, nullptr, [this](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { if (_zoneManager.zonesFromJson(data, len)) { request->send(200, "application/json", "{\"status\":\"success\",\"message\":\"Zones updated\"}"); } else { request->send(400, "application/json", "{\"status\":\"error\",\"message\":\"Invalid JSON\"}"); } }); // Basic health check, handy when bringing up a new node in the // future multi-radar setup. _server.on("/status", HTTP_GET, [](AsyncWebServerRequest *request) { StaticJsonDocument<128> doc; doc["status"] = "ok"; doc["uptime_ms"] = millis(); String out; serializeJson(doc, out); request->send(200, "application/json", out); }); } void WebServerManager::begin(const char *ssid, const char *password) { Serial.print("Connecting to "); Serial.println(ssid); WiFi.begin(ssid, password); // Matches the original project's blocking connect-on-boot // behavior. This only runs once at startup, not in the main // loop, so it doesn't violate the "no long delay() in the // acquisition path" requirement — radar acquisition hasn't // started yet at this point in setup(). while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.print("WiFi connected, IP: "); Serial.println(WiFi.localIP()); setupRoutes(); _server.begin(); Serial.println("HTTP server started."); } void WebServerManager::loop() { if (WiFi.status() != WL_CONNECTED) { // Non-blocking reconnect kick. We don't retry-loop with // delay() here (unlike begin()) so radar acquisition in the // main loop is never stalled by a flaky network. static uint32_t lastAttempt = 0; uint32_t now = millis(); if (now - lastAttempt > 5000) { Serial.println("WiFi disconnected, retrying..."); WiFi.reconnect(); lastAttempt = now; } } _ws.cleanupClients(); } void WebServerManager::broadcastFrame(const RadarFrame &frame) { if (_ws.count() == 0) return; // skip serialization work if nobody's listening StaticJsonDocument<512> doc; doc["sensor"] = "LD2453"; doc["node_id"] = frame.node_id; doc["sequence"] = frame.sequence_number; doc["timestamp"] = frame.timestamp_ms; JsonArray targets = doc.createNestedArray("targets"); for (uint8_t i = 0; i < frame.targetCount && i < 3; i++) { const RadarTarget &t = frame.targets[i]; JsonObject jt = targets.createNestedObject(); jt["id"] = i + 1; // radar slot index, NOT a persistent person ID jt["valid"] = t.valid; jt["x"] = t.x_mm; jt["y"] = t.y_mm; jt["speed"] = t.speed; } String out; serializeJson(doc, out); _ws.textAll(out); }