Files
esp32-Serial-Logger/web_task.cpp

485 lines
14 KiB
C++

#include "web_task.h"
#include "web_html.h"
#include "serial_task.h"
#include "sdcard_task.h"
#include "rtc_task.h"
#include <ArduinoJson.h>
#include <time.h>
#include <sys/time.h>
// 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;
resp["port"] = serialPort; // 0=UART2, 1=USB
String json;
serializeJson(resp, json);
webSocket.sendTXT(num, json);
}
// --- Switch Serial Port ---
else if (strcmp(cmd, "switch_port") == 0) {
uint8_t port = doc["port"] | 0;
switchSerialPort(port);
StaticJsonDocument<256> resp;
resp["type"] = "serial_config";
resp["baud"] = (uint32_t)serialBaud;
resp["dataBits"] = serialDataBits;
resp["parity"] = String((char)serialParity);
resp["stopBits"] = serialStopBits;
resp["port"] = serialPort;
String json;
serializeJson(resp, json);
webSocket.broadcastTXT(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);
}
// --- Toggle Autostart (persistent logging) ---
else if (strcmp(cmd, "toggle_autostart") == 0) {
bool newState = !sdAutoStart;
sdSetAutoStart(newState);
// If just enabled and not already logging, start now
if (newState && !sdLoggingActive) {
sdStartLogging();
// Also send log_status update
StaticJsonDocument<256> logResp;
logResp["type"] = "log_status";
logResp["active"] = sdLoggingActive;
logResp["file"] = currentLogFileName;
String logJson;
serializeJson(logResp, logJson);
webSocket.broadcastTXT(logJson);
}
StaticJsonDocument<128> resp;
resp["type"] = "autostart_status";
resp["enabled"] = (bool)sdAutoStart;
String json;
serializeJson(resp, json);
webSocket.broadcastTXT(json);
}
// --- Get Autostart Status ---
else if (strcmp(cmd, "get_autostart") == 0) {
StaticJsonDocument<128> resp;
resp["type"] = "autostart_status";
resp["enabled"] = (bool)sdAutoStart;
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<const char*>();
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);
}