From 8a6c283f343b2fe090b5d54b379d4e9de95c7c25 Mon Sep 17 00:00:00 2001 From: byun Date: Thu, 5 Mar 2026 21:32:58 +0000 Subject: [PATCH] =?UTF-8?q?pcap=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ESP32_CAN_Logger-a.ino | 4491 ++++++++++++++-------------------------- index.h | 36 +- 2 files changed, 1588 insertions(+), 2939 deletions(-) diff --git a/ESP32_CAN_Logger-a.ino b/ESP32_CAN_Logger-a.ino index b8e56be..e27ebee 100644 --- a/ESP32_CAN_Logger-a.ino +++ b/ESP32_CAN_Logger-a.ino @@ -1,24 +1,33 @@ /* * Byun CAN Logger with Web Interface + Serial Terminal - * Version: 2.3 - PSRAM Optimized (Complete Version) - * + * 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% 유지 - * - 대용량 버퍼/Queue를 PSRAM에 할당 - * - 웹서버, WebSocket, 모든 Task 포함 - * + * - 원본 기능 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) + * - Board: ESP32S3 Dev Module + * - PSRAM: OPI PSRAM ⭐ 필수! + * - Flash Size: 16MB (128Mb) + * - Partition: 16MB Flash (3MB APP/9.9MB FATFS) */ #include #include #include #include -#include // ⭐ SDIO 4-bit +#include #include #include #include @@ -28,7 +37,6 @@ #include #include #include -#include #include #include #include @@ -39,30 +47,28 @@ #include "graph_viewer.h" #include "settings.h" #include "serial_terminal.h" -#include "serial2_terminal.h" // ⭐ Serial2 페이지 추가 +#include "serial2_terminal.h" // GPIO 핀 정의 -#define CAN_INT_PIN 4 -#define SERIAL_TX_PIN 17 -#define SERIAL_RX_PIN 18 -// UART2 (Serial Logger 2) ⭐ 추가 -#define SERIAL2_TX_PIN 6 -#define SERIAL2_RX_PIN 7 +#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 +#define HSPI_MISO 13 +#define HSPI_MOSI 11 +#define HSPI_SCLK 12 +#define HSPI_CS 10 -// ⭐⭐⭐ SDIO 4-bit Pins (ESP32-S3) -// CLK: GPIO39, CMD: GPIO38, D0-D3: GPIO40,41,42,21 +// 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 // ⭐ OPI PSRAM 호환 +#define SDIO_D3 21 // I2C2 핀 (RTC DS3231) #define RTC_SDA 8 @@ -70,28 +76,28 @@ #define DS3231_ADDRESS 0x68 // ======================================== -// PSRAM 최적화 설정 +// ★ RingBuffer 크기 설정 (반드시 2의 거듭제곱!) // ======================================== -#define CAN_QUEUE_SIZE 6000 // 1000 → 6000 (PSRAM 사용) -#define FILE_BUFFER_SIZE 65536 // 16KB → 64KB (PSRAM 사용) -#define SERIAL_QUEUE_SIZE 1200 // 200 → 1200 (PSRAM 사용) -#define SERIAL_CSV_BUFFER_SIZE 32768 +#define CAN_RING_SIZE 8192 // 이전 Queue 6000 → 8192 (2^13, ~1초@8Kfps) +#define SERIAL_RING_SIZE 2048 // 이전 Queue 1200 → 2048 (2^11) -#define SERIAL2_QUEUE_SIZE 1200 // ⭐ Serial2 추가 -#define SERIAL2_CSV_BUFFER_SIZE 32768 // ⭐ Serial2 추가 // 8KB → 32KB (PSRAM 사용) +// 기타 버퍼 크기 +#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 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 +#define LOW_VOLTAGE_THRESHOLD 3.0 +#define MONITORING_VOLT 5 +#define MAX_GRAPH_SIGNALS 20 +#define MAX_SEQUENCES 10 +#define MAX_FILE_COMMENTS 50 // ======================================== // 구조체 정의 @@ -99,61 +105,97 @@ struct CANMessage { uint64_t timestamp_us; uint32_t id; - uint8_t dlc; - uint8_t data[8]; + 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; + 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; + uint8_t dataBits; + uint8_t parity; + uint8_t stopBits; }; struct RecentCANData { CANMessage msg; - uint32_t count; + uint32_t count; }; struct TxMessage { uint32_t id; - bool extended; - uint8_t dlc; - uint8_t data[8]; + bool extended; + uint8_t dlc; + uint8_t data[8]; uint32_t interval; uint32_t lastSent; - bool active; + bool active; }; struct SequenceStep { uint32_t canId; - bool extended; - uint8_t dlc; - uint8_t data[8]; + bool extended; + uint8_t dlc; + uint8_t data[8]; uint32_t delayMs; }; struct CANSequence { - char name[32]; + char name[32]; SequenceStep steps[20]; - uint8_t stepCount; - uint8_t repeatMode; - uint32_t repeatCount; + uint8_t stepCount; + uint8_t repeatMode; + uint32_t repeatCount; }; struct SequenceRuntime { - bool running; + bool running; uint8_t currentStep; uint32_t currentRepeat; uint32_t lastStepTime; - int8_t activeSequenceIndex; + int8_t activeSequenceIndex; }; struct FileComment { @@ -161,191 +203,257 @@ struct FileComment { 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; + bool synchronized; uint64_t lastSyncTime; - int32_t offsetUs; + int32_t offsetUs; uint32_t syncCount; - bool rtcAvailable; + bool rtcAvailable; uint32_t rtcSyncCount; } timeSyncStatus = {false, 0, 0, 0, false, 0}; struct PowerStatus { - float voltage; - float minVoltage; - bool lowVoltage; + 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_NORMAL = 0, MCP_MODE_LISTEN_ONLY = 1, - MCP_MODE_LOOPBACK = 2, - MCP_MODE_TRANSMIT = 3 + MCP_MODE_LOOPBACK = 2, + MCP_MODE_TRANSMIT = 3 }; // ======================================== -// PSRAM 할당 변수 (포인터로 선언) +// ★★★ SPSC Lock-Free RingBuffer 정의 ★★★ +// +// 설계 원칙: +// - head: Producer만 쓰기 (canRxTask / serialRxTask) +// - tail: Consumer만 쓰기 (sdWriteTask / webUpdateTask) +// - __sync_synchronize(): 멀티코어 메모리 배리어 +// - 2의 거듭제곱 size + bitmask: 나눗셈 연산 제거 +// - dropped: 오버플로우 시 drop 카운트 (진단용) // ======================================== -uint8_t *fileBuffer = nullptr; -char *serialCsvBuffer = nullptr; -char *serial2CsvBuffer = nullptr; // ⭐ Serial2 추가 -RecentCANData *recentData = nullptr; -TxMessage *txMessages = nullptr; -CANSequence *sequences = nullptr; -FileComment *fileComments = nullptr; -// Queue 저장소 (PSRAM) -StaticQueue_t *canQueueBuffer = nullptr; -StaticQueue_t *serialQueueBuffer = nullptr; -StaticQueue_t *serial2QueueBuffer = nullptr; // ⭐ Serial2 -uint8_t *canQueueStorage = nullptr; -uint8_t *serialQueueStorage = nullptr; -uint8_t *serial2QueueStorage = nullptr; // ⭐ Serial2 +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된 메시지 수 +}; -// WiFi 설정 (내부 SRAM) -char wifiSSID[32] = "Byun_CAN_Logger"; -char wifiPassword[64] = "12345678"; -bool enableSTAMode = false; -char staSSID[32] = ""; +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 설정 (2개) -// ======================================== -SerialSettings serialSettings = {115200, 8, 0, 1}; // Serial1 -SerialSettings serial2Settings = {115200, 8, 0, 1}; // ⭐ Serial2 추가 +// Serial 설정 +SerialSettings serialSettings = {115200, 8, 0, 1}; +SerialSettings serial2Settings = {115200, 8, 0, 1}; -// 전역 객체 (내부 SRAM) +// 전역 객체 SPIClass hspi(HSPI); -// SPIClass vspi(FSPI); // ⭐ SDIO 사용으로 제거 -// ======================================== -// MCP2515 크리스탈 설정 -// ======================================== -// 8MHz 크리스탈: 500 Kbps까지 안정적 -// 16MHz 크리스탈: 1 Mbps 고속 통신 권장 (고부하 대응) -// -// 사용법: -// - 하드웨어에 16MHz 크리스탈 장착 시: MCP_16MHZ -// - 하드웨어에 8MHz 크리스탈 장착 시: MCP_8MHZ -// -//#define MCP_CRYSTAL MCP_16MHZ // ⭐ 16MHz 크리스탈 (1 Mbps 권장) -#define MCP_CRYSTAL MCP_8MHZ // 8MHz 크리스탈 (500 Kbps까지) -// SPI 속도: 20MHz (MCP2515 최대 속도와 균형) -// 10MHz: 안정적이지만 느림 -// 20MHz: 권장 (고속 + 안정성) -// 25MHz: 불안정할 수 있음 -MCP2515 mcp2515(HSPI_CS, 80000000, &hspi); -HardwareSerial SerialComm(1); // UART1 -HardwareSerial Serial2Comm(2); // ⭐ UART2 추가 +#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 핸들 -QueueHandle_t canQueue = NULL; -QueueHandle_t serialQueue = NULL; -QueueHandle_t serial2Queue = NULL; // ⭐ Serial2 추가 -SemaphoreHandle_t sdMutex = NULL; -SemaphoreHandle_t rtcMutex = NULL; -SemaphoreHandle_t serialMutex = NULL; -SemaphoreHandle_t serial2Mutex = NULL; // ⭐ Serial2 추가 -TaskHandle_t canRxTaskHandle = NULL; -TaskHandle_t sdWriteTaskHandle = NULL; -TaskHandle_t webTaskHandle = NULL; -TaskHandle_t rtcTaskHandle = NULL; +// 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; // ⭐ Serial2 추가 +TaskHandle_t serial2RxTaskHandle= NULL; -// 로깅 변수 -volatile bool loggingEnabled = false; - - -// ============================================ -// 🎯 Auto Trigger 전역 변수 -// ============================================ - -// Auto Trigger 조건 구조체 -struct TriggerCondition { - uint32_t canId; // CAN ID - uint8_t startBit; // 시작 비트 (0-63) - uint8_t bitLength; // 비트 길이 (1-64) - char op[3]; // 연산자: "==", "!=", ">", "<", ">=", "<=" - int64_t value; // 비교 값 - bool enabled; // 조건 활성화 -}; +// 로깅 상태 +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; +int startTriggerCount = 0; +int stopTriggerCount = 0; -String startFormula = ""; // Start 조건 수식 -String stopFormula = ""; // Stop 조건 수식 +String startFormula = ""; +String stopFormula = ""; -bool autoTriggerEnabled = false; -char startLogicOp[4] = "OR"; -char stopLogicOp[4] = "OR"; -bool autoTriggerActive = false; -bool autoTriggerLogCSV = false; // 🆕 Auto Trigger용 CSV 형식 설정 +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 Format 저장 변수 -bool savedCanLogFormatCSV = false; // 저장된 CAN 로그 형식 (BIN=false, CSV=true) - -volatile bool serialLoggingEnabled = false; -volatile bool serial2LoggingEnabled = false; // ⭐ Serial2 추가 -volatile bool sdCardReady = false; +// 파일 핸들 File logFile; File serialLogFile; -File serial2LogFile; // ⭐ Serial2 추가 +File serial2LogFile; char currentFilename[MAX_FILENAME_LEN]; char currentSerialFilename[MAX_FILENAME_LEN]; -char currentSerial2Filename[MAX_FILENAME_LEN]; // ⭐ Serial2 추가 +char currentSerial2Filename[MAX_FILENAME_LEN]; -// ⭐⭐⭐ 더블 버퍼링 변수 -uint8_t* fileBuffer1 = NULL; // 쓰기 버퍼 -uint8_t* fileBuffer2 = NULL; // flush 버퍼 -uint8_t* currentWriteBuffer = NULL; // 현재 쓰기용 -uint8_t* currentFlushBuffer = NULL; // 현재 flush용 -uint32_t writeBufferIndex = 0; // 쓰기 버퍼 인덱스 -uint32_t flushBufferSize = 0; // flush할 크기 -volatile bool flushInProgress = false; // flush 진행중 플래그 -TaskHandle_t sdFlushTaskHandle = NULL; // flush 전용 태스크 +// ★ 더블 버퍼링 (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; // ⭐ uint16_t → uint32_t -uint32_t serial2CsvIndex = 0; // ⭐ uint16_t → uint32_t // ⭐ Serial2 추가 -volatile uint32_t currentFileSize = 0; +uint32_t serialCsvIndex = 0; +uint32_t serial2CsvIndex = 0; +volatile uint32_t currentFileSize = 0; volatile uint32_t currentSerialFileSize = 0; -volatile uint32_t currentSerial2FileSize = 0; // ⭐ Serial2 추가 -volatile bool canLogFormatCSV = false; -volatile bool serialLogFormatCSV = true; -volatile bool serial2LogFormatCSV = true; // ⭐ Serial2 추가 -volatile uint64_t canLogStartTime = 0; -volatile uint64_t serialLogStartTime = 0; -volatile uint64_t serial2LogStartTime = 0; // ⭐ Serial2 추가 +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]; +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}; +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; // ⭐ Serial2 추가 -volatile uint32_t totalSerial2TxCount = 0; // ⭐ Serial2 추가 -uint32_t totalTxCount = 0; -uint8_t sequenceCount = 0; +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; @@ -354,388 +462,263 @@ 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 리셋 시작..."); - - // 1. 로깅 중지 (진행 중이면) - if (loggingEnabled) { - // 버퍼 플러시 및 파일 닫기 - } - - // 2. CAN 큐 비우기 - CANMessage tempMsg; - while (xQueueReceive(canQueue, &tempMsg, 0) == pdTRUE) { - // 큐에서 모든 메시지 제거 - } - - // 3. 하드웨어 리셋 (CS 토글) + + if (loggingEnabled) { /* 필요시 버퍼 플러시 */ } + + // ★ 큐 대신 RingBuffer 클리어 + ring_can_clear(&canRing); + Serial.println(" 1. 하드웨어 리셋..."); digitalWrite(HSPI_CS, LOW); delayMicroseconds(100); digitalWrite(HSPI_CS, HIGH); delay(100); - - // 4. 소프트웨어 리셋 (Configuration 모드로 진입) + Serial.println(" 2. 소프트웨어 리셋..."); mcp2515.reset(); delay(100); - - // ✅ Preferences에서 설정 불러오기 + preferences.begin("can-logger", true); int speedIndex = preferences.getInt("can_speed", 3); - if (speedIndex >= 0 && speedIndex < 4) { - currentCanSpeed = canSpeedValues[speedIndex]; - } + if (speedIndex >= 0 && speedIndex < 4) currentCanSpeed = canSpeedValues[speedIndex]; int savedMode = preferences.getInt("mcp_mode", 1); - if (savedMode >= 0 && savedMode <= 3) { - currentMcpMode = (MCP2515Mode)savedMode; - } + if (savedMode >= 0 && savedMode <= 3) currentMcpMode = (MCP2515Mode)savedMode; preferences.end(); - - Serial.printf(" 3. Speed=%d, Mode=%d\n", (int)currentCanSpeed, (int)currentMcpMode); - // 5. Bitrate 설정 (Configuration 모드에서) mcp2515.setBitrate(currentCanSpeed, MCP_CRYSTAL); delay(10); - - // 6. 필터/마스크 설정 (모든 메시지 수신) - Serial.println(" 4. 필터 설정 (Standard + Extended ID 모두 수신)..."); - // ⭐ RXB0: Standard ID 수신 (false) + mcp2515.setFilterMask(MCP2515::MASK0, false, 0x00000000); mcp2515.setFilter(MCP2515::RXF0, false, 0x00000000); mcp2515.setFilter(MCP2515::RXF1, false, 0x00000000); - - // ⭐ RXB1: Extended ID 수신 (true) 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); - - // 7. 모드 설정 (마지막에!) - Serial.printf(" 5. 모드 설정: %d\n", (int)currentMcpMode); - if (currentMcpMode == MCP_MODE_NORMAL) { - mcp2515.setNormalMode(); - } else if (currentMcpMode == MCP_MODE_LOOPBACK) { - mcp2515.setLoopbackMode(); - } else { - mcp2515.setListenOnlyMode(); - } + + if (currentMcpMode == MCP_MODE_NORMAL) mcp2515.setNormalMode(); + else if (currentMcpMode == MCP_MODE_LOOPBACK) mcp2515.setLoopbackMode(); + else mcp2515.setListenOnlyMode(); delay(50); - - // 8. 버퍼 클리어 (모드 전환 후) - Serial.println(" 6. 버퍼 클리어..."); + struct can_frame dummyFrame; int clearCount = 0; while (mcp2515.readMessage(&dummyFrame) == MCP2515::ERROR_OK) { - clearCount++; - if (clearCount > 100) break; + if (clearCount++ > 100) break; } - if (clearCount > 0) { - Serial.printf(" %d개 메시지 버림\n", clearCount); - } - - // 9. 에러/오버플로우 플래그 클리어 + mcp2515.clearRXnOVRFlags(); mcp2515.clearInterrupts(); mcp2515.clearTXInterrupts(); mcp2515.clearMERR(); mcp2515.clearERRIF(); - delay(10); - - // 10. 에러 상태 확인 + uint8_t errorFlag = mcp2515.getErrorFlags(); - uint8_t txErr = mcp2515.errorCountTX(); - uint8_t rxErr = mcp2515.errorCountRX(); - Serial.printf(" 7. 에러 상태: EFLG=0x%02X, TEC=%d, REC=%d\n", errorFlag, txErr, rxErr); - + 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) { - Serial.println(" ⚠️ 에러 감지됨 - 추가 리셋 시도..."); - // 에러가 있으면 완전 리셋 - 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(); - } + 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(); } - - // 11. 통계 리셋 + totalMsgCount = 0; - lastMsgCount = 0; - msgPerSecond = 0; - - // 12. 최근 메시지 테이블 클리어 - for (int i = 0; i < RECENT_MSG_COUNT; i++) { - recentData[i].count = 0; - } - + lastMsgCount = 0; + msgPerSecond = 0; + + for (int i = 0; i < RECENT_MSG_COUNT; i++) recentData[i].count = 0; + Serial.println("✅ MCP2515 리셋 완료!"); } + // ======================================== -// PSRAM 초기화 함수 +// PSRAM 초기화 (RingBuffer 버전) // ======================================== bool initPSRAM() { Serial.println("\n========================================"); - Serial.println(" PSRAM 메모리 할당"); + Serial.println(" PSRAM 메모리 할당 (RingBuffer 버전)"); Serial.println("========================================"); - + if (!psramFound()) { Serial.println("✗ PSRAM을 찾을 수 없습니다!"); - Serial.println("✗ Arduino IDE 설정:"); - Serial.println(" Tools → PSRAM → OPI 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); - if (!fileBuffer1) { - Serial.println("✗ fileBuffer1 할당 실패"); - return false; - } - Serial.printf("✓ fileBuffer1: %d KB\n", FILE_BUFFER_SIZE / 1024); - fileBuffer2 = (uint8_t*)ps_malloc(FILE_BUFFER_SIZE); - if (!fileBuffer2) { - Serial.println("✗ fileBuffer2 할당 실패"); - return false; - } - Serial.printf("✓ fileBuffer2: %d KB\n", FILE_BUFFER_SIZE / 1024); - - // 초기 버퍼 포인터 설정 + if (!fileBuffer1 || !fileBuffer2) { Serial.println("✗ fileBuffer 할당 실패"); return false; } currentWriteBuffer = fileBuffer1; currentFlushBuffer = fileBuffer2; - writeBufferIndex = 0; - flushBufferSize = 0; - Serial.println("✓ 더블 버퍼링 초기화 완료"); - - serialCsvBuffer = (char*)ps_malloc(SERIAL_CSV_BUFFER_SIZE); - if (!serialCsvBuffer) { - Serial.println("✗ serialCsvBuffer 할당 실패"); - return false; - } - Serial.printf("✓ serialCsvBuffer: %d KB\n", SERIAL_CSV_BUFFER_SIZE / 1024); - - // ⭐ Serial2 CSV Buffer - serial2CsvBuffer = (char*)ps_malloc(SERIAL2_CSV_BUFFER_SIZE); - if (!serial2CsvBuffer) { - Serial.println("✗ serial2CsvBuffer 할당 실패"); - return false; - } - Serial.printf("✓ serial2CsvBuffer: %d KB\n", SERIAL2_CSV_BUFFER_SIZE / 1024); - - recentData = (RecentCANData*)ps_calloc(RECENT_MSG_COUNT, sizeof(RecentCANData)); - if (!recentData) { - Serial.println("✗ recentData 할당 실패"); - return false; - } - Serial.printf("✓ recentData: %.2f KB\n", (float)(RECENT_MSG_COUNT * sizeof(RecentCANData)) / 1024.0); - - txMessages = (TxMessage*)ps_calloc(MAX_TX_MESSAGES, sizeof(TxMessage)); - if (!txMessages) { - Serial.println("✗ txMessages 할당 실패"); - return false; - } - Serial.printf("✓ txMessages: %.2f KB\n", (float)(MAX_TX_MESSAGES * sizeof(TxMessage)) / 1024.0); - - sequences = (CANSequence*)ps_calloc(MAX_SEQUENCES, sizeof(CANSequence)); - if (!sequences) { - Serial.println("✗ sequences 할당 실패"); - return false; - } - Serial.printf("✓ sequences: %.2f KB\n", (float)(MAX_SEQUENCES * sizeof(CANSequence)) / 1024.0); - - fileComments = (FileComment*)ps_calloc(MAX_FILE_COMMENTS, sizeof(FileComment)); - if (!fileComments) { - Serial.println("✗ fileComments 할당 실패"); - return false; - } - Serial.printf("✓ fileComments: %.2f KB\n", (float)(MAX_FILE_COMMENTS * sizeof(FileComment)) / 1024.0); - - // Queue 저장소 할당 - Serial.println("\n📦 Queue 저장소 할당..."); - - canQueueBuffer = (StaticQueue_t*)ps_malloc(sizeof(StaticQueue_t)); - canQueueStorage = (uint8_t*)ps_malloc(CAN_QUEUE_SIZE * sizeof(CANMessage)); - if (!canQueueBuffer || !canQueueStorage) { - Serial.println("✗ CAN Queue 저장소 할당 실패"); - return false; - } - Serial.printf("✓ CAN Queue: %d 개 × %d bytes = %.2f KB\n", - CAN_QUEUE_SIZE, sizeof(CANMessage), - (float)(CAN_QUEUE_SIZE * sizeof(CANMessage)) / 1024.0); - - serialQueueBuffer = (StaticQueue_t*)ps_malloc(sizeof(StaticQueue_t)); - serialQueueStorage = (uint8_t*)ps_malloc(SERIAL_QUEUE_SIZE * sizeof(SerialMessage)); - if (!serialQueueBuffer || !serialQueueStorage) { - Serial.println("✗ Serial Queue 저장소 할당 실패"); - return false; - } - Serial.printf("✓ Serial Queue: %d 개 × %d bytes = %.2f KB\n", - SERIAL_QUEUE_SIZE, sizeof(SerialMessage), - (float)(SERIAL_QUEUE_SIZE * sizeof(SerialMessage)) / 1024.0); - - // ⭐ Serial2 Queue - serial2QueueBuffer = (StaticQueue_t*)ps_malloc(sizeof(StaticQueue_t)); - serial2QueueStorage = (uint8_t*)ps_malloc(SERIAL2_QUEUE_SIZE * sizeof(SerialMessage)); - if (!serial2QueueBuffer || !serial2QueueStorage) { - Serial.println("✗ Serial2 Queue 저장소 할당 실패"); - return false; - } - Serial.printf("✓ Serial2 Queue: %d 개 × %d bytes = %.2f KB\n", - SERIAL2_QUEUE_SIZE, sizeof(SerialMessage), - (float)(SERIAL2_QUEUE_SIZE * sizeof(SerialMessage)) / 1024.0); - - Serial.println("========================================"); - Serial.printf("✓ PSRAM 남은 용량: %.2f KB\n", (float)ESP.getFreePsram() / 1024.0); - Serial.println("========================================\n"); - - return true; -} + writeBufferIndex = 0; + flushBufferSize = 0; + Serial.printf("✓ Double Buffer: 2 × %d KB\n", FILE_BUFFER_SIZE / 1024); -bool createQueues() { - Serial.println("Queue 생성 (PSRAM 사용)..."); - - canQueue = xQueueCreateStatic( - CAN_QUEUE_SIZE, - sizeof(CANMessage), - canQueueStorage, - canQueueBuffer - ); - - if (canQueue == NULL) { - Serial.println("✗ CAN Queue 생성 실패"); - return false; + // ── 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; } - Serial.printf("✓ CAN Queue: %d 개\n", CAN_QUEUE_SIZE); - - serialQueue = xQueueCreateStatic( - SERIAL_QUEUE_SIZE, - sizeof(SerialMessage), - serialQueueStorage, - serialQueueBuffer - ); - - if (serialQueue == NULL) { - Serial.println("✗ Serial Queue 생성 실패"); - return false; - } - Serial.printf("✓ Serial Queue: %d 개\n", SERIAL_QUEUE_SIZE); - - // ⭐ Serial2 Queue 생성 (중요!) - serial2Queue = xQueueCreateStatic( - SERIAL2_QUEUE_SIZE, - sizeof(SerialMessage), - serial2QueueStorage, - serial2QueueBuffer - ); - - if (serial2Queue == NULL) { - Serial.println("✗ Serial2 Queue 생성 실패"); - return false; - } - Serial.printf("✓ Serial2 Queue: %d 개\n\n", SERIAL2_QUEUE_SIZE); - + + // ======================================== + // ★★★ 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); - - // ⭐ Serial2 + 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.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.putUInt("ser_baud", serialSettings.baudRate); + preferences.putUChar("ser_data", serialSettings.dataBits); preferences.putUChar("ser_parity", serialSettings.parity); - preferences.putUChar("ser_stop", serialSettings.stopBits); - - // ⭐ Serial2 - preferences.putUInt("ser2_baud", serial2Settings.baudRate); - preferences.putUChar("ser2_data", serial2Settings.dataBits); + 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); + 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 if (serialSettings.parity == 2) config = SERIAL_5O1; + 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 if (serialSettings.parity == 2) config = SERIAL_6O1; + 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 if (serialSettings.parity == 2) config = SERIAL_7O1; + else 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; + else config = SERIAL_8O1; } - - if (serialSettings.stopBits == 2) { - config |= 0x3000; - } - + if (serialSettings.stopBits == 2) config |= 0x3000; SerialComm.begin(serialSettings.baudRate, config, SERIAL_RX_PIN, SERIAL_TX_PIN); SerialComm.setRxBufferSize(2048); - - // ⭐ Serial2 설정 + 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 if (serial2Settings.parity == 2) config2 = SERIAL_5O1; + 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 if (serial2Settings.parity == 2) config2 = SERIAL_6O1; + 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 if (serial2Settings.parity == 2) config2 = SERIAL_7O1; + else config2 = SERIAL_7O1; } else { if (serial2Settings.parity == 0) config2 = SERIAL_8N1; else if (serial2Settings.parity == 1) config2 = SERIAL_8E1; - else if (serial2Settings.parity == 2) config2 = SERIAL_8O1; + 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)); @@ -743,369 +726,19 @@ void loadSettings() { 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"); - + 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]; - } - + if (speedIndex >= 0 && speedIndex < 4) currentCanSpeed = canSpeedValues[speedIndex]; int savedMode = preferences.getInt("mcp_mode", 1); - Serial.printf("📥 loadSettings: MCP Mode = %d\n", savedMode); - if (savedMode >= 0 && savedMode <= 3) { - currentMcpMode = (MCP2515Mode)savedMode; - } - - // 🆕 File Format 불러오기 - savedCanLogFormatCSV = preferences.getBool("can_format_csv", false); - Serial.printf("📥 loadSettings: CAN Format = %s\n", savedCanLogFormatCSV ? "CSV" : "BIN"); - + 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(); } -// ============================================ -// 🎯 Auto Trigger 함수 -// ============================================ - -// 비트 추출 (Motorola/Big-endian) -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) { - if (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 extractedValue = extractBits(data, trigger.startBit, trigger.bitLength); - - if (strcmp(trigger.op, "==") == 0) return extractedValue == trigger.value; - else if (strcmp(trigger.op, "!=") == 0) return extractedValue != trigger.value; - else if (strcmp(trigger.op, ">") == 0) return extractedValue > trigger.value; - else if (strcmp(trigger.op, "<") == 0) return extractedValue < trigger.value; - else if (strcmp(trigger.op, ">=") == 0) return extractedValue >= trigger.value; - else if (strcmp(trigger.op, "<=") == 0) return extractedValue <= trigger.value; - - return false; -} - -// Auto Trigger 체크 -void checkAutoTriggers(struct can_frame &frame) { - if (!autoTriggerEnabled || !sdCardReady) return; - - // 시작 조건 체크 - 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; - - // 🆕 Auto Trigger 전용 형식 설정 적용 - canLogFormatCSV = autoTriggerLogCSV; - - 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_MMC.open(currentFilename, FILE_WRITE); - - if (logFile) { - if (canLogFormatCSV) { - logFile.println("Time_us,CAN_ID,DLC,Data"); - logFile.flush(); - } - logFile.close(); - - // APPEND 모드로 다시 열기 - logFile = SD_MMC.open(currentFilename, FILE_APPEND); - - if (logFile) { - loggingEnabled = true; - autoTriggerActive = true; - - // ⭐⭐⭐ 더블 버퍼 초기화 - writeBufferIndex = 0; - flushBufferSize = 0; - flushInProgress = false; - - currentFileSize = logFile.size(); - - Serial.println("🎯 Auto Trigger: 로깅 시작!"); - Serial.printf(" 조건: ID 0x%03X 만족\n", frame.can_id); - Serial.printf(" 파일: %s\n", currentFilename); - Serial.printf(" 형식: %s\n", canLogFormatCSV ? "CSV" : "BIN"); - } else { - Serial.println("✗ Auto Trigger: APPEND 모드 파일 열기 실패"); - } - } else { - Serial.println("✗ Auto Trigger: 파일 생성 실패"); - } - xSemaphoreGive(sdMutex); - } else { - Serial.println("✗ Auto Trigger: sdMutex 획득 실패"); - } - } - } - - // 중지 조건 체크 - 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) { - // 🎯 파일 닫기 로직 - Serial.println("🎯 Auto Trigger: 중지 조건 만족"); - - // ⭐⭐⭐ 1단계: 로깅 중지 (새 데이터 수신 차단) - loggingEnabled = false; - autoTriggerActive = false; - - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(5000)) == pdTRUE) { - - // ⭐⭐⭐ 2단계: flush 태스크 완료 대기 - Serial.println("⏳ Flush 태스크 완료 대기 중..."); - int waitCount = 0; - while (flushInProgress && waitCount < 1000) { // 최대 10초 - vTaskDelay(10); - waitCount++; - } - if (waitCount > 0) { - Serial.printf("✓ Flush 완료: %d ms 대기\n", waitCount * 10); - } - - // ⭐⭐⭐ 3단계: 현재 쓰기 버퍼의 남은 데이터 flush - if (writeBufferIndex > 0 && logFile) { - size_t written = logFile.write(currentWriteBuffer, writeBufferIndex); - Serial.printf("✓ 쓰기 버퍼 최종 플러시: %d bytes\n", written); - writeBufferIndex = 0; - } - - // ⭐⭐⭐ 4단계: 최종 flush - if (logFile) { - logFile.flush(); - Serial.println("✓ 최종 flush 완료"); - } - - // ⭐⭐⭐ 5단계: 파일 닫기 - if (logFile) { - size_t finalSize = logFile.size(); - logFile.close(); - Serial.printf("✓ Auto Trigger 파일 닫힘: %s (%lu bytes)\n", currentFilename, finalSize); - } - - currentFilename[0] = '\0'; - writeBufferIndex = 0; - flushBufferSize = 0; - - Serial.println("🎯 Auto Trigger: 로깅 중지 완료!"); - Serial.printf(" 조건: ID 0x%03X 만족\n", frame.can_id); - - xSemaphoreGive(sdMutex); - } - } - } -} - -// Auto Trigger 설정 저장 -void saveAutoTriggerSettings() { - preferences.begin("autotrigger", false); - - preferences.putBool("enabled", autoTriggerEnabled); - preferences.putBool("logCSV", autoTriggerLogCSV); // 🆕 로그 형식 저장 - Serial.printf("💾 Save: autoTriggerLogCSV = %d\n", autoTriggerLogCSV); - preferences.putString("start_logic", startLogicOp); - preferences.putString("stop_logic", stopLogicOp); - - // 디버그: Start Triggers 저장 - Serial.printf("💾 Save: startTriggerCount = %d\n", startTriggerCount); - for (int i = 0; i < startTriggerCount; i++) { - Serial.printf(" [%d] ID=0x%X, Bit=%d, Len=%d, Op=%s, Val=%ld, En=%d\n", - i, startTriggers[i].canId, startTriggers[i].startBit, - startTriggers[i].bitLength, startTriggers[i].op, - startTriggers[i].value, startTriggers[i].enabled); - } - - 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); - } - - // Formula 저장 - preferences.putString("start_formula", startFormula); - preferences.putString("stop_formula", stopFormula); - - preferences.end(); - Serial.println("💾 Auto Trigger 설정 저장 완료"); -} - -// Auto Trigger 설정 로드 -void loadAutoTriggerSettings() { - preferences.begin("autotrigger", true); - - autoTriggerEnabled = preferences.getBool("enabled", false); - autoTriggerLogCSV = preferences.getBool("logCSV", false); // 🆕 로그 형식 로드 - Serial.printf("📥 Load: autoTriggerLogCSV = %d\n", autoTriggerLogCSV); - preferences.getString("start_logic", startLogicOp, sizeof(startLogicOp)); - preferences.getString("stop_logic", stopLogicOp, sizeof(stopLogicOp)); - - // Formula 불러오기 - 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); - } - - // 디버그: Start Triggers 출력 - Serial.printf("📥 Load: startTriggerCount = %d\n", startTriggerCount); - for (int i = 0; i < startTriggerCount; i++) { - Serial.printf(" [%d] ID=0x%X, Bit=%d, Len=%d, Op=%s, Val=%ld, En=%d\n", - i, startTriggers[i].canId, startTriggers[i].startBit, - startTriggers[i].bitLength, startTriggers[i].op, - startTriggers[i].value, startTriggers[i].enabled); - } - - 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(); - - if (autoTriggerEnabled) { - Serial.println("✓ Auto Trigger 설정 로드 완료"); - Serial.printf(" 시작 조건: %d개 (%s)\n", startTriggerCount, startLogicOp); - Serial.printf(" 중지 조건: %d개 (%s)\n", stopTriggerCount, stopLogicOp); - } -} - - void saveSettings() { preferences.begin("can-logger", false); preferences.putString("wifi_ssid", wifiSSID); @@ -1113,113 +746,263 @@ void saveSettings() { 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; - } + if (canSpeedValues[i] == currentCanSpeed) { preferences.putInt("can_speed", i); break; } } - preferences.putInt("mcp_mode", (int)currentMcpMode); - Serial.printf("💾 MCP Mode 저장: %d\n", (int)currentMcpMode); - - // 🆕 File Format 저장 - preferences.putBool("can_format_csv", savedCanLogFormatCSV); - Serial.printf("💾 CAN Format 저장: %s\n", savedCanLogFormatCSV ? "CSV" : "BIN"); - + preferences.putBool("can_format_csv", savedCanLogFormatCSV); + preferences.putBool("can_format_pcap", savedCanLogFormatPCAP); saveSerialSettings(); preferences.end(); } // ======================================== -// RTC 함수 +// 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); - if (rtcWire.endTransmission() == 0) { - timeSyncStatus.rtcAvailable = true; - Serial.println("✓ RTC(DS3231) 감지됨"); - } else { - timeSyncStatus.rtcAvailable = false; - Serial.println("! RTC(DS3231) 없음"); - } + timeSyncStatus.rtcAvailable = (rtcWire.endTransmission() == 0); + Serial.printf("%s RTC(DS3231) %s\n", + timeSyncStatus.rtcAvailable ? "✓" : "!", + timeSyncStatus.rtcAvailable ? "감지됨" : "없음"); } -uint8_t bcdToDec(uint8_t val) { - return (val >> 4) * 10 + (val & 0x0F); -} +uint8_t bcdToDec(uint8_t v) { return (v >> 4) * 10 + (v & 0x0F); } +uint8_t decToBcd(uint8_t v) { return ((v / 10) << 4) | (v % 10); } -uint8_t decToBcd(uint8_t val) { - return ((val / 10) << 4) | (val % 10); -} - -bool readRTC(struct tm *timeinfo) { +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 buffer[7]; - for (int i = 0; i < 7; i++) buffer[i] = rtcWire.read(); + 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); - - timeinfo->tm_sec = bcdToDec(buffer[0] & 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; - + 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 *timeinfo) { +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(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); + 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 success; + return ok; } 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++; - } + struct tm t; time_t now = tv->tv_sec; localtime_r(&now, &t); + if (writeRTC(&t)) timeSyncStatus.rtcSyncCount++; } } @@ -1229,20 +1012,19 @@ void initNTP() { } 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 }; + 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(xDelay); + vTaskDelay(pdMS_TO_TICKS(RTC_SYNC_INTERVAL_MS)); } } @@ -1250,314 +1032,211 @@ void rtcSyncTask(void *parameter) { // MCP2515 모드 // ======================================== bool setMCP2515Mode(MCP2515Mode mode) { - Serial.printf("🔧 MCP Mode 변경 요청: %d → ", (int)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; + 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; } // ======================================== -// 인터럽트 및 Task +// ★★★ CAN 인터럽트 & canRxTask (RingBuffer 버전) ★★★ // ======================================== void IRAM_ATTR canISR() { BaseType_t xHigherPriorityTaskWoken = pdFALSE; - if (canRxTaskHandle != NULL) { + if (canRxTaskHandle != NULL) vTaskNotifyGiveFromISR(canRxTaskHandle, &xHigherPriorityTaskWoken); - portYIELD_FROM_ISR(xHigherPriorityTaskWoken); - } -} - -void serialRxTask(void *parameter) { - SerialMessage serialMsg; - 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); - 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, pdMS_TO_TICKS(10)) == 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, pdMS_TO_TICKS(10)) == pdTRUE) { - totalSerialRxCount++; - } - lineIndex = 0; - } - - vTaskDelay(pdMS_TO_TICKS(1)); - } -} - - -// ⭐ Serial2 RX Task (우선순위 5) -void serial2RxTask(void *parameter) { - SerialMessage serialMsg; - 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); - serialMsg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; - serialMsg.length = lineIndex; - memcpy(serialMsg.data, lineBuffer, lineIndex); - serialMsg.isTx = false; - - if (serial2Queue && xQueueSend(serial2Queue, &serialMsg, pdMS_TO_TICKS(10)) == pdTRUE) { // ⭐ NULL 체크 - totalSerial2RxCount++; - } - 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 (serial2Queue && xQueueSend(serial2Queue, &serialMsg, pdMS_TO_TICKS(10)) == pdTRUE) { // ⭐ NULL 체크 - totalSerial2RxCount++; - } - lineIndex = 0; - } - - vTaskDelay(pdMS_TO_TICKS(1)); - } + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } void canRxTask(void *parameter) { struct can_frame frame; CANMessage msg; - uint32_t lastErrorCheck = 0; + uint32_t lastErrorCheck = 0; uint32_t errorRecoveryCount = 0; - - Serial.println("✓ CAN RX Task 시작 (Core 0, Priority 24 - 절대 최고!)"); - - // ⭐⭐⭐ 초기 버퍼 확인 + + Serial.println("✓ CAN RX Task 시작 (RingBuffer SPSC, Core 1, Pri 24)"); + + // 초기 버퍼 클리어 if (digitalRead(CAN_INT_PIN) == LOW) { - Serial.println("⚠️ 초기 CAN 인터럽트 핀 LOW - 버퍼 클리어"); int readCount = 0; while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK && readCount < 100) { - struct timeval tv; - gettimeofday(&tv, NULL); + 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.id = frame.can_id & 0x1FFFFFFF; msg.dlc = frame.can_dlc; memcpy(msg.data, frame.data, 8); - - if (xQueueSend(canQueue, &msg, 0) == pdTRUE) { - totalMsgCount++; - readCount++; - } + // ★ RingBuffer push (non-blocking, no mutex) + if (ring_can_push(&canRing, &msg)) totalMsgCount++; + readCount++; } - Serial.printf("✓ 초기 버퍼에서 %d개 읽음\n", readCount); } - + while (1) { - ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(100)); // 100ms 타임아웃으로 변경 - - // 🆕 주기적 에러 체크 (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) { // RX0OVR 또는 RX1OVR (오버플로우) - Serial.printf("⚠️ CAN 오버플로우 감지! EFLG=0x%02X - 클리어 중...\n", errorFlag); + uint8_t txErr = mcp2515.errorCountTX(); + uint8_t rxErr = mcp2515.errorCountRX(); + + if (errorFlag & 0xC0) { mcp2515.clearRXnOVRFlags(); mcp2515.clearInterrupts(); errorRecoveryCount++; } - - if (errorFlag & 0x20) { // TXBO (Bus-Off) - Serial.printf("🚨 CAN Bus-Off 감지! EFLG=0x%02X, TEC=%d, REC=%d\n", errorFlag, txErr, rxErr); - - // Bus-Off 복구: 완전 리셋 필요 - Serial.println(" → 자동 복구 시도..."); - - // 1. 완전 리셋 (Configuration 모드로 진입 + 에러 카운터 자동 리셋) - mcp2515.reset(); - delay(100); - - // 2. Bitrate 재설정 - mcp2515.setBitrate(currentCanSpeed, MCP_CRYSTAL); - delay(10); - - // 3. 모든 에러 플래그 클리어 + 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); - - // 4. 모드 재설정 - if (currentMcpMode == MCP_MODE_NORMAL) { - mcp2515.setNormalMode(); - } else if (currentMcpMode == MCP_MODE_LOOPBACK) { - mcp2515.setLoopbackMode(); - } else { - mcp2515.setListenOnlyMode(); - } + mcp2515.clearERRIF(); delay(10); + if (currentMcpMode == MCP_MODE_NORMAL) mcp2515.setNormalMode(); + else if (currentMcpMode == MCP_MODE_LOOPBACK) mcp2515.setLoopbackMode(); + else mcp2515.setListenOnlyMode(); delay(50); - - // 5. 버퍼 클리어 while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {} mcp2515.clearRXnOVRFlags(); - errorRecoveryCount++; - Serial.printf(" ✓ Bus-Off 복구 완료 (총 %lu회 복구)\n", errorRecoveryCount); - } - - // Error Passive 상태 감지 - if ((errorFlag & 0x18) && rxErr > 96) { - Serial.printf("⚠️ CAN Error Passive! REC=%d - 주의 필요\n", rxErr); + Serial.printf("✓ Bus-Off 복구 (총 %lu회)\n", errorRecoveryCount); } + if ((errorFlag & 0x18) && rxErr > 96) + Serial.printf("⚠️ Error Passive! REC=%d\n", rxErr); } - - // 메시지 배치 읽기 (한 번에 최대 20개 처리) + + // ★ 배치 읽기 → RingBuffer push (완전 non-blocking) int batchCount = 0; while (batchCount < 20 && mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) { - struct timeval tv; - // 🎯 Auto Trigger 체크 - checkAutoTriggers(frame); + checkAutoTriggers(frame); - gettimeofday(&tv, NULL); + 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.id = frame.can_id & 0x1FFFFFFF; msg.dlc = frame.can_dlc; memcpy(msg.data, frame.data, 8); - - if (xQueueSend(canQueue, &msg, 0) == pdTRUE) { // ⭐ 타임아웃 10ms → 0 (블로킹 제거) + + // ★ mutex 없음, blocking 없음 - 순수 메모리 쓰기 + if (ring_can_push(&canRing, &msg)) { totalMsgCount++; } + // dropped 시에도 canRing.dropped에 자동 기록됨 + batchCount++; } - - // 배치 처리 후 짧은 yield (다른 Task에 CPU 양보) - if (batchCount == 0) { - vTaskDelay(pdMS_TO_TICKS(1)); - } + if (batchCount == 0) vTaskDelay(pdMS_TO_TICKS(1)); } } // ======================================== -// SD Write Task +// 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 전용 태스크 (더블 버퍼링) +// ★★★ SD Flush Task (더블 버퍼 비동기 flush) ★★★ // ======================================== void sdFlushTask(void *parameter) { - Serial.println("✓ sdFlushTask 시작 (더블 버퍼링 모드)"); - + Serial.println("✓ sdFlushTask 시작 (더블 버퍼링 + RingBuffer)"); while (1) { - uint32_t notifyValue; - - // flush 알림 대기 (무한 대기) - if (xTaskNotifyWait(0, 0xFFFFFFFF, ¬ifyValue, portMAX_DELAY) == pdTRUE) { - uint32_t flushSize = notifyValue; - + uint32_t flushSize; + if (xTaskNotifyWait(0, 0xFFFFFFFF, &flushSize, portMAX_DELAY) == pdTRUE) { if (flushSize > 0 && flushSize <= FILE_BUFFER_SIZE) { flushInProgress = true; - - uint32_t startTime = millis(); - - // ⭐ SD 뮤텍스 획득 (최대 5초 대기) + uint32_t t0 = millis(); if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(5000)) == pdTRUE) { - // ⭐⭐⭐ loggingEnabled 체크 제거! (중지 시에도 flush 필요) - if (logFile && sdCardReady) { - // ⭐ flush 버퍼를 SD에 쓰기 + if (logFile && sdCardReady && loggingEnabled) { size_t written = logFile.write(currentFlushBuffer, flushSize); - - // ⭐ 여기서 flush 호출 (200ms 블로킹되어도 CAN 수신은 계속!) logFile.flush(); - - uint32_t elapsed = millis() - startTime; - Serial.printf("✓ SD Flush 완료: %d bytes, %d ms\n", written, elapsed); - - // 통계 - if (elapsed > 100) { - Serial.printf("⚠️ Flush 지연: %d ms (하지만 CAN은 정상!)\n", elapsed); - } + 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: 뮤텍스 타임아웃"); + Serial.println("✗ SD Flush: Mutex 타임아웃"); } - flushInProgress = false; } } @@ -1565,20 +1244,24 @@ void sdFlushTask(void *parameter) { } // ======================================== -// SD Write Task +// ★★★ 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; - SerialMessage serialMsg; - + while (1) { bool hasWork = false; - - // CAN 메시지 처리 - if (xQueueReceive(canQueue, &canMsg, 0) == pdTRUE) { + + // ★ 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) { @@ -1588,147 +1271,142 @@ void sdWriteTask(void *parameter) { break; } } - if (!found) { for (int i = 0; i < RECENT_MSG_COUNT; i++) { if (recentData[i].count == 0) { - recentData[i].msg = canMsg; + recentData[i].msg = canMsg; recentData[i].count = 1; break; } } } - - // CAN 로깅 - if (loggingEnabled && sdCardReady) { - // ⭐⭐⭐ 뮤텍스 타임아웃 1ms로 감소 (블로킹 방지) - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1)) == pdTRUE) { - if (canLogFormatCSV) { - char csvLine[128]; - uint64_t relativeTime = canMsg.timestamp_us - canLogStartTime; - char dataStr[32]; - int dataLen = 0; - static uint32_t csvReopenCounter = 0; + // 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", - relativeTime, canMsg.id, canMsg.dlc, dataStr); - + "%llu,0x%X,%d,%s\n", relTime, canMsg.id, canMsg.dlc, dataStr); if (logFile) { - size_t written = logFile.write((uint8_t*)csvLine, lineLen); + logFile.write((uint8_t*)csvLine, lineLen); currentFileSize += lineLen; - - static int csvFlushCounter = 0; - if (++csvFlushCounter >= 2000) { // ⭐ 500 → 2000 (SD 블로킹 최소화) - logFile.flush(); - csvFlushCounter = 0; - } - - // ⭐⭐⭐ 파일 재오픈 제거 (고부하 환경 최적화) - // if (++csvReopenCounter >= 2000) { ... } // 제거됨 - - + static int csvFlushCnt = 0; + if (++csvFlushCnt >= 2000) { logFile.flush(); csvFlushCnt = 0; } } - } else { - // ⭐⭐⭐ BIN 형식 - 더블 버퍼링 - - // ⭐ 1단계: 버퍼 가득 찼으면 스왑 - if (writeBufferIndex + sizeof(CANMessage) > FILE_BUFFER_SIZE) { - // flush가 진행중이면 잠시 대기 (매우 짧음, 버퍼 스왑만 기다림) - int waitCount = 0; - while (flushInProgress && waitCount < 100) { - vTaskDelay(1); - waitCount++; - } - - if (waitCount >= 100) { - Serial.println("⚠️ Flush 대기 타임아웃 (버퍼 손실 방지)"); - } - - // ⭐ 버퍼 스왑 (즉시, 블로킹 없음!) - uint8_t* temp = currentWriteBuffer; - currentWriteBuffer = currentFlushBuffer; - currentFlushBuffer = temp; - flushBufferSize = writeBufferIndex; - writeBufferIndex = 0; - - // ⭐ flush 태스크에 알림 (비동기) - if (sdFlushTaskHandle != NULL) { - xTaskNotify(sdFlushTaskHandle, flushBufferSize, eSetValueWithOverwrite); - } - - Serial.printf("✓ 버퍼 스왑: %d bytes → flush 태스크\n", flushBufferSize); - } - - // ⭐ 2단계: 현재 쓰기 버퍼에 데이터 추가 - memcpy(¤tWriteBuffer[writeBufferIndex], &canMsg, sizeof(CANMessage)); - writeBufferIndex += sizeof(CANMessage); - currentFileSize += sizeof(CANMessage); - + xSemaphoreGive(sdMutex); } - xSemaphoreGive(sdMutex); - } - } - } - - if (!hasWork) { - vTaskDelay(pdMS_TO_TICKS(1)); - } - } -} + } 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 불필요 -void sdMonitorTask(void *parameter) { - const TickType_t xDelay = pdMS_TO_TICKS(500); - - 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; + 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); } } - - powerStatus.lowVoltage = (powerStatus.voltage < LOW_VOLTAGE_THRESHOLD); - powerStatus.lastCheck = currentTime; } - - vTaskDelay(xDelay); + + 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 commentFile = SD_MMC.open("/comments.dat", FILE_WRITE); - if (commentFile) { - commentFile.write((uint8_t*)&commentCount, sizeof(commentCount)); - commentFile.write((uint8_t*)fileComments, sizeof(FileComment) * commentCount); - commentFile.close(); + 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); } @@ -1738,12 +1416,12 @@ void loadFileComments() { if (!sdCardReady) return; if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { if (SD_MMC.exists("/comments.dat")) { - File commentFile = SD_MMC.open("/comments.dat", FILE_READ); - if (commentFile) { - commentFile.read((uint8_t*)&commentCount, sizeof(commentCount)); + 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; - commentFile.read((uint8_t*)fileComments, sizeof(FileComment) * commentCount); - commentFile.close(); + f.read((uint8_t*)fileComments, sizeof(FileComment) * commentCount); + f.close(); } } xSemaphoreGive(sdMutex); @@ -1751,11 +1429,9 @@ void loadFileComments() { } const char* getFileComment(const char* filename) { - for (int i = 0; i < commentCount; i++) { - if (strcmp(fileComments[i].filename, filename) == 0) { + for (int i = 0; i < commentCount; i++) + if (strcmp(fileComments[i].filename, filename) == 0) return fileComments[i].comment; - } - } return ""; } @@ -1763,33 +1439,28 @@ 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; + 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'; + 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_MMC.open("/sequences.dat", FILE_WRITE); - if (seqFile) { - seqFile.write((uint8_t*)&sequenceCount, sizeof(sequenceCount)); - seqFile.write((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount); - seqFile.close(); + 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); } @@ -1799,12 +1470,12 @@ void loadSequences() { if (!sdCardReady) return; if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { if (SD_MMC.exists("/sequences.dat")) { - File seqFile = SD_MMC.open("/sequences.dat", FILE_READ); - if (seqFile) { - seqFile.read((uint8_t*)&sequenceCount, sizeof(sequenceCount)); + 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; - seqFile.read((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount); - seqFile.close(); + f.read((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount); + f.close(); } } xSemaphoreGive(sdMutex); @@ -1812,82 +1483,51 @@ void loadSequences() { } // ======================================== -// TX Task +// 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; - } + 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(); - } + if (currentMcpMode == MCP_MODE_TRANSMIT) mcp2515.setListenOnlyMode(); } } } - vTaskDelay(anyActive ? pdMS_TO_TICKS(1) : pdMS_TO_TICKS(10)); } } -// ======================================== -// Sequence Task -// ======================================== void sequenceTask(void *parameter) { while (1) { - if (seqRuntime.running && seqRuntime.activeSequenceIndex >= 0 && + 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(); - } - + 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_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(); - } - + if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) totalTxCount++; + if (currentMcpMode == MCP_MODE_TRANSMIT) mcp2515.setListenOnlyMode(); seqRuntime.currentStep++; seqRuntime.lastStepTime = now; } @@ -1895,19 +1535,12 @@ void sequenceTask(void *parameter) { 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; + 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)); @@ -1916,284 +1549,149 @@ void sequenceTask(void *parameter) { } // ======================================== -// WebSocket 이벤트 처리 (중요!) +// WebSocket 이벤트 // ======================================== void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) { - // 🆕 WebSocket 연결 시 현재 설정 전송 if (type == WStype_CONNECTED) { - Serial.printf("[%u] ✅ WebSocket 연결됨\n", num); - - // ⭐ 모든 데이터를 하나의 JSON으로 통합 전송 (빠른 연결) - DynamicJsonDocument allData(3072); // 통합 JSON + DynamicJsonDocument allData(3072); allData["type"] = "initialData"; - - // 기본 설정 int speedIndex = 3; for (int i = 0; i < 4; i++) { - if (canSpeedValues[i] == currentCanSpeed) { - speedIndex = i; - break; - } + if (canSpeedValues[i] == currentCanSpeed) { speedIndex = i; break; } } - allData["canSpeed"] = speedIndex; - allData["mcpMode"] = (int)currentMcpMode; - allData["autoTriggerEnabled"] = autoTriggerEnabled; + allData["canSpeed"] = speedIndex; + allData["mcpMode"] = (int)currentMcpMode; + allData["autoTriggerEnabled"]= autoTriggerEnabled; allData["autoTriggerLogCSV"] = autoTriggerLogCSV; - - // Auto Trigger 설정도 포함 + allData["autoTriggerLogPCAP"]= autoTriggerLogPCAP; if (autoTriggerEnabled) { - allData["logFormat"] = autoTriggerLogCSV ? "csv" : "bin"; - allData["startLogic"] = startLogicOp; - allData["stopLogic"] = stopLogicOp; - allData["startFormula"] = startFormula; + allData["logFormat"] = autoTriggerLogPCAP ? "pcap" : (autoTriggerLogCSV ? "csv" : "bin"); + allData["startLogic"] = startLogicOp; + allData["stopLogic"] = stopLogicOp; + allData["startFormula"]= startFormula; allData["stopFormula"] = stopFormula; - - JsonArray startArray = allData.createNestedArray("startTriggers"); + JsonArray sa = allData.createNestedArray("startTriggers"); for (int i = 0; i < startTriggerCount; i++) { - JsonObject t = startArray.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; + 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 stopArray = allData.createNestedArray("stopTriggers"); + JsonArray ea = allData.createNestedArray("stopTriggers"); for (int i = 0; i < stopTriggerCount; i++) { - JsonObject t = stopArray.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; + 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; } } - - // ⭐ 하나의 메시지로 전송 (delay 제거) - String json; - serializeJson(allData, json); + String json; serializeJson(allData, json); webSocket.sendTXT(num, json); } else if (type == WStype_TEXT) { DynamicJsonDocument doc(44384); - DeserializationError error = deserializeJson(doc, payload); - - if (error) return; - + if (deserializeJson(doc, payload)) 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); + 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* 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'; - } - + 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 response(256); - response["type"] = "settingsSaved"; - response["success"] = true; - - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); + 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* format = doc["format"]; - if (format && strcmp(format, "csv") == 0) { - canLogFormatCSV = true; - } else { - canLogFormatCSV = false; - } - + 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) { - struct tm timeinfo; - time_t now; - time(&now); - localtime_r(&now, &timeinfo); - - struct timeval tv; - gettimeofday(&tv, NULL); + 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 = canLogFormatCSV ? "csv" : "bin"; + 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); - - // ⭐⭐⭐ 파일 생성 (헤더 쓰기) + 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(); - } - logFile.close(); // ⭐ 헤더 쓰고 닫기 - - // ⭐⭐⭐ APPEND 모드로 다시 열기 + 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(); - Serial.printf("✅ 로깅 시작! (더블 버퍼링 모드)\n"); - Serial.printf(" 파일: %s\n", currentFilename); - Serial.printf(" 형식: %s\n", canLogFormatCSV ? "CSV" : "BIN"); - Serial.printf(" 초기 크기: %lu bytes\n", currentFileSize); - Serial.printf(" sdCardReady: %d\n", sdCardReady); - } else { - Serial.println("✗ APPEND 모드로 파일 열기 실패"); - Serial.printf(" 파일명: %s\n", currentFilename); + 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); } - } else { - Serial.println("✗ 파일 생성 실패"); } - xSemaphoreGive(sdMutex); } } } else if (strcmp(cmd, "stopLogging") == 0) { if (loggingEnabled) { - Serial.println("🛑 로깅 중지 요청..."); - - // ⭐⭐⭐ 1단계: 로깅 중지 (새 데이터 수신 차단) - loggingEnabled = false; - - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(5000)) == pdTRUE) { - - // ⭐⭐⭐ 2단계: flush 태스크 완료 대기 (진행 중인 flush) - Serial.println("⏳ Flush 태스크 완료 대기 중..."); - int waitCount = 0; - while (flushInProgress && waitCount < 1000) { // 최대 10초 - vTaskDelay(10); - waitCount++; - } - if (waitCount > 0) { - Serial.printf("✓ Flush 태스크 완료: %d ms 대기\n", waitCount * 10); - } - - // ⭐⭐⭐ 3단계: 현재 쓰기 버퍼의 남은 데이터 flush + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(2000)) == pdTRUE) { if (writeBufferIndex > 0 && logFile) { - size_t written = logFile.write(currentWriteBuffer, writeBufferIndex); - Serial.printf("✓ 쓰기 버퍼 최종 플러시: %d bytes\n", written); + logFile.write(currentWriteBuffer, writeBufferIndex); + logFile.flush(); writeBufferIndex = 0; } - - // ⭐⭐⭐ 4단계: 최종 flush (메타데이터 동기화) - if (logFile) { - logFile.flush(); - Serial.println("✓ 최종 flush 완료"); - } - - // ⭐⭐⭐ 5단계: 파일 닫기 - if (logFile) { - size_t finalSize = logFile.size(); - logFile.close(); - Serial.printf("✓ 파일 닫힘: %s (%lu bytes)\n", currentFilename, finalSize); - } - + 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; - + writeBufferIndex = 0; flushBufferSize = 0; xSemaphoreGive(sdMutex); - - Serial.println("✅ 로깅 중지 완료!"); - } else { - Serial.println("✗ 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; - } - + const char* fmt = doc["format"]; + serialLogFormatCSV = !(fmt && strcmp(fmt, "bin") == 0); 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); + 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", - timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, - timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, ext); - + 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"); - } + if (serialLogFormatCSV) serialLogFile.println("Time_us,Direction,Data"); serialLoggingEnabled = true; serialCsvIndex = 0; currentSerialFileSize = serialLogFile.size(); } - xSemaphoreGive(sdMutex); } } @@ -2201,16 +1699,10 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) else if (strcmp(cmd, "stopSerialLogging") == 0) { if (serialLoggingEnabled) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - if (serialCsvIndex > 0 && serialLogFile) { + if (serialCsvIndex > 0 && serialLogFile) serialLogFile.write((uint8_t*)serialCsvBuffer, serialCsvIndex); - serialCsvIndex = 0; - } - - if (serialLogFile) { - serialLogFile.close(); - } - - serialLoggingEnabled = false; + if (serialLogFile) serialLogFile.close(); + serialLoggingEnabled = false; serialCsvIndex = 0; xSemaphoreGive(sdMutex); } } @@ -2219,106 +1711,49 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) 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++; - } + 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) { - uint32_t baud = doc["baudRate"] | 115200; - uint8_t data = doc["dataBits"] | 8; - uint8_t parity = doc["parity"] | 0; - uint8_t stop = doc["stopBits"] | 1; - - serialSettings.baudRate = baud; - serialSettings.dataBits = data; - serialSettings.parity = parity; - serialSettings.stopBits = stop; - - saveSerialSettings(); - applySerialSettings(); + 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 리셋 제거! (CAN 수신 중단 방지) - // 속도 변경은 로깅 중지 후 수동으로만 가능하도록 변경 - // mcp2515.reset(); - // mcp2515.setBitrate(currentCanSpeed, MCP_CRYSTAL); - // setMCP2515Mode(currentMcpMode); - saveSettings(); - - // 사용자에게 안내 메시지 - StaticJsonDocument<256> response; - response["type"] = "info"; - response["message"] = "CAN speed saved. Stop logging and restart to apply."; - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); - } + 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* format = doc["format"]; - if (format && strcmp(format, "bin") == 0) { - serial2LogFormatCSV = false; - } else { - serial2LogFormatCSV = true; - } - + const char* fmt = doc["format"]; + serial2LogFormatCSV = !(fmt && strcmp(fmt, "bin") == 0); 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); + 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", - timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, - timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, ext); - + 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"); - } + if (serial2LogFormatCSV) serial2LogFile.println("Time_us,Direction,Data"); serial2LoggingEnabled = true; serial2CsvIndex = 0; currentSerial2FileSize = serial2LogFile.size(); } - xSemaphoreGive(sdMutex); } } @@ -2326,16 +1761,10 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) else if (strcmp(cmd, "stopSerial2Logging") == 0) { if (serial2LoggingEnabled) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - if (serial2CsvIndex > 0 && serial2LogFile) { + if (serial2CsvIndex > 0 && serial2LogFile) serial2LogFile.write((uint8_t*)serial2CsvBuffer, serial2CsvIndex); - serial2CsvIndex = 0; - } - - if (serial2LogFile) { - serial2LogFile.close(); - } - - serial2LoggingEnabled = false; + if (serial2LogFile) serial2LogFile.close(); + serial2LoggingEnabled = false; serial2CsvIndex = 0; xSemaphoreGive(sdMutex); } } @@ -2344,921 +1773,514 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) const char* data = doc["data"]; if (data && strlen(data) > 0) { Serial2Comm.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 (serial2Queue && xQueueSend(serial2Queue, &serialMsg, pdMS_TO_TICKS(10)) == pdTRUE) { // ⭐ NULL 체크 - totalSerial2TxCount++; - } + 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) { - uint32_t baud = doc["baudRate"] | 115200; - uint8_t data = doc["dataBits"] | 8; - uint8_t parity = doc["parity"] | 0; - uint8_t stop = doc["stopBits"] | 1; - - serial2Settings.baudRate = baud; - serial2Settings.dataBits = data; - serial2Settings.parity = parity; - serial2Settings.stopBits = stop; - - saveSerialSettings(); - applySerialSettings(); + 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 response(512); - response["type"] = "serial2Config"; - response["baudRate"] = serial2Settings.baudRate; - response["dataBits"] = serial2Settings.dataBits; - response["parity"] = serial2Settings.parity; - response["stopBits"] = serial2Settings.stopBits; - - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); + 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 speedIndex = doc["speed"]; - if (speedIndex >= 0 && speedIndex < 4) { - currentCanSpeed = canSpeedValues[speedIndex]; - // ⭐⭐⭐ MCP2515 리셋 제거! (CAN 수신 중단 방지) - // 속도 변경은 로깅 중지 후 수동으로만 가능하도록 변경 - // mcp2515.reset(); - // mcp2515.setBitrate(currentCanSpeed, MCP_CRYSTAL); - // setMCP2515Mode(currentMcpMode); + int si = doc["speed"]; + if (si >= 0 && si < 4) { + currentCanSpeed = canSpeedValues[si]; saveSettings(); - - // 사용자에게 안내 메시지 - StaticJsonDocument<256> response; - response["type"] = "info"; - response["message"] = "CAN speed saved. Stop logging and restart to apply."; - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); + 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(); - } + if (mode >= 0 && mode <= 3) { setMCP2515Mode((MCP2515Mode)mode); saveSettings(); } } else if (strcmp(cmd, "syncTimeFromPhone") == 0) { - int year = doc["year"] | 2024; - int month = doc["month"] | 1; - int day = doc["day"] | 1; - int hour = doc["hour"] | 0; - int minute = doc["minute"] | 0; - int second = doc["second"] | 0; - - struct tm timeinfo; - 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 tv = {t, 0}; - settimeofday(&tv, NULL); - + 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(&timeinfo); - } + if (timeSyncStatus.rtcAvailable) writeRTC(&ti); } 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("files"); - + DynamicJsonDocument r(6144); + r["type"] = "files"; + JsonArray files = r.createNestedArray("files"); File root = SD_MMC.open("/"); if (root) { File file = root.openNextFile(); - int fileCount = 0; - - while (file && fileCount < 50) { + int cnt = 0; + while (file && cnt < 50) { if (!file.isDirectory()) { - const char* filename = file.name(); - - // ⭐ 파일명이 '/'로 시작하면 건너뛰기 - if (filename[0] == '/') { - filename++; // 슬래시 제거 - } - - // 숨김 파일과 시스템 폴더 제외 - if (filename[0] != '.' && - strcmp(filename, "System Volume Information") != 0 && - strlen(filename) > 0) { - - JsonObject fileObj = files.createNestedObject(); - fileObj["name"] = filename; - fileObj["size"] = file.size(); - - const char* comment = getFileComment(filename); - if (strlen(comment) > 0) { - fileObj["comment"] = comment; - } - fileCount++; + 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(); - - // ⭐ 디버그 로그 - Serial.printf("getFiles: Found %d files\n", fileCount); - } else { - Serial.println("getFiles: Failed to open root directory"); } - xSemaphoreGive(sdMutex); - - String json; - size_t jsonSize = serializeJson(response, json); - Serial.printf("getFiles: JSON size = %d bytes\n", jsonSize); - webSocket.sendTXT(num, json); - } else { - Serial.println("getFiles: Failed to acquire sdMutex"); - // Mutex 실패 시에도 응답 전송 - DynamicJsonDocument response(256); - response["type"] = "files"; - response["error"] = "SD busy"; - JsonArray files = response.createNestedArray("files"); - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); + String j; serializeJson(r,j); webSocket.sendTXT(num,j); } - } else { - Serial.println("getFiles: SD card not ready"); - // SD 카드 없을 때 빈 목록 전송 - DynamicJsonDocument response(256); - response["type"] = "files"; - JsonArray files = response.createNestedArray("files"); - 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); - + const char* fn = doc["filename"]; + if (fn && strlen(fn) > 0) { + String fp = "/" + String(fn); if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - bool success = false; - - if (SD_MMC.exists(fullPath)) { - if (SD_MMC.remove(fullPath)) { - success = true; - } - } - + bool ok = SD_MMC.exists(fp) && SD_MMC.remove(fp); xSemaphoreGive(sdMutex); - - DynamicJsonDocument response(256); - response["type"] = "deleteResult"; - response["success"] = success; - - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); + 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* filename = doc["filename"]; - const char* comment = doc["comment"]; - - if (filename && comment) { - addFileComment(filename, comment); - } + const char* fn = doc["filename"], *cm = doc["comment"]; + if (fn && cm) addFileComment(fn, cm); } - // 🎯 Auto Trigger 명령 추가 else if (strcmp(cmd, "setAutoTrigger") == 0) { autoTriggerEnabled = doc["enabled"] | false; - - // 🆕 로그 형식 설정 - const char* logFormat = doc["logFormat"]; - if (logFormat) { - autoTriggerLogCSV = (strcmp(logFormat, "csv") == 0); + const char* lf = doc["logFormat"]; + if (lf) { + autoTriggerLogCSV = (strcmp(lf,"csv") == 0); + autoTriggerLogPCAP = (strcmp(lf,"pcap") == 0); } - saveAutoTriggerSettings(); - - DynamicJsonDocument response(256); - response["type"] = "autoTriggerSet"; - response["enabled"] = autoTriggerEnabled; - response["logFormat"] = autoTriggerLogCSV ? "csv" : "bin"; - - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); + 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); } - // 🆕 Auto Trigger 형식 설정 명령 else if (strcmp(cmd, "setAutoTriggerFormat") == 0) { - const char* logFormat = doc["logFormat"]; - if (logFormat) { - autoTriggerLogCSV = (strcmp(logFormat, "csv") == 0); + const char* lf = doc["logFormat"]; + if (lf) { + autoTriggerLogCSV = (strcmp(lf,"csv") == 0); + autoTriggerLogPCAP = (strcmp(lf,"pcap") == 0); saveAutoTriggerSettings(); - Serial.printf("🎯 Auto Trigger 형식 설정: %s\n", autoTriggerLogCSV ? "CSV" : "BIN"); } - - DynamicJsonDocument response(128); - response["type"] = "autoTriggerFormatSet"; - response["logFormat"] = autoTriggerLogCSV ? "csv" : "bin"; - - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); + DynamicJsonDocument r(128); r["type"]="autoTriggerFormatSet"; + r["logFormat"] = autoTriggerLogPCAP ? "pcap" : (autoTriggerLogCSV ? "csv" : "bin"); + String j; serializeJson(r,j); webSocket.sendTXT(num,j); } - // 🆕 CAN File Format 저장 명령 (메인 페이지용) else if (strcmp(cmd, "saveCanFormat") == 0) { - const char* format = doc["format"]; - if (format) { - savedCanLogFormatCSV = (strcmp(format, "csv") == 0); + const char* fmt = doc["format"]; + if (fmt) { + savedCanLogFormatCSV = (strcmp(fmt, "csv") == 0); + savedCanLogFormatPCAP = (strcmp(fmt, "pcap") == 0); saveSettings(); - Serial.printf("💾 CAN File Format 저장: %s\n", savedCanLogFormatCSV ? "CSV" : "BIN"); + Serial.printf("💾 CAN Format 저장: %s\n", + savedCanLogFormatPCAP ? "PCAP" : (savedCanLogFormatCSV ? "CSV" : "BIN")); } - - DynamicJsonDocument response(128); - response["type"] = "canFormatSaved"; - response["format"] = savedCanLogFormatCSV ? "csv" : "bin"; - - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); + 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; + 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; + startTriggers[startTriggerCount].value = t["value"] | 0; + startTriggers[startTriggerCount].enabled = t["enabled"] | true; startTriggerCount++; } - saveAutoTriggerSettings(); - - DynamicJsonDocument response(256); - response["type"] = "startTriggersSet"; - response["count"] = startTriggerCount; - - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); + 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; + 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; + stopTriggers[stopTriggerCount].value = t["value"] | 0; + stopTriggers[stopTriggerCount].enabled = t["enabled"] | true; stopTriggerCount++; } - saveAutoTriggerSettings(); - - DynamicJsonDocument response(256); - response["type"] = "stopTriggersSet"; - response["count"] = stopTriggerCount; - - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); + 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* formula = doc["formula"]; - if (formula) { - startFormula = String(formula); - saveAutoTriggerSettings(); - Serial.println("📐 Start Formula 저장: " + startFormula); - } + const char* f = doc["formula"]; + if (f) { startFormula = String(f); saveAutoTriggerSettings(); } } else if (strcmp(cmd, "setStopFormula") == 0) { - const char* formula = doc["formula"]; - if (formula) { - stopFormula = String(formula); - saveAutoTriggerSettings(); - Serial.println("📐 Stop Formula 저장: " + stopFormula); - } + const char* f = doc["formula"]; + if (f) { stopFormula = String(f); saveAutoTriggerSettings(); } } else if (strcmp(cmd, "getAutoTriggers") == 0) { - DynamicJsonDocument response(2048); - response["type"] = "autoTriggers"; - response["enabled"] = autoTriggerEnabled; - response["logFormat"] = autoTriggerLogCSV ? "csv" : "bin"; // 🆕 로그 형식 전송 - response["startLogic"] = startLogicOp; - response["stopLogic"] = stopLogicOp; - response["startFormula"] = startFormula; // Formula 전송 - response["stopFormula"] = stopFormula; // Formula 전송 - - JsonArray startArray = response.createNestedArray("startTriggers"); + 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 = startArray.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; + 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 stopArray = response.createNestedArray("stopTriggers"); + JsonArray ea = r.createNestedArray("stopTriggers"); for (int i = 0; i < stopTriggerCount; i++) { - JsonObject t = stopArray.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; + 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 json; - serializeJson(response, json); - webSocket.sendTXT(num, json); + String j; serializeJson(r,j); webSocket.sendTXT(num,j); } else if (strcmp(cmd, "sendOnce") == 0) { - if (currentMcpMode == MCP_MODE_TRANSMIT) { - mcp2515.setNormalMode(); - } - + 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_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(); - } + 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) { - // ⭐⭐⭐ Sequence 저장 기능 if (sequenceCount >= MAX_SEQUENCES) { - Serial.println("✗ Sequence 저장 실패: 최대 개수 초과"); - DynamicJsonDocument response(256); - response["type"] = "error"; - response["message"] = "Maximum sequences reached"; - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); + 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"]; - int repeatMode = doc["repeatMode"] | 0; - int repeatCount = doc["repeatCount"] | 1; - JsonArray stepsArray = doc["steps"]; - - if (name && stepsArray.size() > 0) { + 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'; - - // Repeat 설정 - seq->repeatMode = repeatMode; - seq->repeatCount = repeatCount; - - // Steps 복사 + 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 stepObj : stepsArray) { - if (seq->stepCount >= 20) break; // 최대 20개 - + for (JsonObject so : stepsArr) { + if (seq->stepCount >= 20) break; SequenceStep* step = &seq->steps[seq->stepCount]; - - // ID 파싱 (0x 제거) - const char* idStr = stepObj["id"]; - if (idStr) { - if (strncmp(idStr, "0x", 2) == 0 || strncmp(idStr, "0X", 2) == 0) { - step->canId = strtoul(idStr + 2, NULL, 16); - } else { - step->canId = strtoul(idStr, NULL, 16); - } - } - - step->extended = stepObj["ext"] | false; - step->dlc = stepObj["dlc"] | 8; - - // Data 배열 복사 - JsonArray dataArray = stepObj["data"]; - for (int i = 0; i < 8 && i < dataArray.size(); i++) { - step->data[i] = dataArray[i]; - } - - step->delayMs = stepObj["delay"] | 0; - + 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++; - - // SD 카드에 저장 - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - File seqFile = SD_MMC.open("/sequences.dat", FILE_WRITE); - if (seqFile) { - seqFile.write((uint8_t*)&sequenceCount, sizeof(sequenceCount)); - seqFile.write((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount); - seqFile.close(); - Serial.printf("✓ Sequence 저장 완료: %s (Steps: %d)\n", name, seq->stepCount); - } else { - Serial.println("✗ sequences.dat 열기 실패"); - } - xSemaphoreGive(sdMutex); - } - - // 성공 응답 - DynamicJsonDocument response(256); - response["type"] = "sequenceSaved"; - response["name"] = name; - response["steps"] = seq->stepCount; - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); - - // Sequence 목록 다시 전송 + 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 { - Serial.println("✗ Sequence 저장 실패: 잘못된 데이터"); } } } else if (strcmp(cmd, "getSequences") == 0) { - DynamicJsonDocument response(3072); - response["type"] = "sequences"; - JsonArray seqArray = response.createNestedArray("list"); - + DynamicJsonDocument r(3072); r["type"]="sequences"; + JsonArray sa = r.createNestedArray("list"); for (int i = 0; i < sequenceCount; i++) { - JsonObject seqObj = seqArray.createNestedObject(); - seqObj["name"] = sequences[i].name; - seqObj["steps"] = sequences[i].stepCount; - seqObj["repeatMode"] = sequences[i].repeatMode; - seqObj["repeatCount"] = sequences[i].repeatCount; + 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 json; - serializeJson(response, json); - webSocket.sendTXT(num, json); + String j; serializeJson(r,j); webSocket.sendTXT(num,j); } else if (strcmp(cmd, "startSequence") == 0) { - // ⭐⭐⭐ Sequence 실행 - int index = doc["index"] | -1; - - if (index >= 0 && index < sequenceCount) { - seqRuntime.running = true; - seqRuntime.activeSequenceIndex = index; - seqRuntime.currentStep = 0; - seqRuntime.currentRepeat = 0; - seqRuntime.lastStepTime = millis(); - - Serial.printf("✓ Sequence 시작: %s (index: %d)\n", sequences[index].name, index); - - // 성공 응답 - DynamicJsonDocument response(256); - response["type"] = "sequenceStarted"; - response["index"] = index; - response["name"] = sequences[index].name; - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); - } else { - Serial.printf("✗ Sequence 시작 실패: 잘못된 index %d\n", index); + 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) { - // ⭐⭐⭐ Sequence 중지 if (seqRuntime.running) { - Serial.println("✓ Sequence 중지됨"); - seqRuntime.running = false; - seqRuntime.activeSequenceIndex = -1; - - // 성공 응답 - DynamicJsonDocument response(256); - response["type"] = "sequenceStopped"; - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); + 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) { - // ⭐⭐⭐ Sequence 삭제 - int index = doc["index"] | -1; - - if (index >= 0 && index < sequenceCount) { - Serial.printf("✓ Sequence 삭제: %s (index: %d)\n", sequences[index].name, index); - - // 배열에서 제거 (뒤의 항목들을 앞으로 이동) - for (int i = index; i < sequenceCount - 1; i++) { - memcpy(&sequences[i], &sequences[i + 1], sizeof(CANSequence)); - } - + 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--; - - // SD 카드에 저장 - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - File seqFile = SD_MMC.open("/sequences.dat", FILE_WRITE); - if (seqFile) { - seqFile.write((uint8_t*)&sequenceCount, sizeof(sequenceCount)); - seqFile.write((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount); - seqFile.close(); - Serial.printf("✓ SD 카드 업데이트: %d개 sequence\n", sequenceCount); - } - xSemaphoreGive(sdMutex); - } - - // 성공 응답 - DynamicJsonDocument response(256); - response["type"] = "sequenceDeleted"; - response["index"] = index; - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); - - // Sequence 목록 업데이트 + 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 { - Serial.printf("✗ Sequence 삭제 실패: 잘못된 index %d\n", index); } } else if (strcmp(cmd, "getSequenceDetail") == 0) { - // ⭐⭐⭐ Sequence 상세 정보 (Edit용) - int index = doc["index"] | -1; - - if (index >= 0 && index < sequenceCount) { - DynamicJsonDocument response(4096); - response["type"] = "sequenceDetail"; - response["index"] = index; - response["name"] = sequences[index].name; - response["repeatMode"] = sequences[index].repeatMode; - response["repeatCount"] = sequences[index].repeatCount; - - JsonArray stepsArray = response.createNestedArray("steps"); - for (int i = 0; i < sequences[index].stepCount; i++) { - SequenceStep* step = &sequences[index].steps[i]; - JsonObject stepObj = stepsArray.createNestedObject(); - - char idStr[12]; - sprintf(idStr, "0x%X", step->canId); - stepObj["id"] = idStr; - stepObj["ext"] = step->extended; - stepObj["dlc"] = step->dlc; - - JsonArray dataArray = stepObj.createNestedArray("data"); - for (int j = 0; j < 8; j++) { - dataArray.add(step->data[j]); - } - - stepObj["delay"] = step->delayMs; + 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 json; - serializeJson(response, json); - webSocket.sendTXT(num, json); - - Serial.printf("✓ Sequence 상세 전송: %s (index: %d)\n", sequences[index].name, index); - } else { - Serial.printf("✗ Sequence 상세 조회 실패: 잘못된 index %d\n", index); + String j; serializeJson(r,j); webSocket.sendTXT(num,j); } - }else if (strcmp(cmd, "hwReset") == 0) { - // ⭐⭐⭐ ESP32 하드웨어 리셋 (재부팅) - Serial.println("📨 하드웨어 리셋 요청 수신"); - Serial.println("🔄 ESP32 재부팅 중..."); - Serial.println(""); - Serial.println("========================================"); - Serial.println(" ESP32 REBOOTING..."); - Serial.println("========================================"); - - // 응답 전송 - DynamicJsonDocument response(256); - response["type"] = "hwReset"; - response["success"] = true; - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); - - // 약간의 지연 후 재부팅 (응답 전송 시간 확보) + } + 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); - - // ESP32 재부팅 ESP.restart(); } } } // ======================================== -// Web Update Task +// ★★★ Web Update Task (RingBuffer Serial Consumer) ★★★ // ======================================== void webUpdateTask(void *parameter) { - const TickType_t xDelay = pdMS_TO_TICKS(500); // ⭐ 100ms → 500ms (CPU 부하 감소) - - // 🆕 초기화 대기 단축 (부팅 직후 안정화) - vTaskDelay(pdMS_TO_TICKS(500)); // ⭐ 2000ms → 500ms (빠른 연결) - + vTaskDelay(pdMS_TO_TICKS(500)); + while (1) { webSocket.loop(); - + if (webSocket.connectedClients() > 0) { - // 🆕 큐가 초기화되었는지 확인 - if (!canQueue || !serialQueue) { - vTaskDelay(xDelay); - continue; - } - - DynamicJsonDocument doc(16384); // ⭐ 4096 → 8192로 증가 - doc["type"] = "update"; + DynamicJsonDocument doc(16384); + doc["type"] = "update"; doc["logging"] = loggingEnabled; - - // 🎯 Auto Trigger 상태 + + // Auto Trigger 상태 doc["autoTriggerEnabled"] = autoTriggerEnabled; - doc["autoTriggerActive"] = autoTriggerActive; - doc["startTriggerCount"] = startTriggerCount; - doc["stopTriggerCount"] = stopTriggerCount; - doc["serialLogging"] = serialLoggingEnabled; - doc["serial2Logging"] = serial2LoggingEnabled; - doc["totalSerial2Rx"] = totalSerial2RxCount; - doc["totalSerial2Tx"] = totalSerial2TxCount; - doc["serial2QueueUsed"] = serial2Queue ? uxQueueMessagesWaiting(serial2Queue) : 0; // ⭐ NULL 체크 - doc["serial2QueueSize"] = SERIAL2_QUEUE_SIZE; - doc["serial2FileSize"] = currentSerial2FileSize; - - if (serial2LoggingEnabled && currentSerial2Filename[0] != '\0') { - doc["currentSerial2File"] = String(currentSerial2Filename); - } else { - doc["currentSerial2File"] = ""; - } - - doc["sdReady"] = sdCardReady; - doc["totalMsg"] = totalMsgCount; - doc["msgPerSec"] = msgPerSecond; - - // 🆕 CAN 버스 부하율 계산 - // CAN 속도별 이론적 최대 메시지/초 (8바이트 데이터 기준) - // 125 Kbps: ~1,000 msg/s - // 250 Kbps: ~2,000 msg/s - // 500 Kbps: ~4,000 msg/s - // 1 Mbps: ~8,000 msg/s - uint32_t maxMsgPerSec; + 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: maxMsgPerSec = 1000; break; - case CAN_250KBPS: maxMsgPerSec = 2000; break; - case CAN_500KBPS: maxMsgPerSec = 4000; break; - case CAN_1000KBPS: - default: maxMsgPerSec = 8000; break; + case CAN_125KBPS: maxMPS=1000; break; + case CAN_250KBPS: maxMPS=2000; break; + case CAN_500KBPS: maxMPS=4000; break; + default: maxMPS=8000; break; } - - // 부하율 계산 (0~100%) - float busLoad = (msgPerSecond * 100.0) / maxMsgPerSec; - if (busLoad > 100.0) busLoad = 100.0; - doc["busLoad"] = (int)busLoad; // 정수로 전송 - - doc["totalTx"] = totalTxCount; - doc["totalSerialRx"] = totalSerialRxCount; - doc["totalSerialTx"] = totalSerialTxCount; - doc["fileSize"] = currentFileSize; - doc["serialFileSize"] = currentSerialFileSize; - doc["queueUsed"] = canQueue ? uxQueueMessagesWaiting(canQueue) : 0; // 🆕 NULL 체크 - doc["queueSize"] = CAN_QUEUE_SIZE; - doc["serialQueueUsed"] = serialQueue ? uxQueueMessagesWaiting(serialQueue) : 0; // 🆕 NULL 체크 - doc["serialQueueSize"] = SERIAL_QUEUE_SIZE; - 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; - - // 🆕 저장된 File Format 전송 - doc["savedCanFormat"] = savedCanLogFormatCSV ? "csv" : "bin"; - - if (loggingEnabled && currentFilename[0] != '\0') { - doc["currentFile"] = String(currentFilename); - } else { - doc["currentFile"] = ""; - } - - if (serialLoggingEnabled && currentSerialFilename[0] != '\0') { - doc["currentSerialFile"] = String(currentSerialFilename); - } else { - doc["currentSerialFile"] = ""; - } - - time_t now; - time(&now); + 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개만 전송) + + // CAN 최근 메시지 (최대 20개) JsonArray messages = doc.createNestedArray("messages"); - int msgCount = 0; - for (int i = 0; i < RECENT_MSG_COUNT && msgCount < 20; i++) { // ⭐ 최대 20개 + int msgCnt = 0; + for (int i = 0; i < RECENT_MSG_COUNT && msgCnt < 20; 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]); - } - msgCount++; + 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 메시지 배열 + + // ★ Serial RingBuffer pop (최대 10개) 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++; - - // Serial 로깅 + 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 relativeTime = serialMsg.timestamp_us - serialLogStartTime; - + uint64_t rt = 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; + 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; - } + 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; - } + static int bfc=0; if (++bfc>=50){serialLogFile.flush();bfc=0;} } } - xSemaphoreGive(sdMutex); } } } - - // ⭐ Serial2 메시지 배열 처리 + + // ★ Serial2 RingBuffer pop SerialMessage serial2Msg; JsonArray serial2Messages = doc.createNestedArray("serial2Messages"); - int serial2Count = 0; - - while (serial2Queue && serial2Count < 10 && xQueueReceive(serial2Queue, &serial2Msg, 0) == pdTRUE) { // ⭐ NULL 체크 - JsonObject serMsgObj = serial2Messages.createNestedObject(); - serMsgObj["timestamp"] = serial2Msg.timestamp_us; - serMsgObj["isTx"] = serial2Msg.isTx; - - char dataStr[MAX_SERIAL_LINE_LEN + 1]; - memcpy(dataStr, serial2Msg.data, serial2Msg.length); - dataStr[serial2Msg.length] = '\0'; - serMsgObj["data"] = dataStr; - - serial2Count++; - - // Serial2 로깅 + 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 relativeTime = serial2Msg.timestamp_us - serial2LogStartTime; - + uint64_t rt = serial2Msg.timestamp_us - serial2LogStartTime; char csvLine[256]; - int lineLen = snprintf(csvLine, sizeof(csvLine), - "%llu,%s,\"%s\"\n", - relativeTime, - serial2Msg.isTx ? "TX" : "RX", - dataStr); - - if (serial2CsvIndex + lineLen <= SERIAL2_CSV_BUFFER_SIZE) { // < → <= - memcpy(&serial2CsvBuffer[serial2CsvIndex], csvLine, lineLen); - serial2CsvIndex += lineLen; - currentSerial2FileSize += lineLen; + 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; - } + if (serial2LogFile) { serial2LogFile.write((uint8_t*)serial2CsvBuffer, serial2CsvIndex); serial2LogFile.flush(); serial2CsvIndex=0; } } } else { - // BIN 형식 if (serial2LogFile) { serial2LogFile.write((uint8_t*)&serial2Msg, sizeof(SerialMessage)); currentSerial2FileSize += sizeof(SerialMessage); - - static int binFlushCounter2 = 0; - if (++binFlushCounter2 >= 50) { - serial2LogFile.flush(); - binFlushCounter2 = 0; - } + static int bfc2=0; if (++bfc2>=50){serial2LogFile.flush();bfc2=0;} } } - xSemaphoreGive(sdMutex); } } } - + String json; size_t jsonSize = serializeJson(doc, json); - - // JSON 크기 확인 (8KB 이하만 전송) - if (jsonSize > 0 && jsonSize < 8192) { - webSocket.broadcastTXT(json); - } else { - Serial.printf("! JSON 크기 초과: %d bytes\n", jsonSize); - } + if (jsonSize > 0 && jsonSize < 8192) webSocket.broadcastTXT(json); } - - vTaskDelay(xDelay); + + vTaskDelay(pdMS_TO_TICKS(500)); } } @@ -3268,604 +2290,225 @@ void webUpdateTask(void *parameter) { void setup() { Serial.begin(115200); delay(1000); - - // ⭐ 리셋 원인 확인 (전원 부족 감지) - esp_reset_reason_t reset_reason = esp_reset_reason(); - if (reset_reason == ESP_RST_BROWNOUT) { - Serial.println(""); - Serial.println("╔════════════════════════════════════════════╗"); - Serial.println("║ 🚨 브라운아웃 리셋 감지! ║"); - Serial.println("║ ║"); - Serial.println("║ 원인: 전원 공급 부족 ║"); - Serial.println("║ ║"); - Serial.println("║ 필요 전류: ║"); - Serial.println("║ - 평균: 300 mA ║"); - Serial.println("║ - 피크: 550 mA (WiFi TX + SD Write) ║"); - Serial.println("║ ║"); - Serial.println("║ 해결 방법: ║"); - Serial.println("║ 1. 5V 1A USB 어댑터 사용 (권장) ║"); - Serial.println("║ 2. USB 케이블 교체 (짧고 굵은 케이블) ║"); - Serial.println("║ 3. USB 3.0 포트 사용 (900mA 지원) ║"); - Serial.println("╚════════════════════════════════════════════╝"); - Serial.println(""); - delay(5000); // 경고 메시지 읽을 시간 + + esp_reset_reason_t rr = esp_reset_reason(); + if (rr == ESP_RST_BROWNOUT) { + Serial.println("\n🚨 브라운아웃 리셋 감지! 전원 공급 부족."); + delay(5000); } - - // ⭐ WiFi 전력 최적화 (전류 소비 감소) + WiFi.setSleep(false); - WiFi.setTxPower(WIFI_POWER_15dBm); // 19.5dBm → 15dBm (전류 절감) - + WiFi.setTxPower(WIFI_POWER_15dBm); + Serial.println("\n========================================"); - Serial.println(" Byun CAN Logger + Serial Terminal"); - Serial.println(" Version 2.3 - PSRAM Optimized"); - Serial.println(" ESP32-S3 Complete Edition"); + Serial.println(" Byun CAN Logger v3.0"); + Serial.println(" SPSC Lock-Free RingBuffer Edition"); Serial.println("========================================\n"); - - // ★★★ PSRAM 초기화 (가장 먼저!) ★★★ + + // PSRAM 초기화 (RingBuffer 포함) if (!initPSRAM()) { - Serial.println("\n✗ PSRAM 초기화 실패!"); - Serial.println("✗ Arduino IDE 설정:"); - Serial.println(" Tools → PSRAM → OPI PSRAM"); - while (1) { - delay(1000); - Serial.println("✗ 설정 후 재업로드 필요!"); - } + Serial.println("✗ PSRAM 초기화 실패! Tools→PSRAM→OPI PSRAM 확인"); + while (1) { delay(1000); Serial.println("✗ 재업로드 필요!"); } } - + loadSettings(); - - // 🎯 Auto Trigger 설정 로드 loadAutoTriggerSettings(); analogSetPinAttenuation(MONITORING_VOLT, ADC_11db); - pinMode(CAN_INT_PIN, INPUT_PULLUP); analogSetAttenuation(ADC_11db); - - // SPI 초기화 - Serial.println("SPI 초기화..."); + hspi.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS); hspi.beginTransaction(SPISettings(40000000, MSBFIRST, SPI_MODE0)); hspi.endTransaction(); - // ⭐ VSPI 제거: SDIO는 별도 SPI 초기화 불필요 - Serial.println("✓ SPI 초기화 완료 (HSPI only)"); - - // Watchdog 비활성화 + esp_task_wdt_deinit(); - - // ======================================== - // MCP2515 초기화 (개선된 시퀀스) - // ======================================== + + // MCP2515 초기화 Serial.println("MCP2515 초기화..."); - - // 1. CS 핀 초기 상태 설정 pinMode(HSPI_CS, OUTPUT); - digitalWrite(HSPI_CS, HIGH); - delay(10); - - // 2. 하드웨어 리셋 (CS 토글) - Serial.println(" 1. 하드웨어 리셋..."); - digitalWrite(HSPI_CS, LOW); - delayMicroseconds(100); - digitalWrite(HSPI_CS, HIGH); - delay(100); // 리셋 후 충분한 대기 - - // 3. 소프트웨어 리셋 (Configuration 모드로 진입) - Serial.println(" 2. 소프트웨어 리셋..."); - mcp2515.reset(); - delay(100); // Configuration 모드 안정화 대기 - - // 4. Configuration 모드에서 설정 (중요: 이 순서대로!) - Serial.println(" 3. Bitrate 설정..."); - mcp2515.setBitrate(currentCanSpeed, MCP_CRYSTAL); - delay(10); - - // 5. 필터/마스크 설정 (모든 메시지 수신) - Serial.println(" 4. 필터 설정 (Standard + Extended ID 모두 수신)..."); - // ⭐ RXB0: Standard ID 수신 (false) - mcp2515.setFilterMask(MCP2515::MASK0, false, 0x00000000); - mcp2515.setFilter(MCP2515::RXF0, false, 0x00000000); - mcp2515.setFilter(MCP2515::RXF1, false, 0x00000000); - - // ⭐ RXB1: Extended ID 수신 (true) - 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); - - // 6. 수신 버퍼 비우기 (Configuration 모드에서) - Serial.println(" 5. 수신 버퍼 클리어..."); - struct can_frame dummyFrame; - int clearCount = 0; - while (clearCount < 10) { - if (mcp2515.readMessage(&dummyFrame) != MCP2515::ERROR_OK) break; - clearCount++; - } - if (clearCount > 0) { - Serial.printf(" %d개 메시지 버림\n", clearCount); - } - - // 7. 에러/오버플로우 플래그 클리어 - Serial.println(" 6. 에러 플래그 클리어..."); + 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(); - mcp2515.clearInterrupts(); - mcp2515.clearTXInterrupts(); - mcp2515.clearMERR(); - mcp2515.clearERRIF(); - delay(10); - - // 8. 모드 전환 (마지막에!) - Serial.printf(" 7. 모드 설정: %d\n", (int)currentMcpMode); - if (currentMcpMode == MCP_MODE_NORMAL) { - mcp2515.setNormalMode(); - Serial.println(" → Normal Mode"); - } else if (currentMcpMode == MCP_MODE_LOOPBACK) { - mcp2515.setLoopbackMode(); - Serial.println(" → Loopback Mode"); - } else if (currentMcpMode == MCP_MODE_LISTEN_ONLY) { - mcp2515.setListenOnlyMode(); - Serial.println(" → Listen-Only Mode"); - } else { - // TRANSMIT 모드는 Listen-Only로 시작 (TX 시에만 Normal로 전환) - mcp2515.setListenOnlyMode(); - Serial.println(" → Listen-Only Mode (TX mode)"); - } - delay(50); // 모드 전환 안정화 - - // 9. 최종 버퍼 클리어 (모드 전환 후) - Serial.println(" 8. 최종 버퍼 클리어..."); - clearCount = 0; - while (mcp2515.readMessage(&dummyFrame) == MCP2515::ERROR_OK) { - clearCount++; - if (clearCount > 100) break; - } - if (clearCount > 0) { - Serial.printf(" %d개 메시지 버림\n", clearCount); - } - mcp2515.clearRXnOVRFlags(); - - // 10. 에러 상태 확인 - uint8_t errorFlag = mcp2515.getErrorFlags(); - uint8_t txErr = mcp2515.errorCountTX(); - uint8_t rxErr = mcp2515.errorCountRX(); - Serial.printf(" 9. 에러 상태: EFLG=0x%02X, TEC=%d, REC=%d\n", errorFlag, txErr, rxErr); - - if (errorFlag != 0 || txErr > 0 || rxErr > 0) { - Serial.println(" ⚠️ 에러 감지됨 - 추가 리셋 시도..."); - // 에러가 있으면 완전 리셋 재시도 - mcp2515.reset(); - delay(50); - mcp2515.setBitrate(currentCanSpeed, MCP_CRYSTAL); - delay(10); - mcp2515.clearRXnOVRFlags(); - mcp2515.clearInterrupts(); - if (currentMcpMode == MCP_MODE_NORMAL) { - mcp2515.setNormalMode(); - } else if (currentMcpMode == MCP_MODE_LOOPBACK) { - mcp2515.setLoopbackMode(); - } else { - mcp2515.setListenOnlyMode(); - } - delay(50); - } - Serial.println("✓ MCP2515 초기화 완료"); - - // Serial 통신 초기화 + applySerialSettings(); - Serial.println("✓ Serial1 통신 초기화 (GPIO 17/18)"); - Serial.println("✓ Serial2 통신 초기화 (GPIO 6/7)"); // ⭐ Serial2 - - // Mutex 생성 - sdMutex = xSemaphoreCreateMutex(); - rtcMutex = xSemaphoreCreateMutex(); - serialMutex = xSemaphoreCreateMutex(); - serial2Mutex = xSemaphoreCreateMutex(); // ⭐ Serial2 - + + sdMutex = xSemaphoreCreateMutex(); + rtcMutex = xSemaphoreCreateMutex(); + serialMutex = xSemaphoreCreateMutex(); + serial2Mutex = xSemaphoreCreateMutex(); if (!sdMutex || !rtcMutex || !serialMutex) { - Serial.println("✗ Mutex 생성 실패!"); - while (1) delay(1000); + Serial.println("✗ Mutex 생성 실패!"); while(1) delay(1000); } - - // RTC 초기화 + initRTC(); - - // ⭐⭐⭐ SD 카드 초기화 (SDIO 4-bit Mode) + Serial.println("SD 카드 초기화 (SDIO 4-bit)..."); - - // setPins() 호출: clk, cmd, d0, d1, d2, d3 if (!SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0, SDIO_D1, SDIO_D2, SDIO_D3)) { - Serial.println("✗ SD_MMC.setPins() 실패!"); sdCardReady = false; - } else if (SD_MMC.begin("/sdcard", false)) { // false = 4-bit mode + } else if (SD_MMC.begin("/sdcard", false)) { sdCardReady = true; - Serial.println("✓ SD 카드 초기화 완료 (SDIO 4-bit)"); - Serial.printf(" 카드 크기: %llu MB\n", SD_MMC.cardSize() / (1024 * 1024)); - Serial.printf(" 핀: CLK=%d, CMD=%d, D0=%d, D1=%d, D2=%d, D3=%d\n", - SDIO_CLK, SDIO_CMD, SDIO_D0, SDIO_D1, SDIO_D2, SDIO_D3); + Serial.printf("✓ SD 카드 초기화 완료: %llu MB\n", SD_MMC.cardSize()/(1024*1024)); loadFileComments(); loadSequences(); } else { sdCardReady = false; Serial.println("✗ SD 카드 초기화 실패"); - Serial.println(" 확인 사항:"); - Serial.println(" - 배선: CLK=39, CMD=38, D0=40, D1=41, D2=42, D3=21"); - Serial.println(" - 10kΩ 풀업 저항 확인 (CMD, D0-D3)"); - Serial.println(" - SD 카드 포맷 (FAT32)"); - Serial.println(" - SDIO 지원 SD 카드 모듈 사용"); } - - // WiFi 설정 - WiFi.setSleep(false); // ⭐ WiFi 절전 모드 비활성화 (연결 안정성 향상) - + + // WiFi + WiFi.setSleep(false); if (enableSTAMode && strlen(staSSID) > 0) { - Serial.println("\n📶 WiFi APSTA 모드..."); WiFi.mode(WIFI_AP_STA); WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4); - Serial.printf("✓ AP: %s\n", wifiSSID); - Serial.printf("✓ AP IP: %s\n", WiFi.softAPIP().toString().c_str()); - WiFi.begin(staSSID, staPassword); - int attempts = 0; - while (WiFi.status() != WL_CONNECTED && attempts < 20) { - delay(500); - Serial.print("."); - attempts++; - } + 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(); - } + if (WiFi.status() == WL_CONNECTED) { Serial.printf("✓ STA IP: %s\n", WiFi.localIP().toString().c_str()); initNTP(); } } else { - Serial.println("\n📶 WiFi AP 모드..."); WiFi.mode(WIFI_AP); WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4); - Serial.printf("✓ AP: %s\n", wifiSSID); - Serial.printf("✓ AP IP: %s\n", WiFi.softAPIP().toString().c_str()); + Serial.printf("✓ AP: %s | IP: %s\n", wifiSSID, WiFi.softAPIP().toString().c_str()); } - - WiFi.setSleep(false); esp_wifi_set_max_tx_power(84); - - // WebSocket 시작 + 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("/", 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; - } - + if (!server.hasArg("file")) { server.send(400,"text/plain","Bad request"); return; } String filename = "/" + server.arg("file"); - - Serial.println("\n========================================"); - Serial.println("📥 다운로드 준비"); - Serial.println("========================================"); - - // ⭐ 1단계: 모든 로깅 완전 중지 bool wasLogging = loggingEnabled; - if (wasLogging) { - loggingEnabled = false; - delay(200); - Serial.println("⏸ 모든 로깅 중지"); - } - - // ⭐ 2단계: 모든 SD 관련 Task 중단 - if (sdWriteTaskHandle != NULL) { - vTaskSuspend(sdWriteTaskHandle); - delay(100); - Serial.println("⏸ SD 쓰기 Task 중단"); - } - if (webTaskHandle != NULL) { - vTaskSuspend(webTaskHandle); - delay(50); - Serial.println("⏸ 웹 업데이트 Task 중단"); - } - - // ⭐ 3단계: SD Mutex 획득 + 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 != NULL) vTaskResume(webTaskHandle); - if (sdWriteTaskHandle != NULL) vTaskResume(sdWriteTaskHandle); + if (webTaskHandle) vTaskResume(webTaskHandle); + if (sdWriteTaskHandle) vTaskResume(sdWriteTaskHandle); if (wasLogging) loggingEnabled = true; - server.send(503, "text/plain", "SD card busy"); - Serial.println("✗ SD Mutex 획득 실패"); - return; + server.send(503,"text/plain","SD card busy"); return; } - - // ⭐ 4단계: SD 카드 재마운트 (1-bit 모드로) - Serial.println("🔄 SD 카드 재마운트 중..."); - SD_MMC.end(); - delay(200); - - // 1-bit 모드로 재시작 (더 안정적) - if (!SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0)) { - Serial.println("✗ SD_MMC.setPins() 실패"); + SD_MMC.end(); delay(200); + if (!SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0) || !SD_MMC.begin("/sdcard", true)) { xSemaphoreGive(sdMutex); - if (webTaskHandle != NULL) vTaskResume(webTaskHandle); - if (sdWriteTaskHandle != NULL) vTaskResume(sdWriteTaskHandle); + if (webTaskHandle) vTaskResume(webTaskHandle); + if (sdWriteTaskHandle) vTaskResume(sdWriteTaskHandle); if (wasLogging) loggingEnabled = true; - server.send(500, "text/plain", "SD remount failed"); - return; + server.send(500,"text/plain","SD remount failed"); return; } - - if (!SD_MMC.begin("/sdcard", true)) { // true = 1-bit mode - Serial.println("✗ SD 카드 1-bit 재마운트 실패"); - xSemaphoreGive(sdMutex); - if (webTaskHandle != NULL) vTaskResume(webTaskHandle); - if (sdWriteTaskHandle != NULL) vTaskResume(sdWriteTaskHandle); - if (wasLogging) loggingEnabled = true; - server.send(500, "text/plain", "SD 1-bit mode failed"); - return; - } - - Serial.println("✓ SD 카드 1-bit 모드 활성화"); - delay(100); - - // 파일 존재 확인 if (!SD_MMC.exists(filename)) { - Serial.println("✗ 파일 없음"); - - // 4-bit 모드로 복구 - SD_MMC.end(); - delay(100); - SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0, SDIO_D1, SDIO_D2, SDIO_D3); + 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 != NULL) vTaskResume(webTaskHandle); - if (sdWriteTaskHandle != NULL) vTaskResume(sdWriteTaskHandle); + if (webTaskHandle) vTaskResume(webTaskHandle); + if (sdWriteTaskHandle) vTaskResume(sdWriteTaskHandle); if (wasLogging) loggingEnabled = true; - server.send(404, "text/plain", "File not found"); - return; + server.send(404,"text/plain","File not found"); return; } - File file = SD_MMC.open(filename, FILE_READ); if (!file) { - Serial.println("✗ 파일 열기 실패"); - - // 4-bit 모드로 복구 - SD_MMC.end(); - delay(100); - SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0, SDIO_D1, SDIO_D2, SDIO_D3); + 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 != NULL) vTaskResume(webTaskHandle); - if (sdWriteTaskHandle != NULL) vTaskResume(sdWriteTaskHandle); + if (webTaskHandle) vTaskResume(webTaskHandle); + if (sdWriteTaskHandle) vTaskResume(sdWriteTaskHandle); if (wasLogging) loggingEnabled = true; - server.send(500, "text/plain", "Failed to open file"); - return; + server.send(500,"text/plain","Failed to open file"); return; } - size_t fileSize = file.size(); - String displayName = server.arg("file"); - - Serial.printf("📥 다운로드 시작: %s (%u bytes)\n", displayName.c_str(), fileSize); - - // 헤더 전송 + String dispName = server.arg("file"); server.setContentLength(fileSize); - server.sendHeader("Content-Disposition", "attachment; filename=\"" + displayName + "\""); - server.sendHeader("Content-Type", "application/octet-stream"); - server.sendHeader("Connection", "close"); - server.send(200, "application/octet-stream", ""); - - // 512바이트 섹터 단위 전송 - const size_t CHUNK_SIZE = 512; - uint8_t *buffer = (uint8_t*)heap_caps_aligned_alloc(32, CHUNK_SIZE, MALLOC_CAP_DMA); - - if (!buffer) { - buffer = (uint8_t*)malloc(CHUNK_SIZE); + 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 1000) { - float percent = (totalSent * 100.0) / fileSize; - Serial.printf("📤 %.1f%% (%u/%u)\n", percent, totalSent, fileSize); - lastPrint = millis(); - } - - yield(); - } - - free(buffer); file.close(); - - Serial.println("========================================"); - if (downloadSuccess && totalSent == fileSize) { - Serial.printf("✓ 완료: %u bytes (100.0%%)\n", totalSent); - } else { - Serial.printf("⚠ 불완전: %u/%u bytes (%.1f%%)\n", - totalSent, fileSize, (totalSent * 100.0) / fileSize); - } - Serial.println("========================================"); - - // ⭐ SD 카드를 4-bit 모드로 복구 - Serial.println("🔄 SD 카드 4-bit 모드로 복구..."); - SD_MMC.end(); - delay(200); - - if (SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0, SDIO_D1, SDIO_D2, SDIO_D3)) { - if (SD_MMC.begin("/sdcard", false)) { // false = 4-bit mode - Serial.println("✓ SD 카드 4-bit 모드 복구 완료"); - } else { - Serial.println("⚠ 4-bit 모드 복구 실패, 1-bit 유지"); - SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0); - SD_MMC.begin("/sdcard", true); - } - } - + SD_MMC.end(); delay(200); + SD_MMC.setPins(SDIO_CLK,SDIO_CMD,SDIO_D0,SDIO_D1,SDIO_D2,SDIO_D3); + SD_MMC.begin("/sdcard", false); xSemaphoreGive(sdMutex); - - // Task 재개 - if (webTaskHandle != NULL) { - vTaskResume(webTaskHandle); - Serial.println("▶ 웹 업데이트 Task 재개"); - } - if (sdWriteTaskHandle != NULL) { - vTaskResume(sdWriteTaskHandle); - Serial.println("▶ SD 쓰기 Task 재개"); - } - - // 로깅 재개 - if (wasLogging) { - loggingEnabled = true; - Serial.println("▶ 로깅 재개"); - } - - Serial.println("\n"); + if (webTaskHandle) vTaskResume(webTaskHandle); + if (sdWriteTaskHandle) vTaskResume(sdWriteTaskHandle); + if (wasLogging) loggingEnabled = true; }); - + server.begin(); - Serial.println("✓ 웹 서버 시작 완료"); - - // ★★★ Queue 생성 (PSRAM 사용) ★★★ - if (!createQueues()) { - Serial.println("✗ Queue 생성 실패!"); - while (1) delay(1000); - } - - // CAN 인터럽트 활성화 + Serial.println("✓ 웹 서버 시작"); + + // CAN 인터럽트 attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING); - + // ======================================== - // Task 생성 (최적화 버전) + // Task 생성 // ======================================== Serial.println("\nTask 생성 중..."); - - // Core 1: 실시간 처리 + 웹 서비스 - // - CAN RX: 최고 우선순위로 메시지 수신 - // - WEB: Core 0의 SD 쓰기와 분리하여 응답성 확보 - // - TX: CAN 전송 - // - SEQ: 시퀀스 재생 - // - MONITOR: 상태 모니터링 - xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 12288, NULL, 24, &canRxTaskHandle, 1); // ⭐ 8KB → 12KB, Pri 24 (최고) - xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 12288, NULL, 8, &webTaskHandle, 0); // ⭐ Core 0 → 1, Pri 4 (SD와 분리) - xTaskCreatePinnedToCore(txTask, "TX", 4096, NULL, 3, NULL, 1); // Pri 3 - xTaskCreatePinnedToCore(sequenceTask, "SEQ", 4096, NULL, 2, NULL, 1); // Pri 2 - xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 1); // Pri 1 - - // Core 0: I/O 전용 (SD, Serial, RTC) - // - SD 쓰기와 Serial 수신만 처리 - // - 웹은 Core 1에서 처리하므로 SD 지연 영향 없음 - xTaskCreatePinnedToCore(sdFlushTask, "SD_FLUSH", 4096, NULL, 9, &sdFlushTaskHandle, 0); // ⭐ Pri 9 (최우선, flush 전용) - xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 18576, NULL, 8, &sdWriteTaskHandle, 0); // Pri 8 - xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", 6144, NULL, 6, &serialRxTaskHandle, 0); // Pri 6 - xTaskCreatePinnedToCore(serial2RxTask, "SERIAL2_RX", 6144, NULL, 6, &serial2RxTaskHandle,0); // Pri 6 - - if (timeSyncStatus.rtcAvailable) { - xTaskCreatePinnedToCore(rtcSyncTask, "RTC_SYNC", 3072, NULL, 0, &rtcTaskHandle, 0); // Pri 0 (최저) - } - - Serial.println("✓ 모든 Task 시작 완료"); + xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 12288, NULL, 24, &canRxTaskHandle, 1); + xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 12288, NULL, 8, &webTaskHandle, 0); + xTaskCreatePinnedToCore(txTask, "TX", 4096, NULL, 3, NULL, 1); + xTaskCreatePinnedToCore(sequenceTask, "SEQ", 4096, NULL, 2, NULL, 1); + xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 1); + xTaskCreatePinnedToCore(sdFlushTask, "SD_FLUSH", 4096, NULL, 9, &sdFlushTaskHandle, 0); + xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 18576, NULL, 8, &sdWriteTaskHandle, 0); + xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", 6144, NULL, 6, &serialRxTaskHandle, 0); + xTaskCreatePinnedToCore(serial2RxTask, "SERIAL2_RX", 6144, NULL, 6, &serial2RxTaskHandle, 0); + if (timeSyncStatus.rtcAvailable) + xTaskCreatePinnedToCore(rtcSyncTask,"RTC_SYNC", 3072, NULL, 0, &rtcTaskHandle, 0); + Serial.println("\n========================================"); - Serial.println(" Task 구성 (Core 분리 전략)"); + Serial.println(" Task 구성 (RingBuffer v3.0)"); Serial.println("========================================"); - Serial.println("Core 1 (실시간 + 웹 서비스):"); - Serial.println(" - CAN_RX: 12KB, Pri 24 (최고)"); - Serial.println(" - WEB_UPDATE: 12KB, Pri 4 ⭐ SD와 분리"); - Serial.println(" - TX: 4KB, Pri 3"); - Serial.println(" - SEQ: 4KB, Pri 2"); - Serial.println(" - SD_MONITOR: 4KB, Pri 1"); - Serial.println("Core 0 (I/O 전용):"); - Serial.println(" - SD_WRITE: 18KB, Pri 8 (최우선)"); - Serial.println(" - SERIAL_RX: 6KB, Pri 6"); - Serial.println(" - SERIAL2_RX: 6KB, Pri 6"); - if (timeSyncStatus.rtcAvailable) { - Serial.println(" - RTC_SYNC: 3KB, Pri 0"); - } + Serial.println("Core 1:"); + Serial.printf(" CAN_RX Pri 24 → canRing (SPSC push, no mutex)\n"); + Serial.printf(" WEB_UPDATE Pri 8 ← serialRing (SPSC pop, no mutex)\n"); + Serial.printf(" TX/SEQ/MON Pri 3/2/1\n"); + Serial.println("Core 0:"); + Serial.printf(" SD_FLUSH Pri 9 (Double Buffer 비동기 flush)\n"); + Serial.printf(" SD_WRITE Pri 8 ← canRing (SPSC pop, BIN no mutex)\n"); + Serial.printf(" SERIAL_RX Pri 6 → serialRing (SPSC push)\n"); + Serial.printf(" SERIAL2_RX Pri 6 → serial2Ring(SPSC push)\n"); Serial.println("========================================"); - Serial.println("📌 웹을 Core 1로 배치 → SD 쓰기 지연 영향 없음"); - Serial.println("========================================"); - Serial.println("\n========================================"); - Serial.println(" 접속 방법"); - Serial.println("========================================"); - Serial.printf(" WiFi SSID: %s\n", wifiSSID); - Serial.printf(" URL: http://%s\n", WiFi.softAPIP().toString().c_str()); - Serial.println("========================================"); - Serial.println(" PSRAM 상태"); - Serial.println("========================================"); - Serial.printf(" 여유 PSRAM: %d KB\n", ESP.getFreePsram() / 1024); + Serial.printf("RingBuffer: CAN %d슬롯, SER %d슬롯 (PSRAM)\n", + CAN_RING_SIZE, SERIAL_RING_SIZE); Serial.println("========================================\n"); } @@ -3875,55 +2518,31 @@ void setup() { void loop() { server.handleClient(); vTaskDelay(pdMS_TO_TICKS(10)); - - // 주기적 상태 출력 (30초) + + // 30초마다 상태 출력 (RingBuffer 통계 포함) static uint32_t lastPrint = 0; if (millis() - lastPrint > 30000) { - Serial.printf("[상태] CAN: %d/%d | S1: %d/%d | S2: %d/%d | PSRAM: %d KB\n", - uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE, - uxQueueMessagesWaiting(serialQueue), SERIAL_QUEUE_SIZE, - uxQueueMessagesWaiting(serial2Queue), SERIAL2_QUEUE_SIZE, + 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 lastStackCheck = 0; - if (millis() - lastStackCheck > 300000) { + + // 5분마다 스택 사용량 + static uint32_t lastStack = 0; + if (millis() - lastStack > 300000) { Serial.println("\n========== Task Stack Usage =========="); - - if (canRxTaskHandle) { - UBaseType_t stackLeft = uxTaskGetStackHighWaterMark(canRxTaskHandle); - Serial.printf("CAN_RX: %5u bytes free (alloc: 12288)\n", stackLeft * 4); - if (stackLeft * 4 < 2000) Serial.println(" ⚠️ 스택 부족 위험!"); - } - - if (sdWriteTaskHandle) { - UBaseType_t stackLeft = uxTaskGetStackHighWaterMark(sdWriteTaskHandle); - Serial.printf("SD_WRITE: %5u bytes free (alloc: 18576)\n", stackLeft * 4); - if (stackLeft * 4 < 3000) Serial.println(" ⚠️ 스택 부족 위험!"); - } - - if (webTaskHandle) { - UBaseType_t stackLeft = uxTaskGetStackHighWaterMark(webTaskHandle); - Serial.printf("WEB_UPDATE: %5u bytes free (alloc: 12288)\n", stackLeft * 4); - if (stackLeft * 4 < 2000) Serial.println(" ⚠️ 스택 부족 위험!"); - } - - if (serialRxTaskHandle) { - UBaseType_t stackLeft = uxTaskGetStackHighWaterMark(serialRxTaskHandle); - Serial.printf("SERIAL_RX: %5u bytes free (alloc: 6144)\n", stackLeft * 4); - } - - if (serial2RxTaskHandle) { - UBaseType_t stackLeft = uxTaskGetStackHighWaterMark(serial2RxTaskHandle); - Serial.printf("SERIAL2_RX: %5u bytes free (alloc: 6144)\n", stackLeft * 4); - } - - Serial.printf("Free Heap: %u bytes\n", ESP.getFreeHeap()); - Serial.printf("Free PSRAM: %u KB\n", ESP.getFreePsram() / 1024); + 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"); - - lastStackCheck = millis(); + lastStack = millis(); } } \ No newline at end of file diff --git a/index.h b/index.h index 4b1393d..d3da573 100644 --- a/index.h +++ b/index.h @@ -654,11 +654,13 @@ const char index_html[] PROGMEM = R"rawliteral( .format-selector { display: flex; align-items: center; - gap: 15px; + flex-wrap: wrap; + gap: 8px 15px; background: white; padding: 8px 15px; border-radius: 8px; border: 2px solid #667eea; + width: 100%; } .format-selector label { @@ -670,6 +672,7 @@ const char index_html[] PROGMEM = R"rawliteral( color: #2c3e50; font-size: 0.9em; transition: all 0.3s; + white-space: nowrap; } .format-selector label:hover { @@ -688,6 +691,26 @@ const char index_html[] PROGMEM = R"rawliteral( color: #7f8c8d; margin-left: 3px; } + + @media (max-width: 480px) { + .format-selector { + flex-direction: column; + align-items: flex-start; + gap: 10px; + padding: 10px 12px; + } + .format-selector label { + font-size: 1em; + gap: 8px; + } + .format-selector input[type="radio"] { + width: 20px; + height: 20px; + } + .format-info { + font-size: 0.8em; + } + } /* ========== 반응형 디자인 ========== */ @media (max-width: 768px) { @@ -1048,6 +1071,11 @@ const char index_html[] PROGMEM = R"rawliteral( 📄 CSV (Text - Excel Ready) + @@ -1081,6 +1109,7 @@ const char index_html[] PROGMEM = R"rawliteral( @@ -1398,7 +1427,7 @@ const char index_html[] PROGMEM = R"rawliteral( // 🆕 로그 형식 설정 if (data.logFormat) { - document.getElementById('autoTriggerFormat').value = data.logFormat; + document.getElementById('autoTriggerFormat').value = data.logFormat; // bin/csv/pcap 모두 처리 } // 🎯 수식 불러오기 @@ -1437,7 +1466,8 @@ const char index_html[] PROGMEM = R"rawliteral( console.log('✅ CAN Format saved:', data.format); const statusSpan = document.getElementById('format-save-status'); if (statusSpan) { - statusSpan.textContent = '✓ Saved: ' + data.format.toUpperCase(); + const labelMap = { bin: '📦 BIN', csv: '📄 CSV', pcap: '🦈 PCAP' }; + statusSpan.textContent = '✓ Saved: ' + (labelMap[data.format] || data.format.toUpperCase()); setTimeout(() => { statusSpan.textContent = ''; }, 3000); } }