diff --git a/ESP32_CAN_Logger-a.ino b/ESP32_CAN_Logger-a.ino index edb35aa..721df4e 100644 --- a/ESP32_CAN_Logger-a.ino +++ b/ESP32_CAN_Logger-a.ino @@ -39,11 +39,15 @@ #include "graph_viewer.h" #include "settings.h" #include "serial_terminal.h" +#include "serial2_terminal.h" // ⭐ Serial2 페이지 추가 // GPIO 핀 정의 #define CAN_INT_PIN 4 #define SERIAL_TX_PIN 17 #define SERIAL_RX_PIN 18 +// UART2 (Serial Logger 2) ⭐ 추가 +#define SERIAL2_TX_PIN 6 +#define SERIAL2_RX_PIN 7 // HSPI 핀 (CAN) #define HSPI_MISO 13 @@ -68,7 +72,10 @@ #define CAN_QUEUE_SIZE 6000 // 1000 → 6000 (PSRAM 사용) #define FILE_BUFFER_SIZE 65536 // 16KB → 64KB (PSRAM 사용) #define SERIAL_QUEUE_SIZE 1200 // 200 → 1200 (PSRAM 사용) -#define SERIAL_CSV_BUFFER_SIZE 32768 // 8KB → 32KB (PSRAM 사용) +#define SERIAL_CSV_BUFFER_SIZE 32768 + +#define SERIAL2_QUEUE_SIZE 1200 // ⭐ Serial2 추가 +#define SERIAL2_CSV_BUFFER_SIZE 32768 // ⭐ Serial2 추가 // 8KB → 32KB (PSRAM 사용) #define MAX_FILENAME_LEN 64 #define RECENT_MSG_COUNT 100 @@ -105,7 +112,7 @@ struct SerialSettings { uint8_t dataBits; uint8_t parity; uint8_t stopBits; -} serialSettings = {115200, 8, 0, 1}; +}; struct RecentCANData { CANMessage msg; @@ -180,6 +187,7 @@ enum MCP2515Mode { // ======================================== uint8_t *fileBuffer = nullptr; char *serialCsvBuffer = nullptr; +char *serial2CsvBuffer = nullptr; // ⭐ Serial2 추가 RecentCANData *recentData = nullptr; TxMessage *txMessages = nullptr; CANSequence *sequences = nullptr; @@ -188,8 +196,10 @@ FileComment *fileComments = nullptr; // Queue 저장소 (PSRAM) StaticQueue_t *canQueueBuffer = nullptr; StaticQueue_t *serialQueueBuffer = nullptr; +StaticQueue_t *serial2QueueBuffer = nullptr; // ⭐ Serial2 uint8_t *canQueueStorage = nullptr; uint8_t *serialQueueStorage = nullptr; +uint8_t *serial2QueueStorage = nullptr; // ⭐ Serial2 // WiFi 설정 (내부 SRAM) char wifiSSID[32] = "Byun_CAN_Logger"; @@ -198,11 +208,18 @@ bool enableSTAMode = false; char staSSID[32] = ""; char staPassword[64] = ""; +// ======================================== +// Serial 설정 (2개) +// ======================================== +SerialSettings serialSettings = {115200, 8, 0, 1}; // Serial1 +SerialSettings serial2Settings = {115200, 8, 0, 1}; // ⭐ Serial2 추가 + // 전역 객체 (내부 SRAM) SPIClass hspi(HSPI); SPIClass vspi(FSPI); MCP2515 mcp2515(HSPI_CS, 20000000, &hspi); -HardwareSerial SerialComm(1); +HardwareSerial SerialComm(1); // UART1 +HardwareSerial Serial2Comm(2); // ⭐ UART2 추가 WebServer server(80); WebSocketsServer webSocket = WebSocketsServer(81); Preferences preferences; @@ -210,31 +227,41 @@ Preferences preferences; // FreeRTOS 핸들 QueueHandle_t canQueue = NULL; QueueHandle_t serialQueue = NULL; +QueueHandle_t serial2Queue = NULL; // ⭐ Serial2 추가 SemaphoreHandle_t sdMutex = NULL; SemaphoreHandle_t rtcMutex = NULL; SemaphoreHandle_t serialMutex = NULL; +SemaphoreHandle_t serial2Mutex = NULL; // ⭐ Serial2 추가 TaskHandle_t canRxTaskHandle = NULL; TaskHandle_t sdWriteTaskHandle = NULL; TaskHandle_t webTaskHandle = NULL; TaskHandle_t rtcTaskHandle = NULL; TaskHandle_t serialRxTaskHandle = NULL; +TaskHandle_t serial2RxTaskHandle = NULL; // ⭐ Serial2 추가 // 로깅 변수 volatile bool loggingEnabled = false; volatile bool serialLoggingEnabled = false; +volatile bool serial2LoggingEnabled = false; // ⭐ Serial2 추가 volatile bool sdCardReady = false; File logFile; File serialLogFile; +File serial2LogFile; // ⭐ Serial2 추가 char currentFilename[MAX_FILENAME_LEN]; char currentSerialFilename[MAX_FILENAME_LEN]; +char currentSerial2Filename[MAX_FILENAME_LEN]; // ⭐ Serial2 추가 uint16_t bufferIndex = 0; uint16_t serialCsvIndex = 0; +uint16_t serial2CsvIndex = 0; // ⭐ Serial2 추가 volatile uint32_t currentFileSize = 0; volatile uint32_t currentSerialFileSize = 0; +volatile uint32_t currentSerial2FileSize = 0; // ⭐ Serial2 추가 volatile bool canLogFormatCSV = false; volatile bool serialLogFormatCSV = true; +volatile bool serial2LogFormatCSV = true; // ⭐ Serial2 추가 volatile uint64_t canLogStartTime = 0; volatile uint64_t serialLogStartTime = 0; +volatile uint64_t serial2LogStartTime = 0; // ⭐ Serial2 추가 // 기타 전역 변수 MCP2515Mode currentMcpMode = MCP_MODE_NORMAL; @@ -250,6 +277,8 @@ uint32_t lastMsgCountTime = 0; uint32_t lastMsgCount = 0; volatile uint32_t totalSerialRxCount = 0; volatile uint32_t totalSerialTxCount = 0; +volatile uint32_t totalSerial2RxCount = 0; // ⭐ Serial2 추가 +volatile uint32_t totalSerial2TxCount = 0; // ⭐ Serial2 추가 uint32_t totalTxCount = 0; uint8_t sequenceCount = 0; SequenceRuntime seqRuntime = {false, 0, 0, 0, -1}; @@ -292,6 +321,14 @@ bool initPSRAM() { } Serial.printf("✓ serialCsvBuffer: %d KB\n", SERIAL_CSV_BUFFER_SIZE / 1024); + // ⭐ Serial2 CSV Buffer + serial2CsvBuffer = (char*)ps_malloc(SERIAL2_CSV_BUFFER_SIZE); + if (!serial2CsvBuffer) { + Serial.println("✗ serial2CsvBuffer 할당 실패"); + return false; + } + Serial.printf("✓ serial2CsvBuffer: %d KB\n", SERIAL2_CSV_BUFFER_SIZE / 1024); + recentData = (RecentCANData*)ps_calloc(RECENT_MSG_COUNT, sizeof(RecentCANData)); if (!recentData) { Serial.println("✗ recentData 할당 실패"); @@ -343,6 +380,17 @@ bool initPSRAM() { SERIAL_QUEUE_SIZE, sizeof(SerialMessage), (float)(SERIAL_QUEUE_SIZE * sizeof(SerialMessage)) / 1024.0); + // ⭐ Serial2 Queue + serial2QueueBuffer = (StaticQueue_t*)ps_malloc(sizeof(StaticQueue_t)); + serial2QueueStorage = (uint8_t*)ps_malloc(SERIAL2_QUEUE_SIZE * sizeof(SerialMessage)); + if (!serial2QueueBuffer || !serial2QueueStorage) { + Serial.println("✗ Serial2 Queue 저장소 할당 실패"); + return false; + } + Serial.printf("✓ Serial2 Queue: %d 개 × %d bytes = %.2f KB\n", + SERIAL2_QUEUE_SIZE, sizeof(SerialMessage), + (float)(SERIAL2_QUEUE_SIZE * sizeof(SerialMessage)) / 1024.0); + Serial.println("========================================"); Serial.printf("✓ PSRAM 남은 용량: %.2f KB\n", (float)ESP.getFreePsram() / 1024.0); Serial.println("========================================\n"); @@ -377,7 +425,21 @@ bool createQueues() { Serial.println("✗ Serial Queue 생성 실패"); return false; } - Serial.printf("✓ Serial Queue: %d 개\n\n", SERIAL_QUEUE_SIZE); + Serial.printf("✓ Serial Queue: %d 개\n", SERIAL_QUEUE_SIZE); + + // ⭐ Serial2 Queue 생성 (중요!) + serial2Queue = xQueueCreateStatic( + SERIAL2_QUEUE_SIZE, + sizeof(SerialMessage), + serial2QueueStorage, + serial2QueueBuffer + ); + + if (serial2Queue == NULL) { + Serial.println("✗ Serial2 Queue 생성 실패"); + return false; + } + Serial.printf("✓ Serial2 Queue: %d 개\n\n", SERIAL2_QUEUE_SIZE); return true; } @@ -390,6 +452,12 @@ void loadSerialSettings() { serialSettings.dataBits = preferences.getUChar("ser_data", 8); serialSettings.parity = preferences.getUChar("ser_parity", 0); serialSettings.stopBits = preferences.getUChar("ser_stop", 1); + + // ⭐ Serial2 + serial2Settings.baudRate = preferences.getUInt("ser2_baud", 115200); + serial2Settings.dataBits = preferences.getUChar("ser2_data", 8); + serial2Settings.parity = preferences.getUChar("ser2_parity", 0); + serial2Settings.stopBits = preferences.getUChar("ser2_stop", 1); } void saveSerialSettings() { @@ -397,6 +465,12 @@ void saveSerialSettings() { preferences.putUChar("ser_data", serialSettings.dataBits); preferences.putUChar("ser_parity", serialSettings.parity); preferences.putUChar("ser_stop", serialSettings.stopBits); + + // ⭐ Serial2 + preferences.putUInt("ser2_baud", serial2Settings.baudRate); + preferences.putUChar("ser2_data", serial2Settings.dataBits); + preferences.putUChar("ser2_parity", serial2Settings.parity); + preferences.putUChar("ser2_stop", serial2Settings.stopBits); } void applySerialSettings() { @@ -426,6 +500,31 @@ void applySerialSettings() { SerialComm.begin(serialSettings.baudRate, config, SERIAL_RX_PIN, SERIAL_TX_PIN); SerialComm.setRxBufferSize(2048); + + // ⭐ Serial2 설정 + uint32_t config2 = SERIAL_8N1; + + if (serial2Settings.dataBits == 5) { + if (serial2Settings.parity == 0) config2 = SERIAL_5N1; + else if (serial2Settings.parity == 1) config2 = SERIAL_5E1; + else if (serial2Settings.parity == 2) config2 = SERIAL_5O1; + } else if (serial2Settings.dataBits == 6) { + if (serial2Settings.parity == 0) config2 = SERIAL_6N1; + else if (serial2Settings.parity == 1) config2 = SERIAL_6E1; + else if (serial2Settings.parity == 2) config2 = SERIAL_6O1; + } else if (serial2Settings.dataBits == 7) { + if (serial2Settings.parity == 0) config2 = SERIAL_7N1; + else if (serial2Settings.parity == 1) config2 = SERIAL_7E1; + else if (serial2Settings.parity == 2) config2 = SERIAL_7O1; + } else { + if (serial2Settings.parity == 0) config2 = SERIAL_8N1; + else if (serial2Settings.parity == 1) config2 = SERIAL_8E1; + else if (serial2Settings.parity == 2) config2 = SERIAL_8O1; + } + if (serial2Settings.stopBits == 2) config2 |= 0x3000; + + Serial2Comm.begin(serial2Settings.baudRate, config2, SERIAL2_RX_PIN, SERIAL2_TX_PIN); + Serial2Comm.setRxBufferSize(2048); } void loadSettings() { @@ -683,6 +782,57 @@ void serialRxTask(void *parameter) { } } + +// ⭐ Serial2 RX Task (우선순위 5) +void serial2RxTask(void *parameter) { + SerialMessage serialMsg; + uint8_t lineBuffer[MAX_SERIAL_LINE_LEN]; + uint16_t lineIndex = 0; + uint32_t lastActivity = millis(); + + while (1) { + while (Serial2Comm.available()) { + uint8_t c = Serial2Comm.read(); + lineBuffer[lineIndex++] = c; + lastActivity = millis(); + + if (c == '\n' || c == '\r' || lineIndex >= MAX_SERIAL_LINE_LEN - 1) { + if (lineIndex > 0) { + struct timeval tv; + gettimeofday(&tv, NULL); + serialMsg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; + serialMsg.length = lineIndex; + memcpy(serialMsg.data, lineBuffer, lineIndex); + serialMsg.isTx = false; + + if (serial2Queue && xQueueSend(serial2Queue, &serialMsg, pdMS_TO_TICKS(10)) == pdTRUE) { // ⭐ NULL 체크 + totalSerial2RxCount++; + } + lineIndex = 0; + } + } + + if (lineIndex >= MAX_SERIAL_LINE_LEN - 1) lineIndex = 0; + } + + if (lineIndex > 0 && (millis() - lastActivity > 100)) { + struct timeval tv; + gettimeofday(&tv, NULL); + serialMsg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; + serialMsg.length = lineIndex; + memcpy(serialMsg.data, lineBuffer, lineIndex); + serialMsg.isTx = false; + + if (serial2Queue && xQueueSend(serial2Queue, &serialMsg, pdMS_TO_TICKS(10)) == pdTRUE) { // ⭐ NULL 체크 + totalSerial2RxCount++; + } + lineIndex = 0; + } + + vTaskDelay(pdMS_TO_TICKS(1)); + } +} + void canRxTask(void *parameter) { struct can_frame frame; CANMessage msg; @@ -1266,6 +1416,121 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) saveSettings(); } } + else if (strcmp(cmd, "startSerial2Logging") == 0) { + if (!serial2LoggingEnabled && sdCardReady) { + const char* format = doc["format"]; + if (format && strcmp(format, "bin") == 0) { + serial2LogFormatCSV = false; + } else { + serial2LogFormatCSV = true; + } + + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { + struct tm timeinfo; + time_t now; + time(&now); + localtime_r(&now, &timeinfo); + + struct timeval tv; + gettimeofday(&tv, NULL); + serial2LogStartTime = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; + + const char* ext = serial2LogFormatCSV ? "csv" : "bin"; + snprintf(currentSerial2Filename, sizeof(currentSerial2Filename), + "/SER2_%04d%02d%02d_%02d%02d%02d.%s", + timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, + timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, ext); + + serial2LogFile = SD.open(currentSerial2Filename, FILE_WRITE); + + if (serial2LogFile) { + if (serial2LogFormatCSV) { + serial2LogFile.println("Time_us,Direction,Data"); + } + serial2LoggingEnabled = true; + serial2CsvIndex = 0; + currentSerial2FileSize = serial2LogFile.size(); + } + + xSemaphoreGive(sdMutex); + } + } + } + else if (strcmp(cmd, "stopSerial2Logging") == 0) { + if (serial2LoggingEnabled) { + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { + if (serial2CsvIndex > 0 && serial2LogFile) { + serial2LogFile.write((uint8_t*)serial2CsvBuffer, serial2CsvIndex); + serial2CsvIndex = 0; + } + + if (serial2LogFile) { + serial2LogFile.close(); + } + + serial2LoggingEnabled = false; + xSemaphoreGive(sdMutex); + } + } + } + else if (strcmp(cmd, "sendSerial2") == 0) { + const char* data = doc["data"]; + if (data && strlen(data) > 0) { + Serial2Comm.println(data); + + SerialMessage serialMsg; + struct timeval tv; + gettimeofday(&tv, NULL); + serialMsg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; + serialMsg.length = strlen(data) + 2; + if (serialMsg.length > MAX_SERIAL_LINE_LEN - 1) { + serialMsg.length = MAX_SERIAL_LINE_LEN - 1; + } + + snprintf((char*)serialMsg.data, MAX_SERIAL_LINE_LEN, "%s\r\n", data); + serialMsg.isTx = true; + + if (serial2Queue && xQueueSend(serial2Queue, &serialMsg, pdMS_TO_TICKS(10)) == pdTRUE) { // ⭐ NULL 체크 + totalSerial2TxCount++; + } + } + } + else if (strcmp(cmd, "setSerial2Config") == 0) { + uint32_t baud = doc["baudRate"] | 115200; + uint8_t data = doc["dataBits"] | 8; + uint8_t parity = doc["parity"] | 0; + uint8_t stop = doc["stopBits"] | 1; + + serial2Settings.baudRate = baud; + serial2Settings.dataBits = data; + serial2Settings.parity = parity; + serial2Settings.stopBits = stop; + + saveSerialSettings(); + applySerialSettings(); + } + else if (strcmp(cmd, "getSerial2Config") == 0) { + DynamicJsonDocument response(512); + response["type"] = "serial2Config"; + response["baudRate"] = serial2Settings.baudRate; + response["dataBits"] = serial2Settings.dataBits; + response["parity"] = serial2Settings.parity; + response["stopBits"] = serial2Settings.stopBits; + + String json; + serializeJson(response, json); + webSocket.sendTXT(num, json); + } + else if (strcmp(cmd, "setSpeed") == 0) { + int speedIndex = doc["speed"]; + if (speedIndex >= 0 && speedIndex < 4) { + currentCanSpeed = canSpeedValues[speedIndex]; + mcp2515.reset(); + mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ); + setMCP2515Mode(currentMcpMode); + saveSettings(); + } + } else if (strcmp(cmd, "setMcpMode") == 0) { int mode = doc["mode"]; if (mode >= 0 && mode <= 3) { @@ -1306,37 +1571,76 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { DynamicJsonDocument response(6144); response["type"] = "files"; - JsonArray files = response.createNestedArray("list"); + JsonArray files = response.createNestedArray("files"); File root = SD.open("/"); - File file = root.openNextFile(); - - while (file) { - if (!file.isDirectory()) { - const char* filename = file.name(); - - if (filename[0] != '.' && - strcmp(filename, "System Volume Information") != 0) { + if (root) { + File file = root.openNextFile(); + int fileCount = 0; + + while (file && fileCount < 50) { + if (!file.isDirectory()) { + const char* filename = file.name(); - JsonObject fileObj = files.createNestedObject(); - fileObj["name"] = filename; - fileObj["size"] = file.size(); + // ⭐ 파일명이 '/'로 시작하면 건너뛰기 + if (filename[0] == '/') { + filename++; // 슬래시 제거 + } - const char* comment = getFileComment(filename); - if (strlen(comment) > 0) { - fileObj["comment"] = comment; + // 숨김 파일과 시스템 폴더 제외 + if (filename[0] != '.' && + strcmp(filename, "System Volume Information") != 0 && + strlen(filename) > 0) { + + JsonObject fileObj = files.createNestedObject(); + fileObj["name"] = filename; + fileObj["size"] = file.size(); + + const char* comment = getFileComment(filename); + if (strlen(comment) > 0) { + fileObj["comment"] = comment; + } + fileCount++; } } + file.close(); + file = root.openNextFile(); } - file = root.openNextFile(); + + root.close(); + + // ⭐ 디버그 로그 + Serial.printf("getFiles: Found %d files\n", fileCount); + } else { + Serial.println("getFiles: Failed to open root directory"); } xSemaphoreGive(sdMutex); + String json; + size_t jsonSize = serializeJson(response, json); + Serial.printf("getFiles: JSON size = %d bytes\n", jsonSize); + webSocket.sendTXT(num, json); + } else { + Serial.println("getFiles: Failed to acquire sdMutex"); + // Mutex 실패 시에도 응답 전송 + DynamicJsonDocument response(256); + response["type"] = "files"; + response["error"] = "SD busy"; + JsonArray files = response.createNestedArray("files"); String json; serializeJson(response, json); webSocket.sendTXT(num, json); } + } else { + Serial.println("getFiles: SD card not ready"); + // SD 카드 없을 때 빈 목록 전송 + DynamicJsonDocument response(256); + response["type"] = "files"; + JsonArray files = response.createNestedArray("files"); + String json; + serializeJson(response, json); + webSocket.sendTXT(num, json); } } else if (strcmp(cmd, "deleteFile") == 0) { @@ -1425,16 +1729,29 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) // Web Update Task // ======================================== void webUpdateTask(void *parameter) { - const TickType_t xDelay = pdMS_TO_TICKS(100); + const TickType_t xDelay = pdMS_TO_TICKS(200); // ⭐ 100ms → 200ms (WiFi 안정성 향상) while (1) { webSocket.loop(); if (webSocket.connectedClients() > 0) { - DynamicJsonDocument doc(4096); + DynamicJsonDocument doc(8192); // ⭐ 4096 → 8192로 증가 doc["type"] = "update"; doc["logging"] = loggingEnabled; doc["serialLogging"] = serialLoggingEnabled; + doc["serial2Logging"] = serial2LoggingEnabled; + doc["totalSerial2Rx"] = totalSerial2RxCount; + doc["totalSerial2Tx"] = totalSerial2TxCount; + doc["serial2QueueUsed"] = serial2Queue ? uxQueueMessagesWaiting(serial2Queue) : 0; // ⭐ NULL 체크 + doc["serial2QueueSize"] = SERIAL2_QUEUE_SIZE; + doc["serial2FileSize"] = currentSerial2FileSize; + + if (serial2LoggingEnabled && currentSerial2Filename[0] != '\0') { + doc["currentSerial2File"] = String(currentSerial2Filename); + } else { + doc["currentSerial2File"] = ""; + } + doc["sdReady"] = sdCardReady; doc["totalMsg"] = totalMsgCount; doc["msgPerSec"] = msgPerSecond; @@ -1472,9 +1789,10 @@ void webUpdateTask(void *parameter) { time(&now); doc["timestamp"] = (uint64_t)now; - // CAN 메시지 배열 + // CAN 메시지 배열 (최근 20개만 전송) JsonArray messages = doc.createNestedArray("messages"); - for (int i = 0; i < RECENT_MSG_COUNT; i++) { + int msgCount = 0; + for (int i = 0; i < RECENT_MSG_COUNT && msgCount < 20; i++) { // ⭐ 최대 20개 if (recentData[i].count > 0) { JsonObject msgObj = messages.createNestedObject(); msgObj["id"] = recentData[i].msg.id; @@ -1485,6 +1803,7 @@ void webUpdateTask(void *parameter) { for (int j = 0; j < recentData[i].msg.dlc; j++) { dataArray.add(recentData[i].msg.data[j]); } + msgCount++; } } @@ -1549,9 +1868,77 @@ void webUpdateTask(void *parameter) { } } + // ⭐ Serial2 메시지 배열 처리 + SerialMessage serial2Msg; + JsonArray serial2Messages = doc.createNestedArray("serial2Messages"); + int serial2Count = 0; + + while (serial2Queue && serial2Count < 10 && xQueueReceive(serial2Queue, &serial2Msg, 0) == pdTRUE) { // ⭐ NULL 체크 + JsonObject serMsgObj = serial2Messages.createNestedObject(); + serMsgObj["timestamp"] = serial2Msg.timestamp_us; + serMsgObj["isTx"] = serial2Msg.isTx; + + char dataStr[MAX_SERIAL_LINE_LEN + 1]; + memcpy(dataStr, serial2Msg.data, serial2Msg.length); + dataStr[serial2Msg.length] = '\0'; + serMsgObj["data"] = dataStr; + + serial2Count++; + + // Serial2 로깅 + if (serial2LoggingEnabled && sdCardReady) { + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(10)) == pdTRUE) { + if (serial2LogFormatCSV) { + uint64_t relativeTime = serial2Msg.timestamp_us - serial2LogStartTime; + + char csvLine[256]; + int lineLen = snprintf(csvLine, sizeof(csvLine), + "%llu,%s,\"%s\"\n", + relativeTime, + serial2Msg.isTx ? "TX" : "RX", + dataStr); + + if (serial2CsvIndex + lineLen < SERIAL2_CSV_BUFFER_SIZE) { + memcpy(&serial2CsvBuffer[serial2CsvIndex], csvLine, lineLen); + serial2CsvIndex += lineLen; + currentSerial2FileSize += lineLen; + } + + if (serial2CsvIndex >= SERIAL2_CSV_BUFFER_SIZE - 256) { + if (serial2LogFile) { + serial2LogFile.write((uint8_t*)serial2CsvBuffer, serial2CsvIndex); + serial2LogFile.flush(); + serial2CsvIndex = 0; + } + } + } else { + // BIN 형식 + if (serial2LogFile) { + serial2LogFile.write((uint8_t*)&serial2Msg, sizeof(SerialMessage)); + currentSerial2FileSize += sizeof(SerialMessage); + + static int binFlushCounter2 = 0; + if (++binFlushCounter2 >= 50) { + serial2LogFile.flush(); + binFlushCounter2 = 0; + } + } + } + + xSemaphoreGive(sdMutex); + } + } + } + String json; - serializeJson(doc, json); - webSocket.broadcastTXT(json); + size_t jsonSize = serializeJson(doc, json); + + // JSON 크기 확인 (8KB 이하만 전송) + if (jsonSize > 0 && jsonSize < 8192) { + webSocket.broadcastTXT(json); + } else { + Serial.printf("! JSON 크기 초과: %d bytes\n", jsonSize); + } } vTaskDelay(xDelay); @@ -1614,12 +2001,14 @@ void setup() { // Serial 통신 초기화 applySerialSettings(); - Serial.println("✓ Serial 통신 초기화 완료"); + Serial.println("✓ Serial1 통신 초기화 (GPIO 17/18)"); + Serial.println("✓ Serial2 통신 초기화 (GPIO 6/7)"); // ⭐ Serial2 // Mutex 생성 sdMutex = xSemaphoreCreateMutex(); rtcMutex = xSemaphoreCreateMutex(); serialMutex = xSemaphoreCreateMutex(); + serial2Mutex = xSemaphoreCreateMutex(); // ⭐ Serial2 if (!sdMutex || !rtcMutex || !serialMutex) { Serial.println("✗ Mutex 생성 실패!"); @@ -1640,6 +2029,8 @@ void setup() { } // WiFi 설정 + WiFi.setSleep(false); // ⭐ WiFi 절전 모드 비활성화 (연결 안정성 향상) + if (enableSTAMode && strlen(staSSID) > 0) { Serial.println("\n📶 WiFi APSTA 모드..."); WiFi.mode(WIFI_AP_STA); @@ -1699,7 +2090,11 @@ void setup() { server.on("/serial", HTTP_GET, []() { server.send_P(200, "text/html", serial_terminal_html); }); - + + server.on("/serial2", HTTP_GET, []() { + server.send_P(200,"text/html", serial2_terminal_html); + }); + server.on("/download", HTTP_GET, []() { if (server.hasArg("file")) { String filename = "/" + server.arg("file"); @@ -1741,8 +2136,9 @@ void setup() { xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 24576, NULL, 4, &sdWriteTaskHandle, 1); xTaskCreatePinnedToCore(sequenceTask, "SEQ", 4096, NULL, 2, NULL, 1); xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", 6144, NULL, 5, &serialRxTaskHandle, 0); + xTaskCreatePinnedToCore(serial2RxTask, "SERIAL2_RX", 6144, NULL, 5, &serial2RxTaskHandle, 0); // ⭐ Serial2 xTaskCreatePinnedToCore(txTask, "TX", 4096, NULL, 3, NULL, 0); - xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 10240, NULL, 2, &webTaskHandle, 0); + xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 16384, NULL, 2, &webTaskHandle, 0); // ⭐ 10240 → 16384 xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 0); if (timeSyncStatus.rtcAvailable) { @@ -1771,9 +2167,10 @@ void loop() { static uint32_t lastPrint = 0; if (millis() - lastPrint > 30000) { - Serial.printf("[상태] CAN큐: %d/%d | Serial큐: %d/%d | PSRAM: %d KB\n", + Serial.printf("[상태] CAN: %d/%d | S1: %d/%d | S2: %d/%d | PSRAM: %d KB\n", uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE, uxQueueMessagesWaiting(serialQueue), SERIAL_QUEUE_SIZE, + uxQueueMessagesWaiting(serial2Queue), SERIAL2_QUEUE_SIZE, ESP.getFreePsram() / 1024); lastPrint = millis(); } diff --git a/index.h b/index.h index 63ad288..e5f6c24 100644 --- a/index.h +++ b/index.h @@ -645,6 +645,7 @@ const char index_html[] PROGMEM = R"rawliteral( 📊 Graph View ⚙️ Settings 📟 Serial + 📟 Serial2
No log files
'; return; } + console.log('Displaying', files.length, 'files'); // ⭐ 디버그 + files.sort((a, b) => { return b.name.localeCompare(a.name); }); @@ -1298,8 +1347,12 @@ const char index_html[] PROGMEM = R"rawliteral( } function refreshFiles() { + console.log('Requesting file list...'); // ⭐ 디버그 if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({cmd: 'getFiles'})); + console.log('getFiles command sent'); // ⭐ 디버그 + } else { + console.error('WebSocket not connected, readyState:', ws ? ws.readyState : 'null'); // ⭐ 디버그 } } @@ -1456,10 +1509,9 @@ const char index_html[] PROGMEM = R"rawliteral( }); initWebSocket(); - setTimeout(() => { refreshFiles(); }, 2000);