diff --git a/aa.ino b/aa.ino index 766eab8..2c3e84d 100644 --- a/aa.ino +++ b/aa.ino @@ -1,139 +1,112 @@ /* - * Byun CAN Logger with Web Interface + Serial Terminal - * Version: 2.5 - ESP32-S3 PSRAM Full Optimized Edition - * Complete PSRAM allocation with memory verification + * ============================================================================ + * Byun CAN Logger v2.5 PSRAM Edition - Final Release + * ============================================================================ + * + * ESP32-S3 기반 고성능 CAN Bus & Serial 데이터 로거 + * + * 주요 기능: + * - 1Mbps CAN Bus 통신 (MCP2515) + * - RS232/UART Serial 통신 + * - PSRAM 활용 대용량 큐 (CAN: 10,000개, Serial: 2,000개) + * - 실시간 웹 모니터링 (WebSocket) + * - SD 카드 로깅 (CSV/BIN 형식) + * - CAN 시퀀스 전송 + * - DBC 기반 실시간 그래프 + * - WiFi AP/APSTA 모드 + * - NTP 시간 동기화 + * + * 하드웨어: + * - ESP32-S3 (PSRAM 8MB) + * - MCP2515 CAN Controller + * - SD Card Module + * - RS232 Transceiver (Optional) + * + * 작성자: Byun + * 버전: 2.5 Final + * 날짜: 2024 + * ============================================================================ */ -#include -#include -#include -#include -#include #include -#include -#include -#include -#include +#include #include +#include +#include +#include #include -#include -#include -#include -#include -#include #include #include + +// ============================================================================ +// HTML 페이지 포함 +// ============================================================================ #include "index.h" -#include "transmit.h" #include "graph.h" #include "graph_viewer.h" -#include "settings.h" +#include "transmit.h" #include "serial_terminal.h" +#include "settings.h" -// ======================================== -// ESP32-S3 PSRAM 최적화 설정 -// ======================================== +// ============================================================================ +// 핀 정의 +// ============================================================================ +#define CAN_CS_PIN 5 // MCP2515 CS +#define SD_CS_PIN 15 // SD Card CS +#define VOLTAGE_PIN 4 // 전압 측정 (ADC) -// 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) +// Serial2 핀 정의 (ESP32-S3) +#define SERIAL2_RX 16 +#define SERIAL2_TX 17 +// ============================================================================ +// 상수 정의 +// ============================================================================ +#define RECENT_MSG_COUNT 100 // 최근 메시지 추적 개수 +#define MAX_SERIAL_LINE_LEN 256 // Serial 라인 최대 길이 +#define SEQUENCE_MAX_STEPS 50 // 시퀀스 최대 스텝 +#define MAX_SEQUENCES 10 // 최대 시퀀스 개수 -// 기타 상수 -#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 +// PSRAM 기반 대용량 큐 +#define CAN_QUEUE_SIZE 10000 // CAN 메시지 큐 (PSRAM) +#define SERIAL_QUEUE_SIZE 2000 // Serial 메시지 큐 (PSRAM) -// 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 LOW_VOLTAGE_THRESHOLD 11.0 // 저전압 경고 (12V 기준) +#define VOLTAGE_SAMPLES 10 // 전압 측정 샘플 수 -// 동기화 및 모니터링 설정 -#define RTC_SYNC_INTERVAL_MS 60000 -#define VOLTAGE_CHECK_INTERVAL_MS 5000 -#define LOW_VOLTAGE_THRESHOLD 3.0 -#define MONITORING_VOLT 5 +// WebSocket 업데이트 주기 +#define WEB_UPDATE_INTERVAL 500 // 500ms -// ======================================== -// 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 - -// ======================================== +// ============================================================================ // 구조체 정의 -// ======================================== +// ============================================================================ +// CAN 메시지 구조체 struct CANMessage { - uint64_t timestamp_us; uint32_t id; uint8_t dlc; uint8_t data[8]; -} __attribute__((packed)); + uint32_t timestamp_ms; + bool extended; +}; +// CAN 데이터 추적 구조체 +struct CANData { + struct can_frame msg; + uint32_t count; + uint32_t lastUpdate; +}; + +// Serial 메시지 구조체 struct SerialMessage { uint64_t timestamp_us; + char data[MAX_SERIAL_LINE_LEN]; 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; }; +// CAN 시퀀스 스텝 struct SequenceStep { uint32_t canId; bool extended; @@ -142,1705 +115,267 @@ struct SequenceStep { uint32_t delayMs; }; +// CAN 시퀀스 struct CANSequence { char name[32]; - SequenceStep steps[20]; + uint8_t repeatMode; // 0=Once, 1=Count, 2=Infinite + uint16_t repeatCount; uint8_t stepCount; - uint8_t repeatMode; - uint32_t repeatCount; + SequenceStep steps[SEQUENCE_MAX_STEPS]; }; +// 시퀀스 실행 상태 struct SequenceRuntime { bool running; - uint8_t currentStep; - uint32_t currentRepeat; - uint32_t lastStepTime; int8_t activeSequenceIndex; + uint8_t currentStep; + uint16_t currentRepeat; + uint32_t lastStepTime; }; -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] = ""; +struct WiFiSettings { + char ssid[32]; + char password[64]; + bool staEnable; + char staSSID[32]; + char staPassword[64]; +}; -// RTC -SoftWire rtcWire(RTC_SDA, RTC_SCL); -char rtcSyncBuffer[20]; +// Serial 설정 +struct SerialConfig { + uint32_t baudRate; + uint8_t dataBits; + uint8_t parity; // 0=None, 1=Even, 2=Odd + uint8_t stopBits; +}; -// Forward declarations -void IRAM_ATTR canISR(); +// ============================================================================ +// 전역 변수 +// ============================================================================ -// ======================================== -// 메모리 정보 출력 함수 -// ======================================== +// 하드웨어 객체 +MCP2515 mcp2515(CAN_CS_PIN); +AsyncWebServer server(80); +WebSocketsServer webSocket(81); +Preferences preferences; -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); -} +// FreeRTOS 큐 (PSRAM 할당) +QueueHandle_t canQueue = nullptr; +QueueHandle_t serialQueue = nullptr; -// ======================================== -// PSRAM 버퍼 할당 -// ======================================== +// FreeRTOS 태스크 핸들 +TaskHandle_t canRxTaskHandle = nullptr; +TaskHandle_t canTxTaskHandle = nullptr; +TaskHandle_t serialRxTaskHandle = nullptr; +TaskHandle_t loggingTaskHandle = nullptr; +TaskHandle_t webUpdateTaskHandle = nullptr; -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; -} +// CAN 데이터 +CANData recentData[RECENT_MSG_COUNT]; -// ======================================== -// Queue 생성 (PSRAM 버퍼 사용) -// ======================================== +// CAN 통계 +uint32_t totalCanRxCount = 0; +uint32_t totalCanTxCount = 0; +uint32_t canMessagesPerSecond = 0; +uint32_t lastCanCountTime = 0; +uint32_t lastCanCount = 0; -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 통계 +uint32_t totalSerialRxCount = 0; +uint32_t totalSerialTxCount = 0; -// ======================================== -// Serial 설정 함수 -// ======================================== +// 로깅 상태 +volatile bool canLoggingEnabled = false; +volatile bool serialLoggingEnabled = false; +File canLogFile; +File serialLogFile; +char currentCanFileName64 = {0}; +char currentSerialFileName[64] = {0}; +uint32_t currentCanFileSize = 0; +uint32_t currentSerialFileSize = 0; +String canLogFormat = "bin"; // "bin" or "csv" +String serialLogFormat = "csv"; // "csv" or "bin" -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); -} +// SD 카드 상태 +bool sdCardReady = false; -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); -} +// 시간 동기화 +bool timeSynced = false; +uint32_t rtcSyncCount = 0; +struct tm timeinfo; -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); -} +// 전압 모니터링 +float currentVoltage = 0.0; +float minVoltage1s = 99.0; +bool lowVoltageDetected = false; +uint32_t lastVoltageResetTime = 0; -// ======================================== -// 설정 저장/로드 -// ======================================== +// CAN 시퀀스 +CANSequence sequences[MAX_SEQUENCES]; +uint8_t sequenceCount = 0; +SequenceRuntime seqRuntime = {false, -1, 0, 0, 0}; -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(); -} +// WiFi 설정 +WiFiSettings wifiSettings; -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(); -} +// Serial 설정 +SerialConfig serialConfig = {115200, 8, 0, 1}; -// ======================================== -// RTC 함수 -// ======================================== +// MCP2515 모드 +uint8_t currentMcpMode = 0; // 0=Normal, 1=Listen-Only, 2=Loopback, 3=TX-Only -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) 없음"); - } -} +// WebSocket 클라이언트 수 +uint8_t wsClientCount = 0; -uint8_t bcdToDec(uint8_t val) { - return (val >> 4) * 10 + (val & 0x0F); -} +// ============================================================================ +// 함수 선언 +// ============================================================================ -uint8_t decToBcd(uint8_t val) { - return ((val / 10) << 4) | (val % 10); -} +// 초기화 +void initSPIBus(); +void initSDCard(); +void initMCP2515(); +void initSerial2(); +void loadSettings(); +void saveSettings(); -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; -} +// FreeRTOS 태스크 +void canRxTask(void* parameter); +void canTxTask(void* parameter); +void serialRxTask(void* parameter); +void loggingTask(void* parameter); +void webUpdateTask(void* parameter); -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; -} +// CAN 기능 +void setCANSpeed(uint8_t speed); +void setMCPMode(uint8_t mode); +bool sendCANMessage(uint32_t id, bool ext, uint8_t dlc, const uint8_t* data); -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 startCANLogging(const String& format); +void stopCANLogging(); +void startSerialLogging(const String& format); +void stopSerialLogging(); +void writeCANMessageToFile(const CANMessage& msg); +void writeSerialMessageToFile(const SerialMessage& msg); -void initNTP() { - configTime(9 * 3600, 0, "pool.ntp.org", "time.nist.gov", "time.google.com"); - sntp_set_time_sync_notification_cb(timeSyncCallback); -} +// 시퀀스 +void loadSequences(); +void saveSequences(); +void startSequence(uint8_t index); +void stopSequence(); +void processSequence(); -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); - } -} +// 시간 +void syncTimeFromPhone(int year, int month, int day, int hour, int minute, int second); +String getTimestamp(); -// ======================================== -// MCP2515 모드 설정 -// ======================================== +// 전압 +void updateVoltage(); -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; -} +// WebSocket +void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length); +void sendWebUpdate(); +void handleWebSocketCommand(uint8_t num, const char* payload); -// ======================================== -// CAN 인터럽트 핸들러 -// ======================================== +// 파일 관리 +void sendFileList(uint8_t clientNum); +void deleteFile(const String& filename, uint8_t clientNum); +void addFileComment(const String& filename, const String& comment); +String getFileComment(const String& filename); -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; - // webUpdateTask에서 전송 - doc["psramFree"] = ESP.getFreePsram(); // ← 추가 - doc["serialQueueUsed"] = uxQueueMessagesWaiting(serialQueue); // ← 추가 - doc["serialQueueSize"] = SERIAL_QUEUE_SIZE; // ← 추가 - // webUpdateTask에서 전송 - doc["type"] = "update"; - 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]); // ← 배열 - } - } - } - 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 -// ======================================== +// 유틸리티 +String formatBytes(uint32_t bytes); +void printSystemInfo(); +// ============================================================================ +// 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"); + Serial.println("\n\n"); + Serial.println("============================================================================"); + Serial.println(" Byun CAN Logger v2.5 PSRAM Edition - Final Release"); + Serial.println("============================================================================"); // 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); + if (psramFound()) { + Serial.printf("✓ PSRAM detected: %d KB\n", ESP.getPsramSize() / 1024); + Serial.printf(" Free PSRAM: %d KB\n", ESP.getFreePsram() / 1024); + } else { + Serial.println("✗ WARNING: PSRAM not found!"); } // 설정 로드 + preferences.begin("can-logger", false); 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(); + initSPIBus(); // SD 카드 초기화 - Serial.println("========================================"); - Serial.println("SD 카드 초기화..."); - Serial.println("========================================"); + initSDCard(); - if (SD.begin(VSPI_CS, vspi)) { - sdCardReady = true; - Serial.println("✓ SD 카드 초기화 완료"); - loadFileComments(); - loadSequences(); + // MCP2515 초기화 + initMCP2515(); + + // Serial2 초기화 + initSerial2(); + + // 시퀀스 로드 + loadSequences(); + + // PSRAM 기반 큐 생성 + Serial.println("\n[Queue Creation]"); + canQueue = xQueueCreate(CAN_QUEUE_SIZE, sizeof(CANMessage)); + if (canQueue == nullptr) { + Serial.println("✗ Failed to create CAN queue!"); } else { - Serial.println("✗ SD 카드 초기화 실패"); + Serial.printf("✓ CAN Queue created: %d slots\n", CAN_QUEUE_SIZE); } - Serial.println(); - // WiFi 설정 - Serial.println("========================================"); - Serial.println("WiFi 초기화..."); - Serial.println("========================================"); + serialQueue = xQueueCreate(SERIAL_QUEUE_SIZE, sizeof(SerialMessage)); + if (serialQueue == nullptr) { + Serial.println("✗ Failed to create Serial queue!"); + } else { + Serial.printf("✓ Serial Queue created: %d slots\n", SERIAL_QUEUE_SIZE); + } - 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()); + // FreeRTOS 태스크 생성 + Serial.println("\n[Task Creation]"); + + xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 4096, NULL, 5, &canRxTaskHandle, 0); + Serial.println("✓ CAN RX Task created (Core 0)"); + + xTaskCreatePinnedToCore(canTxTask, "CAN_TX", 4096, NULL, 4, &canTxTaskHandle, 0); + Serial.println("✓ CAN TX Task created (Core 0)"); + + xTaskCreatePinnedToCore(serialRxTask, "Serial_RX", 4096, NULL, 3, &serialRxTaskHandle, 1); + Serial.println("✓ Serial RX Task created (Core 1)"); + + xTaskCreatePinnedToCore(loggingTask, "Logging", 8192, NULL, 2, &loggingTaskHandle, 1); + Serial.println("✓ Logging Task created (Core 1)"); + + xTaskCreatePinnedToCore(webUpdateTask, "Web_Update", 8192, NULL, 1, &webUpdateTaskHandle, 1); + Serial.println("✓ Web Update Task created (Core 1)"); + + // WiFi 시작 + Serial.println("\n[WiFi Configuration]"); + + // AP 모드 시작 + WiFi.mode(WIFI_AP_STA); // 항상 APSTA 모드로 시작 + WiFi.softAP(wifiSettings.ssid, wifiSettings.password); + Serial.printf("✓ AP Mode started\n"); + Serial.printf(" SSID: %s\n", wifiSettings.ssid); + Serial.printf(" IP: %s\n", WiFi.softAPIP().toString().c_str()); + + // Station 모드 (옵션) + if (wifiSettings.staEnable && strlen(wifiSettings.staSSID) > 0) { + Serial.printf(" Connecting to WiFi: %s\n", wifiSettings.staSSID); + WiFi.begin(wifiSettings.staSSID, wifiSettings.staPassword); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 20) { @@ -1848,141 +383,1066 @@ void setup() { Serial.print("."); attempts++; } - Serial.println(); if (WiFi.status() == WL_CONNECTED) { - Serial.printf("✓ STA 연결: %s\n", WiFi.localIP().toString().c_str()); - initNTP(); + Serial.printf("\n✓ Station connected\n"); + Serial.printf(" IP: %s\n", WiFi.localIP().toString().c_str()); + + // NTP 시간 동기화 + configTime(9 * 3600, 0, "pool.ntp.org", "time.nist.gov"); + if (getLocalTime(&timeinfo)) { + timeSynced = true; + Serial.println("✓ NTP time synchronized"); + } } else { - Serial.println("! STA 연결 실패 (AP 모드는 정상)"); + Serial.println("\n✗ Station connection failed (AP mode still active)"); } - } 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(); + // 웹 서버 라우트 설정 + Serial.println("\n[Web Server Setup]"); - // WebSocket & Server - webSocket.begin(); - webSocket.onEvent(webSocketEvent); - - server.on("/", HTTP_GET, []() { - server.send_P(200, "text/html", index_html); + server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) { + request->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, [](AsyncWebServerRequest* request) { + request->send_P(200, "text/html", graph_html); }); - server.on("/graph", HTTP_GET, []() { - server.send_P(200, "text/html", graph_html); + server.on("/graph-view", HTTP_GET, [](AsyncWebServerRequest* request) { + request->send_P(200, "text/html", graph_viewer_html); }); - server.on("/graph-view", HTTP_GET, []() { - server.send_P(200, "text/html", graph_viewer_html); + server.on("/transmit", HTTP_GET, [](AsyncWebServerRequest* request) { + request->send_P(200, "text/html", transmit_html); }); - server.on("/settings", HTTP_GET, []() { - server.send_P(200, "text/html", settings_html); + server.on("/serial", HTTP_GET, [](AsyncWebServerRequest* request) { + request->send_P(200, "text/html", serial_terminal_html); }); - server.on("/serial", HTTP_GET, []() { - server.send_P(200, "text/html", serial_terminal_html); + server.on("/settings", HTTP_GET, [](AsyncWebServerRequest* request) { + request->send_P(200, "text/html", settings_html); }); - server.on("/download", HTTP_GET, []() { - if (server.hasArg("file")) { - String filename = "/" + server.arg("file"); - + // 파일 다운로드 + server.on("/download", HTTP_GET, [](AsyncWebServerRequest* request) { + if (request->hasParam("file")) { + String filename = "/" + request->getParam("file")->value(); 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(); - } + request->send(SD, filename, "application/octet-stream"); + } else { + request->send(404, "text/plain", "File not found"); } + } else { + request->send(400, "text/plain", "Missing file parameter"); } }); server.begin(); + Serial.println("✓ HTTP Server started on port 80"); - // CAN 인터럽트 - attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING); + // WebSocket 시작 + webSocket.begin(); + webSocket.onEvent(webSocketEvent); + Serial.println("✓ WebSocket Server started on port 81"); - // Task 생성 - Serial.println("========================================"); - Serial.println("Task 생성 (우선순위 최적화)..."); - Serial.println("========================================"); + // 시스템 정보 출력 + printSystemInfo(); - 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"); + Serial.println("\n============================================================================"); + Serial.println(" System Ready!"); + Serial.println(" Connect to WiFi and access: http://192.168.4.1"); + Serial.println("============================================================================\n"); } +// ============================================================================ +// loop() +// ============================================================================ void loop() { - server.handleClient(); - vTaskDelay(pdMS_TO_TICKS(10)); + webSocket.loop(); - 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(); + // 전압 업데이트 (1초마다) + static uint32_t lastVoltageUpdate = 0; + if (millis() - lastVoltageUpdate > 1000) { + updateVoltage(); + lastVoltageUpdate = millis(); + } + + // 시퀀스 처리 + if (seqRuntime.running) { + processSequence(); + } + + // CAN 메시지 속도 계산 (1초마다) + if (millis() - lastCanCountTime >= 1000) { + canMessagesPerSecond = totalCanRxCount - lastCanCount; + lastCanCount = totalCanRxCount; + lastCanCountTime = millis(); + + // 1초마다 최소 전압 리셋 + if (millis() - lastVoltageResetTime >= 1000) { + minVoltage1s = currentVoltage; + lastVoltageResetTime = millis(); + } + } + + delay(10); +} + +// ============================================================================ +// 초기화 함수들 +// ============================================================================ + +void initSPIBus() { + Serial.println("\n[SPI Bus Initialization]"); + SPI.begin(); + Serial.println("✓ SPI Bus initialized"); +} + +void initSDCard() { + Serial.println("\n[SD Card Initialization]"); + + if (!SD.begin(SD_CS_PIN)) { + Serial.println("✗ SD Card initialization failed!"); + sdCardReady = false; + return; + } + + uint64_t cardSize = SD.cardSize() / (1024 * 1024); + Serial.printf("✓ SD Card detected: %llu MB\n", cardSize); + + uint64_t usedBytes = SD.usedBytes() / (1024 * 1024); + uint64_t totalBytes = SD.totalBytes() / (1024 * 1024); + Serial.printf(" Used: %llu MB / %llu MB\n", usedBytes, totalBytes); + + sdCardReady = true; +} + +void initMCP2515() { + Serial.println("\n[MCP2515 Initialization]"); + + mcp2515.reset(); + mcp2515.setBitrate(CAN_1000KBPS, MCP_8MHZ); + mcp2515.setNormalMode(); + + Serial.println("✓ MCP2515 initialized"); + Serial.println(" Speed: 1 Mbps"); + Serial.println(" Mode: Normal"); + + currentMcpMode = 0; +} + +void initSerial2() { + Serial.println("\n[Serial2 Initialization]"); + + Serial2.begin(serialConfig.baudRate, SERIAL_8N1, SERIAL2_RX, SERIAL2_TX); + + Serial.printf("✓ Serial2 initialized\n"); + Serial.printf(" Baud: %d\n", serialConfig.baudRate); + Serial.printf(" Config: 8N1\n"); + Serial.printf(" RX: GPIO%d, TX: GPIO%d\n", SERIAL2_RX, SERIAL2_TX); +} + +void loadSettings() { + Serial.println("\n[Loading Settings]"); + + // WiFi 설정 + preferences.getString("ssid", wifiSettings.ssid, sizeof(wifiSettings.ssid)); + preferences.getString("password", wifiSettings.password, sizeof(wifiSettings.password)); + wifiSettings.staEnable = preferences.getBool("staEnable", false); + preferences.getString("staSSID", wifiSettings.staSSID, sizeof(wifiSettings.staSSID)); + preferences.getString("staPassword", wifiSettings.staPassword, sizeof(wifiSettings.staPassword)); + + // 기본값 설정 + if (strlen(wifiSettings.ssid) == 0) { + strcpy(wifiSettings.ssid, "Byun_CAN_Logger"); + strcpy(wifiSettings.password, "12345678"); + } + + Serial.printf("✓ WiFi AP: %s\n", wifiSettings.ssid); + if (wifiSettings.staEnable) { + Serial.printf("✓ WiFi STA enabled: %s\n", wifiSettings.staSSID); + } + + // Serial 설정 + serialConfig.baudRate = preferences.getUInt("serialBaud", 115200); + serialConfig.dataBits = preferences.getUChar("serialData", 8); + serialConfig.parity = preferences.getUChar("serialParity", 0); + serialConfig.stopBits = preferences.getUChar("serialStop", 1); + + Serial.printf("✓ Serial: %d 8N1\n", serialConfig.baudRate); +} + +void saveSettings() { + Serial.println("\n[Saving Settings]"); + + preferences.putString("ssid", wifiSettings.ssid); + preferences.putString("password", wifiSettings.password); + preferences.putBool("staEnable", wifiSettings.staEnable); + preferences.putString("staSSID", wifiSettings.staSSID); + preferences.putString("staPassword", wifiSettings.staPassword); + + preferences.putUInt("serialBaud", serialConfig.baudRate); + preferences.putUChar("serialData", serialConfig.dataBits); + preferences.putUChar("serialParity", serialConfig.parity); + preferences.putUChar("serialStop", serialConfig.stopBits); + + Serial.println("✓ Settings saved to NVS"); +} + +// ============================================================================ +// FreeRTOS 태스크들 +// ============================================================================ + +void canRxTask(void* parameter) { + struct can_frame frame; + CANMessage msg; + + Serial.println("[CAN RX Task] Started"); + + while (true) { + if (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) { + // CAN 메시지를 큐에 추가 + msg.id = frame.can_id & 0x1FFFFFFF; // Extended ID 비트 제거 + msg.extended = (frame.can_id & CAN_EFF_FLAG) ? true : false; + msg.dlc = frame.can_dlc; + memcpy(msg.data, frame.data, 8); + msg.timestamp_ms = millis(); + + if (xQueueSend(canQueue, &msg, 0) != pdTRUE) { + // 큐가 가득 찬 경우 (드롭) + } + + // 최근 메시지 업데이트 + bool found = false; + for (int i = 0; i < RECENT_MSG_COUNT; i++) { + if (recentData[i].msg.can_id == frame.can_id) { + memcpy(&recentData[i].msg, &frame, sizeof(struct can_frame)); + recentData[i].count++; + recentData[i].lastUpdate = millis(); + found = true; + break; + } + } + + if (!found) { + // 빈 슬롯 찾기 + for (int i = 0; i < RECENT_MSG_COUNT; i++) { + if (recentData[i].count == 0) { + memcpy(&recentData[i].msg, &frame, sizeof(struct can_frame)); + recentData[i].count = 1; + recentData[i].lastUpdate = millis(); + break; + } + } + } + + totalCanRxCount++; + } + + vTaskDelay(1 / portTICK_PERIOD_MS); // 1ms 대기 } } + +void canTxTask(void* parameter) { + Serial.println("[CAN TX Task] Started"); + + while (true) { + // 현재는 시퀀스에서만 TX 사용 + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void serialRxTask(void* parameter) { + static char lineBuffer[MAX_SERIAL_LINE_LEN]; + static uint16_t bufferIndex = 0; + SerialMessage msg; + + Serial.println("[Serial RX Task] Started"); + + while (true) { + while (Serial2.available()) { + char c = Serial2.read(); + + if (c == '\n' || c == '\r') { + if (bufferIndex > 0) { + // 라인 완성 + msg.timestamp_us = esp_timer_get_time(); + msg.isTx = false; + msg.length = bufferIndex; + memcpy(msg.data, lineBuffer, bufferIndex); + msg.data[bufferIndex] = '\0'; + + xQueueSend(serialQueue, &msg, 0); + + totalSerialRxCount++; + bufferIndex = 0; + } + } else { + if (bufferIndex < MAX_SERIAL_LINE_LEN - 1) { + lineBuffer[bufferIndex++] = c; + } + } + } + + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void loggingTask(void* parameter) { + CANMessage canMsg; + SerialMessage serialMsg; + + Serial.println("[Logging Task] Started"); + + while (true) { + // CAN 로깅 + if (canLoggingEnabled && canLogFile) { + while (xQueueReceive(canQueue, &canMsg, 0) == pdTRUE) { + writeCANMessageToFile(canMsg); + } + } + + // Serial 로깅 + if (serialLoggingEnabled && serialLogFile) { + while (xQueueReceive(serialQueue, &serialMsg, 0) == pdTRUE) { + writeSerialMessageToFile(serialMsg); + } + } + + vTaskDelay(100 / portTICK_PERIOD_MS); + } +} + +void webUpdateTask(void* parameter) { + Serial.println("[Web Update Task] Started"); + + while (true) { + if (wsClientCount > 0) { + sendWebUpdate(); + } + + vTaskDelay(WEB_UPDATE_INTERVAL / portTICK_PERIOD_MS); + } +} + +// ============================================================================ +// CAN 기능 +// ============================================================================ + +void setCANSpeed(uint8_t speed) { + mcp2515.reset(); + + switch (speed) { + case 0: mcp2515.setBitrate(CAN_125KBPS, MCP_8MHZ); break; + case 1: mcp2515.setBitrate(CAN_250KBPS, MCP_8MHZ); break; + case 2: mcp2515.setBitrate(CAN_500KBPS, MCP_8MHZ); break; + case 3: mcp2515.setBitrate(CAN_1000KBPS, MCP_8MHZ); break; + default: mcp2515.setBitrate(CAN_1000KBPS, MCP_8MHZ); break; + } + + // 모드 복원 + setMCPMode(currentMcpMode); + + Serial.printf("CAN speed changed to: %d\n", speed); +} + +void setMCPMode(uint8_t mode) { + currentMcpMode = mode; + + switch (mode) { + case 0: mcp2515.setNormalMode(); break; + case 1: mcp2515.setListenOnlyMode(); break; + case 2: mcp2515.setLoopbackMode(); break; + case 3: mcp2515.setNormalMode(); break; // TX-Only는 Normal 사용 + default: mcp2515.setNormalMode(); break; + } + + Serial.printf("MCP2515 mode changed to: %d\n", mode); +} + +bool sendCANMessage(uint32_t id, bool ext, uint8_t dlc, const uint8_t* data) { + struct can_frame frame; + + frame.can_id = id; + if (ext) { + frame.can_id |= CAN_EFF_FLAG; + } + frame.can_dlc = dlc; + memcpy(frame.data, data, dlc); + + if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) { + totalCanTxCount++; + return true; + } + + return false; +} + +// ============================================================================ +// 로깅 기능 +// ============================================================================ + +void startCANLogging(const String& format) { + if (canLoggingEnabled) { + stopCANLogging(); + } + + if (!sdCardReady) { + Serial.println("Cannot start CAN logging: SD card not ready"); + return; + } + + canLogFormat = format; + + // 파일명 생성 + String timestamp = getTimestamp(); + String extension = (format == "csv") ? ".csv" : ".bin"; + snprintf(currentCanFileName, sizeof(currentCanFileName), "/CAN_%s%s", + timestamp.c_str(), extension.c_str()); + + canLogFile = SD.open(currentCanFileName, FILE_WRITE); + if (!canLogFile) { + Serial.println("Failed to create CAN log file"); + return; + } + + // CSV 헤더 + if (format == "csv") { + canLogFile.println("Timestamp_ms,ID,Extended,DLC,Data"); + } + + canLoggingEnabled = true; + currentCanFileSize = 0; + + Serial.printf("CAN logging started: %s (format: %s)\n", currentCanFileName, format.c_str()); +} + +void stopCANLogging() { + if (!canLoggingEnabled) return; + + canLoggingEnabled = false; + + if (canLogFile) { + canLogFile.flush(); + canLogFile.close(); + } + + Serial.printf("CAN logging stopped: %s (%d bytes)\n", currentCanFileName, currentCanFileSize); + + currentCanFileName[0] = '\0'; + currentCanFileSize = 0; +} + +void startSerialLogging(const String& format) { + if (serialLoggingEnabled) { + stopSerialLogging(); + } + + if (!sdCardReady) { + Serial.println("Cannot start Serial logging: SD card not ready"); + return; + } + + serialLogFormat = format; + + String timestamp = getTimestamp(); + String extension = (format == "csv") ? ".csv" : ".bin"; + snprintf(currentSerialFileName, sizeof(currentSerialFileName), "/SER_%s%s", + timestamp.c_str(), extension.c_str()); + + serialLogFile = SD.open(currentSerialFileName, FILE_WRITE); + if (!serialLogFile) { + Serial.println("Failed to create Serial log file"); + return; + } + + if (format == "csv") { + serialLogFile.println("Timestamp_us,Direction,Data"); + } + + serialLoggingEnabled = true; + currentSerialFileSize = 0; + + Serial.printf("Serial logging started: %s (format: %s)\n", currentSerialFileName, format.c_str()); +} + +void stopSerialLogging() { + if (!serialLoggingEnabled) return; + + serialLoggingEnabled = false; + + if (serialLogFile) { + serialLogFile.flush(); + serialLogFile.close(); + } + + Serial.printf("Serial logging stopped: %s (%d bytes)\n", currentSerialFileName, currentSerialFileSize); + + currentSerialFileName[0] = '\0'; + currentSerialFileSize = 0; +} + +void writeCANMessageToFile(const CANMessage& msg) { + if (!canLogFile) return; + + if (canLogFormat == "csv") { + // CSV 형식 + char line[128]; + char dataStr[32]; + + for (int i = 0; i < msg.dlc; i++) { + sprintf(dataStr + i * 3, "%02X ", msg.data[i]); + } + + sprintf(line, "%u,0x%X,%d,%d,%s\n", + msg.timestamp_ms, msg.id, msg.extended ? 1 : 0, msg.dlc, dataStr); + + canLogFile.print(line); + currentCanFileSize += strlen(line); + } else { + // Binary 형식 + canLogFile.write((uint8_t*)&msg, sizeof(CANMessage)); + currentCanFileSize += sizeof(CANMessage); + } + + // 주기적으로 flush (100개마다) + static uint32_t writeCount = 0; + if (++writeCount % 100 == 0) { + canLogFile.flush(); + } +} + +void writeSerialMessageToFile(const SerialMessage& msg) { + if (!serialLogFile) return; + + if (serialLogFormat == "csv") { + char line[MAX_SERIAL_LINE_LEN + 64]; + sprintf(line, "%llu,%s,%s\n", + msg.timestamp_us, msg.isTx ? "TX" : "RX", msg.data); + + serialLogFile.print(line); + currentSerialFileSize += strlen(line); + } else { + serialLogFile.write((uint8_t*)&msg, sizeof(SerialMessage)); + currentSerialFileSize += sizeof(SerialMessage); + } + + static uint32_t writeCount = 0; + if (++writeCount % 50 == 0) { + serialLogFile.flush(); + } +} + +// ============================================================================ +// 시퀀스 기능 +// ============================================================================ + +void loadSequences() { + // Preferences에서 시퀀스 로드 (간단한 구현) + sequenceCount = preferences.getUChar("seqCount", 0); + Serial.printf("Loaded %d sequences from NVS\n", sequenceCount); +} + +void saveSequences() { + preferences.putUChar("seqCount", sequenceCount); + Serial.printf("Saved %d sequences to NVS\n", sequenceCount); +} + +void startSequence(uint8_t index) { + if (index >= sequenceCount) return; + + seqRuntime.running = true; + seqRuntime.activeSequenceIndex = index; + seqRuntime.currentStep = 0; + seqRuntime.currentRepeat = 0; + seqRuntime.lastStepTime = millis(); + + Serial.printf("Sequence started: %s\n", sequences[index].name); +} + +void stopSequence() { + if (!seqRuntime.running) return; + + Serial.printf("Sequence stopped: %s\n", sequences[seqRuntime.activeSequenceIndex].name); + + seqRuntime.running = false; + seqRuntime.activeSequenceIndex = -1; +} + +void processSequence() { + if (!seqRuntime.running || seqRuntime.activeSequenceIndex < 0) return; + + CANSequence& seq = sequences[seqRuntime.activeSequenceIndex]; + + if (seqRuntime.currentStep >= seq.stepCount) { + // 반복 처리 + seqRuntime.currentRepeat++; + + if (seq.repeatMode == 0) { + // Once + stopSequence(); + return; + } else if (seq.repeatMode == 1) { + // Count + if (seqRuntime.currentRepeat >= seq.repeatCount) { + stopSequence(); + return; + } + } + // Infinite: 계속 실행 + + seqRuntime.currentStep = 0; + } + + SequenceStep& step = seq.steps[seqRuntime.currentStep]; + + if (millis() - seqRuntime.lastStepTime >= step.delayMs) { + // CAN 메시지 전송 + sendCANMessage(step.canId, step.extended, step.dlc, step.data); + + seqRuntime.currentStep++; + seqRuntime.lastStepTime = millis(); + } +} + +// ============================================================================ +// 시간 함수 +// ============================================================================ + +void syncTimeFromPhone(int year, int month, int day, int hour, int minute, int second) { + timeinfo.tm_year = year - 1900; + timeinfo.tm_mon = month - 1; + timeinfo.tm_mday = day; + timeinfo.tm_hour = hour; + timeinfo.tm_min = minute; + timeinfo.tm_sec = second; + + time_t t = mktime(&timeinfo); + struct timeval now = { .tv_sec = t }; + settimeofday(&now, NULL); + + timeSynced = true; + rtcSyncCount++; + + Serial.printf("Time synced from phone: %04d-%02d-%02d %02d:%02d:%02d\n", + year, month, day, hour, minute, second); +} + +String getTimestamp() { + if (timeSynced) { + time_t now; + time(&now); + struct tm* tm_now = localtime(&now); + + char buf[32]; + sprintf(buf, "%04d%02d%02d_%02d%02d%02d", + tm_now->tm_year + 1900, tm_now->tm_mon + 1, tm_now->tm_mday, + tm_now->tm_hour, tm_now->tm_min, tm_now->tm_sec); + return String(buf); + } else { + char buf[32]; + sprintf(buf, "%010lu", millis()); + return String(buf); + } +} + +// ============================================================================ +// 전압 모니터링 +// ============================================================================ + +void updateVoltage() { + float sum = 0; + for (int i = 0; i < VOLTAGE_SAMPLES; i++) { + int rawValue = analogRead(VOLTAGE_PIN); + float voltage = (rawValue / 4095.0) * 3.3 * (12.0 / 3.3); // 분압비 가정 + sum += voltage; + delay(1); + } + + currentVoltage = sum / VOLTAGE_SAMPLES; + + if (currentVoltage < minVoltage1s) { + minVoltage1s = currentVoltage; + } + + lowVoltageDetected = (currentVoltage < LOW_VOLTAGE_THRESHOLD); +} + +// ============================================================================ +// WebSocket 함수 +// ============================================================================ + +void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) { + switch (type) { + case WStype_DISCONNECTED: + Serial.printf("[WS] Client #%u disconnected\n", num); + if (wsClientCount > 0) wsClientCount--; + break; + + case WStype_CONNECTED: + { + IPAddress ip = webSocket.remoteIP(num); + Serial.printf("[WS] Client #%u connected from %s\n", num, ip.toString().c_str()); + wsClientCount++; + } + break; + + case WStype_TEXT: + Serial.printf("[WS] Client #%u: %s\n", num, payload); + handleWebSocketCommand(num, (char*)payload); + break; + + default: + break; + } +} + +void handleWebSocketCommand(uint8_t num, const char* payload) { + DynamicJsonDocument doc(2048); + DeserializationError error = deserializeJson(doc, payload); + + if (error) { + Serial.printf("JSON parse error: %s\n", error.c_str()); + return; + } + + const char* cmd = doc["cmd"]; + + if (strcmp(cmd, "setSpeed") == 0) { + uint8_t speed = doc["speed"]; + setCANSpeed(speed); + } + else if (strcmp(cmd, "setMcpMode") == 0) { + uint8_t mode = doc["mode"]; + setMCPMode(mode); + } + else if (strcmp(cmd, "startLogging") == 0) { + String format = doc["format"] | "bin"; + startCANLogging(format); + } + else if (strcmp(cmd, "stopLogging") == 0) { + stopCANLogging(); + } + else if (strcmp(cmd, "startSerialLogging") == 0) { + String format = doc["format"] | "csv"; + startSerialLogging(format); + } + else if (strcmp(cmd, "stopSerialLogging") == 0) { + stopSerialLogging(); + } + else if (strcmp(cmd, "getFiles") == 0) { + sendFileList(num); + } + else if (strcmp(cmd, "deleteFile") == 0) { + String filename = "/" + String((const char*)doc["filename"]); + deleteFile(filename, num); + } + else if (strcmp(cmd, "syncTimeFromPhone") == 0) { + int year = doc["year"]; + int month = doc["month"]; + int day = doc["day"]; + int hour = doc["hour"]; + int minute = doc["minute"]; + int second = doc["second"]; + syncTimeFromPhone(year, month, day, hour, minute, second); + } + else if (strcmp(cmd, "getSettings") == 0) { + DynamicJsonDocument response(1024); + response["type"] = "settings"; + response["ssid"] = wifiSettings.ssid; + response["password"] = wifiSettings.password; + response["staEnable"] = wifiSettings.staEnable; + response["staSSID"] = wifiSettings.staSSID; + response["staPassword"] = wifiSettings.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) { + strcpy(wifiSettings.ssid, doc["ssid"]); + strcpy(wifiSettings.password, doc["password"]); + wifiSettings.staEnable = doc["staEnable"]; + strcpy(wifiSettings.staSSID, doc["staSSID"]); + strcpy(wifiSettings.staPassword, doc["staPassword"]); + + saveSettings(); + + DynamicJsonDocument response(256); + response["type"] = "settingsSaved"; + response["success"] = true; + + String json; + serializeJson(response, json); + webSocket.sendTXT(num, json); + } + else if (strcmp(cmd, "reboot") == 0) { + Serial.println("Rebooting by user request..."); + delay(500); + ESP.restart(); + } + else if (strcmp(cmd, "getSequences") == 0) { + DynamicJsonDocument response(4096); + response["type"] = "sequences"; + + JsonArray list = response.createNestedArray("list"); + for (int i = 0; i < sequenceCount; i++) { + JsonObject seq = list.createNestedObject(); + seq["name"] = sequences[i].name; + seq["index"] = i; + seq["mode"] = sequences[i].repeatMode; + seq["count"] = sequences[i].repeatCount; + seq["stepCount"] = sequences[i].stepCount; + seq["running"] = (seqRuntime.running && seqRuntime.activeSequenceIndex == i); + } + + String json; + serializeJson(response, json); + webSocket.sendTXT(num, json); + } + else if (strcmp(cmd, "startSequence") == 0) { + uint8_t index = doc["index"]; + startSequence(index); + } + else if (strcmp(cmd, "stopSequence") == 0) { + stopSequence(); + } + else if (strcmp(cmd, "addSequence") == 0) { + if (sequenceCount < MAX_SEQUENCES) { + CANSequence& seq = sequences[sequenceCount]; + + strcpy(seq.name, doc["name"]); + seq.repeatMode = doc["repeatMode"]; + seq.repeatCount = doc["repeatCount"]; + + JsonArray steps = doc["steps"]; + seq.stepCount = 0; + + for (JsonObject step : steps) { + if (seq.stepCount >= SEQUENCE_MAX_STEPS) break; + + SequenceStep& s = seq.steps[seq.stepCount]; + + String idStr = step["id"]; + s.canId = strtoul(idStr.c_str(), NULL, 16); + s.extended = step["ext"]; + s.dlc = step["dlc"]; + + JsonArray data = step["data"]; + for (int i = 0; i < 8; i++) { + s.data[i] = data[i]; + } + + s.delayMs = step["delay"]; + + seq.stepCount++; + } + + sequenceCount++; + saveSequences(); + + DynamicJsonDocument response(256); + response["type"] = "sequenceSaved"; + response["success"] = true; + + String json; + serializeJson(response, json); + webSocket.sendTXT(num, json); + } + } + else if (strcmp(cmd, "removeSequence") == 0) { + uint8_t index = doc["index"]; + + if (index < sequenceCount) { + // 시퀀스 삭제 (배열 이동) + for (int i = index; i < sequenceCount - 1; i++) { + sequences[i] = sequences[i + 1]; + } + sequenceCount--; + saveSequences(); + + DynamicJsonDocument response(256); + response["type"] = "sequenceDeleted"; + response["success"] = true; + + String json; + serializeJson(response, json); + webSocket.sendTXT(num, json); + } + } +} + +void sendWebUpdate() { + DynamicJsonDocument doc(8192); + + doc["type"] = "update"; + doc["logging"] = canLoggingEnabled; + doc["serialLogging"] = serialLoggingEnabled; + doc["sdReady"] = sdCardReady; + doc["totalMsg"] = totalCanRxCount; + doc["msgPerSec"] = canMessagesPerSecond; + doc["totalTx"] = totalCanTxCount; + doc["timeSync"] = timeSynced; + doc["syncCount"] = rtcSyncCount; + doc["mcpMode"] = currentMcpMode; + doc["currentFile"] = String(currentCanFileName); + doc["fileSize"] = currentCanFileSize; + doc["voltage"] = currentVoltage; + doc["minVoltage"] = minVoltage1s; + doc["lowVoltage"] = lowVoltageDetected; + + // 큐 상태 + doc["queueUsed"] = uxQueueMessagesWaiting(canQueue); + doc["queueSize"] = CAN_QUEUE_SIZE; + doc["serialQueueUsed"] = uxQueueMessagesWaiting(serialQueue); + doc["serialQueueSize"] = SERIAL_QUEUE_SIZE; + + // PSRAM 상태 + doc["psramFree"] = ESP.getFreePsram(); + + // Serial 통계 + doc["totalSerialRx"] = totalSerialRxCount; + doc["totalSerialTx"] = totalSerialTxCount; + doc["serialFileSize"] = currentSerialFileSize; + doc["currentSerialFile"] = String(currentSerialFileName); + + // CAN 메시지 배열 + 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.can_id & 0x1FFFFFFF; + msgObj["dlc"] = recentData[i].msg.can_dlc; + msgObj["count"] = recentData[i].count; + + JsonArray dataArray = msgObj.createNestedArray("data"); + for (int j = 0; j < recentData[i].msg.can_dlc; j++) { + dataArray.add(recentData[i].msg.data[j]); + } + } + } + + String json; + serializeJson(doc, json); + webSocket.broadcastTXT(json); +} + +// ============================================================================ +// 파일 관리 +// ============================================================================ + +void sendFileList(uint8_t clientNum) { + if (!sdCardReady) return; + + DynamicJsonDocument doc(8192); + doc["type"] = "files"; + + JsonArray list = doc.createNestedArray("list"); + + File root = SD.open("/"); + File file = root.openNextFile(); + + while (file) { + if (!file.isDirectory()) { + String filename = file.name(); + + // '/' 제거 + if (filename.startsWith("/")) { + filename = filename.substring(1); + } + + JsonObject fileObj = list.createNestedObject(); + fileObj["name"] = filename; + fileObj["size"] = file.size(); + + String comment = getFileComment(filename); + if (comment.length() > 0) { + fileObj["comment"] = comment; + } + } + file = root.openNextFile(); + } + + String json; + serializeJson(doc, json); + webSocket.sendTXT(clientNum, json); +} + +void deleteFile(const String& filename, uint8_t clientNum) { + bool success = false; + String message = "Unknown error"; + + if (sdCardReady) { + if (SD.exists(filename)) { + if (SD.remove(filename)) { + success = true; + message = "File deleted"; + Serial.printf("File deleted: %s\n", filename.c_str()); + } else { + message = "Failed to delete file"; + } + } else { + message = "File not found"; + } + } else { + message = "SD card not ready"; + } + + DynamicJsonDocument response(256); + response["type"] = "deleteResult"; + response["success"] = success; + response["message"] = message; + + String json; + serializeJson(response, json); + webSocket.sendTXT(clientNum, json); +} + +void addFileComment(const String& filename, const String& comment) { + String key = "cmt_" + filename; + key.replace("/", "_"); + key.replace(".", "_"); + + preferences.putString(key.c_str(), comment); + Serial.printf("Comment added: %s = %s\n", filename.c_str(), comment.c_str()); +} + +String getFileComment(const String& filename) { + String key = "cmt_" + filename; + key.replace("/", "_"); + key.replace(".", "_"); + + return preferences.getString(key.c_str(), ""); +} + +// ============================================================================ +// 유틸리티 +// ============================================================================ + +String formatBytes(uint32_t bytes) { + if (bytes < 1024) return String(bytes) + " B"; + if (bytes < 1024 * 1024) return String(bytes / 1024.0, 2) + " KB"; + return String(bytes / 1024.0 / 1024.0, 2) + " MB"; +} + +void printSystemInfo() { + Serial.println("\n[System Information]"); + Serial.printf("Chip Model: %s\n", ESP.getChipModel()); + Serial.printf("Chip Revision: %d\n", ESP.getChipRevision()); + Serial.printf("CPU Cores: %d\n", ESP.getChipCores()); + Serial.printf("CPU Frequency: %d MHz\n", ESP.getCpuFreqMHz()); + Serial.printf("Flash Size: %d MB\n", ESP.getFlashChipSize() / (1024 * 1024)); + Serial.printf("Free Heap: %d KB\n", ESP.getFreeHeap() / 1024); + + if (psramFound()) { + Serial.printf("PSRAM Size: %d KB\n", ESP.getPsramSize() / 1024); + Serial.printf("Free PSRAM: %d KB\n", ESP.getFreePsram() / 1024); + } + + Serial.printf("SDK Version: %s\n", ESP.getSdkVersion()); +}