From ef088b63a17e4bbfaad8d66570df4cab51089d21 Mon Sep 17 00:00:00 2001 From: byun Date: Fri, 5 Jun 2026 08:52:15 +0000 Subject: [PATCH] Delete a.ino --- a.ino | 3153 --------------------------------------------------------- 1 file changed, 3153 deletions(-) delete mode 100644 a.ino diff --git a/a.ino b/a.ino deleted file mode 100644 index cb6449e..0000000 --- a/a.ino +++ /dev/null @@ -1,3153 +0,0 @@ -/* - * Byun CAN Logger with Web Interface + Serial Terminal - * Version: 2.3 - PSRAM Optimized (Complete Version) - * - * PSRAM 최적화 완전판: - * - 원본 기능 100% 유지 - * - 대용량 버퍼/Queue를 PSRAM에 할당 - * - 웹서버, WebSocket, 모든 Task 포함 - * - * Arduino IDE 설정: - * - Board: ESP32S3 Dev Module - * - PSRAM: OPI PSRAM ⭐ 필수! - * - Flash Size: 16MB (128Mb) - * - Partition: 16MB Flash (3MB APP/9.9MB FATFS) - */ - -#include -#include -#include -#include -#include // ⭐ SDIO 4-bit -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "index.h" -#include "transmit.h" -#include "graph.h" -#include "graph_viewer.h" -#include "settings.h" -#include "serial_terminal.h" -#include "serial2_terminal.h" // ⭐ Serial2 페이지 추가 - -// 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 - -// HSPI 핀 (CAN) -#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 -#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 호환 - -// I2C2 핀 (RTC DS3231) -#define RTC_SDA 8 -#define RTC_SCL 9 -#define DS3231_ADDRESS 0x68 - -// ======================================== -// PSRAM 최적화 설정 -// ======================================== -#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 SERIAL2_QUEUE_SIZE 1200 // ⭐ Serial2 추가 -#define SERIAL2_CSV_BUFFER_SIZE 32768 // ⭐ Serial2 추가 // 8KB → 32KB (PSRAM 사용) - -#define MAX_FILENAME_LEN 64 -#define RECENT_MSG_COUNT 100 -#define MAX_TX_MESSAGES 20 -#define MAX_COMMENT_LEN 128 -#define MAX_SERIAL_LINE_LEN 64 -#define RTC_SYNC_INTERVAL_MS 60000 -#define VOLTAGE_CHECK_INTERVAL_MS 5000 -#define LOW_VOLTAGE_THRESHOLD 3.0 -#define MONITORING_VOLT 5 -#define MAX_GRAPH_SIGNALS 20 -#define MAX_SEQUENCES 10 -#define MAX_FILE_COMMENTS 50 - -// ======================================== -// 구조체 정의 -// ======================================== -struct CANMessage { - uint64_t timestamp_us; - uint32_t id; - uint8_t dlc; - uint8_t data[8]; -} __attribute__((packed)); - -struct SerialMessage { - uint64_t timestamp_us; - uint16_t length; - uint8_t data[MAX_SERIAL_LINE_LEN]; - bool isTx; -} __attribute__((packed)); - -struct SerialSettings { - uint32_t baudRate; - uint8_t dataBits; - uint8_t parity; - uint8_t stopBits; -}; - -struct RecentCANData { - CANMessage msg; - uint32_t count; -}; - -struct TxMessage { - uint32_t id; - bool extended; - uint8_t dlc; - uint8_t data[8]; - uint32_t interval; - uint32_t lastSent; - bool active; -}; - -struct SequenceStep { - uint32_t canId; - bool extended; - uint8_t dlc; - uint8_t data[8]; - uint32_t delayMs; -}; - -struct CANSequence { - char name[32]; - SequenceStep steps[20]; - uint8_t stepCount; - uint8_t repeatMode; - uint32_t repeatCount; -}; - -struct SequenceRuntime { - bool running; - uint8_t currentStep; - uint32_t currentRepeat; - uint32_t lastStepTime; - int8_t activeSequenceIndex; -}; - -struct FileComment { - char filename[MAX_FILENAME_LEN]; - char comment[MAX_COMMENT_LEN]; -}; - -struct TimeSyncStatus { - bool synchronized; - uint64_t lastSyncTime; - int32_t offsetUs; - uint32_t syncCount; - bool rtcAvailable; - uint32_t rtcSyncCount; -} timeSyncStatus = {false, 0, 0, 0, false, 0}; - -struct PowerStatus { - float voltage; - float minVoltage; - bool lowVoltage; - uint32_t lastCheck; - uint32_t lastMinReset; -} powerStatus = {0.0, 999.9, false, 0, 0}; - -enum MCP2515Mode { - MCP_MODE_NORMAL = 0, - MCP_MODE_LISTEN_ONLY = 1, - MCP_MODE_LOOPBACK = 2, - MCP_MODE_TRANSMIT = 3 -}; - -// ======================================== -// PSRAM 할당 변수 (포인터로 선언) -// ======================================== -uint8_t *fileBuffer = nullptr; -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 - -// WiFi 설정 (내부 SRAM) -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 추가 - -// 전역 객체 (내부 SRAM) -SPIClass hspi(HSPI); -// SPIClass vspi(FSPI); // ⭐ SDIO 사용으로 제거 -MCP2515 mcp2515(HSPI_CS, 20000000, &hspi); -HardwareSerial SerialComm(1); // UART1 -HardwareSerial Serial2Comm(2); // ⭐ UART2 추가 -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; -TaskHandle_t serialRxTaskHandle = NULL; -TaskHandle_t serial2RxTaskHandle = NULL; // ⭐ Serial2 추가 - -// 로깅 변수 -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; // 조건 활성화 -}; - -#define MAX_TRIGGERS 8 - -TriggerCondition startTriggers[MAX_TRIGGERS]; -TriggerCondition stopTriggers[MAX_TRIGGERS]; -int startTriggerCount = 0; -int stopTriggerCount = 0; - -String startFormula = ""; // Start 조건 수식 -String stopFormula = ""; // Stop 조건 수식 - -bool autoTriggerEnabled = false; -char startLogicOp[4] = "OR"; -char stopLogicOp[4] = "OR"; -bool autoTriggerActive = false; -bool autoTriggerLogCSV = false; // 🆕 Auto Trigger용 CSV 형식 설정 -volatile bool serialLoggingEnabled = false; -volatile bool serial2LoggingEnabled = false; // ⭐ Serial2 추가 -volatile bool sdCardReady = false; -File logFile; -File serialLogFile; -File serial2LogFile; // ⭐ Serial2 추가 -char currentFilename[MAX_FILENAME_LEN]; -char currentSerialFilename[MAX_FILENAME_LEN]; -char currentSerial2Filename[MAX_FILENAME_LEN]; // ⭐ Serial2 추가 -uint32_t bufferIndex = 0; // ⭐ uint16_t → uint32_t (오버플로우 방지) -uint32_t serialCsvIndex = 0; // ⭐ uint16_t → uint32_t -uint32_t serial2CsvIndex = 0; // ⭐ uint16_t → uint32_t // ⭐ Serial2 추가 -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 추가 - -// 기타 전역 변수 -MCP2515Mode currentMcpMode = MCP_MODE_LISTEN_ONLY; -SoftWire rtcWire(RTC_SDA, RTC_SCL); -char rtcSyncBuffer[20]; -CAN_SPEED currentCanSpeed = CAN_1000KBPS; -const char* canSpeedNames[] = {"125K", "250K", "500K", "1M"}; -CAN_SPEED canSpeedValues[] = {CAN_125KBPS, CAN_250KBPS, CAN_500KBPS, CAN_1000KBPS}; - -uint32_t totalMsgCount = 0; -uint32_t msgPerSecond = 0; -uint32_t lastMsgCountTime = 0; -uint32_t lastMsgCount = 0; -volatile uint32_t totalSerialRxCount = 0; -volatile uint32_t totalSerialTxCount = 0; -volatile uint32_t totalSerial2RxCount = 0; // ⭐ Serial2 추가 -volatile uint32_t totalSerial2TxCount = 0; // ⭐ Serial2 추가 -uint32_t totalTxCount = 0; -uint8_t sequenceCount = 0; -SequenceRuntime seqRuntime = {false, 0, 0, 0, -1}; -int commentCount = 0; - -// Forward declarations -void IRAM_ATTR canISR(); -void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length); -void resetMCP2515(); - - - -void resetMCP2515() { - Serial.println("🔄 MCP2515 리셋 시작..."); - - // 1. 로깅 중지 (진행 중이면) - if (loggingEnabled) { - // 버퍼 플러시 및 파일 닫기 - } - - // 2. CAN 큐 비우기 - CANMessage tempMsg; - while (xQueueReceive(canQueue, &tempMsg, 0) == pdTRUE) { - // 큐에서 모든 메시지 제거 - } - - // 3. MCP2515 하드 리셋 - digitalWrite(HSPI_CS, LOW); - delay(10); - digitalWrite(HSPI_CS, HIGH); - delay(50); - - // 4. MCP2515 재초기화 - mcp2515.reset(); - delay(100); - currentCanSpeed = preferences.getInt("can_speed", CAN_500KBPS); - - mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ); - delay(10); - currentMcpMode = preferences.getInt("can_mode", MCP_MODE_LISTEN_ONLY); - //mcp2515.setListenOnlyMode(); - //currentMcpMode = MCP_MODE_LISTEN_ONLY; - - - - // 5. 모드 설정 (Normal/Loopback/Listen Only) - if (currentMcpMode == MCP_MODE_NORMAL) { - mcp2515.setNormalMode(); - } else if (currentMcpMode == MCP_MODE_LOOPBACK) { - mcp2515.setLoopbackMode(); - } else { - mcp2515.setListenOnlyMode(); - } - - // 6. 버퍼 클리어 - struct can_frame dummyFrame; - while (mcp2515.readMessage(&dummyFrame) == MCP2515::ERROR_OK) { - // MCP2515 수신 버퍼 비우기 - } - mcp2515.clearRXnOVRFlags(); - - // 7. 통계 리셋 - totalMsgCount = 0; - lastMsgCount = 0; - msgPerSecond = 0; - - // 8. 최근 메시지 테이블 클리어 - for (int i = 0; i < RECENT_MSG_COUNT; i++) { - recentData[i].count = 0; - } - - Serial.println("✅ MCP2515 리셋 완료!"); -} -// ======================================== -// PSRAM 초기화 함수 -// ======================================== -bool initPSRAM() { - Serial.println("\n========================================"); - Serial.println(" PSRAM 메모리 할당"); - 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); - - // 버퍼 할당 - fileBuffer = (uint8_t*)ps_malloc(FILE_BUFFER_SIZE); - if (!fileBuffer) { - Serial.println("✗ fileBuffer 할당 실패"); - return false; - } - Serial.printf("✓ fileBuffer: %d KB\n", FILE_BUFFER_SIZE / 1024); - - 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; -} - -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.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); - - 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 - serial2Settings.baudRate = preferences.getUInt("ser2_baud", 115200); - serial2Settings.dataBits = preferences.getUChar("ser2_data", 8); - serial2Settings.parity = preferences.getUChar("ser2_parity", 0); - serial2Settings.stopBits = preferences.getUChar("ser2_stop", 1); -} - -void saveSerialSettings() { - preferences.putUInt("ser_baud", serialSettings.baudRate); - preferences.putUChar("ser_data", serialSettings.dataBits); - preferences.putUChar("ser_parity", serialSettings.parity); - preferences.putUChar("ser_stop", serialSettings.stopBits); - - // ⭐ Serial2 - preferences.putUInt("ser2_baud", serial2Settings.baudRate); - preferences.putUChar("ser2_data", serial2Settings.dataBits); - preferences.putUChar("ser2_parity", serial2Settings.parity); - preferences.putUChar("ser2_stop", serial2Settings.stopBits); -} - -void applySerialSettings() { - uint32_t config = SERIAL_8N1; - - if (serialSettings.dataBits == 5) { - if (serialSettings.parity == 0) config = SERIAL_5N1; - else if (serialSettings.parity == 1) config = SERIAL_5E1; - else if (serialSettings.parity == 2) config = SERIAL_5O1; - } else if (serialSettings.dataBits == 6) { - if (serialSettings.parity == 0) config = SERIAL_6N1; - else if (serialSettings.parity == 1) config = SERIAL_6E1; - else if (serialSettings.parity == 2) config = SERIAL_6O1; - } else if (serialSettings.dataBits == 7) { - if (serialSettings.parity == 0) config = SERIAL_7N1; - else if (serialSettings.parity == 1) config = SERIAL_7E1; - else if (serialSettings.parity == 2) config = SERIAL_7O1; - } else { - if (serialSettings.parity == 0) config = SERIAL_8N1; - else if (serialSettings.parity == 1) config = SERIAL_8E1; - else if (serialSettings.parity == 2) config = SERIAL_8O1; - } - - if (serialSettings.stopBits == 2) { - config |= 0x3000; - } - - SerialComm.begin(serialSettings.baudRate, config, SERIAL_RX_PIN, SERIAL_TX_PIN); - SerialComm.setRxBufferSize(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 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 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 { - if (serial2Settings.parity == 0) config2 = SERIAL_8N1; - else if (serial2Settings.parity == 1) config2 = SERIAL_8E1; - else if (serial2Settings.parity == 2) config2 = SERIAL_8O1; - } - if (serial2Settings.stopBits == 2) config2 |= 0x3000; - - Serial2Comm.begin(serial2Settings.baudRate, config2, SERIAL2_RX_PIN, SERIAL2_TX_PIN); - Serial2Comm.setRxBufferSize(2048); -} - -void loadSettings() { - preferences.begin("can-logger", false); - preferences.getString("wifi_ssid", wifiSSID, sizeof(wifiSSID)); - preferences.getString("wifi_pass", wifiPassword, sizeof(wifiPassword)); - enableSTAMode = preferences.getBool("sta_enable", false); - preferences.getString("sta_ssid", staSSID, sizeof(staSSID)); - preferences.getString("sta_pass", staPassword, sizeof(staPassword)); - - if (strlen(wifiSSID) == 0) strcpy(wifiSSID, "Byun_CAN_Logger"); - if (strlen(wifiPassword) == 0) strcpy(wifiPassword, "12345678"); - - int speedIndex = preferences.getInt("can_speed", 3); - if (speedIndex >= 0 && speedIndex < 4) { - currentCanSpeed = canSpeedValues[speedIndex]; - } - - int savedMode = preferences.getInt("mcp_mode", 1); - if (savedMode >= 0 && savedMode <= 3) { - currentMcpMode = (MCP2515Mode)savedMode; - } - - 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; - bufferIndex = 0; - 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) { - // 🎯 파일 닫기 로직 추가 - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(2000)) == pdTRUE) { - // BIN 형식: 버퍼에 남은 데이터 강제 플러시 - if (bufferIndex > 0 && logFile) { - size_t written = logFile.write(fileBuffer, bufferIndex); - logFile.flush(); - Serial.printf("✓ Auto Trigger 최종 플러시: %d bytes\n", written); - bufferIndex = 0; - } - - // CSV 형식: 최종 플러시 - if (canLogFormatCSV && logFile) { - logFile.flush(); - } - - if (logFile) { - size_t finalSize = logFile.size(); - logFile.close(); - Serial.printf("✓ Auto Trigger 파일 닫힘: %s (%lu bytes)\n", currentFilename, finalSize); - } - - loggingEnabled = false; - autoTriggerActive = false; - currentFilename[0] = '\0'; - bufferIndex = 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); // 🆕 로그 형식 저장 - 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); - } - - // 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); // 🆕 로그 형식 로드 - 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); - } - - 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); - preferences.putString("wifi_pass", wifiPassword); - preferences.putBool("sta_enable", enableSTAMode); - preferences.putString("sta_ssid", staSSID); - preferences.putString("sta_pass", staPassword); - - for (int i = 0; i < 4; i++) { - if (canSpeedValues[i] == currentCanSpeed) { - preferences.putInt("can_speed", i); - break; - } - } - - preferences.putInt("mcp_mode", (int)currentMcpMode); - saveSerialSettings(); - preferences.end(); -} - -// ======================================== -// RTC 함수 -// ======================================== -void initRTC() { - rtcWire.begin(); - rtcWire.setClock(100000); - - rtcWire.beginTransmission(DS3231_ADDRESS); - if (rtcWire.endTransmission() == 0) { - timeSyncStatus.rtcAvailable = true; - Serial.println("✓ RTC(DS3231) 감지됨"); - } else { - timeSyncStatus.rtcAvailable = false; - Serial.println("! RTC(DS3231) 없음"); - } -} - -uint8_t bcdToDec(uint8_t val) { - return (val >> 4) * 10 + (val & 0x0F); -} - -uint8_t decToBcd(uint8_t val) { - return ((val / 10) << 4) | (val % 10); -} - -bool readRTC(struct tm *timeinfo) { - if (!timeSyncStatus.rtcAvailable) return false; - if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(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(); - 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; - - return true; -} - -bool writeRTC(const struct tm *timeinfo) { - 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); - xSemaphoreGive(rtcMutex); - return success; -} - -void timeSyncCallback(struct timeval *tv) { - Serial.println("✓ NTP 시간 동기화 완료"); - timeSyncStatus.synchronized = true; - timeSyncStatus.lastSyncTime = (uint64_t)tv->tv_sec * 1000000ULL + tv->tv_usec; - timeSyncStatus.syncCount++; - - if (timeSyncStatus.rtcAvailable) { - struct tm timeinfo; - time_t now = tv->tv_sec; - localtime_r(&now, &timeinfo); - if (writeRTC(&timeinfo)) { - timeSyncStatus.rtcSyncCount++; - } - } -} - -void initNTP() { - configTime(9 * 3600, 0, "pool.ntp.org", "time.nist.gov"); - sntp_set_time_sync_notification_cb(timeSyncCallback); -} - -void rtcSyncTask(void *parameter) { - const TickType_t xDelay = pdMS_TO_TICKS(RTC_SYNC_INTERVAL_MS); - while (1) { - if (timeSyncStatus.rtcAvailable) { - struct tm timeinfo; - if (readRTC(&timeinfo)) { - time_t now = mktime(&timeinfo); - struct timeval tv = { .tv_sec = now, .tv_usec = 0 }; - settimeofday(&tv, NULL); - timeSyncStatus.synchronized = true; - timeSyncStatus.lastSyncTime = (uint64_t)now * 1000000ULL; - timeSyncStatus.rtcSyncCount++; - } - } - vTaskDelay(xDelay); - } -} - -// ======================================== -// MCP2515 모드 -// ======================================== -bool setMCP2515Mode(MCP2515Mode mode) { - const char* modeName; - MCP2515::ERROR result; - - switch (mode) { - case MCP_MODE_NORMAL: - result = mcp2515.setNormalMode(); - modeName = "Normal"; - break; - case MCP_MODE_LISTEN_ONLY: - result = mcp2515.setListenOnlyMode(); - modeName = "Listen-Only"; - break; - case MCP_MODE_LOOPBACK: - result = mcp2515.setLoopbackMode(); - modeName = "Loopback"; - break; - case MCP_MODE_TRANSMIT: - result = mcp2515.setListenOnlyMode(); - modeName = "Transmit-Only"; - break; - default: - return false; - } - - if (result == MCP2515::ERROR_OK) { - currentMcpMode = mode; - Serial.printf("✓ MCP2515 모드: %s\n", modeName); - return true; - } - return false; -} - -// ======================================== -// 인터럽트 및 Task -// ======================================== -void IRAM_ATTR canISR() { - BaseType_t xHigherPriorityTaskWoken = pdFALSE; - 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)); - } -} - -void canRxTask(void *parameter) { - struct can_frame frame; - CANMessage msg; - - Serial.println("✓ CAN RX Task 시작 (Core 0, Priority 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); - msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; - msg.id = frame.can_id & 0x1FFFFFFF; - msg.dlc = frame.can_dlc; - memcpy(msg.data, frame.data, 8); - - if (xQueueSend(canQueue, &msg, 0) == pdTRUE) { - totalMsgCount++; - readCount++; - } - } - Serial.printf("✓ 초기 버퍼에서 %d개 읽음\n", readCount); - } - - while (1) { - ulTaskNotifyTake(pdTRUE, portMAX_DELAY); - - while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) { - struct timeval tv; - // 🎯 Auto Trigger 체크 - checkAutoTriggers(frame); - - gettimeofday(&tv, NULL); - msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; - msg.id = frame.can_id & 0x1FFFFFFF; - msg.dlc = frame.can_dlc; - memcpy(msg.data, frame.data, 8); - - if (xQueueSend(canQueue, &msg, pdMS_TO_TICKS(10)) == pdTRUE) { - totalMsgCount++; - } - } - } -} - -// ======================================== -// SD Write Task -// ======================================== -void sdWriteTask(void *parameter) { - CANMessage canMsg; - SerialMessage serialMsg; - - while (1) { - bool hasWork = false; - - // CAN 메시지 처리 - if (xQueueReceive(canQueue, &canMsg, 0) == pdTRUE) { - hasWork = true; - - // 실시간 모니터링 업데이트 - bool found = false; - for (int i = 0; i < RECENT_MSG_COUNT; i++) { - if (recentData[i].msg.id == canMsg.id) { - recentData[i].msg = canMsg; - recentData[i].count++; - found = true; - break; - } - } - - if (!found) { - for (int i = 0; i < RECENT_MSG_COUNT; i++) { - if (recentData[i].count == 0) { - recentData[i].msg = canMsg; - recentData[i].count = 1; - break; - } - } - } - - // 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; - - for (int i = 0; i < canMsg.dlc; i++) { - dataLen += sprintf(&dataStr[dataLen], "%02X", canMsg.data[i]); - if (i < canMsg.dlc - 1) dataStr[dataLen++] = ' '; - } - dataStr[dataLen] = '\0'; - - int lineLen = snprintf(csvLine, sizeof(csvLine), - "%llu,0x%X,%d,%s\n", - relativeTime, canMsg.id, canMsg.dlc, dataStr); - - if (logFile) { - size_t written = logFile.write((uint8_t*)csvLine, lineLen); - currentFileSize += lineLen; - - static int csvFlushCounter = 0; - if (++csvFlushCounter >= 50) { // ⭐ 20 → 50 (너무 자주 플러시하면 느림) - logFile.flush(); - csvFlushCounter = 0; - } - - // ⭐⭐⭐ 500개마다 파일 재오픈 (핵심!) - if (++csvReopenCounter >= 500) { - logFile.close(); - logFile = SD_MMC.open(currentFilename, FILE_APPEND); - if (logFile) { - Serial.printf("✓ CSV 파일 재오픈: %s (%lu bytes)\n", currentFilename, currentFileSize); - } else { - Serial.println("✗ CSV 파일 재오픈 실패!"); - loggingEnabled = false; - } - csvReopenCounter = 0; - } - - } - } else { - // BIN 형식 - static uint32_t binMsgCounter = 0; - static uint32_t binReopenCounter = 0; - - // ⭐⭐⭐ 1단계: 버퍼 가득 찼으면 먼저 플러시 - if (bufferIndex + sizeof(CANMessage) > FILE_BUFFER_SIZE) { - if (logFile) { - size_t written = logFile.write(fileBuffer, bufferIndex); - logFile.flush(); - Serial.printf("✓ BIN 버퍼 플러시 (FULL): %d bytes\n", written); - bufferIndex = 0; - } - } - - // ⭐⭐⭐ 2단계: 이제 공간 확보됨, 데이터 추가 - memcpy(&fileBuffer[bufferIndex], &canMsg, sizeof(CANMessage)); - bufferIndex += sizeof(CANMessage); - currentFileSize += sizeof(CANMessage); - binMsgCounter++; - binReopenCounter++; - - // ⭐⭐⭐ 3단계: 100개 메시지마다 주기적 플러시 - if (binMsgCounter % 100 == 0) { - if (logFile && bufferIndex > 0) { - size_t written = logFile.write(fileBuffer, bufferIndex); - logFile.flush(); - Serial.printf("✓ BIN 주기 플러시: %d bytes (메시지: %d)\n", written, binMsgCounter); - bufferIndex = 0; - } - } - - // ⭐⭐⭐ 4단계: 500개마다 파일 재오픈 (핵심!) - if (binReopenCounter >= 500) { - // 버퍼 먼저 플러시 - if (logFile && bufferIndex > 0) { - logFile.write(fileBuffer, bufferIndex); - logFile.flush(); - bufferIndex = 0; - } - - // 파일 닫고 다시 열기 - logFile.close(); - logFile = SD_MMC.open(currentFilename, FILE_APPEND); - if (logFile) { - Serial.printf("✓ BIN 파일 재오픈: %s (%lu bytes)\n", currentFilename, currentFileSize); - } else { - Serial.println("✗ BIN 파일 재오픈 실패!"); - loggingEnabled = false; - } - binReopenCounter = 0; - } - } - xSemaphoreGive(sdMutex); - } - } - } - - if (!hasWork) { - vTaskDelay(pdMS_TO_TICKS(1)); - } - } -} - -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; - } - } - - powerStatus.lowVoltage = (powerStatus.voltage < LOW_VOLTAGE_THRESHOLD); - powerStatus.lastCheck = currentTime; - } - - vTaskDelay(xDelay); - } -} - -// ======================================== -// 파일 커멘트 관리 -// ======================================== -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(); - } - xSemaphoreGive(sdMutex); - } -} - -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)); - if (commentCount > MAX_FILE_COMMENTS) commentCount = MAX_FILE_COMMENTS; - commentFile.read((uint8_t*)fileComments, sizeof(FileComment) * commentCount); - commentFile.close(); - } - } - xSemaphoreGive(sdMutex); - } -} - -const char* getFileComment(const char* filename) { - for (int i = 0; i < commentCount; i++) { - if (strcmp(fileComments[i].filename, filename) == 0) { - return fileComments[i].comment; - } - } - return ""; -} - -void addFileComment(const char* filename, const char* comment) { - for (int i = 0; i < commentCount; i++) { - if (strcmp(fileComments[i].filename, filename) == 0) { - strncpy(fileComments[i].comment, comment, MAX_COMMENT_LEN - 1); - fileComments[i].comment[MAX_COMMENT_LEN - 1] = '\0'; - saveFileComments(); - return; - } - } - - if (commentCount < MAX_FILE_COMMENTS) { - strncpy(fileComments[commentCount].filename, filename, MAX_FILENAME_LEN - 1); - fileComments[commentCount].filename[MAX_FILENAME_LEN - 1] = '\0'; - strncpy(fileComments[commentCount].comment, comment, MAX_COMMENT_LEN - 1); - fileComments[commentCount].comment[MAX_COMMENT_LEN - 1] = '\0'; - commentCount++; - saveFileComments(); - } -} - -// ======================================== -// 시퀀스 관리 -// ======================================== -void saveSequences() { - if (!sdCardReady) return; - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - File seqFile = SD_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(); - } - xSemaphoreGive(sdMutex); - } -} - -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)); - if (sequenceCount > MAX_SEQUENCES) sequenceCount = MAX_SEQUENCES; - seqFile.read((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount); - seqFile.close(); - } - } - xSemaphoreGive(sdMutex); - } -} - -// ======================================== -// TX Task -// ======================================== -void txTask(void *parameter) { - struct can_frame frame; - - while (1) { - uint32_t now = millis(); - bool anyActive = false; - - for (int i = 0; i < MAX_TX_MESSAGES; i++) { - if (txMessages[i].active && txMessages[i].interval > 0) { - anyActive = true; - - if (now - txMessages[i].lastSent >= txMessages[i].interval) { - if (currentMcpMode == MCP_MODE_TRANSMIT) { - mcp2515.setNormalMode(); - } - - frame.can_id = txMessages[i].id; - if (txMessages[i].extended) { - frame.can_id |= CAN_EFF_FLAG; - } - frame.can_dlc = txMessages[i].dlc; - memcpy(frame.data, txMessages[i].data, 8); - - if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) { - totalTxCount++; - txMessages[i].lastSent = now; - } - - if (currentMcpMode == MCP_MODE_TRANSMIT) { - mcp2515.setListenOnlyMode(); - } - } - } - } - - vTaskDelay(anyActive ? pdMS_TO_TICKS(1) : pdMS_TO_TICKS(10)); - } -} - -// ======================================== -// Sequence Task -// ======================================== -void sequenceTask(void *parameter) { - while (1) { - if (seqRuntime.running && seqRuntime.activeSequenceIndex >= 0 && - seqRuntime.activeSequenceIndex < sequenceCount) { - - CANSequence* seq = &sequences[seqRuntime.activeSequenceIndex]; - uint32_t now = millis(); - - if (seqRuntime.currentStep < seq->stepCount) { - SequenceStep* step = &seq->steps[seqRuntime.currentStep]; - - if (now - seqRuntime.lastStepTime >= step->delayMs) { - if (currentMcpMode == MCP_MODE_TRANSMIT) { - mcp2515.setNormalMode(); - } - - struct can_frame frame; - frame.can_id = step->canId; - if (step->extended) { - frame.can_id |= CAN_EFF_FLAG; - } - frame.can_dlc = step->dlc; - memcpy(frame.data, step->data, 8); - - if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) { - totalTxCount++; - } - - if (currentMcpMode == MCP_MODE_TRANSMIT) { - mcp2515.setListenOnlyMode(); - } - - seqRuntime.currentStep++; - seqRuntime.lastStepTime = now; - } - } else { - if (seq->repeatMode == 0) { - seqRuntime.running = false; - } else if (seq->repeatMode == 1) { - seqRuntime.currentRepeat++; - if (seqRuntime.currentRepeat >= seq->repeatCount) { - seqRuntime.running = false; - } else { - seqRuntime.currentStep = 0; - seqRuntime.lastStepTime = now; - } - } else if (seq->repeatMode == 2) { - seqRuntime.currentStep = 0; - seqRuntime.lastStepTime = now; - } - } - - vTaskDelay(pdMS_TO_TICKS(1)); - } else { - vTaskDelay(pdMS_TO_TICKS(10)); - } - } -} - -// ======================================== -// WebSocket 이벤트 처리 (중요!) -// ======================================== -void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) { - if (type == WStype_TEXT) { - DynamicJsonDocument doc(44384); - DeserializationError error = deserializeJson(doc, payload); - - if (error) return; - - const char* cmd = doc["cmd"]; - - if (strcmp(cmd, "getSettings") == 0) { - DynamicJsonDocument response(1024); - response["type"] = "settings"; - response["ssid"] = wifiSSID; - response["password"] = wifiPassword; - response["staEnable"] = enableSTAMode; - response["staSSID"] = staSSID; - response["staPassword"] = staPassword; - response["staConnected"] = (WiFi.status() == WL_CONNECTED); - response["staIP"] = WiFi.localIP().toString(); - - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); - } - else if (strcmp(cmd, "saveSettings") == 0) { - const char* newSSID = doc["ssid"]; - const char* newPassword = doc["password"]; - bool newSTAEnable = doc["staEnable"]; - const char* newSTASSID = doc["staSSID"]; - const char* newSTAPassword = doc["staPassword"]; - - if (newSSID && strlen(newSSID) > 0) { - strncpy(wifiSSID, newSSID, sizeof(wifiSSID) - 1); - wifiSSID[sizeof(wifiSSID) - 1] = '\0'; - } - - if (newPassword) { - strncpy(wifiPassword, newPassword, sizeof(wifiPassword) - 1); - wifiPassword[sizeof(wifiPassword) - 1] = '\0'; - } - - enableSTAMode = newSTAEnable; - - if (newSTASSID) { - strncpy(staSSID, newSTASSID, sizeof(staSSID) - 1); - staSSID[sizeof(staSSID) - 1] = '\0'; - } - - if (newSTAPassword) { - strncpy(staPassword, newSTAPassword, sizeof(staPassword) - 1); - staPassword[sizeof(staPassword) - 1] = '\0'; - } - - saveSettings(); - - DynamicJsonDocument response(256); - response["type"] = "settingsSaved"; - response["success"] = true; - - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); - } - else if (strcmp(cmd, "startLogging") == 0) { - if (!loggingEnabled && sdCardReady) { - const char* format = doc["format"]; - if (format && strcmp(format, "csv") == 0) { - canLogFormatCSV = true; - } else { - canLogFormatCSV = false; - } - - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - struct tm timeinfo; - time_t now; - time(&now); - localtime_r(&now, &timeinfo); - - struct timeval tv; - gettimeofday(&tv, NULL); - canLogStartTime = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; - - const char* ext = canLogFormatCSV ? "csv" : "bin"; - snprintf(currentFilename, sizeof(currentFilename), - "/CAN_%04d%02d%02d_%02d%02d%02d.%s", - timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, - timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, ext); - - // ⭐⭐⭐ 파일 생성 (헤더 쓰기) - logFile = SD_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; - bufferIndex = 0; - 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); - } - } else { - Serial.println("✗ 파일 생성 실패"); - } - - xSemaphoreGive(sdMutex); - } - } - } - else if (strcmp(cmd, "stopLogging") == 0) { - if (loggingEnabled) { - Serial.println("🛑 로깅 중지 요청..."); - - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(2000)) == pdTRUE) { - // ⭐⭐⭐ BIN 형식: 버퍼에 남은 데이터 강제 플러시 - if (bufferIndex > 0 && logFile) { - size_t written = logFile.write(fileBuffer, bufferIndex); - logFile.flush(); - Serial.printf("✓ 최종 플러시: %d bytes\n", written); - bufferIndex = 0; - } - - // ⭐⭐⭐ CSV 형식: 최종 플러시 - if (canLogFormatCSV && logFile) { - logFile.flush(); - Serial.println("✓ CSV 최종 플러시"); - } - - if (logFile) { - size_t finalSize = logFile.size(); - logFile.close(); - Serial.printf("✓ 파일 닫힘: %s (%lu bytes)\n", currentFilename, finalSize); - } - - loggingEnabled = false; - currentFilename[0] = '\0'; - bufferIndex = 0; - xSemaphoreGive(sdMutex); - } 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; - } - - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - struct tm timeinfo; - time_t now; - time(&now); - localtime_r(&now, &timeinfo); - - struct timeval tv; - gettimeofday(&tv, NULL); - serialLogStartTime = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; - - const char* ext = serialLogFormatCSV ? "csv" : "bin"; - snprintf(currentSerialFilename, sizeof(currentSerialFilename), - "/SER_%04d%02d%02d_%02d%02d%02d.%s", - timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, - timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, ext); - - serialLogFile = SD_MMC.open(currentSerialFilename, FILE_WRITE); - - if (serialLogFile) { - if (serialLogFormatCSV) { - serialLogFile.println("Time_us,Direction,Data"); - } - serialLoggingEnabled = true; - serialCsvIndex = 0; - currentSerialFileSize = serialLogFile.size(); - } - - xSemaphoreGive(sdMutex); - } - } - } - else if (strcmp(cmd, "stopSerialLogging") == 0) { - if (serialLoggingEnabled) { - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - if (serialCsvIndex > 0 && serialLogFile) { - serialLogFile.write((uint8_t*)serialCsvBuffer, serialCsvIndex); - serialCsvIndex = 0; - } - - if (serialLogFile) { - serialLogFile.close(); - } - - serialLoggingEnabled = false; - xSemaphoreGive(sdMutex); - } - } - } - else if (strcmp(cmd, "sendSerial") == 0) { - const char* data = doc["data"]; - if (data && strlen(data) > 0) { - SerialComm.println(data); - - SerialMessage serialMsg; - struct timeval tv; - gettimeofday(&tv, NULL); - serialMsg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; - serialMsg.length = strlen(data) + 2; - if (serialMsg.length > MAX_SERIAL_LINE_LEN - 1) { - serialMsg.length = MAX_SERIAL_LINE_LEN - 1; - } - - snprintf((char*)serialMsg.data, MAX_SERIAL_LINE_LEN, "%s\r\n", data); - serialMsg.isTx = true; - - if (xQueueSend(serialQueue, &serialMsg, pdMS_TO_TICKS(10)) == pdTRUE) { - totalSerialTxCount++; - } - } - } - else if (strcmp(cmd, "setSerialConfig") == 0) { - 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(); - } - 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_8MHZ); - // 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); - } - } - 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; - } - - 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); - 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); - - serial2LogFile = SD_MMC.open(currentSerial2Filename, FILE_WRITE); - - if (serial2LogFile) { - if (serial2LogFormatCSV) { - serial2LogFile.println("Time_us,Direction,Data"); - } - serial2LoggingEnabled = true; - serial2CsvIndex = 0; - currentSerial2FileSize = serial2LogFile.size(); - } - - xSemaphoreGive(sdMutex); - } - } - } - else if (strcmp(cmd, "stopSerial2Logging") == 0) { - if (serial2LoggingEnabled) { - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - if (serial2CsvIndex > 0 && serial2LogFile) { - serial2LogFile.write((uint8_t*)serial2CsvBuffer, serial2CsvIndex); - serial2CsvIndex = 0; - } - - if (serial2LogFile) { - serial2LogFile.close(); - } - - serial2LoggingEnabled = false; - xSemaphoreGive(sdMutex); - } - } - } - else if (strcmp(cmd, "sendSerial2") == 0) { - 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++; - } - } - } - 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(); - } - 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); - } - 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_8MHZ); - // 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); - } - } - else if (strcmp(cmd, "setMcpMode") == 0) { - int mode = doc["mode"]; - if (mode >= 0 && mode <= 3) { - setMCP2515Mode((MCP2515Mode)mode); - saveSettings(); - } - } - else if (strcmp(cmd, "syncTimeFromPhone") == 0) { - 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); - - timeSyncStatus.synchronized = true; - timeSyncStatus.lastSyncTime = (uint64_t)t * 1000000ULL; - timeSyncStatus.syncCount++; - - if (timeSyncStatus.rtcAvailable) { - writeRTC(&timeinfo); - } - } - else if (strcmp(cmd, "getFiles") == 0) { - if (sdCardReady) { - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - DynamicJsonDocument response(6144); - response["type"] = "files"; - JsonArray files = response.createNestedArray("files"); - - File root = SD_MMC.open("/"); - if (root) { - File file = root.openNextFile(); - int fileCount = 0; - - while (file && fileCount < 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++; - } - } - 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); - } - } 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); - - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - bool success = false; - - if (SD_MMC.exists(fullPath)) { - if (SD_MMC.remove(fullPath)) { - success = true; - } - } - - xSemaphoreGive(sdMutex); - - DynamicJsonDocument response(256); - response["type"] = "deleteResult"; - response["success"] = success; - - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); - } - } - } - else if (strcmp(cmd, "addComment") == 0) { - const char* filename = doc["filename"]; - const char* comment = doc["comment"]; - - if (filename && comment) { - addFileComment(filename, comment); - } - } - // 🎯 Auto Trigger 명령 추가 - else if (strcmp(cmd, "setAutoTrigger") == 0) { - autoTriggerEnabled = doc["enabled"] | false; - - // 🆕 로그 형식 설정 - const char* logFormat = doc["logFormat"]; - if (logFormat) { - autoTriggerLogCSV = (strcmp(logFormat, "csv") == 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); - } - // 🆕 Auto Trigger 형식 설정 명령 - else if (strcmp(cmd, "setAutoTriggerFormat") == 0) { - const char* logFormat = doc["logFormat"]; - if (logFormat) { - autoTriggerLogCSV = (strcmp(logFormat, "csv") == 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); - } - else if (strcmp(cmd, "setStartTriggers") == 0) { - JsonArray triggers = doc["triggers"]; - strcpy(startLogicOp, doc["logic"] | "OR"); - startTriggerCount = 0; - - for (JsonObject t : triggers) { - if (startTriggerCount >= MAX_TRIGGERS) break; - - String idStr = t["canId"] | "0x0"; - startTriggers[startTriggerCount].canId = strtoul(idStr.c_str(), NULL, 16); - startTriggers[startTriggerCount].startBit = t["startBit"] | 0; - startTriggers[startTriggerCount].bitLength = t["bitLength"] | 8; - strcpy(startTriggers[startTriggerCount].op, t["op"] | "=="); - startTriggers[startTriggerCount].value = t["value"] | 0; - startTriggers[startTriggerCount].enabled = t["enabled"] | true; - startTriggerCount++; - } - - saveAutoTriggerSettings(); - - DynamicJsonDocument response(256); - response["type"] = "startTriggersSet"; - response["count"] = startTriggerCount; - - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); - } - else if (strcmp(cmd, "setStopTriggers") == 0) { - JsonArray triggers = doc["triggers"]; - strcpy(stopLogicOp, doc["logic"] | "OR"); - stopTriggerCount = 0; - - for (JsonObject t : triggers) { - if (stopTriggerCount >= MAX_TRIGGERS) break; - - String idStr = t["canId"] | "0x0"; - stopTriggers[stopTriggerCount].canId = strtoul(idStr.c_str(), NULL, 16); - stopTriggers[stopTriggerCount].startBit = t["startBit"] | 0; - stopTriggers[stopTriggerCount].bitLength = t["bitLength"] | 8; - strcpy(stopTriggers[stopTriggerCount].op, t["op"] | "=="); - stopTriggers[stopTriggerCount].value = t["value"] | 0; - stopTriggers[stopTriggerCount].enabled = t["enabled"] | true; - stopTriggerCount++; - } - - saveAutoTriggerSettings(); - - DynamicJsonDocument response(256); - response["type"] = "stopTriggersSet"; - response["count"] = stopTriggerCount; - - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); - } - else if (strcmp(cmd, "setStartFormula") == 0) { - const char* formula = doc["formula"]; - if (formula) { - startFormula = String(formula); - saveAutoTriggerSettings(); - Serial.println("📐 Start Formula 저장: " + startFormula); - } - } - else if (strcmp(cmd, "setStopFormula") == 0) { - const char* formula = doc["formula"]; - if (formula) { - stopFormula = String(formula); - saveAutoTriggerSettings(); - Serial.println("📐 Stop Formula 저장: " + stopFormula); - } - } - 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"); - 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; - } - - JsonArray stopArray = response.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; - } - - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); - } - else if (strcmp(cmd, "sendOnce") == 0) { - if (currentMcpMode == MCP_MODE_TRANSMIT) { - mcp2515.setNormalMode(); - } - - struct can_frame frame; - frame.can_id = strtoul(doc["id"], NULL, 16); - - if (doc["ext"] | false) { - frame.can_id |= CAN_EFF_FLAG; - } - - frame.can_dlc = doc["dlc"] | 8; - - JsonArray dataArray = doc["data"]; - for (int i = 0; i < 8; i++) { - frame.data[i] = dataArray[i] | 0; - } - - if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) { - totalTxCount++; - } - - if (currentMcpMode == MCP_MODE_TRANSMIT) { - mcp2515.setListenOnlyMode(); - } - } - 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); - } 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) { - 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 복사 - seq->stepCount = 0; - for (JsonObject stepObj : stepsArray) { - if (seq->stepCount >= 20) break; // 최대 20개 - - 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; - - 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 목록 다시 전송 - 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"); - - 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; - } - - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); - } - 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); - } - } - 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); - } - } - 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)); - } - - 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 목록 업데이트 - 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; - } - - 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); - } - }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); - - // 약간의 지연 후 재부팅 (응답 전송 시간 확보) - delay(100); - - // ESP32 재부팅 - ESP.restart(); - } - } -} - -// ======================================== -// Web Update Task -// ======================================== -void webUpdateTask(void *parameter) { - const TickType_t xDelay = pdMS_TO_TICKS(200); // ⭐ 100ms → 200ms (WiFi 안정성 향상) - - // 🆕 초기화 대기 (부팅 직후 안정화) - vTaskDelay(pdMS_TO_TICKS(2000)); - - while (1) { - webSocket.loop(); - - if (webSocket.connectedClients() > 0) { - // 🆕 큐가 초기화되었는지 확인 - if (!canQueue || !serialQueue) { - vTaskDelay(xDelay); - continue; - } - - DynamicJsonDocument doc(16384); // ⭐ 4096 → 8192로 증가 - doc["type"] = "update"; - doc["logging"] = loggingEnabled; - - // 🎯 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; - 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; - - 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); - doc["timestamp"] = (uint64_t)now; - - // CAN 메시지 배열 (최근 20개만 전송) - JsonArray messages = doc.createNestedArray("messages"); - int msgCount = 0; - for (int i = 0; i < RECENT_MSG_COUNT && msgCount < 20; i++) { // ⭐ 최대 20개 - 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++; - } - } - - // Serial 메시지 배열 - 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 로깅 - if (serialLoggingEnabled && sdCardReady) { - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(10)) == pdTRUE) { - if (serialLogFormatCSV) { - uint64_t relativeTime = serialMsg.timestamp_us - serialLogStartTime; - - char csvLine[256]; - int lineLen = snprintf(csvLine, sizeof(csvLine), - "%llu,%s,\"%s\"\n", - relativeTime, - serialMsg.isTx ? "TX" : "RX", - dataStr); - - if (serialCsvIndex + lineLen <= SERIAL_CSV_BUFFER_SIZE) { // < → <= - memcpy(&serialCsvBuffer[serialCsvIndex], csvLine, lineLen); - serialCsvIndex += lineLen; - currentSerialFileSize += lineLen; - } - - if (serialCsvIndex >= SERIAL_CSV_BUFFER_SIZE - 256) { - if (serialLogFile) { - serialLogFile.write((uint8_t*)serialCsvBuffer, serialCsvIndex); - serialLogFile.flush(); - serialCsvIndex = 0; - } - } - } else { - if (serialLogFile) { - serialLogFile.write((uint8_t*)&serialMsg, sizeof(SerialMessage)); - currentSerialFileSize += sizeof(SerialMessage); - - static int binFlushCounter = 0; - if (++binFlushCounter >= 50) { - serialLogFile.flush(); - binFlushCounter = 0; - } - } - } - - xSemaphoreGive(sdMutex); - } - } - } - - // ⭐ Serial2 메시지 배열 처리 - 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 로깅 - if (serial2LoggingEnabled && sdCardReady) { - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(10)) == pdTRUE) { - if (serial2LogFormatCSV) { - uint64_t relativeTime = 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; - } - - if (serial2CsvIndex >= SERIAL2_CSV_BUFFER_SIZE - 256) { - 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; - } - } - } - - 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); - } - } - - vTaskDelay(xDelay); - } -} - -// ======================================== -// Setup -// ======================================== -void setup() { - Serial.begin(115200); - delay(1000); - WiFi.setSleep(false); - 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("========================================\n"); - - // ★★★ PSRAM 초기화 (가장 먼저!) ★★★ - if (!initPSRAM()) { - Serial.println("\n✗ PSRAM 초기화 실패!"); - Serial.println("✗ Arduino IDE 설정:"); - Serial.println(" 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 초기화 - Serial.println("MCP2515 초기화..."); - - // ⭐⭐⭐ 하드 리셋 - digitalWrite(HSPI_CS, LOW); - delay(10); - digitalWrite(HSPI_CS, HIGH); - delay(50); - - mcp2515.reset(); - delay(100); - currentCanSpeed = preferences.getInt("can_speed", CAN_500KBPS); - mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ); - delay(10); - currentMcpMode = preferences.getInt("can_mode", MCP_MODE_LISTEN_ONLY); - - // 5. 모드 설정 (Normal/Loopback/Listen Only) - if (currentMcpMode == MCP_MODE_NORMAL) { - mcp2515.setNormalMode(); - } else if (currentMcpMode == MCP_MODE_LOOPBACK) { - mcp2515.setLoopbackMode(); - } else { - mcp2515.setListenOnlyMode(); - } - - delay(50); - - // 버퍼 클리어 - Serial.println("CAN 버퍼 클리어..."); - struct can_frame dummyFrame; - int clearCount = 0; - while (mcp2515.readMessage(&dummyFrame) == MCP2515::ERROR_OK) { - clearCount++; - if (clearCount > 100) break; - } - if (clearCount > 0) { - Serial.printf("✓ 버퍼 클리어 완료 (%d개)\n", clearCount); - } - mcp2515.clearRXnOVRFlags(); - - 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 - - if (!sdMutex || !rtcMutex || !serialMutex) { - 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 - 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); - 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 절전 모드 비활성화 (연결 안정성 향상) - - 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++; - } - Serial.println(); - - 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()); - } - - 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("/download", HTTP_GET, []() { - if (server.hasArg("file")) { - String filename = "/" + server.arg("file"); - - if (SD_MMC.exists(filename)) { - File file = SD_MMC.open(filename, FILE_READ); - if (file) { - String displayName = server.arg("file"); - server.sendHeader("Content-Disposition", - "attachment; filename=\"" + displayName + "\""); - server.sendHeader("Content-Type", "application/octet-stream"); - server.streamFile(file, "application/octet-stream"); - file.close(); - } else { - server.send(500, "text/plain", "Failed to open file"); - } - } else { - server.send(404, "text/plain", "File not found"); - } - } else { - server.send(400, "text/plain", "Bad request"); - } - }); - - server.begin(); - Serial.println("✓ 웹 서버 시작 완료"); - - // ★★★ Queue 생성 (PSRAM 사용) ★★★ - if (!createQueues()) { - Serial.println("✗ Queue 생성 실패!"); - while (1) delay(1000); - } - - // CAN 인터럽트 활성화 - attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING); - - // Task 생성 - xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 8192, NULL, configMAX_PRIORITIES - 1, &canRxTaskHandle, 1); // Core 0, Pri 24 - xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 18576, NULL, 6, &sdWriteTaskHandle, 0); - xTaskCreatePinnedToCore(sequenceTask, "SEQ", 4096, NULL, 2, NULL, 1); - xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", 6144, NULL, 5, &serialRxTaskHandle, 0); - xTaskCreatePinnedToCore(serial2RxTask, "SERIAL2_RX", 6144, NULL, 5, &serial2RxTaskHandle, 0); // ⭐ Serial2 - xTaskCreatePinnedToCore(txTask, "TX", 4096, NULL, 3, NULL, 1); - xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 10384, NULL, 2, &webTaskHandle, 0); // ⭐ 10240 → 16384 - xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 1); - - if (timeSyncStatus.rtcAvailable) { - xTaskCreatePinnedToCore(rtcSyncTask, "RTC_SYNC", 3072, NULL, 0, &rtcTaskHandle, 0); - } - - Serial.println("✓ 모든 Task 시작 완료"); - 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.println("========================================\n"); -} - -// ======================================== -// Loop -// ======================================== -void loop() { - server.handleClient(); - vTaskDelay(pdMS_TO_TICKS(10)); - - 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, - ESP.getFreePsram() / 1024); - lastPrint = millis(); - } -} -