diff --git a/aa.ino b/aa.ino index ba92cd6..e6080d1 100644 --- a/aa.ino +++ b/aa.ino @@ -1,46 +1,395 @@ -// PSRAM에 Queue 버퍼 할당 -uint8_t *canQueueStorage = nullptr; -uint8_t *serialQueueStorage = nullptr; +/* + * 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; -bool allocateQueueBuffers() { - // CAN Queue 버퍼 (PSRAM) +// ======================================== +// 전역 변수 +// ======================================== + +// 하드웨어 객체 +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.println("✗ CAN Queue 버퍼 할당 실패!"); + Serial.printf("✗ CAN Queue 버퍼 할당 실패 (%.1f KB)\n", canQueueSize / 1024.0); + heap_caps_free(fileBuffer); + heap_caps_free(serialCsvBuffer); return false; } + memset(canQueueStorage, 0, canQueueSize); - // Serial Queue 버퍼 (PSRAM) + // 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.println("✗ Serial Queue 버퍼 할당 실패!"); + 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.printf("✓ CAN Queue 버퍼: %.1f KB (PSRAM)\n", canQueueSize / 1024.0); - Serial.printf("✓ Serial Queue 버퍼: %.1f KB (PSRAM)\n", serialQueueSize / 1024.0); + // 메모리 정보 출력 + 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 버퍼 사용) -void createQueues() { +// ======================================== + +bool createQueues() { + Serial.println("========================================"); + Serial.println("Queue 생성 중 (PSRAM 버퍼 사용)..."); + Serial.println("========================================"); + + // CAN Queue (PSRAM 버퍼 사용) canQueue = xQueueCreateStatic( - CAN_QUEUE_SIZE, // Queue 길이 - sizeof(CANMessage), // 아이템 크기 - canQueueStorage, // PSRAM 버퍼 - &canQueueBuffer // 정적 Queue 구조체 + 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), @@ -48,10 +397,1570 @@ void createQueues() { &serialQueueBuffer ); - if (canQueue == NULL || serialQueue == NULL) { + 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); } - Serial.println("✓ Queue 생성 완료 (PSRAM)"); + // 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(); + } }