From a153f924b252f5886f0f4ade76bf347a0b972a11 Mon Sep 17 00:00:00 2001 From: byun Date: Thu, 13 Nov 2025 03:37:17 +0000 Subject: [PATCH] 1-a --- test_i2c_reset/ESP32_CAN_Logger.ino | 480 ++++++++++++++++++++++++++++ 1 file changed, 480 insertions(+) diff --git a/test_i2c_reset/ESP32_CAN_Logger.ino b/test_i2c_reset/ESP32_CAN_Logger.ino index 5ed794c..0db5681 100644 --- a/test_i2c_reset/ESP32_CAN_Logger.ino +++ b/test_i2c_reset/ESP32_CAN_Logger.ino @@ -25,6 +25,7 @@ #include "graph.h" #include "graph_viewer.h" #include "settings.h" +#include "serial_terminal.h" // GPIO 핀 정의 #define CAN_INT_PIN 27 @@ -46,6 +47,10 @@ #define RTC_SCL 26 #define DS3231_ADDRESS 0x68 +// Serial2 핀 (RS232 통신) +#define SERIAL2_RX 16 +#define SERIAL2_TX 17 + // 버퍼 설정 #define CAN_QUEUE_SIZE 2000 // 1000 → 2000으로 증가 #define FILE_BUFFER_SIZE 16384 // 8192 → 16384 (16KB)로 증가 @@ -53,6 +58,8 @@ #define RECENT_MSG_COUNT 100 #define MAX_TX_MESSAGES 20 #define MAX_COMMENT_LEN 128 +#define SERIAL_BUFFER_SIZE 1024 +#define SERIAL_LOG_BUFFER_SIZE 32768 // RTC 동기화 설정 #define RTC_SYNC_INTERVAL_MS 60000 // 1분마다 RTC와 동기화 @@ -119,6 +126,29 @@ struct FileComment { char comment[MAX_COMMENT_LEN]; }; +// Serial 설정 구조체 +struct SerialTermConfig { + uint32_t baudRate; + uint8_t dataBits; + uint8_t parity; + uint8_t stopBits; + bool enabled; + bool echoEnabled; + bool timestampEnabled; + bool autoSaveEnabled; + char logFilename[MAX_FILENAME_LEN]; +}; + +// 시리얼 데이터 버퍼 +struct SerialDataBuffer { + char data[SERIAL_LOG_BUFFER_SIZE]; + uint32_t writeIndex; + uint32_t readIndex; + bool overflow; + SemaphoreHandle_t mutex; +}; + + // 시간 동기화 상태 struct TimeSyncStatus { bool synchronized; @@ -173,6 +203,24 @@ WebServer server(80); WebSocketsServer webSocket = WebSocketsServer(81); Preferences preferences; +// 시리얼 터미널 전역 변수 +SerialTermConfig serialTermConfig = { + 9600, 8, 0, 1, false, true, true, false, "serial_log.txt" +}; +SerialDataBuffer serialBuffer; +WebSocketsServer serialWebSocket = WebSocketsServer(82); +QueueHandle_t serialQueue; +TaskHandle_t serialTaskHandle = NULL; +File serialLogFile; +volatile uint32_t serialRxCount = 0; +volatile uint32_t serialTxCount = 0; +volatile uint32_t serialRxPerSecond = 0; +volatile uint32_t serialTxPerSecond = 0; +uint32_t lastSerialCountTime = 0; +uint32_t lastSerialRxCount = 0; +uint32_t lastSerialTxCount = 0; + + // Forward declaration void IRAM_ATTR canISR(); @@ -275,6 +323,9 @@ void loadSettings() { if (enableSTAMode && strlen(staSSID) > 0) { Serial.printf("✓ WiFi STA 모드: 활성화 (SSID: %s)\n", staSSID); } + + // 시리얼 설정 로드 + loadSerialSettings(); } void saveSettings() { @@ -305,6 +356,430 @@ void saveSettings() { Serial.println("⚠️ 재부팅 후 WiFi 설정이 적용됩니다."); } + +// ======================================== +// 시리얼 터미널 함수들 +// ======================================== + +void loadSerialSettings() { + preferences.begin("serial-term", false); + + serialTermConfig.baudRate = preferences.getUInt("baud", 9600); + serialTermConfig.dataBits = preferences.getUChar("databits", 8); + serialTermConfig.parity = preferences.getUChar("parity", 0); + serialTermConfig.stopBits = preferences.getUChar("stopbits", 1); + serialTermConfig.enabled = preferences.getBool("enabled", false); + serialTermConfig.echoEnabled = preferences.getBool("echo", true); + serialTermConfig.timestampEnabled = preferences.getBool("timestamp", true); + serialTermConfig.autoSaveEnabled = preferences.getBool("autosave", false); + preferences.getString("logfile", serialTermConfig.logFilename, sizeof(serialTermConfig.logFilename)); + + if (strlen(serialTermConfig.logFilename) == 0) { + strcpy(serialTermConfig.logFilename, "serial_log.txt"); + } + + preferences.end(); + + Serial.printf("✓ 시리얼 설정 로드: %d-%d-%c-%d\n", + serialTermConfig.baudRate, + serialTermConfig.dataBits, + serialTermConfig.parity == 0 ? 'N' : (serialTermConfig.parity == 1 ? 'E' : 'O'), + serialTermConfig.stopBits); +} + +void saveSerialSettings() { + preferences.begin("serial-term", false); + + preferences.putUInt("baud", serialTermConfig.baudRate); + preferences.putUChar("databits", serialTermConfig.dataBits); + preferences.putUChar("parity", serialTermConfig.parity); + preferences.putUChar("stopbits", serialTermConfig.stopBits); + preferences.putBool("enabled", serialTermConfig.enabled); + preferences.putBool("echo", serialTermConfig.echoEnabled); + preferences.putBool("timestamp", serialTermConfig.timestampEnabled); + preferences.putBool("autosave", serialTermConfig.autoSaveEnabled); + preferences.putString("logfile", serialTermConfig.logFilename); + + preferences.end(); + + Serial.println("✓ 시리얼 설정 저장 완료"); +} + +void configureSerial2() { + Serial2.end(); + + uint32_t config = SERIAL_8N1; + + switch(serialTermConfig.dataBits) { + case 5: config = SERIAL_5N1; break; + case 6: config = SERIAL_6N1; break; + case 7: config = SERIAL_7N1; break; + case 8: default: config = SERIAL_8N1; break; + } + + if (serialTermConfig.parity == 1) { + switch(serialTermConfig.dataBits) { + case 5: config = SERIAL_5E1; break; + case 6: config = SERIAL_6E1; break; + case 7: config = SERIAL_7E1; break; + case 8: default: config = SERIAL_8E1; break; + } + } else if (serialTermConfig.parity == 2) { + switch(serialTermConfig.dataBits) { + case 5: config = SERIAL_5O1; break; + case 6: config = SERIAL_6O1; break; + case 7: config = SERIAL_7O1; break; + case 8: default: config = SERIAL_8O1; break; + } + } + + if (serialTermConfig.stopBits == 2) { + switch(serialTermConfig.dataBits) { + case 5: + if (serialTermConfig.parity == 0) config = SERIAL_5N2; + else if (serialTermConfig.parity == 1) config = SERIAL_5E2; + else config = SERIAL_5O2; + break; + case 6: + if (serialTermConfig.parity == 0) config = SERIAL_6N2; + else if (serialTermConfig.parity == 1) config = SERIAL_6E2; + else config = SERIAL_6O2; + break; + case 7: + if (serialTermConfig.parity == 0) config = SERIAL_7N2; + else if (serialTermConfig.parity == 1) config = SERIAL_7E2; + else config = SERIAL_7O2; + break; + case 8: default: + if (serialTermConfig.parity == 0) config = SERIAL_8N2; + else if (serialTermConfig.parity == 1) config = SERIAL_8E2; + else config = SERIAL_8O2; + break; + } + } + + Serial2.begin(serialTermConfig.baudRate, config, SERIAL2_RX, SERIAL2_TX); + Serial.printf("✓ Serial2 구성: %d baud, config=0x%X\n", serialTermConfig.baudRate, config); +} + +void addToSerialBuffer(const char* data, size_t len, bool isTx) { + if (!serialBuffer.mutex) return; + + xSemaphoreTake(serialBuffer.mutex, portMAX_DELAY); + + if (serialTermConfig.timestampEnabled) { + char timestamp[32]; + struct timeval tv; + gettimeofday(&tv, NULL); + struct tm *tm_info = localtime(&tv.tv_sec); + snprintf(timestamp, sizeof(timestamp), "[%02d:%02d:%02d.%03ld] %s: ", + tm_info->tm_hour, tm_info->tm_min, tm_info->tm_sec, + tv.tv_usec / 1000, isTx ? "TX" : "RX"); + + size_t tsLen = strlen(timestamp); + for (size_t i = 0; i < tsLen; i++) { + serialBuffer.data[serialBuffer.writeIndex] = timestamp[i]; + serialBuffer.writeIndex = (serialBuffer.writeIndex + 1) % SERIAL_LOG_BUFFER_SIZE; + if (serialBuffer.writeIndex == serialBuffer.readIndex) { + serialBuffer.overflow = true; + serialBuffer.readIndex = (serialBuffer.readIndex + 1) % SERIAL_LOG_BUFFER_SIZE; + } + } + } + + for (size_t i = 0; i < len; i++) { + serialBuffer.data[serialBuffer.writeIndex] = data[i]; + serialBuffer.writeIndex = (serialBuffer.writeIndex + 1) % SERIAL_LOG_BUFFER_SIZE; + if (serialBuffer.writeIndex == serialBuffer.readIndex) { + serialBuffer.overflow = true; + serialBuffer.readIndex = (serialBuffer.readIndex + 1) % SERIAL_LOG_BUFFER_SIZE; + } + } + + xSemaphoreGive(serialBuffer.mutex); +} + +String getSerialBufferData() { + if (!serialBuffer.mutex) return ""; + + String result; + xSemaphoreTake(serialBuffer.mutex, portMAX_DELAY); + + uint32_t idx = serialBuffer.readIndex; + while (idx != serialBuffer.writeIndex) { + result += serialBuffer.data[idx]; + idx = (idx + 1) % SERIAL_LOG_BUFFER_SIZE; + } + + xSemaphoreGive(serialBuffer.mutex); + return result; +} + +void clearSerialBuffer() { + if (!serialBuffer.mutex) return; + + xSemaphoreTake(serialBuffer.mutex, portMAX_DELAY); + serialBuffer.readIndex = serialBuffer.writeIndex; + serialBuffer.overflow = false; + xSemaphoreGive(serialBuffer.mutex); +} + +bool saveSerialLog() { + if (!sdCardReady) return false; + + String data = getSerialBufferData(); + if (data.length() == 0) return true; + + xSemaphoreTake(sdMutex, portMAX_DELAY); + + File file = SD.open(serialTermConfig.logFilename, FILE_APPEND); + if (!file) { + xSemaphoreGive(sdMutex); + return false; + } + + size_t written = file.print(data); + file.close(); + + xSemaphoreGive(sdMutex); + + if (written > 0) { + clearSerialBuffer(); + return true; + } + return false; +} + +void serialTask(void *pvParameters) { + char rxBuffer[SERIAL_BUFFER_SIZE]; + size_t rxIndex = 0; + + Serial.println("✓ Serial Task 시작 (Core 0, Priority 1)"); + + while (true) { + if (serialTermConfig.enabled && Serial2.available()) { + while (Serial2.available() && rxIndex < SERIAL_BUFFER_SIZE - 1) { + char c = Serial2.read(); + rxBuffer[rxIndex++] = c; + serialRxCount++; + + if (serialTermConfig.echoEnabled) { + Serial2.write(c); + serialTxCount++; + } + } + + if (rxIndex > 0) { + addToSerialBuffer(rxBuffer, rxIndex, false); + + String data; + for (size_t i = 0; i < rxIndex; i++) { + if (rxBuffer[i] >= 32 && rxBuffer[i] < 127) { + data += rxBuffer[i]; + } else if (rxBuffer[i] == '\r' || rxBuffer[i] == '\n') { + data += rxBuffer[i]; + } else { + char hex[5]; + snprintf(hex, sizeof(hex), "<%02X>", (uint8_t)rxBuffer[i]); + data += hex; + } + } + serialWebSocket.broadcastTXT(data); + + if (serialTermConfig.autoSaveEnabled && rxIndex > 100) { + saveSerialLog(); + } + + rxIndex = 0; + } + } + + vTaskDelay(pdMS_TO_TICKS(10)); + } +} + +void handleSerialTerminal() { + server.send_P(200, "text/html", SERIAL_TERMINAL_HTML); +} + +void handleSerialConfig() { + String json = "{"; + json += "\"baudRate\":" + String(serialTermConfig.baudRate) + ","; + json += "\"dataBits\":" + String(serialTermConfig.dataBits) + ","; + json += "\"parity\":" + String(serialTermConfig.parity) + ","; + json += "\"stopBits\":" + String(serialTermConfig.stopBits) + ","; + json += "\"enabled\":" + String(serialTermConfig.enabled ? "true" : "false") + ","; + json += "\"echo\":" + String(serialTermConfig.echoEnabled ? "true" : "false") + ","; + json += "\"timestamp\":" + String(serialTermConfig.timestampEnabled ? "true" : "false") + ","; + json += "\"autosave\":" + String(serialTermConfig.autoSaveEnabled ? "true" : "false") + ","; + json += "\"logFilename\":\"" + String(serialTermConfig.logFilename) + "\","; + json += "\"rxCount\":" + String(serialRxCount) + ","; + json += "\"txCount\":" + String(serialTxCount) + ","; + json += "\"rxPerSec\":" + String(serialRxPerSecond) + ","; + json += "\"txPerSec\":" + String(serialTxPerSecond); + json += "}"; + + server.send(200, "application/json", json); +} + +void handleSerialSetConfig() { + if (server.hasArg("plain")) { + String body = server.arg("plain"); + + if (body.indexOf("baudRate") >= 0) { + int idx = body.indexOf("baudRate\":") + 10; + serialTermConfig.baudRate = body.substring(idx, body.indexOf(',', idx)).toInt(); + } + if (body.indexOf("dataBits") >= 0) { + int idx = body.indexOf("dataBits\":") + 10; + serialTermConfig.dataBits = body.substring(idx, body.indexOf(',', idx)).toInt(); + } + if (body.indexOf("parity") >= 0) { + int idx = body.indexOf("parity\":") + 8; + serialTermConfig.parity = body.substring(idx, body.indexOf(',', idx)).toInt(); + } + if (body.indexOf("stopBits") >= 0) { + int idx = body.indexOf("stopBits\":") + 10; + serialTermConfig.stopBits = body.substring(idx, body.indexOf(',', idx)).toInt(); + } + if (body.indexOf("enabled") >= 0) { + serialTermConfig.enabled = body.indexOf("\"enabled\":true") >= 0; + } + if (body.indexOf("echo") >= 0) { + serialTermConfig.echoEnabled = body.indexOf("\"echo\":true") >= 0; + } + if (body.indexOf("timestamp") >= 0) { + serialTermConfig.timestampEnabled = body.indexOf("\"timestamp\":true") >= 0; + } + if (body.indexOf("autosave") >= 0) { + serialTermConfig.autoSaveEnabled = body.indexOf("\"autosave\":true") >= 0; + } + if (body.indexOf("logFilename") >= 0) { + int idx = body.indexOf("logFilename\":\"") + 14; + String filename = body.substring(idx, body.indexOf('\"', idx)); + filename.toCharArray(serialTermConfig.logFilename, sizeof(serialTermConfig.logFilename)); + } + + configureSerial2(); + saveSerialSettings(); + + server.send(200, "application/json", "{\"status\":\"ok\"}"); + } else { + server.send(400, "application/json", "{\"status\":\"error\"}"); + } +} + +void handleSerialSend() { + if (server.hasArg("data")) { + String data = server.arg("data"); + + if (serialTermConfig.enabled) { + data.replace("\\r", "\r"); + data.replace("\\n", "\n"); + + Serial2.print(data); + serialTxCount += data.length(); + + addToSerialBuffer(data.c_str(), data.length(), true); + + server.send(200, "application/json", "{\"status\":\"ok\"}"); + } else { + server.send(400, "application/json", "{\"status\":\"error\",\"message\":\"Serial not enabled\"}"); + } + } else { + server.send(400, "application/json", "{\"status\":\"error\",\"message\":\"No data\"}"); + } +} + +void handleSerialSaveLog() { + if (saveSerialLog()) { + server.send(200, "application/json", "{\"status\":\"ok\"}"); + } else { + server.send(500, "application/json", "{\"status\":\"error\"}"); + } +} + +void handleSerialClearBuffer() { + clearSerialBuffer(); + server.send(200, "application/json", "{\"status\":\"ok\"}"); +} + +void serialWebSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { + switch(type) { + case WStype_DISCONNECTED: + Serial.printf("[%u] Serial WebSocket Disconnected\n", num); + break; + + case WStype_CONNECTED: { + IPAddress ip = serialWebSocket.remoteIP(num); + Serial.printf("[%u] Serial WebSocket Connected from %d.%d.%d.%d\n", + num, ip[0], ip[1], ip[2], ip[3]); + + String data = getSerialBufferData(); + if (data.length() > 0) { + serialWebSocket.sendTXT(num, data); + } + break; + } + + case WStype_TEXT: + if (serialTermConfig.enabled && length > 0) { + Serial2.write(payload, length); + serialTxCount += length; + addToSerialBuffer((const char*)payload, length, true); + } + break; + + default: + break; + } +} + +void setupSerial() { + serialBuffer.writeIndex = 0; + serialBuffer.readIndex = 0; + serialBuffer.overflow = false; + serialBuffer.mutex = xSemaphoreCreateMutex(); + + configureSerial2(); + + serialQueue = xQueueCreate(100, sizeof(char) * SERIAL_BUFFER_SIZE); + + xTaskCreatePinnedToCore( + serialTask, + "SerialTask", + 4096, + NULL, + 1, + &serialTaskHandle, + 0 + ); + + serialWebSocket.begin(); + serialWebSocket.onEvent(serialWebSocketEvent); + + server.on("/serial", HTTP_GET, handleSerialTerminal); + server.on("/serial/config", HTTP_GET, handleSerialConfig); + server.on("/serial/setconfig", HTTP_POST, handleSerialSetConfig); + server.on("/serial/send", HTTP_POST, handleSerialSend); + server.on("/serial/savelog", HTTP_POST, handleSerialSaveLog); + server.on("/serial/clear", HTTP_POST, handleSerialClearBuffer); + + Serial.println("✓ 시리얼 터미널 초기화 완료 (CAN과 독립 동작)"); +} + +void updateSerialStats() { + uint32_t currentTime = millis(); + if (currentTime - lastSerialCountTime >= 1000) { + serialRxPerSecond = serialRxCount - lastSerialRxCount; + serialTxPerSecond = serialTxCount - lastSerialTxCount; + lastSerialRxCount = serialRxCount; + lastSerialTxCount = serialTxCount; + lastSerialCountTime = currentTime; + } +} + + void saveCANSettings() { preferences.begin("can-logger", false); @@ -1834,6 +2309,9 @@ void setup() { } }); + // 시리얼 터미널 초기화 + setupSerial(); + server.begin(); // Queue 생성 @@ -1884,6 +2362,8 @@ void setup() { void loop() { server.handleClient(); + serialWebSocket.loop(); + updateSerialStats(); vTaskDelay(pdMS_TO_TICKS(10)); static uint32_t lastPrint = 0;