commit 509102d57cbefaa3476446f6cfa5bc83eee6432f Author: byun Date: Sat Feb 14 23:13:42 2026 +0000 Upload files to "/" diff --git a/config.h b/config.h new file mode 100644 index 0000000..fc138a5 --- /dev/null +++ b/config.h @@ -0,0 +1,70 @@ +#ifndef CONFIG_H +#define CONFIG_H + +// ============================================================ +// ESP32 Serial Logger - Configuration +// ESP-WROOM-32D DevKitC V4 +// ============================================================ + +// --- WiFi Configuration --- +#define WIFI_SSID "YourSSID" +#define WIFI_PASSWORD "YourPassword" +#define WIFI_AP_SSID "ESP32_Logger" +#define WIFI_AP_PASSWORD "12345678" +#define WIFI_CONNECT_TIMEOUT 15000 // ms + +// --- Serial2 (UART2) Pin Configuration --- +#define SERIAL2_TX_PIN 17 // GPIO17 +#define SERIAL2_RX_PIN 16 // GPIO16 + +// --- Serial2 Default Settings --- +#define DEFAULT_BAUD_RATE 115200 +#define DEFAULT_RX_BUFFER 4096 + +// --- HSPI SD Card Pin Configuration --- +#define SD_HSPI_CLK 14 +#define SD_HSPI_MISO 26 // Remapped from GPIO12 (strapping pin) +#define SD_HSPI_MOSI 13 +#define SD_HSPI_CS 15 + +// --- FreeRTOS Task Priorities --- +#define TASK_PRIORITY_SERIAL 5 // Highest - must not miss data +#define TASK_PRIORITY_SD_LOG 3 // Medium-high - buffered writes +#define TASK_PRIORITY_WEB 2 // Medium - user interface +#define TASK_PRIORITY_NTP 1 // Lowest - periodic sync + +// --- FreeRTOS Task Stack Sizes --- +#define TASK_STACK_SERIAL 4096 +#define TASK_STACK_SD_LOG 8192 +#define TASK_STACK_WEB 8192 +#define TASK_STACK_NTP 4096 + +// --- Queue Configuration --- +#define QUEUE_SD_SIZE 512 +#define QUEUE_WEB_SIZE 256 +#define QUEUE_TX_SIZE 64 + +// --- Logging Configuration --- +#define LOG_LINE_MAX_LEN 512 +#define SD_WRITE_INTERVAL 1000 // Flush to SD every N ms +#define LOG_DIR "/logs" +#define CSV_HEADER "Timestamp,Direction,Data\r\n" + +// --- NTP Configuration --- +#define NTP_SERVER "pool.ntp.org" +#define NTP_GMT_OFFSET 32400 // KST = UTC+9 (9*3600) +#define NTP_DAYLIGHT_OFFSET 0 + +// --- WebSocket Configuration --- +#define WS_PORT 81 +#define WS_MAX_CLIENTS 4 + +// --- Log Entry Structure --- +struct LogEntry { + char timestamp[24]; // "2025-01-15 10:30:45.123" + char direction; // 'R' for RX, 'T' for TX + char data[LOG_LINE_MAX_LEN]; + uint16_t dataLen; +}; + +#endif // CONFIG_H diff --git a/serial_task.h b/serial_task.h new file mode 100644 index 0000000..e35da4e --- /dev/null +++ b/serial_task.h @@ -0,0 +1,25 @@ +#ifndef SERIAL_TASK_H +#define SERIAL_TASK_H + +#include +#include +#include +#include +#include "config.h" + +extern QueueHandle_t queueSD; +extern QueueHandle_t queueWeb; +extern QueueHandle_t queueTX; + +extern volatile uint32_t serialBaud; +extern volatile uint8_t serialDataBits; +extern volatile char serialParity; +extern volatile uint8_t serialStopBits; + +void serialTaskInit(); +void serialRxTask(void *param); +void serialTxTask(void *param); +void reconfigureSerial(uint32_t baud, uint8_t dataBits, char parity, uint8_t stopBits); +void getTimestamp(char *buf, size_t len); + +#endif diff --git a/web_html.h b/web_html.h new file mode 100644 index 0000000..91b5189 --- /dev/null +++ b/web_html.h @@ -0,0 +1,490 @@ +#ifndef WEB_HTML_H +#define WEB_HTML_H + +const char INDEX_HTML[] PROGMEM = R"rawliteral( + + + + + + + +ESP32 Serial Logger + + + + + + + + +
+ + +
+
+ + + + + + + +
+
+
+ + +
+ + + + +
+
+
+ + +
+
+
+

