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