#include "web_task.h" #include "web_html.h" #include "serial_task.h" #include "sdcard_task.h" #include "rtc_task.h" #include #include #include // WiFi STA control (defined in .ino) extern volatile bool staEnabled; extern volatile bool staConnected; extern char staSSID[]; extern char staPW[]; extern bool wifiEnableSTA(const char *ssid, const char *password); extern void wifiDisableSTA(); // --- Web Server (port 80) & WebSocket (port 81) --- WebServer server(80); WebSocketsServer webSocket = WebSocketsServer(WS_PORT); // ============================================================ // WebSocket Event Handler (Links2004 pattern) // ============================================================ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) { switch (type) { case WStype_CONNECTED: { Serial.printf("[WS] Client #%u connected\n", num); // Send current status on connect StaticJsonDocument<256> doc; doc["type"] = "log_status"; doc["active"] = sdLoggingActive; doc["file"] = currentLogFileName; String json; serializeJson(doc, json); webSocket.sendTXT(num, json); break; } case WStype_DISCONNECTED: Serial.printf("[WS] Client #%u disconnected\n", num); break; case WStype_TEXT: handleWsMessage(num, (const char*)payload); break; } } // ============================================================ // Process WebSocket Commands from Browser // ============================================================ void handleWsMessage(uint8_t num, const char *message) { StaticJsonDocument<512> doc; DeserializationError err = deserializeJson(doc, message); if (err) return; const char *cmd = doc["cmd"]; if (!cmd) return; // --- Set Time from browser → System Clock + RTC --- if (strcmp(cmd, "set_time") == 0) { uint32_t epoch = doc["epoch"] | 0; uint16_t ms = doc["ms"] | 0; if (epoch > 1700000000) { // Sanity check: after 2023 // 1) Set ESP32 system clock struct timeval tv; tv.tv_sec = (time_t)epoch; tv.tv_usec = (suseconds_t)ms * 1000; settimeofday(&tv, NULL); struct tm timeinfo; localtime_r(&tv.tv_sec, &timeinfo); Serial.printf("[Time] Browser sync: %04d-%02d-%02d %02d:%02d:%02d\n", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); // 2) Also write to DS3231 RTC (if available) bool rtcOk = rtcSyncFromSystem(); // 3) Broadcast confirmation StaticJsonDocument<128> resp; resp["type"] = "time_synced"; resp["ok"] = true; resp["rtc"] = rtcOk; String json; serializeJson(resp, json); webSocket.sendTXT(num, json); } return; } // --- TX: Send data through serial --- if (strcmp(cmd, "tx") == 0) { const char *data = doc["data"]; const char *lineEnd = doc["lineEnd"]; const char *mode = doc["mode"]; if (!data) return; LogEntry *entry = (LogEntry*)pvPortMalloc(sizeof(LogEntry)); if (!entry) return; getTimestamp(entry->timestamp, sizeof(entry->timestamp)); entry->direction = 'T'; if (mode && strcmp(mode, "hex") == 0) { String hexStr = data; hexStr.trim(); int pos = 0; for (unsigned int i = 0; i < hexStr.length() && pos < LOG_LINE_MAX_LEN - 1; ) { while (i < hexStr.length() && hexStr[i] == ' ') i++; if (i + 1 < hexStr.length()) { char hex[3] = { hexStr[i], hexStr[i+1], 0 }; entry->data[pos++] = (char)strtol(hex, NULL, 16); i += 2; } else break; } entry->data[pos] = '\0'; entry->dataLen = pos; } else { int dLen = strlen(data); if (dLen > LOG_LINE_MAX_LEN - 4) dLen = LOG_LINE_MAX_LEN - 4; memcpy(entry->data, data, dLen); entry->dataLen = dLen; // Append line ending if (lineEnd) { if (strcmp(lineEnd, "crlf") == 0) { entry->data[entry->dataLen++] = '\r'; entry->data[entry->dataLen++] = '\n'; } else if (strcmp(lineEnd, "cr") == 0) { entry->data[entry->dataLen++] = '\r'; } else if (strcmp(lineEnd, "lf") == 0) { entry->data[entry->dataLen++] = '\n'; } } entry->data[entry->dataLen] = '\0'; } if (xQueueSend(queueTX, &entry, pdMS_TO_TICKS(100)) != pdTRUE) { vPortFree(entry); } } // --- Serial Config --- else if (strcmp(cmd, "serial_config") == 0) { uint32_t baud = doc["baud"] | (uint32_t)DEFAULT_BAUD_RATE; uint8_t dataBits = doc["dataBits"] | 8; const char *p = doc["parity"] | "N"; uint8_t stopBits = doc["stopBits"] | 1; reconfigureSerial(baud, dataBits, p[0], stopBits); StaticJsonDocument<256> resp; resp["type"] = "serial_config"; resp["baud"] = baud; resp["dataBits"] = dataBits; resp["parity"] = String(p[0]); resp["stopBits"] = stopBits; String json; serializeJson(resp, json); webSocket.broadcastTXT(json); } // --- Get Serial Config --- else if (strcmp(cmd, "get_serial_config") == 0) { StaticJsonDocument<256> resp; resp["type"] = "serial_config"; resp["baud"] = (uint32_t)serialBaud; resp["dataBits"] = serialDataBits; resp["parity"] = String((char)serialParity); resp["stopBits"] = serialStopBits; String json; serializeJson(resp, json); webSocket.sendTXT(num, json); } // --- System Info (includes RTC + WiFi status) --- else if (strcmp(cmd, "sysinfo") == 0) { StaticJsonDocument<768> resp; resp["type"] = "sysinfo"; resp["heap"] = ESP.getFreeHeap(); resp["uptime"] = millis() / 1000; char timeBuf[24]; getTimestamp(timeBuf, sizeof(timeBuf)); resp["time"] = timeBuf; // WiFi AP info (always active) resp["apIp"] = WiFi.softAPIP().toString(); resp["apSSID"] = WIFI_AP_SSID; resp["apClients"] = WiFi.softAPgetStationNum(); // WiFi STA info resp["staOn"] = (bool)staEnabled; resp["staConn"] = (bool)staConnected; if (staEnabled) { resp["staSSID"] = staSSID; resp["staIp"] = WiFi.localIP().toString(); resp["staRssi"] = WiFi.RSSI(); } // RTC info resp["rtcOk"] = rtcStatus.available; resp["rtcSync"] = rtcStatus.timeSynced; resp["rtcSyncs"] = rtcStatus.syncCount; resp["rtcTemp"] = rtcStatus.temperature; String json; serializeJson(resp, json); webSocket.sendTXT(num, json); } // --- Toggle Logging --- else if (strcmp(cmd, "toggle_log") == 0) { if (sdLoggingActive) sdStopLogging(); else sdStartLogging(); StaticJsonDocument<256> resp; resp["type"] = "log_status"; resp["active"] = sdLoggingActive; resp["file"] = currentLogFileName; String json; serializeJson(resp, json); webSocket.broadcastTXT(json); } // --- New Log File --- else if (strcmp(cmd, "new_log") == 0) { sdCreateNewLogFile(); sdLoggingActive = true; StaticJsonDocument<256> resp; resp["type"] = "log_status"; resp["active"] = true; resp["file"] = currentLogFileName; String json; serializeJson(resp, json); webSocket.broadcastTXT(json); } // --- WiFi STA Enable --- else if (strcmp(cmd, "wifi_sta_on") == 0) { const char *ssid = doc["ssid"]; const char *pw = doc["pw"] | ""; if (!ssid || strlen(ssid) == 0) return; bool ok = wifiEnableSTA(ssid, pw); StaticJsonDocument<256> resp; resp["type"] = "wifi_status"; resp["staOn"] = (bool)staEnabled; resp["staConn"] = ok; resp["staSSID"] = staSSID; if (ok) { resp["staIp"] = WiFi.localIP().toString(); resp["staRssi"] = WiFi.RSSI(); } String json; serializeJson(resp, json); webSocket.broadcastTXT(json); } // --- WiFi STA Disable --- else if (strcmp(cmd, "wifi_sta_off") == 0) { wifiDisableSTA(); StaticJsonDocument<128> resp; resp["type"] = "wifi_status"; resp["staOn"] = false; resp["staConn"] = false; String json; serializeJson(resp, json); webSocket.broadcastTXT(json); } // --- Get WiFi Status --- else if (strcmp(cmd, "get_wifi") == 0) { StaticJsonDocument<256> resp; resp["type"] = "wifi_status"; resp["staOn"] = (bool)staEnabled; resp["staConn"] = (bool)staConnected; resp["staSSID"] = staSSID; resp["apIp"] = WiFi.softAPIP().toString(); if (staConnected) { resp["staIp"] = WiFi.localIP().toString(); resp["staRssi"] = WiFi.RSSI(); } String json; serializeJson(resp, json); webSocket.sendTXT(num, json); } } // ============================================================ // Setup Web Server Routes // ============================================================ void setupWebRoutes() { server.on("/", HTTP_GET, []() { server.send_P(200, "text/html", INDEX_HTML); }); server.on("/api/files", HTTP_GET, []() { String json = sdGetFileList(); server.send(200, "application/json", json); }); server.on("/api/delete", HTTP_POST, []() { if (!server.hasArg("plain")) { server.send(400, "application/json", "{\"error\":\"no body\"}"); return; } StaticJsonDocument<1024> doc; DeserializationError err = deserializeJson(doc, server.arg("plain")); if (err) { server.send(400, "application/json", "{\"error\":\"invalid json\"}"); return; } JsonArray files = doc["files"]; int deleted = 0; for (JsonVariant f : files) { const char *fname = f.as(); if (fname && sdDeleteFile(fname)) deleted++; } String resp = "{\"deleted\":" + String(deleted) + "}"; server.send(200, "application/json", resp); }); server.on("/download", HTTP_GET, []() { if (!server.hasArg("file")) { server.send(400, "text/plain", "Missing file parameter"); return; } String filename = server.arg("file"); if (filename.indexOf("..") >= 0) { server.send(403, "text/plain", "Forbidden"); return; } String path = String(LOG_DIR) + "/" + filename; if (!SD.exists(path)) { server.send(404, "text/plain", "File not found"); return; } File file = SD.open(path, FILE_READ); if (!file) { server.send(500, "text/plain", "Cannot open file"); return; } size_t fileSize = file.size(); server.sendHeader("Content-Disposition", "attachment; filename=\"" + filename + "\""); server.sendHeader("Content-Length", String(fileSize)); server.streamFile(file, "text/csv"); file.close(); }); // --- Set/Update file comment --- server.on("/api/comment", HTTP_POST, []() { if (!server.hasArg("plain")) { server.send(400, "application/json", "{\"error\":\"no body\"}"); return; } StaticJsonDocument<512> doc; DeserializationError err = deserializeJson(doc, server.arg("plain")); if (err) { server.send(400, "application/json", "{\"error\":\"invalid json\"}"); return; } const char *fname = doc["file"]; const char *comment = doc["comment"] | ""; if (!fname) { server.send(400, "application/json", "{\"error\":\"missing file\"}"); return; } bool ok = sdSetComment(fname, comment); String resp = "{\"ok\":" + String(ok ? "true" : "false") + "}"; server.send(200, "application/json", resp); }); } // ============================================================ // Web Broadcast Task // ============================================================ void webBroadcastTask(void *param) { Serial.println("[Task] WebBroadcast started on core " + String(xPortGetCoreID())); vTaskDelay(pdMS_TO_TICKS(500)); while (true) { webSocket.loop(); if (webSocket.connectedClients() > 0) { LogEntry *entry; int processed = 0; while (processed < 10 && xQueueReceive(queueWeb, &entry, 0) == pdTRUE) { StaticJsonDocument<768> doc; doc["type"] = (entry->direction == 'T') ? "tx" : "rx"; doc["ts"] = entry->timestamp; doc["data"] = entry->data; String json; serializeJson(doc, json); webSocket.broadcastTXT(json); vPortFree(entry); processed++; } } else { LogEntry *entry; while (xQueueReceive(queueWeb, &entry, 0) == pdTRUE) { vPortFree(entry); } } vTaskDelay(pdMS_TO_TICKS(10)); } } // ============================================================ // Initialize Web Server & WebSocket // ============================================================ void webTaskInit() { setupWebRoutes(); webSocket.begin(); webSocket.onEvent(webSocketEvent); Serial.printf("[Web] WebSocket started on port %d\n", WS_PORT); server.begin(); Serial.println("[Web] HTTP server started on port 80"); xTaskCreatePinnedToCore(webBroadcastTask, "WebBC", TASK_STACK_WEB, NULL, TASK_PRIORITY_WEB, NULL, 0); }