// web_server.cpp - Web Server Implementation with WebServer and WebSocketsServer #include #include #include #include #include #include #include #include "web_server.h" #include "task_config.h" #include "can_handler.h" #include "sd_logger.h" #include "rtc_manager.h" #include "signal_manager.h" #include "dbc_parser.h" #include "auto_trigger.h" #include "psram_buffer.h" #include "test_handler.h" #include "data/web_index.h" #include "data/web_settings.h" #include "data/web_files.h" #include "data/web_can.h" #include "data/web_graph.h" #include "data/web_test.h" WebServer server(WEB_SERVER_PORT); WebSocketsServer webSocket(81); bool wifiInitialized = false; bool apModeActive = false; bool staModeActive = false; WiFiConfig wifiConfig; static char wsBuffer[2048]; bool initWiFi() { Serial.println("Initializing WiFi..."); loadWiFiConfig(); if (!startAPMode()) { Serial.println("Failed to start AP mode!"); return false; } if (wifiConfig.useSTA && strlen(wifiConfig.staSSID) > 0) { startSTAMode(wifiConfig.staSSID, wifiConfig.staPassword); } wifiInitialized = true; return true; } bool startAPMode() { Serial.println("Starting WiFi AP mode..."); WiFi.mode(WIFI_AP_STA); bool result = WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD, WIFI_AP_CHANNEL, 0, WIFI_AP_MAX_CLIENTS); if (result) { apModeActive = true; Serial.printf("AP Started: %s\n", WIFI_AP_SSID); Serial.printf("AP IP: %s\n", WiFi.softAPIP().toString().c_str()); initMDNS(); return true; } return false; } bool startSTAMode(const char* ssid, const char* password) { Serial.printf("Connecting to WiFi: %s\n", ssid); WiFi.begin(ssid, password); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 20) { delay(500); Serial.print("."); attempts++; } if (WiFi.status() == WL_CONNECTED) { staModeActive = true; Serial.println("\nWiFi Connected!"); Serial.printf("STA IP: %s\n", WiFi.localIP().toString().c_str()); return true; } else { Serial.println("\nWiFi connection failed!"); return false; } } void stopWiFi() { WiFi.disconnect(true); WiFi.mode(WIFI_OFF); apModeActive = false; staModeActive = false; } bool initMDNS() { if (!MDNS.begin("esp32-can")) { Serial.println("mDNS failed to start!"); return false; } MDNS.addService("http", "tcp", WEB_SERVER_PORT); MDNS.addService("ws", "tcp", 81); Serial.println("mDNS started: esp32-can.local"); return true; } void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { switch(type) { case WStype_DISCONNECTED: Serial.printf("[%u] Disconnected!\n", num); break; case WStype_CONNECTED: Serial.printf("[%u] Connected from %s\n", num, webSocket.remoteIP(num).toString().c_str()); break; case WStype_TEXT: Serial.printf("[%u] Message: %s\n", num, payload); break; case WStype_BIN: case WStype_ERROR: case WStype_FRAGMENT_TEXT_START: case WStype_FRAGMENT_BIN_START: case WStype_FRAGMENT: case WStype_FRAGMENT_FIN: break; } } void broadcastToClients(const char* message) { webSocket.broadcastTXT(message); } void broadcastSignalData(const GraphSignal* signals, uint8_t count) { StaticJsonDocument<2048> doc; doc["type"] = "signal"; doc["timestamp"] = millis(); JsonArray sigArray = doc.createNestedArray("signals"); for (uint8_t i = 0; i < count; i++) { JsonObject sig = sigArray.createNestedObject(); sig["id"] = signals[i].signal_id; sig["value"] = signals[i].value; } serializeJson(doc, wsBuffer, sizeof(wsBuffer)); webSocket.broadcastTXT(wsBuffer); } void handleRoot() { server.send(200, "text/html", HTML_INDEX); } void handleSettings() { server.send(200, "text/html", HTML_SETTINGS); } void handleFiles() { server.send(200, "text/html", HTML_FILES); } void handleCAN() { server.send(200, "text/html", HTML_CAN); } void handleGraph() { server.send(200, "text/html", HTML_GRAPH); } void handleTest() { server.send(200, "text/html", HTML_TEST); } void handleAPIStatus() { StaticJsonDocument<1024> doc; doc["wifi"] = wifiInitialized; doc["ap"] = apModeActive; doc["sta"] = staModeActive; doc["ap_ip"] = WiFi.softAPIP().toString(); if (staModeActive) { doc["sta_ip"] = WiFi.localIP().toString(); } doc["can"]["initialized"] = canInitialized; uint32_t rx, tx, err; getCANStats(rx, tx, err); doc["can"]["rx_count"] = rx; doc["can"]["tx_count"] = tx; doc["can"]["error_count"] = err; doc["can"]["mode"] = getCANMode(); doc["can"]["buffer_used"] = canFrameBuffer.available(); doc["can"]["buffer_capacity"] = canFrameBuffer.capacity(); doc["sd"]["initialized"] = sdInitialized; doc["sd"]["total_mb"] = getSDCardSize() / (1024 * 1024); doc["sd"]["free_mb"] = getFreeSpace() / (1024 * 1024); doc["rtc"]["initialized"] = rtcInitialized; doc["log"]["filename"] = getCurrentLogFilename(); doc["memory"]["heap_free"] = ESP.getFreeHeap(); doc["memory"]["heap_total"] = ESP.getHeapSize(); if (psramFound()) { doc["memory"]["psram_free"] = ESP.getFreePsram(); doc["memory"]["psram_total"] = ESP.getPsramSize(); doc["memory"]["psram_used_mb"] = (ESP.getPsramSize() - ESP.getFreePsram()) / (1024 * 1024); } String output; serializeJson(doc, output); server.send(200, "application/json", output); } void handleAPIMemory() { StaticJsonDocument<512> doc; doc["heap"]["free"] = ESP.getFreeHeap(); doc["heap"]["total"] = ESP.getHeapSize(); doc["heap"]["used"] = ESP.getHeapSize() - ESP.getFreeHeap(); if (psramFound()) { doc["psram"]["found"] = true; doc["psram"]["free"] = ESP.getFreePsram(); doc["psram"]["total"] = ESP.getPsramSize(); doc["psram"]["used"] = ESP.getPsramSize() - ESP.getFreePsram(); doc["psram"]["free_mb"] = ESP.getFreePsram() / (1024 * 1024); doc["psram"]["total_mb"] = ESP.getPsramSize() / (1024 * 1024); } else { doc["psram"]["found"] = false; } doc["can_buffer"]["used"] = canFrameBuffer.available(); doc["can_buffer"]["capacity"] = canFrameBuffer.capacity(); doc["can_buffer"]["free"] = canFrameBuffer.freeSpace(); String output; serializeJson(doc, output); server.send(200, "application/json", output); } void handleAPIFileList() { StaticJsonDocument<4096> doc; JsonArray files = doc.to(); if (sdInitialized) { File root = SD_MMC.open(LOGS_DIR); if (root && root.isDirectory()) { File file = root.openNextFile(); while (file) { if (!file.isDirectory() && String(file.name()).endsWith(".pcap")) { JsonObject f = files.createNestedObject(); f["name"] = file.name(); f["size"] = file.size(); f["time"] = file.getLastWrite(); } file = root.openNextFile(); } } } String output; serializeJson(doc, output); server.send(200, "application/json", output); } void handleAPIFileDownload() { if (!server.hasArg("name")) { server.send(400, "application/json", "{\"error\":\"Missing name parameter\"}"); return; } String filename = server.arg("name"); String path = String(LOGS_DIR) + "/" + filename; if (!SD_MMC.exists(path)) { server.send(404, "application/json", "{\"error\":\"File not found\"}"); return; } File file = SD_MMC.open(path, "r"); if (!file) { server.send(500, "application/json", "{\"error\":\"Cannot open file\"}"); return; } server.streamFile(file, "application/octet-stream"); file.close(); } void handleAPIFileDelete() { if (!server.hasArg("name")) { server.send(400, "application/json", "{\"error\":\"Missing name parameter\"}"); return; } String filename = server.arg("name"); if (deleteLogFile(filename.c_str())) { server.send(200, "application/json", "{\"status\":\"deleted\"}"); } else { server.send(500, "application/json", "{\"error\":\"Delete failed\"}"); } } void handleAPICANSend() { if (!server.hasArg("plain")) { server.send(400, "application/json", "{\"error\":\"Missing body\"}"); return; } String body = server.arg("plain"); StaticJsonDocument<512> doc; DeserializationError error = deserializeJson(doc, body); if (error) { server.send(400, "application/json", "{\"error\":\"Invalid JSON\"}"); return; } String idStr = doc["id"] | "0x100"; uint32_t id = 0; if (idStr.startsWith("0x") || idStr.startsWith("0X")) { id = strtol(idStr.c_str(), NULL, 16); } else { id = idStr.toInt(); } String frameType = doc["type"] | "standard"; bool ext = (frameType == "extended"); bool fd = (frameType == "fd") || (doc["isFD"] | false); JsonArray dataArr = doc["data"]; uint8_t data[64] = {0}; uint8_t len = doc["length"] | 8; int i = 0; for (JsonVariant v : dataArr) { if (i < 64) { data[i++] = v.as(); } } if (ext) id |= 0x80000000; if (sendCANFrame(id, data, len, fd)) { server.send(200, "application/json", "{\"status\":\"sent\"}"); } else { server.send(500, "application/json", "{\"error\":\"Send failed\"}"); } } void handleAPIWiFiConfig() { if (!server.hasArg("plain")) { server.send(400, "application/json", "{\"error\":\"Missing body\"}"); return; } String body = server.arg("plain"); StaticJsonDocument<256> doc; DeserializationError error = deserializeJson(doc, body); if (error) { server.send(400, "application/json", "{\"error\":\"Invalid JSON\"}"); return; } if (doc.containsKey("ssid") && doc.containsKey("password")) { strlcpy(wifiConfig.staSSID, doc["ssid"], sizeof(wifiConfig.staSSID)); strlcpy(wifiConfig.staPassword, doc["password"], sizeof(wifiConfig.staPassword)); wifiConfig.useSTA = true; saveWiFiConfig(); server.send(200, "application/json", "{\"status\":\"saved\",\"reconnect\":true}"); } else { server.send(400, "application/json", "{\"error\":\"Missing ssid or password\"}"); } } void handleAPILoggingStart() { if (startLogFile()) { server.send(200, "application/json", "{\"status\":\"started\"}"); } else { server.send(500, "application/json", "{\"error\":\"Start failed\"}"); } } void handleAPILoggingStop() { closeLogFile(); server.send(200, "application/json", "{\"status\":\"stopped\"}"); } void handleAPIDBCUpload() { if (!server.hasArg("plain")) { server.send(400, "application/json", "{\"error\":\"Missing body\"}"); return; } String content = server.arg("plain"); if (parseDBC(content.c_str())) { server.send(200, "application/json", "{\"status\":\"loaded\"}"); } else { server.send(400, "application/json", "{\"error\":\"Parse failed\"}"); } } void handleAPITimeSync() { if (!server.hasArg("plain")) { server.send(400, "application/json", "{\"error\":\"Missing body\"}"); return; } String body = server.arg("plain"); StaticJsonDocument<128> doc; DeserializationError error = deserializeJson(doc, body); if (error) { server.send(400, "application/json", "{\"error\":\"Invalid JSON\"}"); return; } if (doc.containsKey("timestamp")) { uint32_t timestamp = doc["timestamp"]; setRTCTime(timestamp); server.send(200, "application/json", "{\"status\":\"synced\"}"); } else { server.send(400, "application/json", "{\"error\":\"Missing timestamp\"}"); } } void handleAPIRestart() { server.send(200, "application/json", "{\"status\":\"restarting\"}"); delay(100); ESP.restart(); } void handleAPICANConfig() { if (!server.hasArg("plain")) { server.send(400, "application/json", "{\"error\":\"Missing body\"}"); return; } String body = server.arg("plain"); StaticJsonDocument<256> doc; DeserializationError error = deserializeJson(doc, body); if (error) { server.send(400, "application/json", "{\"error\":\"Invalid JSON\"}"); return; } uint32_t arbBaud = doc["arbBaud"] | CAN_DEFAULT_ARBITRATION_BAUDRATE; uint32_t dataBaud = doc["dataBaud"] | CAN_DEFAULT_DATA_BAUDRATE; uint8_t mode = doc["mode"] | 0; bool enableFD = doc["enableFD"] | true; if (setCANBaudrateAndMode(arbBaud, dataBaud, mode, enableFD)) { server.send(200, "application/json", "{\"status\":\"configured\"}"); } else { server.send(500, "application/json", "{\"error\":\"Configuration failed\"}"); } } void handleAPITriggerConfig() { if (!server.hasArg("plain")) { server.send(200, "application/json", "{\"error\":\"Missing body\"}"); return; } String body = server.arg("plain"); StaticJsonDocument<512> doc; deserializeJson(doc, body); if (doc.containsKey("enabled")) { enableTrigger(doc["enabled"]); } if (doc.containsKey("logic")) { setLogicalOperator(doc["logic"] == "AND" ? LOGIC_AND : LOGIC_OR); } char buffer[256]; getTriggerStatusJSON(buffer, sizeof(buffer)); server.send(200, "application/json", buffer); } void handleAPISignalAdd() { if (!server.hasArg("plain")) { server.send(400, "application/json", "{\"error\":\"Missing body\"}"); return; } String body = server.arg("plain"); StaticJsonDocument<256> doc; deserializeJson(doc, body); const char* name = doc["name"]; uint32_t canId = doc["canId"]; uint32_t startBit = doc["startBit"]; uint32_t length = doc["length"]; bool littleEndian = doc["littleEndian"] | true; bool isSigned = doc["signed"] | false; float factor = doc["factor"] | 1.0; float offset = doc["offset"] | 0.0; if (addManualSignal(name, canId, startBit, length, littleEndian, isSigned, factor, offset)) { server.send(200, "application/json", "{\"status\":\"added\"}"); } else { server.send(500, "application/json", "{\"error\":\"Failed to add signal\"}"); } } void handleAPISignalList() { char buffer[2048]; getSignalsJSON(buffer, sizeof(buffer)); server.send(200, "application/json", buffer); } void handleAPITestStart() { if (isTestRunning()) { server.send(400, "application/json", "{\"error\":\"Test already running\"}"); return; } if (!server.hasArg("plain")) { server.send(400, "application/json", "{\"error\":\"Missing body\"}"); return; } String body = server.arg("plain"); StaticJsonDocument<256> doc; deserializeJson(doc, body); String testType = doc["type"] | "loopback"; uint32_t frameCount = doc["frames"] | 1000; uint32_t interval = doc["interval"] | 1000; uint8_t dataLen = doc["dataLen"] | 64; bool useFD = doc["useFD"] | true; uint32_t canId = doc["canId"] | 0x100; bool started = false; if (testType == "loopback") { started = startLoopbackTest(frameCount, interval); } else if (testType == "stress") { started = startStressTest(frameCount, dataLen, useFD); } else if (testType == "sequence") { started = startSequenceTest(frameCount, canId); } if (started) { server.send(200, "application/json", "{\"status\":\"started\"}"); } else { server.send(500, "application/json", "{\"error\":\"Failed to start test\"}"); } } void handleAPITestStop() { stopTest(); server.send(200, "application/json", "{\"status\":\"stopped\"}"); } void handleAPITestStatus() { char buffer[512]; getTestResultJSON(buffer, sizeof(buffer)); server.send(200, "application/json", buffer); } void handleNotFound() { server.send(404, "text/plain", "Not Found"); } bool initWebServer() { Serial.println("Initializing Web Server..."); server.on("/", handleRoot); server.on("/settings", handleSettings); server.on("/files", handleFiles); server.on("/can", handleCAN); server.on("/graph", handleGraph); server.on("/test", handleTest); server.on("/api/status", handleAPIStatus); server.on("/api/memory", handleAPIMemory); server.on("/api/files", handleAPIFileList); server.on("/api/files/download", handleAPIFileDownload); server.on("/api/files/delete", handleAPIFileDelete); server.on("/api/can/send", HTTP_POST, handleAPICANSend); server.on("/api/wifi", HTTP_POST, handleAPIWiFiConfig); server.on("/api/logging/start", handleAPILoggingStart); server.on("/api/logging/stop", handleAPILoggingStop); server.on("/api/dbc/upload", HTTP_POST, handleAPIDBCUpload); server.on("/api/time", HTTP_POST, handleAPITimeSync); server.on("/api/restart", HTTP_POST, handleAPIRestart); server.on("/api/can/config", HTTP_POST, handleAPICANConfig); server.on("/api/trigger", handleAPITriggerConfig); server.on("/api/signal/add", HTTP_POST, handleAPISignalAdd); server.on("/api/signal/list", handleAPISignalList); server.on("/api/test/start", HTTP_POST, handleAPITestStart); server.on("/api/test/stop", handleAPITestStop); server.on("/api/test/status", handleAPITestStatus); server.onNotFound(handleNotFound); server.begin(); webSocket.begin(); webSocket.onEvent(webSocketEvent); Serial.println("Web Server started on port 80"); Serial.println("WebSocket started on port 81"); return true; } void webServerTask(void *pvParameters) { Serial.println("Web Server Task started on Core 1"); if (!initWiFi()) { Serial.println("WiFi initialization failed!"); } if (!initWebServer()) { Serial.println("Web Server initialization failed!"); } while (1) { server.handleClient(); webSocket.loop(); vTaskDelay(pdMS_TO_TICKS(10)); } } void wsTxTask(void *pvParameters) { Serial.println("WebSocket TX Task started on Core 1"); CanFrame frame; SignalValue signals[10]; uint16_t signalCount = 0; uint32_t lastUpdate = 0; while (1) { if (xQueueReceive(graphQueue, &frame, pdMS_TO_TICKS(50)) == pdTRUE) { updateAllSignals(&frame); } uint32_t now = millis(); if (now - lastUpdate >= 100) { signalCount = getEnabledSignals(signals, 10); if (signalCount > 0) { GraphSignal graphSignals[10]; for (uint16_t i = 0; i < signalCount; i++) { strncpy(graphSignals[i].signal_id, signals[i].name, 32); graphSignals[i].value = signals[i].value; graphSignals[i].timestamp = signals[i].timestamp; } broadcastSignalData(graphSignals, signalCount); } lastUpdate = now; } updateTrigger(); } } bool loadWiFiConfig() { wifiConfig.useSTA = false; wifiConfig.staSSID[0] = '\0'; wifiConfig.staPassword[0] = '\0'; return true; } bool saveWiFiConfig() { return true; }