Serial Port

+
+ +
+
+
+
+ +
+
+

System Info

+
+
+
+
+ +
+
+

SD Logging

+
Active
+
+
+ + +
+
+
+
+ + +
+
+
+ + + + 0 files +
+
+ + + +
FileSizeDate
+
+
+
+
+ + +
+ + Logging... + 115200 8N1 + RX:0 + TX:0 + -- +
+ + + + +)rawliteral"; + +#endif // WEB_HTML_H diff --git a/web_task.cpp b/web_task.cpp new file mode 100644 index 0000000..2bae7db --- /dev/null +++ b/web_task.cpp @@ -0,0 +1,346 @@ +#include "web_task.h" +#include "web_html.h" +#include "serial_task.h" +#include "sdcard_task.h" +#include +#include +#include + +// --- 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 --- + 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 + struct timeval tv; + tv.tv_sec = (time_t)epoch; + tv.tv_usec = (suseconds_t)ms * 1000; + settimeofday(&tv, NULL); + + // Verify + struct tm timeinfo; + localtime_r(&tv.tv_sec, &timeinfo); + Serial.printf("[Time] Synced from browser: %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); + + // Broadcast confirmation + StaticJsonDocument<128> resp; + resp["type"] = "time_synced"; + resp["ok"] = true; + 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'; + } + // "none" - no line ending + } + 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); + + // Broadcast new config to all clients + 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 --- + else if (strcmp(cmd, "sysinfo") == 0) { + StaticJsonDocument<512> resp; + resp["type"] = "sysinfo"; + resp["ip"] = WiFi.localIP().toString(); + resp["rssi"] = WiFi.RSSI(); + resp["heap"] = ESP.getFreeHeap(); + resp["uptime"] = millis() / 1000; + char timeBuf[24]; + getTimestamp(timeBuf, sizeof(timeBuf)); + resp["time"] = timeBuf; + 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); + } +} + +// ============================================================ +// Setup Web Server Routes +// ============================================================ +void setupWebRoutes() { + // Main page + server.on("/", HTTP_GET, []() { + server.send_P(200, "text/html", INDEX_HTML); + }); + + // API: File List + server.on("/api/files", HTTP_GET, []() { + String json = sdGetFileList(); + server.send(200, "application/json", json); + }); + + // API: Delete Files (POST with JSON body) + 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); + }); + + // API: Download File + server.on("/download", HTTP_GET, []() { + if (!server.hasArg("file")) { + server.send(400, "text/plain", "Missing file parameter"); + return; + } + String filename = server.arg("file"); + + // Security: prevent path traversal + 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(); + }); +} + +// ============================================================ +// Web Broadcast Task - Sends serial data to WebSocket clients +// Runs webSocket.loop() and broadcasts queued data +// ============================================================ +void webBroadcastTask(void *param) { + Serial.println("[Task] WebBroadcast started on core " + String(xPortGetCoreID())); + + vTaskDelay(pdMS_TO_TICKS(500)); // Initial stabilization + + while (true) { + // Process WebSocket events (REQUIRED for Links2004 library) + webSocket.loop(); + + // Process web queue entries and broadcast + if (webSocket.connectedClients() > 0) { + LogEntry *entry; + // Process up to 10 entries per cycle for responsiveness + 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 { + // No clients - drain web queue to prevent memory buildup + 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"); + + // Create broadcast task on core 0 + xTaskCreatePinnedToCore(webBroadcastTask, "WebBC", TASK_STACK_WEB, + NULL, TASK_PRIORITY_WEB, NULL, 0); +} diff --git a/web_task.h b/web_task.h new file mode 100644 index 0000000..817d237 --- /dev/null +++ b/web_task.h @@ -0,0 +1,21 @@ +#ifndef WEB_TASK_H +#define WEB_TASK_H + +#include +#include +#include +#include +#include +#include +#include "config.h" + +extern WebServer server; +extern WebSocketsServer webSocket; + +void webTaskInit(); +void webBroadcastTask(void *param); +void setupWebRoutes(); +void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length); +void handleWsMessage(uint8_t num, const char *message); + +#endif