From 0620d035bf614a609bc9d753868b5be9cf1aa825 Mon Sep 17 00:00:00 2001 From: byun Date: Thu, 13 Nov 2025 06:22:25 +0000 Subject: [PATCH] 3-1 --- test_i2c_reset/ESP32_CAN_Logger.ino | 470 +++++++++++++++++++++++++++- 1 file changed, 459 insertions(+), 11 deletions(-) diff --git a/test_i2c_reset/ESP32_CAN_Logger.ino b/test_i2c_reset/ESP32_CAN_Logger.ino index 4be3488..6652b3c 100644 --- a/test_i2c_reset/ESP32_CAN_Logger.ino +++ b/test_i2c_reset/ESP32_CAN_Logger.ino @@ -1,7 +1,7 @@ /* * Byun CAN Logger with Web Interface + RTC Time Synchronization - * Version: 2.0 - * Added: Phone time sync to RTC, File comments, MCP2515 mode control + * Version: 3.0 + * Added: Phone time sync to RTC, File comments, MCP2515 mode control, Serial Terminal */ #include @@ -25,10 +25,15 @@ #include "graph.h" #include "graph_viewer.h" #include "settings.h" +#include "serial_terminal.h" // GPIO 핀 정의 #define CAN_INT_PIN 27 +// Serial2 핀 정의 (RS232 통신용) +#define SERIAL_RX_PIN 16 +#define SERIAL_TX_PIN 17 + // HSPI 핀 (CAN) #define HSPI_MISO 12 #define HSPI_MOSI 13 @@ -54,6 +59,11 @@ #define MAX_TX_MESSAGES 20 #define MAX_COMMENT_LEN 128 +// Serial 버퍼 설정 +#define SERIAL_RX_BUFFER_SIZE 4096 +#define SERIAL_TX_QUEUE_SIZE 100 +#define SERIAL_LOG_BUFFER_SIZE 8192 + // RTC 동기화 설정 #define RTC_SYNC_INTERVAL_MS 60000 // 1분마다 RTC와 동기화 @@ -138,6 +148,40 @@ struct PowerStatus { uint32_t lastMinReset; // 최소값 리셋 시간 } powerStatus = {0.0, 999.9, false, 0, 0}; +// ======================================== +// Serial 통신 관련 구조체 +// ======================================== + +// Serial 설정 구조체 +struct SerialConfig { + uint32_t baudRate; + uint8_t dataBits; // 5, 6, 7, 8 + uint8_t parity; // 0=None, 1=Even, 2=Odd + uint8_t stopBits; // 1, 2 + bool flowControl; // RTS/CTS +} serialConfig = {115200, 8, 0, 1, false}; + +// Serial 데이터 구조체 (로깅용) +struct SerialLogData { + uint64_t timestamp_us; + bool isTx; // true=송신, false=수신 + uint16_t length; + uint8_t data[256]; +} __attribute__((packed)); + +// Serial 상태 +struct SerialStatus { + bool logging; + uint32_t rxCount; + uint32_t txCount; + uint32_t rxBytesPerSec; + uint32_t txBytesPerSec; + uint32_t lastRxBytes; + uint32_t lastTxBytes; + uint32_t lastStatTime; +} serialStatus = {false, 0, 0, 0, 0, 0, 0, 0}; + + // MCP2515 레지스터 주소 정의 (라이브러리에 없는 경우) #ifndef MCP_CANCTRL #define MCP_CANCTRL 0x0F @@ -171,18 +215,27 @@ MCP2515 mcp2515(HSPI_CS, 20000000, &hspi); // 20MHz로 증가 (10MHz → 20MHz) WebServer server(80); WebSocketsServer webSocket = WebSocketsServer(81); +WebSocketsServer serialWebSocket = WebSocketsServer(82); Preferences preferences; // Forward declaration void IRAM_ATTR canISR(); +void serialWebSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length); QueueHandle_t canQueue; +QueueHandle_t serialTxQueue; +QueueHandle_t serialLogQueue; SemaphoreHandle_t sdMutex; SemaphoreHandle_t rtcMutex; +SemaphoreHandle_t serialMutex; TaskHandle_t canRxTaskHandle = NULL; TaskHandle_t sdWriteTaskHandle = NULL; TaskHandle_t webTaskHandle = NULL; TaskHandle_t rtcTaskHandle = NULL; +TaskHandle_t serialRxTaskHandle = NULL; +TaskHandle_t serialTxTaskHandle = NULL; +TaskHandle_t serialLogTaskHandle = NULL; +TaskHandle_t serialWebTaskHandle = NULL; volatile bool loggingEnabled = false; volatile bool sdCardReady = false; @@ -231,6 +284,378 @@ SequenceRuntime seqRuntime = {false, 0, 0, 0, -1}; FileComment fileComments[MAX_FILE_COMMENTS]; int commentCount = 0; +// Serial 로그 파일 +File serialLogFile; +char currentSerialFilename[MAX_FILENAME_LEN]; +uint8_t serialLogBuffer[SERIAL_LOG_BUFFER_SIZE]; +uint16_t serialLogBufferIndex = 0; +volatile uint32_t currentSerialFileSize = 0; + + + + +// ======================================== +// Serial 설정 저장/로드 함수 +// ======================================== + +void saveSerialSettings() { + preferences.begin("serial-cfg", false); + preferences.putUInt("baudrate", serialConfig.baudRate); + preferences.putUChar("databits", serialConfig.dataBits); + preferences.putUChar("parity", serialConfig.parity); + preferences.putUChar("stopbits", serialConfig.stopBits); + preferences.putBool("flowctrl", serialConfig.flowControl); + preferences.end(); + Serial.println("✓ Serial 설정 저장 완료"); +} + +void loadSerialSettings() { + preferences.begin("serial-cfg", true); + serialConfig.baudRate = preferences.getUInt("baudrate", 115200); + serialConfig.dataBits = preferences.getUChar("databits", 8); + serialConfig.parity = preferences.getUChar("parity", 0); + serialConfig.stopBits = preferences.getUChar("stopbits", 1); + serialConfig.flowControl = preferences.getBool("flowctrl", false); + preferences.end(); + + Serial.printf("✓ Serial 설정 로드: %u bps, %dN%d\n", + serialConfig.baudRate, serialConfig.dataBits, serialConfig.stopBits); +} + +void applySerialConfig() { + Serial2.end(); + + uint32_t config = SERIAL_8N1; + + if (serialConfig.dataBits == 5) config = SERIAL_5N1; + else if (serialConfig.dataBits == 6) config = SERIAL_6N1; + else if (serialConfig.dataBits == 7) config = SERIAL_7N1; + else config = SERIAL_8N1; + + if (serialConfig.parity == 1) { + if (serialConfig.dataBits == 5) config = SERIAL_5E1; + else if (serialConfig.dataBits == 6) config = SERIAL_6E1; + else if (serialConfig.dataBits == 7) config = SERIAL_7E1; + else config = SERIAL_8E1; + } else if (serialConfig.parity == 2) { + if (serialConfig.dataBits == 5) config = SERIAL_5O1; + else if (serialConfig.dataBits == 6) config = SERIAL_6O1; + else if (serialConfig.dataBits == 7) config = SERIAL_7O1; + else config = SERIAL_8O1; + } + + if (serialConfig.stopBits == 2) { + config |= 0x3000; + } + + Serial2.begin(serialConfig.baudRate, config, SERIAL_RX_PIN, SERIAL_TX_PIN); + Serial2.setRxBufferSize(SERIAL_RX_BUFFER_SIZE); + + Serial.printf("✓ Serial2 재구성: %u bps\n", serialConfig.baudRate); +} + +// ======================================== +// Serial 로깅 함수 +// ======================================== + +void flushSerialLogBuffer() { + if (serialLogBufferIndex > 0 && serialLogFile) { + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(100)) == pdTRUE) { + serialLogFile.write(serialLogBuffer, serialLogBufferIndex); + currentSerialFileSize += serialLogBufferIndex; + serialLogBufferIndex = 0; + xSemaphoreGive(sdMutex); + } + } +} + +void startSerialLogging() { + if (!sdCardReady) { + Serial.println("✗ SD 카드 없음 - Serial 로깅 불가"); + return; + } + + if (serialStatus.logging) { + Serial.println("⚠ Serial 로깅이 이미 실행 중입니다"); + return; + } + + struct tm timeinfo; + if (!getLocalTime(&timeinfo)) { + sprintf(currentSerialFilename, "/SERIAL_%lu.log", millis()); + } else { + sprintf(currentSerialFilename, "/SERIAL_%04d%02d%02d_%02d%02d%02d.log", + timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, + timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); + } + + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { + serialLogFile = SD.open(currentSerialFilename, FILE_WRITE); + if (serialLogFile) { + serialStatus.logging = true; + currentSerialFileSize = 0; + serialLogBufferIndex = 0; + + char header[256]; + snprintf(header, sizeof(header), + "Serial Log - BaudRate: %u, DataBits: %d, Parity: %d, StopBits: %d\n", + serialConfig.baudRate, serialConfig.dataBits, + serialConfig.parity, serialConfig.stopBits); + serialLogFile.print(header); + + Serial.printf("✓ Serial 로깅 시작: %s\n", currentSerialFilename); + } else { + Serial.println("✗ Serial 로그 파일 생성 실패"); + } + xSemaphoreGive(sdMutex); + } +} + +void stopSerialLogging() { + if (!serialStatus.logging) return; + + serialStatus.logging = false; + flushSerialLogBuffer(); + + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { + if (serialLogFile) { + serialLogFile.close(); + Serial.printf("✓ Serial 로깅 종료: %s (크기: %u bytes)\n", + currentSerialFilename, currentSerialFileSize); + } + xSemaphoreGive(sdMutex); + } +} + +// ======================================== +// Serial Task 함수들 +// ======================================== + +void serialRxTask(void* parameter) { + SerialLogData logData; + uint8_t rxBuffer[256]; + + Serial.println("✓ Serial RX Task 시작"); + + while (true) { + if (Serial2.available()) { + int len = Serial2.readBytes(rxBuffer, sizeof(rxBuffer)); + + if (len > 0) { + serialStatus.rxCount += len; + + String hexStr = ""; + String asciiStr = ""; + + for (int i = 0; i < len; i++) { + char hexBuf[4]; + sprintf(hexBuf, "%02X ", rxBuffer[i]); + hexStr += hexBuf; + + if (rxBuffer[i] >= 32 && rxBuffer[i] < 127) { + asciiStr += (char)rxBuffer[i]; + } else { + asciiStr += '.'; + } + } + + String wsData = "RX:" + hexStr + "|" + asciiStr; + serialWebSocket.broadcastTXT(wsData); + + if (serialStatus.logging) { + logData.timestamp_us = esp_timer_get_time(); + logData.isTx = false; + logData.length = len; + memcpy(logData.data, rxBuffer, len); + + xQueueSend(serialLogQueue, &logData, 0); + } + } + } + + vTaskDelay(pdMS_TO_TICKS(5)); + } +} + +void serialTxTask(void* parameter) { + uint8_t txData[256]; + int txLen; + + Serial.println("✓ Serial TX Task 시작"); + + while (true) { + if (xQueueReceive(serialTxQueue, txData, pdMS_TO_TICKS(100))) { + txLen = txData[0]; + + if (txLen > 0 && txLen < 256) { + Serial2.write(&txData[1], txLen); + serialStatus.txCount += txLen; + + if (serialStatus.logging) { + SerialLogData logData; + logData.timestamp_us = esp_timer_get_time(); + logData.isTx = true; + logData.length = txLen; + memcpy(logData.data, &txData[1], txLen); + + xQueueSend(serialLogQueue, &logData, 0); + } + + String hexStr = "TX:"; + for (int i = 0; i < txLen; i++) { + char hexBuf[4]; + sprintf(hexBuf, "%02X ", txData[i + 1]); + hexStr += hexBuf; + } + serialWebSocket.broadcastTXT(hexStr); + } + } + + vTaskDelay(pdMS_TO_TICKS(1)); + } +} + +void serialLogTask(void* parameter) { + SerialLogData logData; + char logLine[512]; + + Serial.println("✓ Serial Log Task 시작"); + + while (true) { + if (xQueueReceive(serialLogQueue, &logData, pdMS_TO_TICKS(100))) { + if (serialStatus.logging && serialLogFile) { + int offset = snprintf(logLine, sizeof(logLine), + "[%llu] %s: ", logData.timestamp_us, logData.isTx ? "TX" : "RX"); + + for (int i = 0; i < logData.length && offset < sizeof(logLine) - 10; i++) { + offset += snprintf(logLine + offset, sizeof(logLine) - offset, + "%02X ", logData.data[i]); + } + + offset += snprintf(logLine + offset, sizeof(logLine) - offset, " | "); + for (int i = 0; i < logData.length && offset < sizeof(logLine) - 5; i++) { + if (logData.data[i] >= 32 && logData.data[i] < 127) { + logLine[offset++] = logData.data[i]; + } else { + logLine[offset++] = '.'; + } + } + logLine[offset++] = '\n'; + logLine[offset] = '\0'; + + int lineLen = strlen(logLine); + if (serialLogBufferIndex + lineLen >= SERIAL_LOG_BUFFER_SIZE) { + flushSerialLogBuffer(); + } + + memcpy(&serialLogBuffer[serialLogBufferIndex], logLine, lineLen); + serialLogBufferIndex += lineLen; + } + } else { + if (serialLogBufferIndex > 0) { + flushSerialLogBuffer(); + } + } + + vTaskDelay(pdMS_TO_TICKS(10)); + } +} + +void serialWebSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) { + if (type == WStype_TEXT) { + String msg = String((char*)payload); + + if (msg.startsWith("CONFIG:")) { + int idx2 = msg.indexOf(','); + int idx3 = msg.indexOf(',', idx2 + 1); + int idx4 = msg.indexOf(',', idx3 + 1); + + serialConfig.baudRate = msg.substring(7, idx2).toInt(); + serialConfig.dataBits = msg.substring(idx2 + 1, idx3).toInt(); + serialConfig.parity = msg.substring(idx3 + 1, idx4).toInt(); + serialConfig.stopBits = msg.substring(idx4 + 1).toInt(); + + applySerialConfig(); + saveSerialSettings(); + + serialWebSocket.sendTXT(num, "CONFIG_OK"); + + } else if (msg == "START_LOG") { + startSerialLogging(); + serialWebSocket.sendTXT(num, serialStatus.logging ? "LOG_STARTED" : "LOG_FAILED"); + + } else if (msg == "STOP_LOG") { + stopSerialLogging(); + serialWebSocket.sendTXT(num, "LOG_STOPPED"); + + } else if (msg == "GET_CONFIG") { + char cfgStr[128]; + snprintf(cfgStr, sizeof(cfgStr), "CFG:%u,%d,%d,%d,%d", + serialConfig.baudRate, serialConfig.dataBits, serialConfig.parity, + serialConfig.stopBits, serialConfig.flowControl ? 1 : 0); + serialWebSocket.sendTXT(num, cfgStr); + + } else if (msg == "GET_STATUS") { + char statStr[256]; + snprintf(statStr, sizeof(statStr), "STAT:%d,%u,%u,%u,%u,%s", + serialStatus.logging ? 1 : 0, + serialStatus.rxCount, serialStatus.txCount, + serialStatus.rxBytesPerSec, serialStatus.txBytesPerSec, + currentSerialFilename); + serialWebSocket.sendTXT(num, statStr); + + } else if (msg.startsWith("TX:")) { + String hexData = msg.substring(3); + hexData.trim(); + + uint8_t txBuffer[256]; + int txLen = 0; + + for (int i = 0; i < hexData.length() && txLen < 255; i += 2) { + if (i + 1 < hexData.length()) { + String hexByte = hexData.substring(i, i + 2); + txBuffer[txLen + 1] = (uint8_t)strtol(hexByte.c_str(), NULL, 16); + txLen++; + } + } + + if (txLen > 0) { + txBuffer[0] = txLen; + xQueueSend(serialTxQueue, txBuffer, 0); + } + } else if (msg.startsWith("TXT:")) { + String text = msg.substring(4); + uint8_t txBuffer[256]; + int txLen = text.length(); + + if (txLen > 0 && txLen < 255) { + txBuffer[0] = txLen; + memcpy(&txBuffer[1], text.c_str(), txLen); + xQueueSend(serialTxQueue, txBuffer, 0); + } + } + } +} + +void serialWebUpdateTask(void* parameter) { + Serial.println("✓ Serial Web Update Task 시작"); + + while (true) { + serialWebSocket.loop(); + + uint32_t now = millis(); + if (now - serialStatus.lastStatTime >= 1000) { + serialStatus.rxBytesPerSec = serialStatus.rxCount - serialStatus.lastRxBytes; + serialStatus.txBytesPerSec = serialStatus.txCount - serialStatus.lastTxBytes; + serialStatus.lastRxBytes = serialStatus.rxCount; + serialStatus.lastTxBytes = serialStatus.txCount; + serialStatus.lastStatTime = now; + } + + vTaskDelay(pdMS_TO_TICKS(10)); + } +} + // ======================================== // 설정 저장/로드 함수 // ======================================== @@ -1633,6 +2058,7 @@ void setup() { // 설정 로드 loadSettings(); + loadSerialSettings(); // 설정값 표시 Serial.println("\n📋 현재 설정값:"); @@ -1648,6 +2074,10 @@ void setup() { pinMode(CAN_INT_PIN, INPUT_PULLUP); + // Serial2 초기화 + applySerialConfig(); + Serial.println("✓ Serial2 초기화 완료"); + // ADC 설정 (전압 모니터링용) analogSetAttenuation(ADC_11db); // 0-3.3V 범위 @@ -1669,8 +2099,9 @@ void setup() { // Mutex 생성 (다른 초기화보다 먼저!) sdMutex = xSemaphoreCreateMutex(); rtcMutex = xSemaphoreCreateMutex(); + serialMutex = xSemaphoreCreateMutex(); - if (sdMutex == NULL || rtcMutex == NULL) { + if (sdMutex == NULL || rtcMutex == NULL || serialMutex == NULL) { Serial.println("✗ Mutex 생성 실패!"); while (1) delay(1000); } @@ -1747,6 +2178,11 @@ void setup() { webSocket.begin(); webSocket.onEvent(webSocketEvent); + // Serial WebSocket 시작 + serialWebSocket.begin(); + serialWebSocket.onEvent(serialWebSocketEvent); + Serial.println("✓ Serial WebSocket 시작 (포트 82)"); + // 웹 서버 라우팅 server.on("/", HTTP_GET, []() { server.send_P(200, "text/html", index_html); @@ -1768,6 +2204,10 @@ void setup() { server.send_P(200, "text/html", settings_html); }); + server.on("/serial", HTTP_GET, []() { + server.send_P(200, "text/html", serial_terminal_html); + }); + server.on("/download", HTTP_GET, []() { if (server.hasArg("file")) { String filename = "/" + server.arg("file"); @@ -1838,11 +2278,14 @@ void setup() { // Queue 생성 canQueue = xQueueCreate(CAN_QUEUE_SIZE, sizeof(CANMessage)); + serialTxQueue = xQueueCreate(SERIAL_TX_QUEUE_SIZE, 256); + serialLogQueue = xQueueCreate(200, sizeof(SerialLogData)); - if (canQueue == NULL) { + if (canQueue == NULL || serialTxQueue == NULL || serialLogQueue == NULL) { Serial.println("✗ Queue 생성 실패!"); while (1) delay(1000); } + Serial.println("✓ Serial Queue 생성 완료"); // CAN 인터럽트 활성화 attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING); @@ -1855,6 +2298,13 @@ void setup() { xTaskCreatePinnedToCore(txTask, "TX_TASK", 4096, NULL, 2, NULL, 1); xTaskCreatePinnedToCore(sequenceTask, "SEQ_TASK", 4096, NULL, 2, NULL, 1); // 시퀀스 Task 추가 + // Serial Task 생성 + xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", 4096, NULL, 4, &serialRxTaskHandle, 1); + xTaskCreatePinnedToCore(serialTxTask, "SERIAL_TX", 4096, NULL, 3, &serialTxTaskHandle, 1); + xTaskCreatePinnedToCore(serialLogTask, "SERIAL_LOG", 8192, NULL, 2, &serialLogTaskHandle, 1); + xTaskCreatePinnedToCore(serialWebUpdateTask, "SERIAL_WEB", 4096, NULL, 2, &serialWebTaskHandle, 0); + Serial.println("✓ Serial Task 시작 완료"); + // RTC 동기화 Task if (timeSyncStatus.rtcAvailable) { xTaskCreatePinnedToCore(rtcSyncTask, "RTC_SYNC", 4096, NULL, 0, &rtcTaskHandle, 0); @@ -1879,6 +2329,7 @@ void setup() { Serial.println(" - Transmit : /transmit"); Serial.println(" - Graph : /graph"); Serial.println(" - Settings : /settings"); + Serial.println(" - Serial : /serial"); Serial.println("========================================\n"); } @@ -1888,18 +2339,15 @@ void loop() { static uint32_t lastPrint = 0; if (millis() - lastPrint > 10000) { - Serial.printf("[상태] 큐: %d/%d | 로깅: %s | SD: %s | RX: %lu | TX: %lu | 파일크기: %u | 시간: %s | RTC: %s(%u) | 전압: %.2fV%s | 모드: %d\n", + Serial.printf("[상태] CAN큐: %d/%d | CAN: %s | Serial: RX=%u TX=%u LOG=%s | SD: %s | 시간: %s | 전압: %.2fV%s\n", uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE, loggingEnabled ? "ON " : "OFF", + serialStatus.rxCount, serialStatus.txCount, + serialStatus.logging ? "ON" : "OFF", sdCardReady ? "OK" : "NO", - totalMsgCount, totalTxCount, - currentFileSize, timeSyncStatus.synchronized ? "OK" : "NO", - timeSyncStatus.rtcAvailable ? "OK" : "NO", - timeSyncStatus.rtcSyncCount, powerStatus.voltage, - powerStatus.lowVoltage ? " ⚠️" : "", - currentMcpMode); + powerStatus.lowVoltage ? " ⚠️" : ""); lastPrint = millis(); } }