485 lines
14 KiB
C++
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);
|
|
}
|