/* * Byun CAN Logger with Web Interface + Serial Terminal * Version: 2.5 - ESP32-S3 PSRAM Full Optimized Edition * Complete PSRAM allocation with memory verification */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "index.h" #include "transmit.h" #include "graph.h" #include "graph_viewer.h" #include "settings.h" #include "serial_terminal.h" // ======================================== // ESP32-S3 PSRAM 최적화 설정 // ======================================== // PSRAM 활용 - 대용량 버퍼 (8MB PSRAM 기준) #define CAN_QUEUE_SIZE 10000 // 10,000개 (210KB PSRAM) #define SERIAL_QUEUE_SIZE 2000 // 2,000개 (150KB PSRAM) #define FILE_BUFFER_SIZE 131072 // 128KB (PSRAM) #define SERIAL_CSV_BUFFER_SIZE 65536 // 64KB (PSRAM) // 기타 상수 #define MAX_FILENAME_LEN 64 #define RECENT_MSG_COUNT 100 #define MAX_TX_MESSAGES 20 #define MAX_COMMENT_LEN 128 #define MAX_SERIAL_LINE_LEN 64 // GPIO 핀 정의 #define CAN_INT_PIN 4 #define SERIAL_TX_PIN 17 #define SERIAL_RX_PIN 18 #define HSPI_MISO 13 #define HSPI_MOSI 11 #define HSPI_SCLK 12 #define HSPI_CS 10 #define VSPI_MISO 41 #define VSPI_MOSI 40 #define VSPI_SCLK 39 #define VSPI_CS 42 #define RTC_SDA 8 #define RTC_SCL 9 #define DS3231_ADDRESS 0x68 // 동기화 및 모니터링 설정 #define RTC_SYNC_INTERVAL_MS 60000 #define VOLTAGE_CHECK_INTERVAL_MS 5000 #define LOW_VOLTAGE_THRESHOLD 3.0 #define MONITORING_VOLT 5 // ======================================== // RTOS 우선순위 정의 // ======================================== #define PRIORITY_CAN_RX 24 // 최고 우선순위 #define PRIORITY_SD_WRITE 20 // 매우 높음 #define PRIORITY_SERIAL_RX 18 // 높음 #define PRIORITY_TX_TASK 15 // 중간-높음 #define PRIORITY_SEQUENCE 12 // 중간 #define PRIORITY_WEB_UPDATE 8 // 중간-낮음 #define PRIORITY_SD_MONITOR 5 // 낮음 #define PRIORITY_RTC_SYNC 2 // 최저 // ======================================== // Stack 크기 정의 // ======================================== #define STACK_CAN_RX 6144 #define STACK_SD_WRITE 32768 #define STACK_SERIAL_RX 8192 #define STACK_TX_TASK 6144 #define STACK_SEQUENCE 6144 #define STACK_WEB_UPDATE 16384 #define STACK_SD_MONITOR 4096 #define STACK_RTC_SYNC 3072 // ======================================== // 구조체 정의 // ======================================== struct CANMessage { uint64_t timestamp_us; uint32_t id; uint8_t dlc; uint8_t data[8]; } __attribute__((packed)); struct SerialMessage { uint64_t timestamp_us; uint16_t length; uint8_t data[MAX_SERIAL_LINE_LEN]; bool isTx; } __attribute__((packed)); struct SerialSettings { uint32_t baudRate; uint8_t dataBits; uint8_t parity; uint8_t stopBits; } serialSettings = {115200, 8, 0, 1}; struct RecentCANData { CANMessage msg; uint32_t count; }; struct TxMessage { uint32_t id; bool extended; uint8_t dlc; uint8_t data[8]; uint32_t interval; uint32_t lastSent; bool active; }; struct SequenceStep { uint32_t canId; bool extended; uint8_t dlc; uint8_t data[8]; uint32_t delayMs; }; struct CANSequence { char name[32]; SequenceStep steps[20]; uint8_t stepCount; uint8_t repeatMode; uint32_t repeatCount; }; struct SequenceRuntime { bool running; uint8_t currentStep; uint32_t currentRepeat; uint32_t lastStepTime; int8_t activeSequenceIndex; }; struct FileComment { char filename[MAX_FILENAME_LEN]; char comment[MAX_COMMENT_LEN]; }; struct TimeSyncStatus { bool synchronized; uint64_t lastSyncTime; int32_t offsetUs; uint32_t syncCount; bool rtcAvailable; uint32_t rtcSyncCount; } timeSyncStatus = {false, 0, 0, 0, false, 0}; struct PowerStatus { float voltage; float minVoltage; bool lowVoltage; uint32_t lastCheck; uint32_t lastMinReset; } powerStatus = {0.0, 999.9, false, 0, 0}; enum MCP2515Mode { MCP_MODE_NORMAL = 0, MCP_MODE_LISTEN_ONLY = 1, MCP_MODE_LOOPBACK = 2, MCP_MODE_TRANSMIT = 3 }; // ======================================== // PSRAM 버퍼 포인터 // ======================================== uint8_t *fileBuffer = nullptr; // 128KB PSRAM char *serialCsvBuffer = nullptr; // 64KB PSRAM uint8_t *canQueueStorage = nullptr; // 210KB PSRAM uint8_t *serialQueueStorage = nullptr; // 150KB PSRAM // FreeRTOS 정적 Queue 구조체 (DRAM - 작은 크기) StaticQueue_t canQueueBuffer; StaticQueue_t serialQueueBuffer; // ======================================== // 전역 변수 // ======================================== // 하드웨어 객체 SPIClass hspi(HSPI); SPIClass vspi(FSPI); MCP2515 mcp2515(HSPI_CS, 20000000, &hspi); HardwareSerial SerialComm(1); WebServer server(80); WebSocketsServer webSocket = WebSocketsServer(81); Preferences preferences; // FreeRTOS 핸들 QueueHandle_t canQueue; QueueHandle_t serialQueue; 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; // 상태 변수 volatile bool loggingEnabled = false; volatile bool serialLoggingEnabled = false; volatile bool sdCardReady = false; File logFile; File serialLogFile; char currentFilename[MAX_FILENAME_LEN]; char currentSerialFilename[MAX_FILENAME_LEN]; uint16_t bufferIndex = 0; uint16_t serialCsvIndex = 0; volatile uint32_t currentFileSize = 0; volatile uint32_t currentSerialFileSize = 0; volatile bool canLogFormatCSV = false; volatile bool serialLogFormatCSV = true; volatile uint64_t canLogStartTime = 0; volatile uint64_t serialLogStartTime = 0; // CAN 설정 MCP2515Mode currentMcpMode = MCP_MODE_NORMAL; CAN_SPEED currentCanSpeed = CAN_1000KBPS; const char* canSpeedNames[] = {"125K", "250K", "500K", "1M"}; CAN_SPEED canSpeedValues[] = {CAN_125KBPS, CAN_250KBPS, CAN_500KBPS, CAN_1000KBPS}; // 통계 RecentCANData recentData[RECENT_MSG_COUNT]; uint32_t totalMsgCount = 0; uint32_t msgPerSecond = 0; uint32_t lastMsgCountTime = 0; uint32_t lastMsgCount = 0; volatile uint32_t totalSerialRxCount = 0; volatile uint32_t totalSerialTxCount = 0; uint32_t totalTxCount = 0; // CAN 송신 및 시퀀스 TxMessage txMessages[MAX_TX_MESSAGES]; #define MAX_SEQUENCES 10 CANSequence sequences[MAX_SEQUENCES]; uint8_t sequenceCount = 0; SequenceRuntime seqRuntime = {false, 0, 0, 0, -1}; // 파일 커멘트 #define MAX_FILE_COMMENTS 50 FileComment fileComments[MAX_FILE_COMMENTS]; int commentCount = 0; // WiFi 설정 char wifiSSID[32] = "Byun_CAN_Logger"; char wifiPassword[64] = "12345678"; bool enableSTAMode = false; char staSSID[32] = ""; char staPassword[64] = ""; // RTC SoftWire rtcWire(RTC_SDA, RTC_SCL); char rtcSyncBuffer[20]; // Forward declarations void IRAM_ATTR canISR(); // ======================================== // 메모리 정보 출력 함수 // ======================================== void printMemoryInfo(const char* name, void* ptr) { if (ptr == nullptr) { Serial.printf(" %-25s: NULL\n", name); return; } bool isPSRAM = esp_ptr_external_ram(ptr); Serial.printf(" %-25s: %s @ 0x%08X\n", name, isPSRAM ? "PSRAM" : "DRAM ", (uint32_t)ptr); } // ======================================== // PSRAM 버퍼 할당 // ======================================== bool allocatePSRAMBuffers() { Serial.println("\n========================================"); Serial.println("PSRAM 버퍼 할당 중..."); Serial.println("========================================"); // 1. 파일 버퍼 (128KB) fileBuffer = (uint8_t*)heap_caps_malloc(FILE_BUFFER_SIZE, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if (fileBuffer == nullptr) { Serial.printf("✗ 파일 버퍼 할당 실패 (%d KB)\n", FILE_BUFFER_SIZE / 1024); return false; } memset(fileBuffer, 0, FILE_BUFFER_SIZE); // 2. Serial CSV 버퍼 (64KB) serialCsvBuffer = (char*)heap_caps_malloc(SERIAL_CSV_BUFFER_SIZE, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if (serialCsvBuffer == nullptr) { Serial.printf("✗ Serial CSV 버퍼 할당 실패 (%d KB)\n", SERIAL_CSV_BUFFER_SIZE / 1024); heap_caps_free(fileBuffer); return false; } memset(serialCsvBuffer, 0, SERIAL_CSV_BUFFER_SIZE); // 3. CAN Queue 버퍼 (210KB) size_t canQueueSize = CAN_QUEUE_SIZE * sizeof(CANMessage); canQueueStorage = (uint8_t*)heap_caps_malloc(canQueueSize, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if (canQueueStorage == nullptr) { Serial.printf("✗ CAN Queue 버퍼 할당 실패 (%.1f KB)\n", canQueueSize / 1024.0); heap_caps_free(fileBuffer); heap_caps_free(serialCsvBuffer); return false; } memset(canQueueStorage, 0, canQueueSize); // 4. Serial Queue 버퍼 (150KB) size_t serialQueueSize = SERIAL_QUEUE_SIZE * sizeof(SerialMessage); serialQueueStorage = (uint8_t*)heap_caps_malloc(serialQueueSize, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if (serialQueueStorage == nullptr) { Serial.printf("✗ Serial Queue 버퍼 할당 실패 (%.1f KB)\n", serialQueueSize / 1024.0); heap_caps_free(fileBuffer); heap_caps_free(serialCsvBuffer); heap_caps_free(canQueueStorage); return false; } memset(serialQueueStorage, 0, serialQueueSize); // 메모리 정보 출력 Serial.println("\n✓ PSRAM 버퍼 할당 완료:"); printMemoryInfo("파일 버퍼 (128KB)", fileBuffer); printMemoryInfo("Serial CSV 버퍼 (64KB)", serialCsvBuffer); printMemoryInfo("CAN Queue 버퍼 (210KB)", canQueueStorage); printMemoryInfo("Serial Queue 버퍼 (150KB)", serialQueueStorage); float totalPSRAM = (FILE_BUFFER_SIZE + SERIAL_CSV_BUFFER_SIZE + canQueueSize + serialQueueSize) / 1024.0; Serial.printf("\n총 PSRAM 사용: %.1f KB\n", totalPSRAM); Serial.printf("PSRAM 여유: %u KB\n\n", ESP.getFreePsram() / 1024); return true; } // ======================================== // Queue 생성 (PSRAM 버퍼 사용) // ======================================== bool createQueues() { Serial.println("========================================"); Serial.println("Queue 생성 중 (PSRAM 버퍼 사용)..."); Serial.println("========================================"); // CAN Queue (PSRAM 버퍼 사용) canQueue = xQueueCreateStatic( CAN_QUEUE_SIZE, sizeof(CANMessage), canQueueStorage, &canQueueBuffer ); if (canQueue == NULL) { Serial.println("✗ CAN Queue 생성 실패!"); return false; } // Serial Queue (PSRAM 버퍼 사용) serialQueue = xQueueCreateStatic( SERIAL_QUEUE_SIZE, sizeof(SerialMessage), serialQueueStorage, &serialQueueBuffer ); if (serialQueue == NULL) { Serial.println("✗ Serial Queue 생성 실패!"); return false; } Serial.println("\n✓ Queue 생성 완료:"); Serial.printf(" - CAN Queue : %d개 × %d bytes = %.1f KB\n", CAN_QUEUE_SIZE, sizeof(CANMessage), (CAN_QUEUE_SIZE * sizeof(CANMessage)) / 1024.0); Serial.printf(" - Serial Queue : %d개 × %d bytes = %.1f KB\n", SERIAL_QUEUE_SIZE, sizeof(SerialMessage), (SERIAL_QUEUE_SIZE * sizeof(SerialMessage)) / 1024.0); Serial.println("\n메모리 위치 확인:"); printMemoryInfo("CAN Queue 버퍼", canQueueStorage); printMemoryInfo("CAN Queue 핸들", canQueue); printMemoryInfo("Serial Queue 버퍼", serialQueueStorage); printMemoryInfo("Serial Queue 핸들", serialQueue); Serial.println(); return true; } // ======================================== // Serial 설정 함수 // ======================================== void loadSerialSettings() { serialSettings.baudRate = preferences.getUInt("ser_baud", 115200); serialSettings.dataBits = preferences.getUChar("ser_data", 8); serialSettings.parity = preferences.getUChar("ser_parity", 0); serialSettings.stopBits = preferences.getUChar("ser_stop", 1); } void saveSerialSettings() { preferences.putUInt("ser_baud", serialSettings.baudRate); preferences.putUChar("ser_data", serialSettings.dataBits); preferences.putUChar("ser_parity", serialSettings.parity); preferences.putUChar("ser_stop", serialSettings.stopBits); } void applySerialSettings() { uint32_t config = SERIAL_8N1; if (serialSettings.dataBits == 5) { if (serialSettings.parity == 0) config = SERIAL_5N1; else if (serialSettings.parity == 1) config = SERIAL_5E1; else if (serialSettings.parity == 2) config = SERIAL_5O1; } else if (serialSettings.dataBits == 6) { if (serialSettings.parity == 0) config = SERIAL_6N1; else if (serialSettings.parity == 1) config = SERIAL_6E1; else if (serialSettings.parity == 2) config = SERIAL_6O1; } else if (serialSettings.dataBits == 7) { if (serialSettings.parity == 0) config = SERIAL_7N1; else if (serialSettings.parity == 1) config = SERIAL_7E1; else if (serialSettings.parity == 2) config = SERIAL_7O1; } else { if (serialSettings.parity == 0) config = SERIAL_8N1; else if (serialSettings.parity == 1) config = SERIAL_8E1; else if (serialSettings.parity == 2) config = SERIAL_8O1; } if (serialSettings.stopBits == 2) { config |= 0x3000; } SerialComm.begin(serialSettings.baudRate, config, SERIAL_RX_PIN, SERIAL_TX_PIN); SerialComm.setRxBufferSize(4096); Serial.printf("✓ Serial 설정: %u-%u-%u-%u\n", serialSettings.baudRate, serialSettings.dataBits, serialSettings.parity, serialSettings.stopBits); } // ======================================== // 설정 저장/로드 // ======================================== void loadSettings() { preferences.begin("can-logger", false); preferences.getString("wifi_ssid", wifiSSID, sizeof(wifiSSID)); preferences.getString("wifi_pass", wifiPassword, sizeof(wifiPassword)); enableSTAMode = preferences.getBool("sta_enable", false); preferences.getString("sta_ssid", staSSID, sizeof(staSSID)); preferences.getString("sta_pass", staPassword, sizeof(staPassword)); if (strlen(wifiSSID) == 0) strcpy(wifiSSID, "Byun_CAN_Logger"); if (strlen(wifiPassword) == 0) strcpy(wifiPassword, "12345678"); int speedIndex = preferences.getInt("can_speed", 3); if (speedIndex >= 0 && speedIndex < 4) { currentCanSpeed = canSpeedValues[speedIndex]; } int savedMode = preferences.getInt("mcp_mode", 0); if (savedMode >= 0 && savedMode <= 3) { currentMcpMode = (MCP2515Mode)savedMode; } loadSerialSettings(); preferences.end(); } void saveSettings() { preferences.begin("can-logger", false); preferences.putString("wifi_ssid", wifiSSID); preferences.putString("wifi_pass", wifiPassword); preferences.putBool("sta_enable", enableSTAMode); preferences.putString("sta_ssid", staSSID); preferences.putString("sta_pass", staPassword); for (int i = 0; i < 4; i++) { if (canSpeedValues[i] == currentCanSpeed) { preferences.putInt("can_speed", i); break; } } preferences.putInt("mcp_mode", (int)currentMcpMode); saveSerialSettings(); preferences.end(); } // ======================================== // RTC 함수 // ======================================== void initRTC() { rtcWire.begin(); rtcWire.setClock(100000); rtcWire.beginTransmission(DS3231_ADDRESS); if (rtcWire.endTransmission() == 0) { timeSyncStatus.rtcAvailable = true; Serial.println("✓ RTC(DS3231) 감지"); } else { timeSyncStatus.rtcAvailable = false; Serial.println("! RTC(DS3231) 없음"); } } uint8_t bcdToDec(uint8_t val) { return (val >> 4) * 10 + (val & 0x0F); } uint8_t decToBcd(uint8_t val) { return ((val / 10) << 4) | (val % 10); } bool readRTC(struct tm *timeinfo) { if (!timeSyncStatus.rtcAvailable) return false; if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(50)) != pdTRUE) { return false; } rtcWire.beginTransmission(DS3231_ADDRESS); rtcWire.write(0x00); if (rtcWire.endTransmission() != 0) { xSemaphoreGive(rtcMutex); return false; } if (rtcWire.requestFrom(DS3231_ADDRESS, 7) != 7) { xSemaphoreGive(rtcMutex); return false; } uint8_t buffer[7]; for (int i = 0; i < 7; i++) { buffer[i] = rtcWire.read(); } xSemaphoreGive(rtcMutex); timeinfo->tm_sec = bcdToDec(buffer0 & 0x7F); timeinfo->tm_min = bcdToDec(buffer[1] & 0x7F); timeinfo->tm_hour = bcdToDec(buffer[2] & 0x3F); timeinfo->tm_wday = bcdToDec(buffer[3] & 0x07) - 1; timeinfo->tm_mday = bcdToDec(buffer[4] & 0x3F); timeinfo->tm_mon = bcdToDec(buffer[5] & 0x1F) - 1; timeinfo->tm_year = bcdToDec(buffer[6]) + 100; return true; } bool writeRTC(const struct tm *timeinfo) { if (!timeSyncStatus.rtcAvailable) return false; if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(50)) != pdTRUE) { return false; } rtcWire.beginTransmission(DS3231_ADDRESS); rtcWire.write(0x00); rtcWire.write(decToBcd(timeinfo->tm_sec)); rtcWire.write(decToBcd(timeinfo->tm_min)); rtcWire.write(decToBcd(timeinfo->tm_hour)); rtcWire.write(decToBcd(timeinfo->tm_wday + 1)); rtcWire.write(decToBcd(timeinfo->tm_mday)); rtcWire.write(decToBcd(timeinfo->tm_mon + 1)); rtcWire.write(decToBcd(timeinfo->tm_year - 100)); bool success = (rtcWire.endTransmission() == 0); xSemaphoreGive(rtcMutex); return success; } void timeSyncCallback(struct timeval *tv) { Serial.println("✓ NTP 시간 동기화 완료"); timeSyncStatus.synchronized = true; timeSyncStatus.lastSyncTime = (uint64_t)tv->tv_sec * 1000000ULL + tv->tv_usec; timeSyncStatus.syncCount++; if (timeSyncStatus.rtcAvailable) { struct tm timeinfo; time_t now = tv->tv_sec; localtime_r(&now, &timeinfo); if (writeRTC(&timeinfo)) { timeSyncStatus.rtcSyncCount++; } } } void initNTP() { configTime(9 * 3600, 0, "pool.ntp.org", "time.nist.gov", "time.google.com"); sntp_set_time_sync_notification_cb(timeSyncCallback); } void rtcSyncTask(void *parameter) { const TickType_t xDelay = pdMS_TO_TICKS(RTC_SYNC_INTERVAL_MS); while (1) { if (timeSyncStatus.rtcAvailable) { struct tm timeinfo; if (readRTC(&timeinfo)) { time_t now = mktime(&timeinfo); struct timeval tv = { .tv_sec = now, .tv_usec = 0 }; settimeofday(&tv, NULL); timeSyncStatus.synchronized = true; timeSyncStatus.lastSyncTime = (uint64_t)now * 1000000ULL; timeSyncStatus.rtcSyncCount++; } } vTaskDelay(xDelay); } } // ======================================== // MCP2515 모드 설정 // ======================================== bool setMCP2515Mode(MCP2515Mode mode) { const char* modeName; MCP2515::ERROR result; switch (mode) { case MCP_MODE_NORMAL: result = mcp2515.setNormalMode(); modeName = "Normal"; break; case MCP_MODE_LISTEN_ONLY: result = mcp2515.setListenOnlyMode(); modeName = "Listen-Only"; break; case MCP_MODE_LOOPBACK: result = mcp2515.setLoopbackMode(); modeName = "Loopback"; break; case MCP_MODE_TRANSMIT: result = mcp2515.setListenOnlyMode(); modeName = "Transmit-Only"; break; default: return false; } if (result == MCP2515::ERROR_OK) { currentMcpMode = mode; Serial.printf("✓ MCP2515 모드: %s\n", modeName); return true; } return false; } // ======================================== // CAN 인터럽트 핸들러 // ======================================== void IRAM_ATTR canISR() { BaseType_t xHigherPriorityTaskWoken = pdFALSE; if (canRxTaskHandle != NULL) { vTaskNotifyGiveFromISR(canRxTaskHandle, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } } // ======================================== // Serial RX Task (Priority 18) // ======================================== void serialRxTask(void *parameter) { SerialMessage serialMsg; uint8_t lineBuffer[MAX_SERIAL_LINE_LEN]; uint16_t lineIndex = 0; uint32_t lastActivity = millis(); Serial.println("✓ Serial RX Task 시작 (Priority 18)"); while (1) { while (SerialComm.available()) { uint8_t c = SerialComm.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 (xQueueSend(serialQueue, &serialMsg, 0) == pdTRUE) { totalSerialRxCount++; } 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 (xQueueSend(serialQueue, &serialMsg, 0) == pdTRUE) { totalSerialRxCount++; } lineIndex = 0; } vTaskDelay(1); } } // ======================================== // CAN RX Task (Priority 24 - 최고) // ======================================== void canRxTask(void *parameter) { struct can_frame frame; CANMessage msg; Serial.println("✓ CAN RX Task 시작 (Priority 24 - 최고)"); while (1) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) { struct timeval tv; gettimeofday(&tv, NULL); msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; msg.id = frame.can_id & 0x1FFFFFFF; msg.dlc = frame.can_dlc; memcpy(msg.data, frame.data, 8); if (xQueueSend(canQueue, &msg, 0) == pdTRUE) { totalMsgCount++; } } } } // ======================================== // SD Write Task (Priority 20 - 매우 높음) // ======================================== void sdWriteTask(void *parameter) { CANMessage canMsg; uint32_t csvFlushCounter = 0; Serial.println("✓ SD Write Task 시작 (Priority 20)"); while (1) { bool hasWork = false; int batchCount = 0; while (batchCount < 100 && xQueueReceive(canQueue, &canMsg, 0) == pdTRUE) { hasWork = true; batchCount++; // 실시간 모니터링 업데이트 bool found = false; for (int i = 0; i < RECENT_MSG_COUNT; i++) { if (recentData[i].msg.id == canMsg.id) { recentData[i].msg = canMsg; recentData[i].count++; found = true; break; } } if (!found) { for (int i = 0; i < RECENT_MSG_COUNT; i++) { if (recentData[i].count == 0) { recentData[i].msg = canMsg; recentData[i].count = 1; break; } } } // CAN 로깅 if (loggingEnabled && sdCardReady) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(10)) == pdTRUE) { if (canLogFormatCSV) { char csvLine[128]; uint64_t relativeTime = canMsg.timestamp_us - canLogStartTime; char dataStr[32]; int dataLen = 0; for (int i = 0; i < canMsg.dlc; i++) { dataLen += sprintf(&dataStr[dataLen], "%02X", canMsg.data[i]); if (i < canMsg.dlc - 1) dataStr[dataLen++] = ' '; } dataStr[dataLen] = '\0'; int lineLen = snprintf(csvLine, sizeof(csvLine), "%llu,0x%X,%d,%s\n", relativeTime, canMsg.id, canMsg.dlc, dataStr); if (logFile) { logFile.write((uint8_t*)csvLine, lineLen); currentFileSize += lineLen; if (++csvFlushCounter >= 500) { logFile.flush(); csvFlushCounter = 0; } } } else { // BIN 형식 if (bufferIndex + sizeof(CANMessage) <= FILE_BUFFER_SIZE) { memcpy(&fileBuffer[bufferIndex], &canMsg, sizeof(CANMessage)); bufferIndex += sizeof(CANMessage); currentFileSize += sizeof(CANMessage); } if (bufferIndex >= FILE_BUFFER_SIZE * 0.9) { if (logFile) { logFile.write(fileBuffer, bufferIndex); logFile.flush(); bufferIndex = 0; } } } xSemaphoreGive(sdMutex); } } } if (!hasWork) { vTaskDelay(1); } else { taskYIELD(); } } } // ======================================== // SD Monitor Task (Priority 5) // ======================================== void sdMonitorTask(void *parameter) { const TickType_t xDelay = pdMS_TO_TICKS(1000); uint32_t lastStatusPrint = 0; Serial.println("✓ SD Monitor Task 시작 (Priority 5)"); while (1) { uint32_t currentTime = millis(); // 메시지/초 계산 if (currentTime - lastMsgCountTime >= 1000) { msgPerSecond = totalMsgCount - lastMsgCount; lastMsgCount = totalMsgCount; lastMsgCountTime = currentTime; } // 전압 체크 if (currentTime - powerStatus.lastCheck >= VOLTAGE_CHECK_INTERVAL_MS) { float rawVoltage = analogRead(MONITORING_VOLT) * (3.3 / 4095.0); powerStatus.voltage = rawVoltage * 1.0; if (currentTime - powerStatus.lastMinReset >= 1000) { powerStatus.minVoltage = powerStatus.voltage; powerStatus.lastMinReset = currentTime; } else { if (powerStatus.voltage < powerStatus.minVoltage) { powerStatus.minVoltage = powerStatus.voltage; } } powerStatus.lowVoltage = (powerStatus.voltage < LOW_VOLTAGE_THRESHOLD); powerStatus.lastCheck = currentTime; } // 10초마다 상태 출력 if (currentTime - lastStatusPrint >= 10000) { uint32_t canQueueUsed = uxQueueMessagesWaiting(canQueue); uint32_t serialQueueUsed = uxQueueMessagesWaiting(serialQueue); float canQueuePercent = (float)canQueueUsed / CAN_QUEUE_SIZE * 100.0; float serialQueuePercent = (float)serialQueueUsed / SERIAL_QUEUE_SIZE * 100.0; Serial.printf("[상태] CAN: %u msg/s | CAN큐: %u/%u (%.1f%%) | Serial큐: %u/%u (%.1f%%) | PSRAM: %u KB\n", msgPerSecond, canQueueUsed, CAN_QUEUE_SIZE, canQueuePercent, serialQueueUsed, SERIAL_QUEUE_SIZE, serialQueuePercent, ESP.getFreePsram() / 1024); if (canQueuePercent >= 80.0) { Serial.printf("⚠️ 경고: CAN Queue 사용률 %.1f%%\n", canQueuePercent); } if (serialQueuePercent >= 80.0) { Serial.printf("⚠️ 경고: Serial Queue 사용률 %.1f%%\n", serialQueuePercent); } lastStatusPrint = currentTime; } vTaskDelay(xDelay); } } // ======================================== // TX Task (Priority 15) // ======================================== void txTask(void *parameter) { struct can_frame frame; Serial.println("✓ TX Task 시작 (Priority 15)"); while (1) { uint32_t now = millis(); bool anyActive = false; for (int i = 0; i < MAX_TX_MESSAGES; i++) { if (txMessages[i].active && txMessages[i].interval > 0) { anyActive = true; if (now - txMessages[i].lastSent >= txMessages[i].interval) { if (currentMcpMode == MCP_MODE_TRANSMIT) { mcp2515.setNormalMode(); } frame.can_id = txMessages[i].id; if (txMessages[i].extended) { frame.can_id |= CAN_EFF_FLAG; } frame.can_dlc = txMessages[i].dlc; memcpy(frame.data, txMessages[i].data, 8); if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) { totalTxCount++; txMessages[i].lastSent = now; } if (currentMcpMode == MCP_MODE_TRANSMIT) { mcp2515.setListenOnlyMode(); } } } } vTaskDelay(pdMS_TO_TICKS(anyActive ? 1 : 10)); } } // ======================================== // Sequence Task (Priority 12) // ======================================== void sequenceTask(void *parameter) { Serial.println("✓ Sequence Task 시작 (Priority 12)"); while (1) { if (seqRuntime.running && seqRuntime.activeSequenceIndex >= 0 && seqRuntime.activeSequenceIndex < sequenceCount) { CANSequence* seq = &sequences[seqRuntime.activeSequenceIndex]; uint32_t now = millis(); if (seqRuntime.currentStep < seq->stepCount) { SequenceStep* step = &seq->steps[seqRuntime.currentStep]; if (now - seqRuntime.lastStepTime >= step->delayMs) { if (currentMcpMode == MCP_MODE_TRANSMIT) { mcp2515.setNormalMode(); } struct can_frame frame; frame.can_id = step->canId; if (step->extended) { frame.can_id |= CAN_EFF_FLAG; } frame.can_dlc = step->dlc; memcpy(frame.data, step->data, 8); if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) { totalTxCount++; } if (currentMcpMode == MCP_MODE_TRANSMIT) { mcp2515.setListenOnlyMode(); } seqRuntime.currentStep++; seqRuntime.lastStepTime = now; } } else { if (seq->repeatMode == 0) { seqRuntime.running = false; } else if (seq->repeatMode == 1) { seqRuntime.currentRepeat++; if (seqRuntime.currentRepeat >= seq->repeatCount) { seqRuntime.running = false; } else { seqRuntime.currentStep = 0; seqRuntime.lastStepTime = now; } } else if (seq->repeatMode == 2) { seqRuntime.currentStep = 0; seqRuntime.lastStepTime = now; } } vTaskDelay(pdMS_TO_TICKS(1)); } else { vTaskDelay(pdMS_TO_TICKS(10)); } } } // ======================================== // 파일 커멘트 관리 // ======================================== void saveFileComments() { if (!sdCardReady) return; if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { File commentFile = SD.open("/comments.dat", FILE_WRITE); if (commentFile) { commentFile.write((uint8_t*)&commentCount, sizeof(commentCount)); commentFile.write((uint8_t*)fileComments, sizeof(FileComment) * commentCount); commentFile.close(); } xSemaphoreGive(sdMutex); } } void loadFileComments() { if (!sdCardReady) return; if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { if (SD.exists("/comments.dat")) { File commentFile = SD.open("/comments.dat", FILE_READ); if (commentFile) { commentFile.read((uint8_t*)&commentCount, sizeof(commentCount)); if (commentCount > MAX_FILE_COMMENTS) commentCount = MAX_FILE_COMMENTS; commentFile.read((uint8_t*)fileComments, sizeof(FileComment) * commentCount); commentFile.close(); } } xSemaphoreGive(sdMutex); } } const char* getFileComment(const char* filename) { for (int i = 0; i < commentCount; i++) { if (strcmp(fileComments[i].filename, filename) == 0) { return fileComments[i].comment; } } return ""; } void addFileComment(const char* filename, const char* comment) { for (int i = 0; i < commentCount; i++) { if (strcmp(fileComments[i].filename, filename) == 0) { strncpy(fileComments[i].comment, comment, MAX_COMMENT_LEN - 1); fileComments[i].comment[MAX_COMMENT_LEN - 1] = '\0'; saveFileComments(); return; } } if (commentCount < MAX_FILE_COMMENTS) { strncpy(fileComments[commentCount].filename, filename, MAX_FILENAME_LEN - 1); fileComments[commentCount].filename[MAX_FILENAME_LEN - 1] = '\0'; strncpy(fileComments[commentCount].comment, comment, MAX_COMMENT_LEN - 1); fileComments[commentCount].comment[MAX_COMMENT_LEN - 1] = '\0'; commentCount++; saveFileComments(); } } // ======================================== // 시퀀스 저장/로드 // ======================================== void saveSequences() { if (!sdCardReady) return; if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { File seqFile = SD.open("/sequences.dat", FILE_WRITE); if (seqFile) { seqFile.write((uint8_t*)&sequenceCount, sizeof(sequenceCount)); seqFile.write((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount); seqFile.close(); } xSemaphoreGive(sdMutex); } } void loadSequences() { if (!sdCardReady) return; if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { if (SD.exists("/sequences.dat")) { File seqFile = SD.open("/sequences.dat", FILE_READ); if (seqFile) { seqFile.read((uint8_t*)&sequenceCount, sizeof(sequenceCount)); if (sequenceCount > MAX_SEQUENCES) sequenceCount = MAX_SEQUENCES; seqFile.read((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount); seqFile.close(); } } xSemaphoreGive(sdMutex); } } // ======================================== // WebSocket 이벤트 처리 // ======================================== void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) { if (type == WStype_TEXT) { DynamicJsonDocument doc(2048); DeserializationError error = deserializeJson(doc, payload); if (error) return; const char* cmd = doc["cmd"]; if (strcmp(cmd, "getSettings") == 0) { DynamicJsonDocument response(1024); response["type"] = "settings"; response["ssid"] = wifiSSID; response["password"] = wifiPassword; response["staEnable"] = enableSTAMode; response["staSSID"] = staSSID; response["staPassword"] = staPassword; response["staConnected"] = (WiFi.status() == WL_CONNECTED); response["staIP"] = WiFi.localIP().toString(); String json; serializeJson(response, json); webSocket.sendTXT(num, json); } else if (strcmp(cmd, "saveSettings") == 0) { const char* newSSID = doc["ssid"]; const char* newPassword = doc["password"]; bool newSTAEnable = doc["staEnable"]; const char* newSTASSID = doc["staSSID"]; const char* newSTAPassword = doc["staPassword"]; if (newSSID && strlen(newSSID) > 0) { strncpy(wifiSSID, newSSID, sizeof(wifiSSID) - 1); wifiSSID[sizeof(wifiSSID) - 1] = '\0'; } if (newPassword) { strncpy(wifiPassword, newPassword, sizeof(wifiPassword) - 1); wifiPassword[sizeof(wifiPassword) - 1] = '\0'; } enableSTAMode = newSTAEnable; if (newSTASSID) { strncpy(staSSID, newSTASSID, sizeof(staSSID) - 1); staSSID[sizeof(staSSID) - 1] = '\0'; } if (newSTAPassword) { strncpy(staPassword, newSTAPassword, sizeof(staPassword) - 1); staPassword[sizeof(staPassword) - 1] = '\0'; } saveSettings(); DynamicJsonDocument response(256); response["type"] = "settingsSaved"; response["success"] = true; String json; serializeJson(response, json); webSocket.sendTXT(num, json); } else if (strcmp(cmd, "startLogging") == 0) { if (!loggingEnabled && sdCardReady) { const char* format = doc["format"]; if (format && strcmp(format, "csv") == 0) { canLogFormatCSV = true; } else { canLogFormatCSV = false; } 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); canLogStartTime = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; const char* ext = canLogFormatCSV ? "csv" : "bin"; snprintf(currentFilename, sizeof(currentFilename), "/CAN_%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); logFile = SD.open(currentFilename, FILE_WRITE); if (logFile) { if (canLogFormatCSV) { logFile.println("Time_us,CAN_ID,DLC,Data"); } loggingEnabled = true; bufferIndex = 0; currentFileSize = logFile.size(); Serial.printf("✓ CAN 로깅 시작: %s (%s)\n", currentFilename, canLogFormatCSV ? "CSV" : "BIN"); } xSemaphoreGive(sdMutex); } } } else if (strcmp(cmd, "stopLogging") == 0) { if (loggingEnabled) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { if (bufferIndex > 0 && logFile) { logFile.write(fileBuffer, bufferIndex); bufferIndex = 0; } if (logFile) { logFile.close(); } loggingEnabled = false; Serial.printf("✓ CAN 로깅 종료: %u bytes\n", currentFileSize); xSemaphoreGive(sdMutex); } } } else if (strcmp(cmd, "startSerialLogging") == 0) { if (!serialLoggingEnabled && sdCardReady) { const char* format = doc["format"]; if (format && strcmp(format, "bin") == 0) { serialLogFormatCSV = false; } else { serialLogFormatCSV = 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); serialLogStartTime = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; const char* ext = serialLogFormatCSV ? "csv" : "bin"; snprintf(currentSerialFilename, sizeof(currentSerialFilename), "/SER_%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); serialLogFile = SD.open(currentSerialFilename, FILE_WRITE); if (serialLogFile) { if (serialLogFormatCSV) { serialLogFile.println("Time_us,Direction,Data"); } serialLoggingEnabled = true; serialCsvIndex = 0; currentSerialFileSize = serialLogFile.size(); Serial.printf("✓ Serial 로깅 시작: %s\n", currentSerialFilename); } xSemaphoreGive(sdMutex); } } } else if (strcmp(cmd, "stopSerialLogging") == 0) { if (serialLoggingEnabled) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { if (serialCsvIndex > 0 && serialLogFile) { serialLogFile.write((uint8_t*)serialCsvBuffer, serialCsvIndex); serialCsvIndex = 0; } if (serialLogFile) { serialLogFile.close(); } serialLoggingEnabled = false; Serial.printf("✓ Serial 로깅 종료: %u bytes\n", currentSerialFileSize); xSemaphoreGive(sdMutex); } } } else if (strcmp(cmd, "sendSerial") == 0) { const char* data = doc["data"]; if (data && strlen(data) > 0) { SerialComm.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 (xQueueSend(serialQueue, &serialMsg, pdMS_TO_TICKS(10)) == pdTRUE) { totalSerialTxCount++; } } } else if (strcmp(cmd, "setSerialConfig") == 0) { serialSettings.baudRate = doc["baudRate"] | 115200; serialSettings.dataBits = doc["dataBits"] | 8; serialSettings.parity = doc["parity"] | 0; serialSettings.stopBits = doc["stopBits"] | 1; saveSerialSettings(); applySerialSettings(); } else if (strcmp(cmd, "getSerialConfig") == 0) { DynamicJsonDocument response(512); response["type"] = "serialConfig"; response["baudRate"] = serialSettings.baudRate; response["dataBits"] = serialSettings.dataBits; response["parity"] = serialSettings.parity; response["stopBits"] = serialSettings.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) { setMCP2515Mode((MCP2515Mode)mode); saveSettings(); } } else if (strcmp(cmd, "syncTime") == 0) { uint64_t phoneTime = doc["time"]; if (phoneTime > 0) { time_t seconds = phoneTime / 1000; suseconds_t microseconds = (phoneTime % 1000) * 1000; struct timeval tv = {seconds, microseconds}; settimeofday(&tv, NULL); timeSyncStatus.synchronized = true; timeSyncStatus.lastSyncTime = phoneTime * 1000; timeSyncStatus.syncCount++; if (timeSyncStatus.rtcAvailable) { struct tm timeinfo; localtime_r(&seconds, &timeinfo); writeRTC(&timeinfo); } } } else if (strcmp(cmd, "getFiles") == 0) { if (sdCardReady) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { DynamicJsonDocument response(6144); response["type"] = "files"; JsonArray files = response.createNestedArray("list"); 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) { JsonObject fileObj = files.createNestedObject(); fileObj["name"] = filename; fileObj["size"] = file.size(); const char* comment = getFileComment(filename); if (strlen(comment) > 0) { fileObj["comment"] = comment; } } } file = root.openNextFile(); } xSemaphoreGive(sdMutex); String json; serializeJson(response, json); webSocket.sendTXT(num, json); } } } else if (strcmp(cmd, "deleteFile") == 0) { const char* filename = doc["filename"]; if (filename && strlen(filename) > 0) { String fullPath = "/" + String(filename); if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { bool success = false; if (SD.exists(fullPath)) { if (SD.remove(fullPath)) { success = true; } } xSemaphoreGive(sdMutex); DynamicJsonDocument response(256); response["type"] = "deleteResult"; response["success"] = success; String json; serializeJson(response, json); webSocket.sendTXT(num, json); } } } else if (strcmp(cmd, "addComment") == 0) { const char* filename = doc["filename"]; const char* comment = doc["comment"]; if (filename && comment) { addFileComment(filename, comment); } } else if (strcmp(cmd, "addTx") == 0) { for (int i = 0; i < MAX_TX_MESSAGES; i++) { if (!txMessages[i].active) { txMessages[i].id = strtoul(doc["id"], NULL, 16); txMessages[i].extended = doc["ext"] | false; txMessages[i].dlc = doc["dlc"] | 8; JsonArray dataArray = doc["data"]; for (int j = 0; j < 8; j++) { txMessages[i].data[j] = dataArray[j] | 0; } txMessages[i].interval = doc["interval"] | 1000; txMessages[i].active = true; txMessages[i].lastSent = 0; break; } } } else if (strcmp(cmd, "removeTx") == 0) { uint32_t id = strtoul(doc["id"], NULL, 16); for (int i = 0; i < MAX_TX_MESSAGES; i++) { if (txMessages[i].active && txMessages[i].id == id) { txMessages[i].active = false; break; } } } else if (strcmp(cmd, "sendOnce") == 0) { if (currentMcpMode == MCP_MODE_TRANSMIT) { mcp2515.setNormalMode(); } struct can_frame frame; frame.can_id = strtoul(doc["id"], NULL, 16); if (doc["ext"] | false) { frame.can_id |= CAN_EFF_FLAG; } frame.can_dlc = doc["dlc"] | 8; JsonArray dataArray = doc["data"]; for (int i = 0; i < 8; i++) { frame.data[i] = dataArray[i] | 0; } if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) { totalTxCount++; } if (currentMcpMode == MCP_MODE_TRANSMIT) { mcp2515.setListenOnlyMode(); } } } } // ======================================== // Web Update Task (Priority 8) // ======================================== void webUpdateTask(void *parameter) { const TickType_t xDelay = pdMS_TO_TICKS(100); Serial.println("✓ Web Update Task 시작 (Priority 8)"); while (1) { webSocket.loop(); if (webSocket.connectedClients() > 0) { DynamicJsonDocument doc(4096); doc["type"] = "update"; doc["logging"] = loggingEnabled; doc["serialLogging"] = serialLoggingEnabled; doc["sdReady"] = sdCardReady; doc["totalMsg"] = totalMsgCount; doc["msgPerSec"] = msgPerSecond; doc["totalTx"] = totalTxCount; doc["totalSerialRx"] = totalSerialRxCount; doc["totalSerialTx"] = totalSerialTxCount; doc["fileSize"] = currentFileSize; doc["serialFileSize"] = currentSerialFileSize; doc["queueUsed"] = uxQueueMessagesWaiting(canQueue); doc["queueSize"] = CAN_QUEUE_SIZE; doc["serialQueueUsed"] = uxQueueMessagesWaiting(serialQueue); doc["serialQueueSize"] = SERIAL_QUEUE_SIZE; doc["timeSync"] = timeSyncStatus.synchronized; doc["rtcAvail"] = timeSyncStatus.rtcAvailable; doc["voltage"] = powerStatus.voltage; doc["mcpMode"] = (int)currentMcpMode; if (loggingEnabled && currentFilename[0] != '\0') { doc["currentFile"] = String(currentFilename); } if (serialLoggingEnabled && currentSerialFilename[0] != '\0') { doc["currentSerialFile"] = String(currentSerialFilename); } time_t now; time(&now); doc["timestamp"] = (uint64_t)now; JsonArray messages = doc.createNestedArray("messages"); for (int i = 0; i < RECENT_MSG_COUNT; i++) { if (recentData[i].count > 0) { JsonObject msgObj = messages.createNestedObject(); msgObj["id"] = recentData[i].msg.id; msgObj["dlc"] = recentData[i].msg.dlc; msgObj["count"] = recentData[i].count; JsonArray dataArray = msgObj.createNestedArray("data"); for (int j = 0; j < recentData[i].msg.dlc; j++) { dataArray.add(recentData[i].msg.data[j]); } } } SerialMessage serialMsg; JsonArray serialMessages = doc.createNestedArray("serialMessages"); int serialCount = 0; while (serialCount < 10 && xQueueReceive(serialQueue, &serialMsg, 0) == pdTRUE) { JsonObject serMsgObj = serialMessages.createNestedObject(); serMsgObj["timestamp"] = serialMsg.timestamp_us; serMsgObj["isTx"] = serialMsg.isTx; char dataStr[MAX_SERIAL_LINE_LEN + 1]; memcpy(dataStr, serialMsg.data, serialMsg.length); dataStr[serialMsg.length] = '\0'; serMsgObj["data"] = dataStr; serialCount++; if (serialLoggingEnabled && sdCardReady) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(10)) == pdTRUE) { if (serialLogFormatCSV) { uint64_t relativeTime = serialMsg.timestamp_us - serialLogStartTime; char csvLine[256]; int lineLen = snprintf(csvLine, sizeof(csvLine), "%llu,%s,\"%s\"\n", relativeTime, serialMsg.isTx ? "TX" : "RX", dataStr); if (serialCsvIndex + lineLen < SERIAL_CSV_BUFFER_SIZE) { memcpy(&serialCsvBuffer[serialCsvIndex], csvLine, lineLen); serialCsvIndex += lineLen; currentSerialFileSize += lineLen; } if (serialCsvIndex >= SERIAL_CSV_BUFFER_SIZE * 0.9) { if (serialLogFile) { serialLogFile.write((uint8_t*)serialCsvBuffer, serialCsvIndex); serialLogFile.flush(); serialCsvIndex = 0; } } } else { if (serialLogFile) { serialLogFile.write((uint8_t*)&serialMsg, sizeof(SerialMessage)); currentSerialFileSize += sizeof(SerialMessage); static int binFlushCounter = 0; if (++binFlushCounter >= 50) { serialLogFile.flush(); binFlushCounter = 0; } } } xSemaphoreGive(sdMutex); } } } String json; serializeJson(doc, json); webSocket.broadcastTXT(json); } vTaskDelay(xDelay); } } // ======================================== // Setup // ======================================== void setup() { Serial.begin(115200); delay(1000); Serial.println("\n========================================"); Serial.println(" Byun CAN Logger - PSRAM Edition"); Serial.println(" Version 2.5 - Full Optimization"); Serial.println("========================================\n"); // PSRAM 확인 if (!psramFound()) { Serial.println("✗ PSRAM 없음!"); Serial.println(" Arduino IDE → Tools → PSRAM → OPI PSRAM"); while (1) delay(1000); } Serial.printf("✓ PSRAM 감지: %d MB\n", ESP.getPsramSize() / 1024 / 1024); Serial.printf(" 초기 여유: %u KB\n", ESP.getFreePsram() / 1024); // PSRAM 버퍼 할당 if (!allocatePSRAMBuffers()) { Serial.println("\n✗ PSRAM 버퍼 할당 실패!"); while (1) delay(1000); } // 설정 로드 loadSettings(); // GPIO 초기화 pinMode(CAN_INT_PIN, INPUT_PULLUP); analogSetPinAttenuation(MONITORING_VOLT, ADC_11db); analogSetAttenuation(ADC_11db); memset(recentData, 0, sizeof(recentData)); memset(txMessages, 0, sizeof(txMessages)); memset(fileComments, 0, sizeof(fileComments)); // SPI 초기화 Serial.println("========================================"); Serial.println("SPI 초기화..."); Serial.println("========================================"); hspi.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS); hspi.setFrequency(20000000); pinMode(VSPI_CS, OUTPUT); digitalWrite(VSPI_CS, HIGH); vspi.begin(VSPI_SCLK, VSPI_MISO, VSPI_MOSI, VSPI_CS); vspi.setFrequency(40000000); Serial.println("✓ SPI 초기화 완료\n"); // Watchdog 비활성화 esp_task_wdt_deinit(); // MCP2515 초기화 Serial.println("========================================"); Serial.println("MCP2515 초기화..."); Serial.println("========================================"); mcp2515.reset(); delay(50); mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ); setMCP2515Mode(currentMcpMode); Serial.println("✓ MCP2515 초기화 완료\n"); // Serial 통신 초기화 applySerialSettings(); // Mutex 생성 Serial.println("========================================"); Serial.println("Mutex 생성..."); Serial.println("========================================"); sdMutex = xSemaphoreCreateMutex(); rtcMutex = xSemaphoreCreateMutex(); serialMutex = xSemaphoreCreateMutex(); if (!sdMutex || !rtcMutex || !serialMutex) { Serial.println("✗ Mutex 생성 실패!"); while (1) delay(1000); } Serial.println("✓ Mutex 생성 완료 (DRAM)\n"); // Queue 생성 if (!createQueues()) { Serial.println("✗ Queue 생성 실패!"); while (1) delay(1000); } // RTC 초기화 Serial.println("========================================"); Serial.println("RTC 초기화..."); Serial.println("========================================"); initRTC(); Serial.println(); // SD 카드 초기화 Serial.println("========================================"); Serial.println("SD 카드 초기화..."); Serial.println("========================================"); if (SD.begin(VSPI_CS, vspi)) { sdCardReady = true; Serial.println("✓ SD 카드 초기화 완료"); loadFileComments(); loadSequences(); } else { Serial.println("✗ SD 카드 초기화 실패"); } Serial.println(); // WiFi 설정 Serial.println("========================================"); Serial.println("WiFi 초기화..."); Serial.println("========================================"); if (enableSTAMode && strlen(staSSID) > 0) { WiFi.mode(WIFI_AP_STA); WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4); WiFi.begin(staSSID, staPassword); Serial.printf("AP SSID: %s\n", wifiSSID); Serial.printf("AP IP: %s\n", WiFi.softAPIP().toString().c_str()); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 20) { delay(500); Serial.print("."); attempts++; } Serial.println(); if (WiFi.status() == WL_CONNECTED) { Serial.printf("✓ STA 연결: %s\n", WiFi.localIP().toString().c_str()); initNTP(); } else { Serial.println("! STA 연결 실패 (AP 모드는 정상)"); } } else { WiFi.mode(WIFI_AP); WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4); Serial.printf("AP SSID: %s\n", wifiSSID); Serial.printf("AP IP: %s\n", WiFi.softAPIP().toString().c_str()); } WiFi.setSleep(false); esp_wifi_set_max_tx_power(84); Serial.println(); // WebSocket & Server webSocket.begin(); webSocket.onEvent(webSocketEvent); server.on("/", HTTP_GET, []() { server.send_P(200, "text/html", index_html); }); server.on("/transmit", HTTP_GET, []() { server.send_P(200, "text/html", transmit_html); }); server.on("/graph", HTTP_GET, []() { server.send_P(200, "text/html", graph_html); }); server.on("/graph-view", HTTP_GET, []() { server.send_P(200, "text/html", graph_viewer_html); }); server.on("/settings", HTTP_GET, []() { 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"); if (SD.exists(filename)) { File file = SD.open(filename, FILE_READ); if (file) { server.sendHeader("Content-Disposition", "attachment; filename=\"" + server.arg("file") + "\""); server.streamFile(file, "application/octet-stream"); file.close(); } } } }); server.begin(); // CAN 인터럽트 attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING); // Task 생성 Serial.println("========================================"); Serial.println("Task 생성 (우선순위 최적화)..."); Serial.println("========================================"); xTaskCreatePinnedToCore(canRxTask, "CAN_RX", STACK_CAN_RX, NULL, PRIORITY_CAN_RX, &canRxTaskHandle, 1); xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", STACK_SD_WRITE, NULL, PRIORITY_SD_WRITE, &sdWriteTaskHandle, 1); xTaskCreatePinnedToCore(sequenceTask, "SEQ_TASK", STACK_SEQUENCE, NULL, PRIORITY_SEQUENCE, NULL, 1); xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", STACK_SERIAL_RX, NULL, PRIORITY_SERIAL_RX, &serialRxTaskHandle, 0); xTaskCreatePinnedToCore(txTask, "TX_TASK", STACK_TX_TASK, NULL, PRIORITY_TX_TASK, NULL, 0); xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", STACK_WEB_UPDATE, NULL, PRIORITY_WEB_UPDATE, &webTaskHandle, 0); xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", STACK_SD_MONITOR, NULL, PRIORITY_SD_MONITOR, NULL, 0); if (timeSyncStatus.rtcAvailable) { xTaskCreatePinnedToCore(rtcSyncTask, "RTC_SYNC", STACK_RTC_SYNC, NULL, PRIORITY_RTC_SYNC, &rtcTaskHandle, 0); } Serial.println("✓ 모든 Task 생성 완료\n"); // 최종 메모리 상태 Serial.println("========================================"); Serial.println("최종 메모리 상태"); Serial.println("========================================"); Serial.printf("PSRAM 사용: %u KB\n", (ESP.getPsramSize() - ESP.getFreePsram()) / 1024); Serial.printf("PSRAM 여유: %u KB\n", ESP.getFreePsram() / 1024); Serial.printf("DRAM 여유: %u KB\n", ESP.getFreeHeap() / 1024); Serial.println(); Serial.println("========================================"); Serial.println("시스템 준비 완료!"); Serial.println("========================================"); Serial.printf("접속: http://%s\n", WiFi.softAPIP().toString().c_str()); Serial.println("페이지:"); Serial.println(" - Monitor : /"); Serial.println(" - Transmit : /transmit"); Serial.println(" - Graph : /graph"); Serial.println(" - Settings : /settings"); Serial.println(" - Serial : /serial"); Serial.println("========================================\n"); } void loop() { server.handleClient(); vTaskDelay(pdMS_TO_TICKS(10)); static uint32_t lastPrint = 0; if (millis() - lastPrint > 30000) { Serial.printf("[30초 통계] CAN: %lu msg/s | Queue: %d/%d (%.1f%%) | PSRAM: %u KB\n", msgPerSecond, uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE, (float)uxQueueMessagesWaiting(canQueue) / CAN_QUEUE_SIZE * 100.0, ESP.getFreePsram() / 1024); lastPrint = millis(); } }