/* * Byun CAN Logger with Web Interface + Serial Terminal * Version: 3.0 - RingBuffer Optimized (SPSC Lock-Free) * * ★ RingBuffer 핵심 변경사항 (v2.3 → v3.0): * - FreeRTOS Queue (xQueueSend/xQueueReceive) → SPSC Lock-Free RingBuffer * - CAN Queue 6000개 → CAN RingBuffer 8192개 (power-of-2, 비트마스크 연산) * - Serial/Serial2 Queue 1200개 → RingBuffer 2048개 * - Producer(canRxTask) 완전 non-blocking: 큐 full 시 drop 없이 즉시 복귀 * - Consumer(sdWriteTask) 뮤텍스 없이 BIN 버퍼 처리 (hot path 최적화) * - dropped 카운터: 웹 UI에서 손실 메시지 수 실시간 확인 * - __sync_synchronize() 메모리 배리어: 멀티코어 안전 * * PSRAM 최적화 완전판: * - 원본 기능 100% 유지 * - 대용량 버퍼/RingBuffer를 PSRAM에 할당 * - 웹서버, WebSocket, 모든 Task 포함 * * Arduino IDE 설정: * - Board: ESP32S3 Dev Module * - PSRAM: OPI PSRAM ⭐ 필수! * - Flash Size: 16MB (128Mb) * - Partition: 16MB Flash (3MB APP/9.9MB FATFS) */ #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" #include "serial2_terminal.h" // GPIO 핀 정의 #define CAN_INT_PIN 4 #define SERIAL_TX_PIN 17 #define SERIAL_RX_PIN 18 #define SERIAL2_TX_PIN 6 #define SERIAL2_RX_PIN 7 // HSPI 핀 (CAN) #define HSPI_MISO 13 #define HSPI_MOSI 11 #define HSPI_SCLK 12 #define HSPI_CS 10 // SDIO 4-bit Pins #define SDIO_CLK 39 #define SDIO_CMD 38 #define SDIO_D0 40 #define SDIO_D1 41 #define SDIO_D2 42 #define SDIO_D3 21 // I2C2 핀 (RTC DS3231) #define RTC_SDA 8 #define RTC_SCL 9 #define DS3231_ADDRESS 0x68 // ======================================== // ★ RingBuffer 크기 설정 (반드시 2의 거듭제곱!) // ======================================== #define CAN_RING_SIZE 8192 // 이전 Queue 6000 → 8192 (2^13, ~1초@8Kfps) #define SERIAL_RING_SIZE 2048 // 이전 Queue 1200 → 2048 (2^11) // 기타 버퍼 크기 #define FILE_BUFFER_SIZE 65536 // 64KB 더블버퍼 (SD BIN write) #define SERIAL_CSV_BUFFER_SIZE 32768 // 32KB Serial CSV 버퍼 #define SERIAL2_CSV_BUFFER_SIZE 32768 // 32KB Serial2 CSV 버퍼 #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 #define RTC_SYNC_INTERVAL_MS 60000 #define VOLTAGE_CHECK_INTERVAL_MS 5000 #define LOW_VOLTAGE_THRESHOLD 3.0 #define MONITORING_VOLT 5 #define MAX_GRAPH_SIGNALS 20 #define MAX_SEQUENCES 10 #define MAX_FILE_COMMENTS 50 // ======================================== // 구조체 정의 // ======================================== struct CANMessage { uint64_t timestamp_us; uint32_t id; uint8_t dlc; uint8_t data[8]; } __attribute__((packed)); // ======================================== // ★ PCAP 구조체 (Wireshark 호환) // LINKTYPE_CAN_SOCKETCAN = 227 // ======================================== struct PcapGlobalHeader { uint32_t magic_number; // 0xa1b2c3d4 uint16_t version_major; // 2 uint16_t version_minor; // 4 int32_t thiszone; // 0 uint32_t sigfigs; // 0 uint32_t snaplen; // 65535 uint32_t network; // 227 = LINKTYPE_CAN_SOCKETCAN } __attribute__((packed)); // 24 bytes struct PcapPacketHeader { uint32_t ts_sec; // 타임스탬프 초 uint32_t ts_usec; // 타임스탬프 마이크로초 uint32_t incl_len; // 캡처 길이 (16) uint32_t orig_len; // 원본 길이 (16) } __attribute__((packed)); // 16 bytes struct SocketCANFrame { uint32_t can_id; // CAN ID (bit31=EFF, bit30=RTR) uint8_t can_dlc; // 데이터 길이 uint8_t pad; uint8_t res0; uint8_t res1; uint8_t data[8]; } __attribute__((packed)); // 16 bytes // PCAP 패킷 1개 = PcapPacketHeader(16) + SocketCANFrame(16) = 32 bytes struct PcapRecord { PcapPacketHeader hdr; SocketCANFrame frame; } __attribute__((packed)); // 32 bytes 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; }; 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]; }; // ★ Arduino IDE auto-prototype 오류 방지: TriggerCondition을 상단에 정의 struct TriggerCondition { uint32_t canId; uint8_t startBit; uint8_t bitLength; char op[3]; int64_t value; bool enabled; }; 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 }; // ======================================== // ★★★ SPSC Lock-Free RingBuffer 정의 ★★★ // // 설계 원칙: // - head: Producer만 쓰기 (canRxTask / serialRxTask) // - tail: Consumer만 쓰기 (sdWriteTask / webUpdateTask) // - __sync_synchronize(): 멀티코어 메모리 배리어 // - 2의 거듭제곱 size + bitmask: 나눗셈 연산 제거 // - dropped: 오버플로우 시 drop 카운트 (진단용) // ======================================== struct CANRingBuffer { volatile uint32_t head; // Producer write index (절대값, 증가만) volatile uint32_t tail; // Consumer read index (절대값, 증가만) uint32_t size; // 버퍼 용량 (반드시 2의 거듭제곱) uint32_t mask; // size - 1 (비트마스크 인덱싱용) CANMessage* buf; // PSRAM 할당 데이터 배열 volatile uint32_t dropped; // 버퍼 full 시 drop된 메시지 수 }; struct SerialRingBuffer { volatile uint32_t head; volatile uint32_t tail; uint32_t size; uint32_t mask; SerialMessage* buf; volatile uint32_t dropped; }; // RingBuffer 전역 인스턴스 CANRingBuffer canRing; SerialRingBuffer serialRing; SerialRingBuffer serial2Ring; // ── Producer Push (IRAM에 배치: ISR 경로 최적화) ────────────────────────── // 반환: true=성공, false=버퍼 full(drop) static inline bool IRAM_ATTR ring_can_push(CANRingBuffer* rb, const CANMessage* msg) { uint32_t h = rb->head; if ((h - rb->tail) >= rb->size) { // 버퍼 full 체크 rb->dropped++; return false; // drop: 손실 카운트 후 즉시 반환 } rb->buf[h & rb->mask] = *msg; // 데이터 기록 __sync_synchronize(); // ★ 메모리 배리어: buf 쓰기 완료 후 head 갱신 rb->head = h + 1; return true; } // ── Consumer Pop ────────────────────────────────────────────────────────── static inline bool ring_can_pop(CANRingBuffer* rb, CANMessage* msg) { uint32_t t = rb->tail; if (rb->head == t) return false; // 빈 버퍼 *msg = rb->buf[t & rb->mask]; __sync_synchronize(); // ★ 메모리 배리어: 데이터 읽기 완료 후 tail 갱신 rb->tail = t + 1; return true; } // ── 현재 사용 중인 슬롯 수 ──────────────────────────────────────────────── static inline uint32_t ring_can_count(const CANRingBuffer* rb) { return rb->head - rb->tail; // uint32_t 언더플로우 자동 처리 } // ── Serial RingBuffer (동일한 SPSC 패턴) ───────────────────────────────── static inline bool ring_serial_push(SerialRingBuffer* rb, const SerialMessage* msg) { uint32_t h = rb->head; if ((h - rb->tail) >= rb->size) { rb->dropped++; return false; } rb->buf[h & rb->mask] = *msg; __sync_synchronize(); rb->head = h + 1; return true; } static inline bool ring_serial_pop(SerialRingBuffer* rb, SerialMessage* msg) { uint32_t t = rb->tail; if (rb->head == t) return false; *msg = rb->buf[t & rb->mask]; __sync_synchronize(); rb->tail = t + 1; return true; } static inline uint32_t ring_serial_count(const SerialRingBuffer* rb) { return rb->head - rb->tail; } // ── RingBuffer 초기화/클리어 ───────────────────────────────────────────── static inline void ring_can_clear(CANRingBuffer* rb) { rb->tail = rb->head; // head == tail → 빈 상태 rb->dropped = 0; } static inline void ring_serial_clear(SerialRingBuffer* rb) { rb->tail = rb->head; rb->dropped = 0; } // ======================================== // PSRAM 할당 변수 // ======================================== char *serialCsvBuffer = nullptr; char *serial2CsvBuffer = nullptr; RecentCANData *recentData = nullptr; TxMessage *txMessages = nullptr; CANSequence *sequences = nullptr; FileComment *fileComments = nullptr; // WiFi 설정 char wifiSSID[32] = "Byun_CAN_Logger"; char wifiPassword[64]= "12345678"; bool enableSTAMode = false; char staSSID[32] = ""; char staPassword[64] = ""; // Serial 설정 SerialSettings serialSettings = {115200, 8, 0, 1}; SerialSettings serial2Settings = {115200, 8, 0, 1}; // 전역 객체 SPIClass hspi(HSPI); #define MCP_CRYSTAL MCP_16MHZ MCP2515 mcp2515(HSPI_CS, 40000000, &hspi); HardwareSerial SerialComm(1); HardwareSerial Serial2Comm(2); WebServer server(80); WebSocketsServer webSocket = WebSocketsServer(81); Preferences preferences; // FreeRTOS 핸들 (Mutex / Task만 유지, Queue 핸들 제거됨) SemaphoreHandle_t sdMutex = NULL; SemaphoreHandle_t rtcMutex = NULL; SemaphoreHandle_t serialMutex = NULL; SemaphoreHandle_t serial2Mutex = NULL; TaskHandle_t canRxTaskHandle = NULL; TaskHandle_t sdWriteTaskHandle = NULL; TaskHandle_t webTaskHandle = NULL; TaskHandle_t rtcTaskHandle = NULL; TaskHandle_t serialRxTaskHandle = NULL; TaskHandle_t serial2RxTaskHandle= NULL; // 로깅 상태 volatile bool loggingEnabled = false; volatile bool serialLoggingEnabled = false; volatile bool serial2LoggingEnabled = false; volatile bool sdCardReady = false; // Auto Trigger #define MAX_TRIGGERS 8 TriggerCondition startTriggers[MAX_TRIGGERS]; TriggerCondition stopTriggers[MAX_TRIGGERS]; int startTriggerCount = 0; int stopTriggerCount = 0; String startFormula = ""; String stopFormula = ""; bool autoTriggerEnabled = false; char startLogicOp[4] = "OR"; char stopLogicOp[4] = "OR"; bool autoTriggerActive = false; bool autoTriggerLogCSV = false; bool autoTriggerLogPCAP = false; // ★ PCAP 추가 bool savedCanLogFormatCSV= false; bool savedCanLogFormatPCAP=false; // ★ PCAP 추가 // 파일 핸들 File logFile; File serialLogFile; File serial2LogFile; char currentFilename[MAX_FILENAME_LEN]; char currentSerialFilename[MAX_FILENAME_LEN]; char currentSerial2Filename[MAX_FILENAME_LEN]; // ★ 더블 버퍼링 (SD BIN write 전용) uint8_t* fileBuffer1 = NULL; uint8_t* fileBuffer2 = NULL; uint8_t* currentWriteBuffer = NULL; uint8_t* currentFlushBuffer = NULL; uint32_t writeBufferIndex = 0; uint32_t flushBufferSize = 0; volatile bool flushInProgress = false; TaskHandle_t sdFlushTaskHandle = NULL; uint32_t serialCsvIndex = 0; uint32_t serial2CsvIndex = 0; volatile uint32_t currentFileSize = 0; volatile uint32_t currentSerialFileSize = 0; volatile uint32_t currentSerial2FileSize= 0; volatile bool canLogFormatCSV = false; volatile bool canLogFormatPCAP = false; // ★ PCAP 추가 volatile bool serialLogFormatCSV = true; volatile bool serial2LogFormatCSV = true; volatile uint64_t canLogStartTime = 0; volatile uint64_t serialLogStartTime = 0; volatile uint64_t serial2LogStartTime = 0; // 통계 MCP2515Mode currentMcpMode; SoftWire rtcWire(RTC_SDA, RTC_SCL); char rtcSyncBuffer[20]; CAN_SPEED currentCanSpeed = CAN_1000KBPS; const char* canSpeedNames[] = {"125K", "250K", "500K", "1M"}; CAN_SPEED canSpeedValues[]= {CAN_125KBPS, CAN_250KBPS, CAN_500KBPS, CAN_1000KBPS}; 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; volatile uint32_t totalSerial2RxCount = 0; volatile uint32_t totalSerial2TxCount = 0; uint32_t totalTxCount = 0; uint8_t sequenceCount = 0; SequenceRuntime seqRuntime = {false, 0, 0, 0, -1}; int commentCount = 0; // Forward declarations void IRAM_ATTR canISR(); void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length); void resetMCP2515(); // ======================================== // MCP2515 리셋 // ======================================== void resetMCP2515() { Serial.println("🔄 MCP2515 리셋 시작..."); if (loggingEnabled) { /* 필요시 버퍼 플러시 */ } // ★ 큐 대신 RingBuffer 클리어 ring_can_clear(&canRing); Serial.println(" 1. 하드웨어 리셋..."); digitalWrite(HSPI_CS, LOW); delayMicroseconds(100); digitalWrite(HSPI_CS, HIGH); delay(100); Serial.println(" 2. 소프트웨어 리셋..."); mcp2515.reset(); delay(100); preferences.begin("can-logger", true); int speedIndex = preferences.getInt("can_speed", 3); if (speedIndex >= 0 && speedIndex < 4) currentCanSpeed = canSpeedValues[speedIndex]; int savedMode = preferences.getInt("mcp_mode", 1); if (savedMode >= 0 && savedMode <= 3) currentMcpMode = (MCP2515Mode)savedMode; preferences.end(); mcp2515.setBitrate(currentCanSpeed, MCP_CRYSTAL); delay(10); mcp2515.setFilterMask(MCP2515::MASK0, false, 0x00000000); mcp2515.setFilter(MCP2515::RXF0, false, 0x00000000); mcp2515.setFilter(MCP2515::RXF1, false, 0x00000000); mcp2515.setFilterMask(MCP2515::MASK1, true, 0x00000000); mcp2515.setFilter(MCP2515::RXF2, true, 0x00000000); mcp2515.setFilter(MCP2515::RXF3, true, 0x00000000); mcp2515.setFilter(MCP2515::RXF4, true, 0x00000000); mcp2515.setFilter(MCP2515::RXF5, true, 0x00000000); delay(10); if (currentMcpMode == MCP_MODE_NORMAL) mcp2515.setNormalMode(); else if (currentMcpMode == MCP_MODE_LOOPBACK) mcp2515.setLoopbackMode(); else mcp2515.setListenOnlyMode(); delay(50); struct can_frame dummyFrame; int clearCount = 0; while (mcp2515.readMessage(&dummyFrame) == MCP2515::ERROR_OK) { if (clearCount++ > 100) break; } mcp2515.clearRXnOVRFlags(); mcp2515.clearInterrupts(); mcp2515.clearTXInterrupts(); mcp2515.clearMERR(); mcp2515.clearERRIF(); uint8_t errorFlag = mcp2515.getErrorFlags(); uint8_t txErr = mcp2515.errorCountTX(); uint8_t rxErr = mcp2515.errorCountRX(); Serial.printf(" 에러 상태: EFLG=0x%02X, TEC=%d, REC=%d\n", errorFlag, txErr, rxErr); if (errorFlag != 0 || txErr > 0 || rxErr > 0) { mcp2515.reset(); delay(50); mcp2515.setBitrate(currentCanSpeed, MCP_CRYSTAL); delay(10); if (currentMcpMode == MCP_MODE_NORMAL) mcp2515.setNormalMode(); else if (currentMcpMode == MCP_MODE_LOOPBACK) mcp2515.setLoopbackMode(); else mcp2515.setListenOnlyMode(); delay(50); mcp2515.clearRXnOVRFlags(); mcp2515.clearInterrupts(); } totalMsgCount = 0; lastMsgCount = 0; msgPerSecond = 0; for (int i = 0; i < RECENT_MSG_COUNT; i++) recentData[i].count = 0; Serial.println("✅ MCP2515 리셋 완료!"); } // ======================================== // PSRAM 초기화 (RingBuffer 버전) // ======================================== bool initPSRAM() { Serial.println("\n========================================"); Serial.println(" PSRAM 메모리 할당 (RingBuffer 버전)"); Serial.println("========================================"); if (!psramFound()) { Serial.println("✗ PSRAM을 찾을 수 없습니다!"); return false; } Serial.printf("✓ PSRAM 총 용량: %d MB\n", ESP.getPsramSize() / 1024 / 1024); Serial.printf("✓ PSRAM 여유: %d KB\n\n", ESP.getFreePsram() / 1024); // ── 더블 버퍼 (SD BIN write) ────────────────────────────── fileBuffer1 = (uint8_t*)ps_malloc(FILE_BUFFER_SIZE); fileBuffer2 = (uint8_t*)ps_malloc(FILE_BUFFER_SIZE); if (!fileBuffer1 || !fileBuffer2) { Serial.println("✗ fileBuffer 할당 실패"); return false; } currentWriteBuffer = fileBuffer1; currentFlushBuffer = fileBuffer2; writeBufferIndex = 0; flushBufferSize = 0; Serial.printf("✓ Double Buffer: 2 × %d KB\n", FILE_BUFFER_SIZE / 1024); // ── Serial CSV 버퍼 ──────────────────────────────────────── serialCsvBuffer = (char*)ps_malloc(SERIAL_CSV_BUFFER_SIZE); serial2CsvBuffer = (char*)ps_malloc(SERIAL2_CSV_BUFFER_SIZE); if (!serialCsvBuffer || !serial2CsvBuffer) { Serial.println("✗ SerialCsvBuffer 할당 실패"); return false; } Serial.printf("✓ Serial CSV Buffer: 2 × %d KB\n", SERIAL_CSV_BUFFER_SIZE / 1024); // ── 기타 PSRAM 데이터 ──────────────────────────────────── recentData = (RecentCANData*)ps_calloc(RECENT_MSG_COUNT, sizeof(RecentCANData)); txMessages = (TxMessage*) ps_calloc(MAX_TX_MESSAGES, sizeof(TxMessage)); sequences = (CANSequence*) ps_calloc(MAX_SEQUENCES, sizeof(CANSequence)); fileComments = (FileComment*) ps_calloc(MAX_FILE_COMMENTS,sizeof(FileComment)); if (!recentData || !txMessages || !sequences || !fileComments) { Serial.println("✗ 데이터 버퍼 할당 실패"); return false; } // ======================================== // ★★★ RingBuffer 할당 (FreeRTOS Queue 대체) ★★★ // ======================================== Serial.println("\n📦 RingBuffer 할당 (Lock-Free SPSC)..."); // CAN RingBuffer canRing.buf = (CANMessage*)ps_malloc(CAN_RING_SIZE * sizeof(CANMessage)); if (!canRing.buf) { Serial.println("✗ CAN RingBuffer 할당 실패"); return false; } canRing.size = CAN_RING_SIZE; canRing.mask = CAN_RING_SIZE - 1; canRing.head = 0; canRing.tail = 0; canRing.dropped = 0; Serial.printf("✓ CAN RingBuffer: %d 슬롯 × %d bytes = %.2f KB\n", CAN_RING_SIZE, sizeof(CANMessage), (float)(CAN_RING_SIZE * sizeof(CANMessage)) / 1024.0f); // Serial RingBuffer serialRing.buf = (SerialMessage*)ps_malloc(SERIAL_RING_SIZE * sizeof(SerialMessage)); if (!serialRing.buf) { Serial.println("✗ Serial RingBuffer 할당 실패"); return false; } serialRing.size = SERIAL_RING_SIZE; serialRing.mask = SERIAL_RING_SIZE - 1; serialRing.head = 0; serialRing.tail = 0; serialRing.dropped = 0; Serial.printf("✓ SER RingBuffer: %d 슬롯 × %d bytes = %.2f KB\n", SERIAL_RING_SIZE, sizeof(SerialMessage), (float)(SERIAL_RING_SIZE * sizeof(SerialMessage)) / 1024.0f); // Serial2 RingBuffer serial2Ring.buf = (SerialMessage*)ps_malloc(SERIAL_RING_SIZE * sizeof(SerialMessage)); if (!serial2Ring.buf) { Serial.println("✗ Serial2 RingBuffer 할당 실패"); return false; } serial2Ring.size = SERIAL_RING_SIZE; serial2Ring.mask = SERIAL_RING_SIZE - 1; serial2Ring.head = 0; serial2Ring.tail = 0; serial2Ring.dropped = 0; Serial.printf("✓ SER2 RingBuffer: %d 슬롯 × %d bytes = %.2f KB\n", SERIAL_RING_SIZE, sizeof(SerialMessage), (float)(SERIAL_RING_SIZE * sizeof(SerialMessage)) / 1024.0f); Serial.println("========================================"); Serial.printf("✓ PSRAM 남은 용량: %.2f KB\n", (float)ESP.getFreePsram() / 1024.0f); Serial.println("========================================\n"); return true; } // ======================================== // 설정 저장/로드 // ======================================== 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); serial2Settings.baudRate = preferences.getUInt("ser2_baud", 115200); serial2Settings.dataBits = preferences.getUChar("ser2_data", 8); serial2Settings.parity = preferences.getUChar("ser2_parity", 0); serial2Settings.stopBits = preferences.getUChar("ser2_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); preferences.putUInt("ser2_baud", serial2Settings.baudRate); preferences.putUChar("ser2_data", serial2Settings.dataBits); preferences.putUChar("ser2_parity", serial2Settings.parity); preferences.putUChar("ser2_stop", serial2Settings.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 config = SERIAL_5O1; } else if (serialSettings.dataBits == 6) { if (serialSettings.parity == 0) config = SERIAL_6N1; else if (serialSettings.parity == 1) config = SERIAL_6E1; else config = SERIAL_6O1; } else if (serialSettings.dataBits == 7) { if (serialSettings.parity == 0) config = SERIAL_7N1; else if (serialSettings.parity == 1) config = SERIAL_7E1; else config = SERIAL_7O1; } else { if (serialSettings.parity == 0) config = SERIAL_8N1; else if (serialSettings.parity == 1) config = SERIAL_8E1; else config = SERIAL_8O1; } if (serialSettings.stopBits == 2) config |= 0x3000; SerialComm.begin(serialSettings.baudRate, config, SERIAL_RX_PIN, SERIAL_TX_PIN); SerialComm.setRxBufferSize(2048); uint32_t config2 = SERIAL_8N1; if (serial2Settings.dataBits == 5) { if (serial2Settings.parity == 0) config2 = SERIAL_5N1; else if (serial2Settings.parity == 1) config2 = SERIAL_5E1; else config2 = SERIAL_5O1; } else if (serial2Settings.dataBits == 6) { if (serial2Settings.parity == 0) config2 = SERIAL_6N1; else if (serial2Settings.parity == 1) config2 = SERIAL_6E1; else config2 = SERIAL_6O1; } else if (serial2Settings.dataBits == 7) { if (serial2Settings.parity == 0) config2 = SERIAL_7N1; else if (serial2Settings.parity == 1) config2 = SERIAL_7E1; else config2 = SERIAL_7O1; } else { if (serial2Settings.parity == 0) config2 = SERIAL_8N1; else if (serial2Settings.parity == 1) config2 = SERIAL_8E1; else config2 = SERIAL_8O1; } if (serial2Settings.stopBits == 2) config2 |= 0x3000; Serial2Comm.begin(serial2Settings.baudRate, config2, SERIAL2_RX_PIN, SERIAL2_TX_PIN); Serial2Comm.setRxBufferSize(2048); } // ======================================== // ★ PCAP 전역 헤더 쓰기 (파일 오픈 직후 1회 호출) // ======================================== bool writePcapGlobalHeader(File &f) { PcapGlobalHeader gh; gh.magic_number = 0xa1b2c3d4; gh.version_major = 2; gh.version_minor = 4; gh.thiszone = 0; gh.sigfigs = 0; gh.snaplen = 65535; gh.network = 227; // LINKTYPE_CAN_SOCKETCAN return f.write((uint8_t*)&gh, sizeof(gh)) == sizeof(gh); } // ======================================== 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", 1); if (savedMode >= 0 && savedMode <= 3) currentMcpMode = (MCP2515Mode)savedMode; savedCanLogFormatCSV = preferences.getBool("can_format_csv", false); savedCanLogFormatPCAP = preferences.getBool("can_format_pcap", false); // 둘 다 false면 BIN이 기본값 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); preferences.putBool("can_format_csv", savedCanLogFormatCSV); preferences.putBool("can_format_pcap", savedCanLogFormatPCAP); saveSerialSettings(); preferences.end(); } // ======================================== // Auto Trigger // ======================================== int64_t extractBits(uint8_t *data, uint8_t startBit, uint8_t bitLength) { if (bitLength == 0 || bitLength > 64 || startBit >= 64) return 0; int64_t result = 0; for (int i = 0; i < bitLength; i++) { uint8_t bitPos = startBit + i; uint8_t byteIdx = bitPos / 8; uint8_t bitIdx = 7 - (bitPos % 8); if (byteIdx < 8 && (data[byteIdx] & (1 << bitIdx))) result |= (1LL << (bitLength - 1 - i)); } return result; } bool checkCondition(TriggerCondition &trigger, uint8_t *data) { if (!trigger.enabled) return false; int64_t v = extractBits(data, trigger.startBit, trigger.bitLength); if (strcmp(trigger.op, "==") == 0) return v == trigger.value; if (strcmp(trigger.op, "!=") == 0) return v != trigger.value; if (strcmp(trigger.op, ">") == 0) return v > trigger.value; if (strcmp(trigger.op, "<") == 0) return v < trigger.value; if (strcmp(trigger.op, ">=") == 0) return v >= trigger.value; if (strcmp(trigger.op, "<=") == 0) return v <= trigger.value; return false; } void checkAutoTriggers(struct can_frame &frame) { if (!autoTriggerEnabled || !sdCardReady) return; // Start 조건 if (!loggingEnabled && startTriggerCount > 0) { bool result = (strcmp(startLogicOp, "AND") == 0); bool anyMatch = false; for (int i = 0; i < startTriggerCount; i++) { if (startTriggers[i].canId == frame.can_id && startTriggers[i].enabled) { bool match = checkCondition(startTriggers[i], frame.data); anyMatch = true; if (strcmp(startLogicOp, "AND") == 0) { result = result && match; if (!match) break; } else { if (match) { result = true; break; } } } } if (anyMatch && result) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { time_t now; struct tm timeinfo; time(&now); localtime_r(&now, &timeinfo); struct timeval tv; gettimeofday(&tv, NULL); canLogStartTime = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; canLogFormatCSV = autoTriggerLogCSV; canLogFormatPCAP = autoTriggerLogPCAP; const char* ext; if (canLogFormatPCAP) ext = "pcap"; else if (canLogFormatCSV) ext = "csv"; else ext = "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_MMC.open(currentFilename, FILE_WRITE); if (logFile) { if (canLogFormatCSV) { logFile.println("Time_us,CAN_ID,DLC,Data"); logFile.flush(); } else if (canLogFormatPCAP) { writePcapGlobalHeader(logFile); logFile.flush(); } logFile.close(); logFile = SD_MMC.open(currentFilename, FILE_APPEND); if (logFile) { loggingEnabled = true; autoTriggerActive = true; writeBufferIndex = 0; flushBufferSize = 0; flushInProgress = false; currentFileSize = logFile.size(); Serial.printf("🎯 Auto Trigger 시작: %s\n", currentFilename); } } xSemaphoreGive(sdMutex); } } } // Stop 조건 if (loggingEnabled && stopTriggerCount > 0) { bool result = (strcmp(stopLogicOp, "AND") == 0); bool anyMatch = false; for (int i = 0; i < stopTriggerCount; i++) { if (stopTriggers[i].canId == frame.can_id && stopTriggers[i].enabled) { bool match = checkCondition(stopTriggers[i], frame.data); anyMatch = true; if (strcmp(stopLogicOp, "AND") == 0) { result = result && match; if (!match) break; } else { if (match) { result = true; break; } } } } if (anyMatch && result) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(2000)) == pdTRUE) { if (writeBufferIndex > 0 && logFile) { logFile.write(currentWriteBuffer, writeBufferIndex); logFile.flush(); writeBufferIndex = 0; } int waitCount = 0; while (flushInProgress && waitCount++ < 500) vTaskDelay(10); if ((canLogFormatCSV || canLogFormatPCAP) && logFile) logFile.flush(); if (logFile) { logFile.close(); } loggingEnabled = false; autoTriggerActive = false; currentFilename[0]= '\0'; writeBufferIndex = 0; flushBufferSize = 0; Serial.println("🎯 Auto Trigger 중지"); xSemaphoreGive(sdMutex); } } } } void saveAutoTriggerSettings() { preferences.begin("autotrigger", false); preferences.putBool("enabled", autoTriggerEnabled); preferences.putBool("logCSV", autoTriggerLogCSV); preferences.putBool("logPCAP", autoTriggerLogPCAP); preferences.putString("start_logic", startLogicOp); preferences.putString("stop_logic", stopLogicOp); preferences.putInt("start_count", startTriggerCount); for (int i = 0; i < startTriggerCount; i++) { char key[32]; sprintf(key, "s%d_id", i); preferences.putUInt(key, startTriggers[i].canId); sprintf(key, "s%d_bit", i); preferences.putUChar(key, startTriggers[i].startBit); sprintf(key, "s%d_len", i); preferences.putUChar(key, startTriggers[i].bitLength); sprintf(key, "s%d_op", i); preferences.putString(key, startTriggers[i].op); sprintf(key, "s%d_val", i); preferences.putLong(key, startTriggers[i].value); sprintf(key, "s%d_en", i); preferences.putBool(key, startTriggers[i].enabled); } preferences.putInt("stop_count", stopTriggerCount); for (int i = 0; i < stopTriggerCount; i++) { char key[32]; sprintf(key, "p%d_id", i); preferences.putUInt(key, stopTriggers[i].canId); sprintf(key, "p%d_bit", i); preferences.putUChar(key, stopTriggers[i].startBit); sprintf(key, "p%d_len", i); preferences.putUChar(key, stopTriggers[i].bitLength); sprintf(key, "p%d_op", i); preferences.putString(key, stopTriggers[i].op); sprintf(key, "p%d_val", i); preferences.putLong(key, stopTriggers[i].value); sprintf(key, "p%d_en", i); preferences.putBool(key, stopTriggers[i].enabled); } preferences.putString("start_formula", startFormula); preferences.putString("stop_formula", stopFormula); preferences.end(); } void loadAutoTriggerSettings() { preferences.begin("autotrigger", true); autoTriggerEnabled = preferences.getBool("enabled", false); autoTriggerLogCSV = preferences.getBool("logCSV", false); autoTriggerLogPCAP = preferences.getBool("logPCAP", false); preferences.getString("start_logic", startLogicOp, sizeof(startLogicOp)); preferences.getString("stop_logic", stopLogicOp, sizeof(stopLogicOp)); startFormula = preferences.getString("start_formula", ""); stopFormula = preferences.getString("stop_formula", ""); if (strlen(startLogicOp) == 0) strcpy(startLogicOp, "OR"); if (strlen(stopLogicOp) == 0) strcpy(stopLogicOp, "OR"); startTriggerCount = preferences.getInt("start_count", 0); if (startTriggerCount > MAX_TRIGGERS) startTriggerCount = MAX_TRIGGERS; for (int i = 0; i < startTriggerCount; i++) { char key[32]; sprintf(key, "s%d_id", i); startTriggers[i].canId = preferences.getUInt(key, 0); sprintf(key, "s%d_bit", i); startTriggers[i].startBit = preferences.getUChar(key, 0); sprintf(key, "s%d_len", i); startTriggers[i].bitLength = preferences.getUChar(key, 8); sprintf(key, "s%d_op", i); preferences.getString(key, startTriggers[i].op, sizeof(startTriggers[i].op)); if (strlen(startTriggers[i].op) == 0) strcpy(startTriggers[i].op, "=="); sprintf(key, "s%d_val", i); startTriggers[i].value = preferences.getLong(key, 0); sprintf(key, "s%d_en", i); startTriggers[i].enabled = preferences.getBool(key, true); } stopTriggerCount = preferences.getInt("stop_count", 0); if (stopTriggerCount > MAX_TRIGGERS) stopTriggerCount = MAX_TRIGGERS; for (int i = 0; i < stopTriggerCount; i++) { char key[32]; sprintf(key, "p%d_id", i); stopTriggers[i].canId = preferences.getUInt(key, 0); sprintf(key, "p%d_bit", i); stopTriggers[i].startBit = preferences.getUChar(key, 0); sprintf(key, "p%d_len", i); stopTriggers[i].bitLength = preferences.getUChar(key, 8); sprintf(key, "p%d_op", i); preferences.getString(key, stopTriggers[i].op, sizeof(stopTriggers[i].op)); if (strlen(stopTriggers[i].op) == 0) strcpy(stopTriggers[i].op, "=="); sprintf(key, "p%d_val", i); stopTriggers[i].value = preferences.getLong(key, 0); sprintf(key, "p%d_en", i); stopTriggers[i].enabled = preferences.getBool(key, true); } preferences.end(); } // ======================================== // RTC // ======================================== void initRTC() { rtcWire.begin(); rtcWire.setClock(100000); rtcWire.beginTransmission(DS3231_ADDRESS); timeSyncStatus.rtcAvailable = (rtcWire.endTransmission() == 0); Serial.printf("%s RTC(DS3231) %s\n", timeSyncStatus.rtcAvailable ? "✓" : "!", timeSyncStatus.rtcAvailable ? "감지됨" : "없음"); } uint8_t bcdToDec(uint8_t v) { return (v >> 4) * 10 + (v & 0x0F); } uint8_t decToBcd(uint8_t v) { return ((v / 10) << 4) | (v % 10); } bool readRTC(struct tm *t) { if (!timeSyncStatus.rtcAvailable) return false; if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(100)) != 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 buf[7]; for (int i = 0; i < 7; i++) buf[i] = rtcWire.read(); xSemaphoreGive(rtcMutex); t->tm_sec = bcdToDec(buf[0] & 0x7F); t->tm_min = bcdToDec(buf[1] & 0x7F); t->tm_hour = bcdToDec(buf[2] & 0x3F); t->tm_wday = bcdToDec(buf[3] & 0x07) - 1; t->tm_mday = bcdToDec(buf[4] & 0x3F); t->tm_mon = bcdToDec(buf[5] & 0x1F) - 1; t->tm_year = bcdToDec(buf[6]) + 100; return true; } bool writeRTC(const struct tm *t) { if (!timeSyncStatus.rtcAvailable) return false; if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(100)) != pdTRUE) return false; rtcWire.beginTransmission(DS3231_ADDRESS); rtcWire.write(0x00); rtcWire.write(decToBcd(t->tm_sec)); rtcWire.write(decToBcd(t->tm_min)); rtcWire.write(decToBcd(t->tm_hour)); rtcWire.write(decToBcd(t->tm_wday + 1)); rtcWire.write(decToBcd(t->tm_mday)); rtcWire.write(decToBcd(t->tm_mon + 1)); rtcWire.write(decToBcd(t->tm_year - 100)); bool ok = (rtcWire.endTransmission() == 0); xSemaphoreGive(rtcMutex); return ok; } void timeSyncCallback(struct timeval *tv) { timeSyncStatus.synchronized = true; timeSyncStatus.lastSyncTime = (uint64_t)tv->tv_sec * 1000000ULL + tv->tv_usec; timeSyncStatus.syncCount++; if (timeSyncStatus.rtcAvailable) { struct tm t; time_t now = tv->tv_sec; localtime_r(&now, &t); if (writeRTC(&t)) timeSyncStatus.rtcSyncCount++; } } void initNTP() { configTime(9 * 3600, 0, "pool.ntp.org", "time.nist.gov"); sntp_set_time_sync_notification_cb(timeSyncCallback); } void rtcSyncTask(void *parameter) { while (1) { if (timeSyncStatus.rtcAvailable) { struct tm t; if (readRTC(&t)) { time_t now = mktime(&t); struct timeval tv = {now, 0}; settimeofday(&tv, NULL); timeSyncStatus.synchronized = true; timeSyncStatus.lastSyncTime = (uint64_t)now * 1000000ULL; timeSyncStatus.rtcSyncCount++; } } vTaskDelay(pdMS_TO_TICKS(RTC_SYNC_INTERVAL_MS)); } } // ======================================== // MCP2515 모드 // ======================================== bool setMCP2515Mode(MCP2515Mode mode) { MCP2515::ERROR result; switch (mode) { case MCP_MODE_NORMAL: result = mcp2515.setNormalMode(); break; case MCP_MODE_LISTEN_ONLY: result = mcp2515.setListenOnlyMode(); break; case MCP_MODE_LOOPBACK: result = mcp2515.setLoopbackMode(); break; case MCP_MODE_TRANSMIT: result = mcp2515.setListenOnlyMode(); break; default: return false; } if (result == MCP2515::ERROR_OK) { currentMcpMode = mode; return true; } return false; } // ======================================== // ★★★ CAN 인터럽트 & canRxTask (RingBuffer 버전) ★★★ // ======================================== void IRAM_ATTR canISR() { BaseType_t xHigherPriorityTaskWoken = pdFALSE; if (canRxTaskHandle != NULL) vTaskNotifyGiveFromISR(canRxTaskHandle, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } void canRxTask(void *parameter) { struct can_frame frame; CANMessage msg; uint32_t lastErrorCheck = 0; uint32_t errorRecoveryCount = 0; Serial.println("✓ CAN RX Task 시작 (RingBuffer SPSC, Core 1, Pri 24)"); // 초기 버퍼 클리어 if (digitalRead(CAN_INT_PIN) == LOW) { int readCount = 0; while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK && readCount < 100) { 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); // ★ RingBuffer push (non-blocking, no mutex) if (ring_can_push(&canRing, &msg)) totalMsgCount++; readCount++; } } while (1) { ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(100)); // 주기적 에러 체크 (1초마다) uint32_t now = millis(); if (now - lastErrorCheck > 1000) { lastErrorCheck = now; uint8_t errorFlag = mcp2515.getErrorFlags(); uint8_t txErr = mcp2515.errorCountTX(); uint8_t rxErr = mcp2515.errorCountRX(); if (errorFlag & 0xC0) { mcp2515.clearRXnOVRFlags(); mcp2515.clearInterrupts(); errorRecoveryCount++; } if (errorFlag & 0x20) { mcp2515.reset(); delay(100); mcp2515.setBitrate(currentCanSpeed, MCP_CRYSTAL); delay(10); mcp2515.clearRXnOVRFlags(); mcp2515.clearInterrupts(); mcp2515.clearTXInterrupts(); mcp2515.clearMERR(); mcp2515.clearERRIF(); delay(10); if (currentMcpMode == MCP_MODE_NORMAL) mcp2515.setNormalMode(); else if (currentMcpMode == MCP_MODE_LOOPBACK) mcp2515.setLoopbackMode(); else mcp2515.setListenOnlyMode(); delay(50); while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {} mcp2515.clearRXnOVRFlags(); errorRecoveryCount++; Serial.printf("✓ Bus-Off 복구 (총 %lu회)\n", errorRecoveryCount); } if ((errorFlag & 0x18) && rxErr > 96) Serial.printf("⚠️ Error Passive! REC=%d\n", rxErr); } // ★ 배치 읽기 → RingBuffer push (완전 non-blocking) int batchCount = 0; while (batchCount < 20 && mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) { checkAutoTriggers(frame); 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); // ★ mutex 없음, blocking 없음 - 순수 메모리 쓰기 if (ring_can_push(&canRing, &msg)) { totalMsgCount++; } // dropped 시에도 canRing.dropped에 자동 기록됨 batchCount++; } if (batchCount == 0) vTaskDelay(pdMS_TO_TICKS(1)); } } // ======================================== // Serial RX Tasks (RingBuffer 버전) // ======================================== void serialRxTask(void *parameter) { SerialMessage msg; uint8_t lineBuffer[MAX_SERIAL_LINE_LEN]; uint16_t lineIndex = 0; uint32_t lastActivity = millis(); 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); msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; msg.length = lineIndex; memcpy(msg.data, lineBuffer, lineIndex); msg.isTx = false; if (ring_serial_push(&serialRing, &msg)) totalSerialRxCount++; lineIndex = 0; } } if (lineIndex >= MAX_SERIAL_LINE_LEN - 1) lineIndex = 0; } if (lineIndex > 0 && (millis() - lastActivity > 100)) { struct timeval tv; gettimeofday(&tv, NULL); msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; msg.length = lineIndex; memcpy(msg.data, lineBuffer, lineIndex); msg.isTx = false; if (ring_serial_push(&serialRing, &msg)) totalSerialRxCount++; lineIndex = 0; } vTaskDelay(pdMS_TO_TICKS(1)); } } void serial2RxTask(void *parameter) { SerialMessage msg; uint8_t lineBuffer[MAX_SERIAL_LINE_LEN]; uint16_t lineIndex = 0; uint32_t lastActivity = millis(); while (1) { while (Serial2Comm.available()) { uint8_t c = Serial2Comm.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); msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; msg.length = lineIndex; memcpy(msg.data, lineBuffer, lineIndex); msg.isTx = false; if (ring_serial_push(&serial2Ring, &msg)) totalSerial2RxCount++; lineIndex = 0; } } if (lineIndex >= MAX_SERIAL_LINE_LEN - 1) lineIndex = 0; } if (lineIndex > 0 && (millis() - lastActivity > 100)) { struct timeval tv; gettimeofday(&tv, NULL); msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; msg.length = lineIndex; memcpy(msg.data, lineBuffer, lineIndex); msg.isTx = false; if (ring_serial_push(&serial2Ring, &msg)) totalSerial2RxCount++; lineIndex = 0; } vTaskDelay(pdMS_TO_TICKS(1)); } } // ======================================== // ★★★ SD Flush Task (더블 버퍼 비동기 flush) ★★★ // ======================================== void sdFlushTask(void *parameter) { Serial.println("✓ sdFlushTask 시작 (더블 버퍼링 + RingBuffer)"); while (1) { uint32_t flushSize; if (xTaskNotifyWait(0, 0xFFFFFFFF, &flushSize, portMAX_DELAY) == pdTRUE) { if (flushSize > 0 && flushSize <= FILE_BUFFER_SIZE) { flushInProgress = true; uint32_t t0 = millis(); if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(5000)) == pdTRUE) { if (logFile && sdCardReady && loggingEnabled) { size_t written = logFile.write(currentFlushBuffer, flushSize); logFile.flush(); uint32_t elapsed = millis() - t0; if (elapsed > 100) Serial.printf("⚠️ SD Flush 지연: %d ms (%d bytes)\n", elapsed, written); } xSemaphoreGive(sdMutex); } else { Serial.println("✗ SD Flush: Mutex 타임아웃"); } flushInProgress = false; } } } } // ======================================== // ★★★ SD Write Task (RingBuffer Consumer) ★★★ // // 변경점: // - xQueueReceive() → ring_can_pop() (non-blocking, no mutex) // - BIN hot path: sdMutex 제거 (sdWriteTask만 쓰는 버퍼) // - 버퍼 스왑 시에만 sdMutex 획득 (최소화) // ======================================== void sdWriteTask(void *parameter) { CANMessage canMsg; while (1) { bool hasWork = false; // ★ RingBuffer에서 pop (mutex 없음, 완전 non-blocking) if (ring_can_pop(&canRing, &canMsg)) { hasWork = true; // 실시간 모니터링 테이블 업데이트 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; } } } // SD 로깅 if (loggingEnabled && sdCardReady) { if (canLogFormatCSV) { // ── CSV: logFile 직접 쓰기 → sdMutex 필요 ──────────────── if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1)) == pdTRUE) { char csvLine[128]; uint64_t relTime = 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", relTime, canMsg.id, canMsg.dlc, dataStr); if (logFile) { logFile.write((uint8_t*)csvLine, lineLen); currentFileSize += lineLen; static int csvFlushCnt = 0; if (++csvFlushCnt >= 2000) { logFile.flush(); csvFlushCnt = 0; } } xSemaphoreGive(sdMutex); } } else if (canLogFormatPCAP) { // ── PCAP: 32bytes/record → 더블 버퍼 (BIN과 동일 구조) ── if (writeBufferIndex + sizeof(PcapRecord) > FILE_BUFFER_SIZE) { int waitCount = 0; while (flushInProgress && waitCount++ < 100) vTaskDelay(1); uint8_t* tmp = currentWriteBuffer; currentWriteBuffer = currentFlushBuffer; currentFlushBuffer = tmp; flushBufferSize = writeBufferIndex; writeBufferIndex = 0; if (sdFlushTaskHandle) xTaskNotify(sdFlushTaskHandle, flushBufferSize, eSetValueWithOverwrite); } // PcapRecord 조립 후 버퍼에 기록 PcapRecord rec; uint32_t sec = (uint32_t)(canMsg.timestamp_us / 1000000ULL); uint32_t usec = (uint32_t)(canMsg.timestamp_us % 1000000ULL); rec.hdr.ts_sec = sec; rec.hdr.ts_usec = usec; rec.hdr.incl_len = sizeof(SocketCANFrame); // 16 rec.hdr.orig_len = sizeof(SocketCANFrame); // 16 // SocketCAN ID: 확장 프레임이면 bit31 세트 rec.frame.can_id = canMsg.id; if (canMsg.id > 0x7FF) rec.frame.can_id |= 0x80000000U; // CAN_EFF_FLAG rec.frame.can_dlc = canMsg.dlc; rec.frame.pad = 0; rec.frame.res0 = 0; rec.frame.res1 = 0; memcpy(rec.frame.data, canMsg.data, 8); memcpy(¤tWriteBuffer[writeBufferIndex], &rec, sizeof(PcapRecord)); writeBufferIndex += sizeof(PcapRecord); currentFileSize += sizeof(PcapRecord); } else { // ── BIN: 더블 버퍼 (hot path, sdMutex 없음!) ───────────── // writeBuffer는 sdWriteTask만 쓰므로 mutex 불필요 if (writeBufferIndex + sizeof(CANMessage) > FILE_BUFFER_SIZE) { // 버퍼 스왑 - flushTask 완료 대기 (최대 100ms) int waitCount = 0; while (flushInProgress && waitCount++ < 100) vTaskDelay(1); // ★ 버퍼 포인터 스왑 (즉시, 블로킹 없음) uint8_t* tmp = currentWriteBuffer; currentWriteBuffer = currentFlushBuffer; currentFlushBuffer = tmp; flushBufferSize = writeBufferIndex; writeBufferIndex = 0; // sdFlushTask에 비동기 알림 if (sdFlushTaskHandle) xTaskNotify(sdFlushTaskHandle, flushBufferSize, eSetValueWithOverwrite); } // 현재 쓰기 버퍼에 메시지 추가 (순수 memcpy) memcpy(¤tWriteBuffer[writeBufferIndex], &canMsg, sizeof(CANMessage)); writeBufferIndex += sizeof(CANMessage); currentFileSize += sizeof(CANMessage); } } } if (!hasWork) vTaskDelay(pdMS_TO_TICKS(1)); } } // ======================================== // 모니터 Task // ======================================== void sdMonitorTask(void *parameter) { while (1) { uint32_t now = millis(); if (now - lastMsgCountTime >= 1000) { msgPerSecond = totalMsgCount - lastMsgCount; lastMsgCount = totalMsgCount; lastMsgCountTime= now; } if (now - powerStatus.lastCheck >= VOLTAGE_CHECK_INTERVAL_MS) { float rawV = analogRead(MONITORING_VOLT) * (3.3f / 4095.0f); powerStatus.voltage = rawV; if (now - powerStatus.lastMinReset >= 1000) { powerStatus.minVoltage = rawV; powerStatus.lastMinReset= now; } else { if (rawV < powerStatus.minVoltage) powerStatus.minVoltage = rawV; } powerStatus.lowVoltage = (powerStatus.voltage < LOW_VOLTAGE_THRESHOLD); powerStatus.lastCheck = now; } vTaskDelay(pdMS_TO_TICKS(500)); } } // ======================================== // 파일 코멘트 / 시퀀스 관리 // ======================================== void saveFileComments() { if (!sdCardReady) return; if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { File f = SD_MMC.open("/comments.dat", FILE_WRITE); if (f) { f.write((uint8_t*)&commentCount, sizeof(commentCount)); f.write((uint8_t*)fileComments, sizeof(FileComment) * commentCount); f.close(); } xSemaphoreGive(sdMutex); } } void loadFileComments() { if (!sdCardReady) return; if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { if (SD_MMC.exists("/comments.dat")) { File f = SD_MMC.open("/comments.dat", FILE_READ); if (f) { f.read((uint8_t*)&commentCount, sizeof(commentCount)); if (commentCount > MAX_FILE_COMMENTS) commentCount = MAX_FILE_COMMENTS; f.read((uint8_t*)fileComments, sizeof(FileComment) * commentCount); f.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 f = SD_MMC.open("/sequences.dat", FILE_WRITE); if (f) { f.write((uint8_t*)&sequenceCount, sizeof(sequenceCount)); f.write((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount); f.close(); } xSemaphoreGive(sdMutex); } } void loadSequences() { if (!sdCardReady) return; if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { if (SD_MMC.exists("/sequences.dat")) { File f = SD_MMC.open("/sequences.dat", FILE_READ); if (f) { f.read((uint8_t*)&sequenceCount, sizeof(sequenceCount)); if (sequenceCount > MAX_SEQUENCES) sequenceCount = MAX_SEQUENCES; f.read((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount); f.close(); } } xSemaphoreGive(sdMutex); } } // ======================================== // TX / Sequence Task // ======================================== void txTask(void *parameter) { struct can_frame frame; 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(anyActive ? pdMS_TO_TICKS(1) : pdMS_TO_TICKS(10)); } } void sequenceTask(void *parameter) { 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) { if (++seqRuntime.currentRepeat >= seq->repeatCount) seqRuntime.running = false; else { seqRuntime.currentStep = 0; seqRuntime.lastStepTime = now; } } else { seqRuntime.currentStep = 0; seqRuntime.lastStepTime = now; } } vTaskDelay(pdMS_TO_TICKS(1)); } else { vTaskDelay(pdMS_TO_TICKS(10)); } } } // ======================================== // WebSocket 이벤트 // ======================================== void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) { if (type == WStype_CONNECTED) { DynamicJsonDocument allData(3072); allData["type"] = "initialData"; int speedIndex = 3; for (int i = 0; i < 4; i++) { if (canSpeedValues[i] == currentCanSpeed) { speedIndex = i; break; } } allData["canSpeed"] = speedIndex; allData["mcpMode"] = (int)currentMcpMode; allData["autoTriggerEnabled"]= autoTriggerEnabled; allData["autoTriggerLogCSV"] = autoTriggerLogCSV; allData["autoTriggerLogPCAP"]= autoTriggerLogPCAP; if (autoTriggerEnabled) { allData["logFormat"] = autoTriggerLogPCAP ? "pcap" : (autoTriggerLogCSV ? "csv" : "bin"); allData["startLogic"] = startLogicOp; allData["stopLogic"] = stopLogicOp; allData["startFormula"]= startFormula; allData["stopFormula"] = stopFormula; JsonArray sa = allData.createNestedArray("startTriggers"); for (int i = 0; i < startTriggerCount; i++) { JsonObject t = sa.createNestedObject(); char idStr[10]; sprintf(idStr, "0x%03X", startTriggers[i].canId); t["canId"] = idStr; t["startBit"] = startTriggers[i].startBit; t["bitLength"] = startTriggers[i].bitLength; t["op"] = startTriggers[i].op; t["value"] = (long)startTriggers[i].value; t["enabled"] = startTriggers[i].enabled; } JsonArray ea = allData.createNestedArray("stopTriggers"); for (int i = 0; i < stopTriggerCount; i++) { JsonObject t = ea.createNestedObject(); char idStr[10]; sprintf(idStr, "0x%03X", stopTriggers[i].canId); t["canId"] = idStr; t["startBit"] = stopTriggers[i].startBit; t["bitLength"] = stopTriggers[i].bitLength; t["op"] = stopTriggers[i].op; t["value"] = (long)stopTriggers[i].value; t["enabled"] = stopTriggers[i].enabled; } } String json; serializeJson(allData, json); webSocket.sendTXT(num, json); } else if (type == WStype_TEXT) { DynamicJsonDocument doc(44384); if (deserializeJson(doc, payload)) return; const char* cmd = doc["cmd"]; if (strcmp(cmd, "getSettings") == 0) { DynamicJsonDocument r(1024); r["type"] = "settings"; r["ssid"] = wifiSSID; r["password"] = wifiPassword; r["staEnable"] = enableSTAMode; r["staSSID"] = staSSID; r["staPassword"] = staPassword; r["staConnected"] = (WiFi.status() == WL_CONNECTED); r["staIP"] = WiFi.localIP().toString(); String j; serializeJson(r, j); webSocket.sendTXT(num, j); } else if (strcmp(cmd, "saveSettings") == 0) { const char* s = doc["ssid"]; if (s && strlen(s) > 0) { strncpy(wifiSSID, s, sizeof(wifiSSID)-1); wifiSSID[sizeof(wifiSSID)-1]='\0'; } const char* p = doc["password"]; if (p) { strncpy(wifiPassword, p, sizeof(wifiPassword)-1); wifiPassword[sizeof(wifiPassword)-1]='\0'; } enableSTAMode = doc["staEnable"]; const char* ss = doc["staSSID"]; if (ss) { strncpy(staSSID, ss, sizeof(staSSID)-1); staSSID[sizeof(staSSID)-1]='\0'; } const char* sp = doc["staPassword"]; if (sp) { strncpy(staPassword, sp, sizeof(staPassword)-1); staPassword[sizeof(staPassword)-1]='\0'; } saveSettings(); DynamicJsonDocument r(256); r["type"]="settingsSaved"; r["success"]=true; String j; serializeJson(r,j); webSocket.sendTXT(num,j); } else if (strcmp(cmd, "startLogging") == 0) { if (!loggingEnabled && sdCardReady) { const char* fmt = doc["format"]; canLogFormatCSV = (fmt && strcmp(fmt, "csv") == 0); canLogFormatPCAP = (fmt && strcmp(fmt, "pcap") == 0); if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { time_t now; struct tm ti; time(&now); localtime_r(&now, &ti); struct timeval tv; gettimeofday(&tv, NULL); canLogStartTime = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; const char* ext; if (canLogFormatPCAP) ext = "pcap"; else if (canLogFormatCSV) ext = "csv"; else ext = "bin"; snprintf(currentFilename, sizeof(currentFilename), "/CAN_%04d%02d%02d_%02d%02d%02d.%s", ti.tm_year+1900, ti.tm_mon+1, ti.tm_mday, ti.tm_hour, ti.tm_min, ti.tm_sec, ext); logFile = SD_MMC.open(currentFilename, FILE_WRITE); if (logFile) { if (canLogFormatCSV) { logFile.println("Time_us,CAN_ID,DLC,Data"); logFile.flush(); } else if (canLogFormatPCAP) { writePcapGlobalHeader(logFile); logFile.flush(); } logFile.close(); logFile = SD_MMC.open(currentFilename, FILE_APPEND); if (logFile) { loggingEnabled = true; writeBufferIndex = 0; flushBufferSize = 0; flushInProgress = false; currentFileSize = logFile.size(); const char* fmtName = canLogFormatPCAP ? "PCAP" : (canLogFormatCSV ? "CSV" : "BIN"); Serial.printf("✅ 로깅 시작 [%s]: %s\n", fmtName, currentFilename); } } xSemaphoreGive(sdMutex); } } } else if (strcmp(cmd, "stopLogging") == 0) { if (loggingEnabled) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(2000)) == pdTRUE) { if (writeBufferIndex > 0 && logFile) { logFile.write(currentWriteBuffer, writeBufferIndex); logFile.flush(); writeBufferIndex = 0; } int wc = 0; while (flushInProgress && wc++ < 500) vTaskDelay(10); // CSV/PCAP 모두 최종 flush if ((canLogFormatCSV || canLogFormatPCAP) && logFile) logFile.flush(); if (logFile) { logFile.close(); } loggingEnabled = false; currentFilename[0] = '\0'; writeBufferIndex = 0; flushBufferSize = 0; xSemaphoreGive(sdMutex); } } } else if (strcmp(cmd, "startSerialLogging") == 0) { if (!serialLoggingEnabled && sdCardReady) { const char* fmt = doc["format"]; serialLogFormatCSV = !(fmt && strcmp(fmt, "bin") == 0); if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { time_t now; struct tm ti; time(&now); localtime_r(&now, &ti); 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", ti.tm_year+1900, ti.tm_mon+1, ti.tm_mday, ti.tm_hour, ti.tm_min, ti.tm_sec, ext); serialLogFile = SD_MMC.open(currentSerialFilename, FILE_WRITE); if (serialLogFile) { if (serialLogFormatCSV) serialLogFile.println("Time_us,Direction,Data"); serialLoggingEnabled = true; serialCsvIndex = 0; currentSerialFileSize = serialLogFile.size(); } 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); if (serialLogFile) serialLogFile.close(); serialLoggingEnabled = false; serialCsvIndex = 0; xSemaphoreGive(sdMutex); } } } else if (strcmp(cmd, "sendSerial") == 0) { const char* data = doc["data"]; if (data && strlen(data) > 0) { SerialComm.println(data); SerialMessage msg; struct timeval tv; gettimeofday(&tv, NULL); msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; msg.length = min((int)(strlen(data)+2), MAX_SERIAL_LINE_LEN-1); snprintf((char*)msg.data, MAX_SERIAL_LINE_LEN, "%s\r\n", data); msg.isTx = true; if (ring_serial_push(&serialRing, &msg)) 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 r(512); r["type"]="serialConfig"; r["baudRate"]=serialSettings.baudRate; r["dataBits"]=serialSettings.dataBits; r["parity"]=serialSettings.parity; r["stopBits"]=serialSettings.stopBits; String j; serializeJson(r,j); webSocket.sendTXT(num,j); } else if (strcmp(cmd, "startSerial2Logging") == 0) { if (!serial2LoggingEnabled && sdCardReady) { const char* fmt = doc["format"]; serial2LogFormatCSV = !(fmt && strcmp(fmt, "bin") == 0); if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { time_t now; struct tm ti; time(&now); localtime_r(&now, &ti); struct timeval tv; gettimeofday(&tv, NULL); serial2LogStartTime = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; const char* ext = serial2LogFormatCSV ? "csv" : "bin"; snprintf(currentSerial2Filename, sizeof(currentSerial2Filename), "/SER2_%04d%02d%02d_%02d%02d%02d.%s", ti.tm_year+1900, ti.tm_mon+1, ti.tm_mday, ti.tm_hour, ti.tm_min, ti.tm_sec, ext); serial2LogFile = SD_MMC.open(currentSerial2Filename, FILE_WRITE); if (serial2LogFile) { if (serial2LogFormatCSV) serial2LogFile.println("Time_us,Direction,Data"); serial2LoggingEnabled = true; serial2CsvIndex = 0; currentSerial2FileSize = serial2LogFile.size(); } xSemaphoreGive(sdMutex); } } } else if (strcmp(cmd, "stopSerial2Logging") == 0) { if (serial2LoggingEnabled) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { if (serial2CsvIndex > 0 && serial2LogFile) serial2LogFile.write((uint8_t*)serial2CsvBuffer, serial2CsvIndex); if (serial2LogFile) serial2LogFile.close(); serial2LoggingEnabled = false; serial2CsvIndex = 0; xSemaphoreGive(sdMutex); } } } else if (strcmp(cmd, "sendSerial2") == 0) { const char* data = doc["data"]; if (data && strlen(data) > 0) { Serial2Comm.println(data); SerialMessage msg; struct timeval tv; gettimeofday(&tv, NULL); msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; msg.length = min((int)(strlen(data)+2), MAX_SERIAL_LINE_LEN-1); snprintf((char*)msg.data, MAX_SERIAL_LINE_LEN, "%s\r\n", data); msg.isTx = true; if (ring_serial_push(&serial2Ring, &msg)) totalSerial2TxCount++; } } else if (strcmp(cmd, "setSerial2Config") == 0) { serial2Settings.baudRate = doc["baudRate"] | 115200; serial2Settings.dataBits = doc["dataBits"] | 8; serial2Settings.parity = doc["parity"] | 0; serial2Settings.stopBits = doc["stopBits"] | 1; saveSerialSettings(); applySerialSettings(); } else if (strcmp(cmd, "getSerial2Config") == 0) { DynamicJsonDocument r(512); r["type"]="serial2Config"; r["baudRate"]=serial2Settings.baudRate; r["dataBits"]=serial2Settings.dataBits; r["parity"]=serial2Settings.parity; r["stopBits"]=serial2Settings.stopBits; String j; serializeJson(r,j); webSocket.sendTXT(num,j); } else if (strcmp(cmd, "setSpeed") == 0) { int si = doc["speed"]; if (si >= 0 && si < 4) { currentCanSpeed = canSpeedValues[si]; saveSettings(); StaticJsonDocument<256> r; r["type"]="info"; r["message"]="CAN speed saved. Restart to apply."; String j; serializeJson(r,j); webSocket.sendTXT(num,j); } } else if (strcmp(cmd, "setMcpMode") == 0) { int mode = doc["mode"]; if (mode >= 0 && mode <= 3) { setMCP2515Mode((MCP2515Mode)mode); saveSettings(); } } else if (strcmp(cmd, "syncTimeFromPhone") == 0) { struct tm ti; ti.tm_year = (int)doc["year"] - 1900; ti.tm_mon = (int)doc["month"] - 1; ti.tm_mday = doc["day"] | 1; ti.tm_hour = doc["hour"] | 0; ti.tm_min = doc["minute"] | 0; ti.tm_sec = doc["second"] | 0; time_t t = mktime(&ti); struct timeval tv = {t, 0}; settimeofday(&tv, NULL); timeSyncStatus.synchronized = true; timeSyncStatus.lastSyncTime = (uint64_t)t * 1000000ULL; timeSyncStatus.syncCount++; if (timeSyncStatus.rtcAvailable) writeRTC(&ti); } else if (strcmp(cmd, "getFiles") == 0) { if (sdCardReady) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { DynamicJsonDocument r(6144); r["type"] = "files"; JsonArray files = r.createNestedArray("files"); File root = SD_MMC.open("/"); if (root) { File file = root.openNextFile(); int cnt = 0; while (file && cnt < 50) { if (!file.isDirectory()) { const char* fn = file.name(); if (fn[0] == '/') fn++; if (fn[0] != '.' && strcmp(fn,"System Volume Information")!=0 && strlen(fn)>0) { JsonObject fo = files.createNestedObject(); fo["name"] = fn; fo["size"] = file.size(); const char* cm = getFileComment(fn); if (strlen(cm) > 0) fo["comment"] = cm; cnt++; } } file.close(); file = root.openNextFile(); } root.close(); } xSemaphoreGive(sdMutex); String j; serializeJson(r,j); webSocket.sendTXT(num,j); } } } else if (strcmp(cmd, "deleteFile") == 0) { const char* fn = doc["filename"]; if (fn && strlen(fn) > 0) { String fp = "/" + String(fn); if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { bool ok = SD_MMC.exists(fp) && SD_MMC.remove(fp); xSemaphoreGive(sdMutex); DynamicJsonDocument r(256); r["type"]="deleteResult"; r["success"]=ok; String j; serializeJson(r,j); webSocket.sendTXT(num,j); } } } else if (strcmp(cmd, "addComment") == 0) { const char* fn = doc["filename"], *cm = doc["comment"]; if (fn && cm) addFileComment(fn, cm); } else if (strcmp(cmd, "setAutoTrigger") == 0) { autoTriggerEnabled = doc["enabled"] | false; const char* lf = doc["logFormat"]; if (lf) { autoTriggerLogCSV = (strcmp(lf,"csv") == 0); autoTriggerLogPCAP = (strcmp(lf,"pcap") == 0); } saveAutoTriggerSettings(); DynamicJsonDocument r(256); r["type"]="autoTriggerSet"; r["enabled"]=autoTriggerEnabled; r["logFormat"] = autoTriggerLogPCAP ? "pcap" : (autoTriggerLogCSV ? "csv" : "bin"); String j; serializeJson(r,j); webSocket.sendTXT(num,j); } else if (strcmp(cmd, "setAutoTriggerFormat") == 0) { const char* lf = doc["logFormat"]; if (lf) { autoTriggerLogCSV = (strcmp(lf,"csv") == 0); autoTriggerLogPCAP = (strcmp(lf,"pcap") == 0); saveAutoTriggerSettings(); } DynamicJsonDocument r(128); r["type"]="autoTriggerFormatSet"; r["logFormat"] = autoTriggerLogPCAP ? "pcap" : (autoTriggerLogCSV ? "csv" : "bin"); String j; serializeJson(r,j); webSocket.sendTXT(num,j); } else if (strcmp(cmd, "saveCanFormat") == 0) { const char* fmt = doc["format"]; if (fmt) { savedCanLogFormatCSV = (strcmp(fmt, "csv") == 0); savedCanLogFormatPCAP = (strcmp(fmt, "pcap") == 0); saveSettings(); Serial.printf("💾 CAN Format 저장: %s\n", savedCanLogFormatPCAP ? "PCAP" : (savedCanLogFormatCSV ? "CSV" : "BIN")); } DynamicJsonDocument r(128); r["type"]="canFormatSaved"; r["format"] = savedCanLogFormatPCAP ? "pcap" : (savedCanLogFormatCSV ? "csv" : "bin"); String j; serializeJson(r,j); webSocket.sendTXT(num,j); } else if (strcmp(cmd, "setStartTriggers") == 0) { JsonArray triggers = doc["triggers"]; strcpy(startLogicOp, doc["logic"] | "OR"); startTriggerCount = 0; for (JsonObject t : triggers) { if (startTriggerCount >= MAX_TRIGGERS) break; String idStr = t["canId"] | "0x0"; startTriggers[startTriggerCount].canId = strtoul(idStr.c_str(),NULL,16); startTriggers[startTriggerCount].startBit = t["startBit"] | 0; startTriggers[startTriggerCount].bitLength = t["bitLength"] | 8; strcpy(startTriggers[startTriggerCount].op, t["op"] | "=="); startTriggers[startTriggerCount].value = t["value"] | 0; startTriggers[startTriggerCount].enabled = t["enabled"] | true; startTriggerCount++; } saveAutoTriggerSettings(); DynamicJsonDocument r(256); r["type"]="startTriggersSet"; r["count"]=startTriggerCount; String j; serializeJson(r,j); webSocket.sendTXT(num,j); } else if (strcmp(cmd, "setStopTriggers") == 0) { JsonArray triggers = doc["triggers"]; strcpy(stopLogicOp, doc["logic"] | "OR"); stopTriggerCount = 0; for (JsonObject t : triggers) { if (stopTriggerCount >= MAX_TRIGGERS) break; String idStr = t["canId"] | "0x0"; stopTriggers[stopTriggerCount].canId = strtoul(idStr.c_str(),NULL,16); stopTriggers[stopTriggerCount].startBit = t["startBit"] | 0; stopTriggers[stopTriggerCount].bitLength = t["bitLength"] | 8; strcpy(stopTriggers[stopTriggerCount].op, t["op"] | "=="); stopTriggers[stopTriggerCount].value = t["value"] | 0; stopTriggers[stopTriggerCount].enabled = t["enabled"] | true; stopTriggerCount++; } saveAutoTriggerSettings(); DynamicJsonDocument r(256); r["type"]="stopTriggersSet"; r["count"]=stopTriggerCount; String j; serializeJson(r,j); webSocket.sendTXT(num,j); } else if (strcmp(cmd, "setStartFormula") == 0) { const char* f = doc["formula"]; if (f) { startFormula = String(f); saveAutoTriggerSettings(); } } else if (strcmp(cmd, "setStopFormula") == 0) { const char* f = doc["formula"]; if (f) { stopFormula = String(f); saveAutoTriggerSettings(); } } else if (strcmp(cmd, "getAutoTriggers") == 0) { DynamicJsonDocument r(2048); r["type"]="autoTriggers"; r["enabled"]=autoTriggerEnabled; r["logFormat"] = autoTriggerLogPCAP ? "pcap" : (autoTriggerLogCSV ? "csv" : "bin"); r["startLogic"]=startLogicOp; r["stopLogic"]=stopLogicOp; r["startFormula"]=startFormula; r["stopFormula"]=stopFormula; JsonArray sa = r.createNestedArray("startTriggers"); for (int i = 0; i < startTriggerCount; i++) { JsonObject t = sa.createNestedObject(); char ids[10]; sprintf(ids,"0x%03X",startTriggers[i].canId); t["canId"]=ids; t["startBit"]=startTriggers[i].startBit; t["bitLength"]=startTriggers[i].bitLength; t["op"]=startTriggers[i].op; t["value"]=(long)startTriggers[i].value; t["enabled"]=startTriggers[i].enabled; } JsonArray ea = r.createNestedArray("stopTriggers"); for (int i = 0; i < stopTriggerCount; i++) { JsonObject t = ea.createNestedObject(); char ids[10]; sprintf(ids,"0x%03X",stopTriggers[i].canId); t["canId"]=ids; t["startBit"]=stopTriggers[i].startBit; t["bitLength"]=stopTriggers[i].bitLength; t["op"]=stopTriggers[i].op; t["value"]=(long)stopTriggers[i].value; t["enabled"]=stopTriggers[i].enabled; } String j; serializeJson(r,j); webSocket.sendTXT(num,j); } 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 da = doc["data"]; for (int i = 0; i < 8; i++) frame.data[i] = da[i] | 0; if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) totalTxCount++; if (currentMcpMode == MCP_MODE_TRANSMIT) mcp2515.setListenOnlyMode(); } else if (strcmp(cmd, "addSequence") == 0) { if (sequenceCount >= MAX_SEQUENCES) { DynamicJsonDocument r(256); r["type"]="error"; r["message"]="Max sequences reached"; String j; serializeJson(r,j); webSocket.sendTXT(num,j); } else { const char* name = doc["name"]; JsonArray stepsArr = doc["steps"]; if (name && stepsArr.size() > 0) { CANSequence* seq = &sequences[sequenceCount]; strncpy(seq->name, name, sizeof(seq->name)-1); seq->name[sizeof(seq->name)-1]='\0'; seq->repeatMode = doc["repeatMode"] | 0; seq->repeatCount = doc["repeatCount"] | 1; seq->stepCount = 0; for (JsonObject so : stepsArr) { if (seq->stepCount >= 20) break; SequenceStep* step = &seq->steps[seq->stepCount]; const char* ids = so["id"]; if (ids) step->canId = strtoul( (strncmp(ids,"0x",2)==0||strncmp(ids,"0X",2)==0) ? ids+2 : ids, NULL, 16); step->extended = so["ext"] | false; step->dlc = so["dlc"] | 8; JsonArray da = so["data"]; for (int i = 0; i < 8 && i < (int)da.size(); i++) step->data[i] = da[i]; step->delayMs = so["delay"] | 0; seq->stepCount++; } sequenceCount++; saveSequences(); DynamicJsonDocument r(256); r["type"]="sequenceSaved"; r["name"]=name; r["steps"]=seq->stepCount; String j; serializeJson(r,j); webSocket.sendTXT(num,j); delay(100); webSocket.sendTXT(num, "{\"cmd\":\"getSequences\"}"); } } } else if (strcmp(cmd, "getSequences") == 0) { DynamicJsonDocument r(3072); r["type"]="sequences"; JsonArray sa = r.createNestedArray("list"); for (int i = 0; i < sequenceCount; i++) { JsonObject so = sa.createNestedObject(); so["name"]=sequences[i].name; so["steps"]=sequences[i].stepCount; so["repeatMode"]=sequences[i].repeatMode; so["repeatCount"]=sequences[i].repeatCount; } String j; serializeJson(r,j); webSocket.sendTXT(num,j); } else if (strcmp(cmd, "startSequence") == 0) { int idx = doc["index"] | -1; if (idx >= 0 && idx < sequenceCount) { seqRuntime = {true, 0, 0, millis(), (int8_t)idx}; DynamicJsonDocument r(256); r["type"]="sequenceStarted"; r["index"]=idx; r["name"]=sequences[idx].name; String j; serializeJson(r,j); webSocket.sendTXT(num,j); } } else if (strcmp(cmd, "stopSequence") == 0) { if (seqRuntime.running) { seqRuntime.running = false; seqRuntime.activeSequenceIndex = -1; DynamicJsonDocument r(256); r["type"]="sequenceStopped"; String j; serializeJson(r,j); webSocket.sendTXT(num,j); } } else if (strcmp(cmd, "removeSequence") == 0) { int idx = doc["index"] | -1; if (idx >= 0 && idx < sequenceCount) { for (int i = idx; i < sequenceCount-1; i++) memcpy(&sequences[i], &sequences[i+1], sizeof(CANSequence)); sequenceCount--; saveSequences(); DynamicJsonDocument r(256); r["type"]="sequenceDeleted"; r["index"]=idx; String j; serializeJson(r,j); webSocket.sendTXT(num,j); delay(100); webSocket.sendTXT(num, "{\"cmd\":\"getSequences\"}"); } } else if (strcmp(cmd, "getSequenceDetail") == 0) { int idx = doc["index"] | -1; if (idx >= 0 && idx < sequenceCount) { DynamicJsonDocument r(4096); r["type"]="sequenceDetail"; r["index"]=idx; r["name"]=sequences[idx].name; r["repeatMode"]=sequences[idx].repeatMode; r["repeatCount"]=sequences[idx].repeatCount; JsonArray sa = r.createNestedArray("steps"); for (int i = 0; i < sequences[idx].stepCount; i++) { SequenceStep* s = &sequences[idx].steps[i]; JsonObject so = sa.createNestedObject(); char ids[12]; sprintf(ids,"0x%X",s->canId); so["id"]=ids; so["ext"]=s->extended; so["dlc"]=s->dlc; JsonArray da = so.createNestedArray("data"); for (int j=0;j<8;j++) da.add(s->data[j]); so["delay"]=s->delayMs; } String j; serializeJson(r,j); webSocket.sendTXT(num,j); } } else if (strcmp(cmd, "hwReset") == 0) { DynamicJsonDocument r(256); r["type"]="hwReset"; r["success"]=true; String j; serializeJson(r,j); webSocket.sendTXT(num,j); delay(100); ESP.restart(); } } } // ======================================== // ★★★ Web Update Task (RingBuffer Serial Consumer) ★★★ // ======================================== void webUpdateTask(void *parameter) { vTaskDelay(pdMS_TO_TICKS(500)); while (1) { webSocket.loop(); if (webSocket.connectedClients() > 0) { DynamicJsonDocument doc(16384); doc["type"] = "update"; doc["logging"] = loggingEnabled; // Auto Trigger 상태 doc["autoTriggerEnabled"] = autoTriggerEnabled; doc["autoTriggerActive"] = autoTriggerActive; doc["startTriggerCount"] = startTriggerCount; doc["stopTriggerCount"] = stopTriggerCount; // Serial2 doc["serialLogging"] = serialLoggingEnabled; doc["serial2Logging"] = serial2LoggingEnabled; doc["totalSerial2Rx"] = totalSerial2RxCount; doc["totalSerial2Tx"] = totalSerial2TxCount; // ★ RingBuffer 사용량 (queueUsed 호환 필드명 유지) doc["queueUsed"] = ring_can_count(&canRing); doc["queueSize"] = CAN_RING_SIZE; doc["serialQueueUsed"] = ring_serial_count(&serialRing); doc["serialQueueSize"] = SERIAL_RING_SIZE; doc["serial2QueueUsed"] = ring_serial_count(&serial2Ring); doc["serial2QueueSize"] = SERIAL_RING_SIZE; // ★ 드랍 카운터 (새로 추가: 웹 UI에서 손실 감지용) doc["canDropped"] = canRing.dropped; doc["serDropped"] = serialRing.dropped; doc["ser2Dropped"] = serial2Ring.dropped; doc["serial2FileSize"] = currentSerial2FileSize; doc["currentSerial2File"] = (serial2LoggingEnabled && currentSerial2Filename[0]) ? String(currentSerial2Filename) : ""; doc["sdReady"] = sdCardReady; doc["totalMsg"] = totalMsgCount; doc["msgPerSec"] = msgPerSecond; // 버스 부하율 uint32_t maxMPS; switch(currentCanSpeed) { case CAN_125KBPS: maxMPS=1000; break; case CAN_250KBPS: maxMPS=2000; break; case CAN_500KBPS: maxMPS=4000; break; default: maxMPS=8000; break; } float busLoad = (msgPerSecond * 100.0f) / maxMPS; if (busLoad > 100.0f) busLoad = 100.0f; doc["busLoad"] = (int)busLoad; doc["totalTx"] = totalTxCount; doc["totalSerialRx"] = totalSerialRxCount; doc["totalSerialTx"] = totalSerialTxCount; doc["fileSize"] = currentFileSize; doc["serialFileSize"] = currentSerialFileSize; doc["timeSync"] = timeSyncStatus.synchronized; doc["rtcAvail"] = timeSyncStatus.rtcAvailable; doc["rtcSyncCnt"] = timeSyncStatus.rtcSyncCount; doc["syncCount"] = timeSyncStatus.syncCount; doc["voltage"] = powerStatus.voltage; doc["minVoltage"] = powerStatus.minVoltage; doc["lowVoltage"] = powerStatus.lowVoltage; doc["mcpMode"] = (int)currentMcpMode; doc["savedCanFormat"] = savedCanLogFormatPCAP ? "pcap" : (savedCanLogFormatCSV ? "csv" : "bin"); doc["currentFile"] = (loggingEnabled && currentFilename[0]) ? String(currentFilename) : ""; doc["currentSerialFile"]= (serialLoggingEnabled && currentSerialFilename[0]) ? String(currentSerialFilename) : ""; time_t now; time(&now); doc["timestamp"] = (uint64_t)now; // CAN 최근 메시지 (최대 20개) JsonArray messages = doc.createNestedArray("messages"); int msgCnt = 0; for (int i = 0; i < RECENT_MSG_COUNT && msgCnt < 20; i++) { if (recentData[i].count > 0) { JsonObject mo = messages.createNestedObject(); mo["id"] = recentData[i].msg.id; mo["dlc"] = recentData[i].msg.dlc; mo["count"] = recentData[i].count; JsonArray da = mo.createNestedArray("data"); for (int j = 0; j < recentData[i].msg.dlc; j++) da.add(recentData[i].msg.data[j]); msgCnt++; } } // ★ Serial RingBuffer pop (최대 10개) SerialMessage serialMsg; JsonArray serialMessages = doc.createNestedArray("serialMessages"); int serialCnt = 0; while (serialCnt < 10 && ring_serial_pop(&serialRing, &serialMsg)) { JsonObject so = serialMessages.createNestedObject(); so["timestamp"] = serialMsg.timestamp_us; so["isTx"] = serialMsg.isTx; char ds[MAX_SERIAL_LINE_LEN+1]; memcpy(ds, serialMsg.data, serialMsg.length); ds[serialMsg.length] = '\0'; so["data"] = ds; serialCnt++; // Serial SD 로깅 if (serialLoggingEnabled && sdCardReady) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(10)) == pdTRUE) { if (serialLogFormatCSV) { uint64_t rt = serialMsg.timestamp_us - serialLogStartTime; char csvLine[256]; int ll = snprintf(csvLine, sizeof(csvLine), "%llu,%s,\"%s\"\n", rt, serialMsg.isTx?"TX":"RX", ds); if (serialCsvIndex + ll <= SERIAL_CSV_BUFFER_SIZE) { memcpy(&serialCsvBuffer[serialCsvIndex], csvLine, ll); serialCsvIndex += ll; currentSerialFileSize += ll; } if (serialCsvIndex >= SERIAL_CSV_BUFFER_SIZE - 256) { 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 bfc=0; if (++bfc>=50){serialLogFile.flush();bfc=0;} } } xSemaphoreGive(sdMutex); } } } // ★ Serial2 RingBuffer pop SerialMessage serial2Msg; JsonArray serial2Messages = doc.createNestedArray("serial2Messages"); int s2Cnt = 0; while (s2Cnt < 10 && ring_serial_pop(&serial2Ring, &serial2Msg)) { JsonObject so = serial2Messages.createNestedObject(); so["timestamp"] = serial2Msg.timestamp_us; so["isTx"] = serial2Msg.isTx; char ds[MAX_SERIAL_LINE_LEN+1]; memcpy(ds, serial2Msg.data, serial2Msg.length); ds[serial2Msg.length] = '\0'; so["data"] = ds; s2Cnt++; if (serial2LoggingEnabled && sdCardReady) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(10)) == pdTRUE) { if (serial2LogFormatCSV) { uint64_t rt = serial2Msg.timestamp_us - serial2LogStartTime; char csvLine[256]; int ll = snprintf(csvLine, sizeof(csvLine), "%llu,%s,\"%s\"\n", rt, serial2Msg.isTx?"TX":"RX", ds); if (serial2CsvIndex + ll <= SERIAL2_CSV_BUFFER_SIZE) { memcpy(&serial2CsvBuffer[serial2CsvIndex], csvLine, ll); serial2CsvIndex += ll; currentSerial2FileSize += ll; } if (serial2CsvIndex >= SERIAL2_CSV_BUFFER_SIZE - 256) { if (serial2LogFile) { serial2LogFile.write((uint8_t*)serial2CsvBuffer, serial2CsvIndex); serial2LogFile.flush(); serial2CsvIndex=0; } } } else { if (serial2LogFile) { serial2LogFile.write((uint8_t*)&serial2Msg, sizeof(SerialMessage)); currentSerial2FileSize += sizeof(SerialMessage); static int bfc2=0; if (++bfc2>=50){serial2LogFile.flush();bfc2=0;} } } xSemaphoreGive(sdMutex); } } } String json; size_t jsonSize = serializeJson(doc, json); if (jsonSize > 0 && jsonSize < 8192) webSocket.broadcastTXT(json); } vTaskDelay(pdMS_TO_TICKS(500)); } } // ======================================== // Setup // ======================================== void setup() { Serial.begin(115200); delay(1000); esp_reset_reason_t rr = esp_reset_reason(); if (rr == ESP_RST_BROWNOUT) { Serial.println("\n🚨 브라운아웃 리셋 감지! 전원 공급 부족."); delay(5000); } WiFi.setSleep(false); WiFi.setTxPower(WIFI_POWER_15dBm); Serial.println("\n========================================"); Serial.println(" Byun CAN Logger v3.0"); Serial.println(" SPSC Lock-Free RingBuffer Edition"); Serial.println("========================================\n"); // PSRAM 초기화 (RingBuffer 포함) if (!initPSRAM()) { Serial.println("✗ PSRAM 초기화 실패! Tools→PSRAM→OPI PSRAM 확인"); while (1) { delay(1000); Serial.println("✗ 재업로드 필요!"); } } loadSettings(); loadAutoTriggerSettings(); analogSetPinAttenuation(MONITORING_VOLT, ADC_11db); pinMode(CAN_INT_PIN, INPUT_PULLUP); analogSetAttenuation(ADC_11db); hspi.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS); hspi.beginTransaction(SPISettings(40000000, MSBFIRST, SPI_MODE0)); hspi.endTransaction(); esp_task_wdt_deinit(); // MCP2515 초기화 Serial.println("MCP2515 초기화..."); pinMode(HSPI_CS, OUTPUT); digitalWrite(HSPI_CS, HIGH); delay(10); digitalWrite(HSPI_CS, LOW); delayMicroseconds(100); digitalWrite(HSPI_CS, HIGH); delay(100); mcp2515.reset(); delay(100); mcp2515.setBitrate(currentCanSpeed, MCP_CRYSTAL); delay(10); mcp2515.setFilterMask(MCP2515::MASK0, false, 0); mcp2515.setFilter(MCP2515::RXF0, false, 0); mcp2515.setFilter(MCP2515::RXF1, false, 0); mcp2515.setFilterMask(MCP2515::MASK1, true, 0); mcp2515.setFilter(MCP2515::RXF2, true, 0); mcp2515.setFilter(MCP2515::RXF3, true, 0); mcp2515.setFilter(MCP2515::RXF4, true, 0); mcp2515.setFilter(MCP2515::RXF5, true, 0); mcp2515.clearRXnOVRFlags(); mcp2515.clearInterrupts(); mcp2515.clearTXInterrupts(); mcp2515.clearMERR(); mcp2515.clearERRIF(); if (currentMcpMode == MCP_MODE_NORMAL) { mcp2515.setNormalMode(); Serial.println(" → Normal Mode"); } else if (currentMcpMode == MCP_MODE_LOOPBACK) { mcp2515.setLoopbackMode(); Serial.println(" → Loopback Mode"); } else { mcp2515.setListenOnlyMode();Serial.println(" → Listen-Only Mode"); } delay(50); struct can_frame df; int cc=0; while (mcp2515.readMessage(&df)==MCP2515::ERROR_OK && cc++<100); mcp2515.clearRXnOVRFlags(); Serial.println("✓ MCP2515 초기화 완료"); applySerialSettings(); sdMutex = xSemaphoreCreateMutex(); rtcMutex = xSemaphoreCreateMutex(); serialMutex = xSemaphoreCreateMutex(); serial2Mutex = xSemaphoreCreateMutex(); if (!sdMutex || !rtcMutex || !serialMutex) { Serial.println("✗ Mutex 생성 실패!"); while(1) delay(1000); } initRTC(); Serial.println("SD 카드 초기화 (SDIO 4-bit)..."); if (!SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0, SDIO_D1, SDIO_D2, SDIO_D3)) { sdCardReady = false; } else if (SD_MMC.begin("/sdcard", false)) { sdCardReady = true; Serial.printf("✓ SD 카드 초기화 완료: %llu MB\n", SD_MMC.cardSize()/(1024*1024)); loadFileComments(); loadSequences(); } else { sdCardReady = false; Serial.println("✗ SD 카드 초기화 실패"); } // WiFi WiFi.setSleep(false); if (enableSTAMode && strlen(staSSID) > 0) { WiFi.mode(WIFI_AP_STA); WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4); WiFi.begin(staSSID, staPassword); int att = 0; while (WiFi.status() != WL_CONNECTED && att++ < 20) { delay(500); Serial.print("."); } Serial.println(); if (WiFi.status() == WL_CONNECTED) { Serial.printf("✓ STA IP: %s\n", WiFi.localIP().toString().c_str()); initNTP(); } } else { WiFi.mode(WIFI_AP); WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4); Serial.printf("✓ AP: %s | IP: %s\n", wifiSSID, WiFi.softAPIP().toString().c_str()); } esp_wifi_set_max_tx_power(84); 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("/serial2", HTTP_GET, [](){server.send_P(200,"text/html",serial2_terminal_html);}); server.on("/download", HTTP_GET, []() { if (!server.hasArg("file")) { server.send(400,"text/plain","Bad request"); return; } String filename = "/" + server.arg("file"); bool wasLogging = loggingEnabled; if (wasLogging) { loggingEnabled = false; delay(200); } if (sdWriteTaskHandle) { vTaskSuspend(sdWriteTaskHandle); delay(100); } if (webTaskHandle) { vTaskSuspend(webTaskHandle); delay(50); } if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(10000)) != pdTRUE) { if (webTaskHandle) vTaskResume(webTaskHandle); if (sdWriteTaskHandle) vTaskResume(sdWriteTaskHandle); if (wasLogging) loggingEnabled = true; server.send(503,"text/plain","SD card busy"); return; } SD_MMC.end(); delay(200); if (!SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0) || !SD_MMC.begin("/sdcard", true)) { xSemaphoreGive(sdMutex); if (webTaskHandle) vTaskResume(webTaskHandle); if (sdWriteTaskHandle) vTaskResume(sdWriteTaskHandle); if (wasLogging) loggingEnabled = true; server.send(500,"text/plain","SD remount failed"); return; } if (!SD_MMC.exists(filename)) { SD_MMC.end(); delay(100); SD_MMC.setPins(SDIO_CLK,SDIO_CMD,SDIO_D0,SDIO_D1,SDIO_D2,SDIO_D3); SD_MMC.begin("/sdcard", false); xSemaphoreGive(sdMutex); if (webTaskHandle) vTaskResume(webTaskHandle); if (sdWriteTaskHandle) vTaskResume(sdWriteTaskHandle); if (wasLogging) loggingEnabled = true; server.send(404,"text/plain","File not found"); return; } File file = SD_MMC.open(filename, FILE_READ); if (!file) { SD_MMC.end(); delay(100); SD_MMC.setPins(SDIO_CLK,SDIO_CMD,SDIO_D0,SDIO_D1,SDIO_D2,SDIO_D3); SD_MMC.begin("/sdcard", false); xSemaphoreGive(sdMutex); if (webTaskHandle) vTaskResume(webTaskHandle); if (sdWriteTaskHandle) vTaskResume(sdWriteTaskHandle); if (wasLogging) loggingEnabled = true; server.send(500,"text/plain","Failed to open file"); return; } size_t fileSize = file.size(); String dispName = server.arg("file"); server.setContentLength(fileSize); server.sendHeader("Content-Disposition","attachment; filename=\""+dispName+"\""); server.sendHeader("Content-Type","application/octet-stream"); server.sendHeader("Connection","close"); server.send(200,"application/octet-stream",""); const size_t CHUNK = 512; uint8_t* buf = (uint8_t*)heap_caps_aligned_alloc(32, CHUNK, MALLOC_CAP_DMA); if (!buf) buf = (uint8_t*)malloc(CHUNK); if (buf) { size_t total=0; WiFiClient client = server.client(); while (file.available() && total < fileSize && client.connected()) { size_t r = file.read(buf, CHUNK); if (!r) break; size_t w=0; while (w 30000) { Serial.printf("[RingBuf] CAN: %u/%u (drop:%u) | SER: %u/%u (drop:%u) | SER2: %u/%u (drop:%u) | PSRAM: %d KB\n", ring_can_count(&canRing), CAN_RING_SIZE, canRing.dropped, ring_serial_count(&serialRing), SERIAL_RING_SIZE, serialRing.dropped, ring_serial_count(&serial2Ring), SERIAL_RING_SIZE, serial2Ring.dropped, ESP.getFreePsram() / 1024); lastPrint = millis(); } // 5분마다 스택 사용량 static uint32_t lastStack = 0; if (millis() - lastStack > 300000) { Serial.println("\n========== Task Stack Usage =========="); if (canRxTaskHandle) Serial.printf("CAN_RX: %5u bytes free\n", uxTaskGetStackHighWaterMark(canRxTaskHandle)*4); if (sdWriteTaskHandle) Serial.printf("SD_WRITE: %5u bytes free\n", uxTaskGetStackHighWaterMark(sdWriteTaskHandle)*4); if (webTaskHandle) Serial.printf("WEB_UPDATE: %5u bytes free\n", uxTaskGetStackHighWaterMark(webTaskHandle)*4); if (serialRxTaskHandle) Serial.printf("SERIAL_RX: %5u bytes free\n", uxTaskGetStackHighWaterMark(serialRxTaskHandle)*4); if (serial2RxTaskHandle) Serial.printf("SERIAL2_RX: %5u bytes free\n", uxTaskGetStackHighWaterMark(serial2RxTaskHandle)*4); Serial.printf("Free Heap: %u bytes\n", ESP.getFreeHeap()); Serial.printf("Free PSRAM: %u KB\n", ESP.getFreePsram()/1024); Serial.printf("CAN Dropped Total: %u\n", canRing.dropped); Serial.println("======================================\n"); lastStack = millis(); } }