From b91536429240eb62f3b01a8346c66705f9dad7c2 Mon Sep 17 00:00:00 2001 From: byun Date: Fri, 27 Feb 2026 10:02:27 +0000 Subject: [PATCH] =?UTF-8?q?CAN-FD=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC?= =?UTF-8?q?=EB=A6=AC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ESP32_CANFD_Logger.ino | 2721 +++++++++++++++++++--------------------- canfd_graph.h | 2 +- canfd_graph_viewer.h | 2 +- canfd_index.h | 1 + canfd_settings.h | 30 +- 5 files changed, 1317 insertions(+), 1439 deletions(-) diff --git a/ESP32_CANFD_Logger.ino b/ESP32_CANFD_Logger.ino index 5148a93..2725fc5 100644 --- a/ESP32_CANFD_Logger.ino +++ b/ESP32_CANFD_Logger.ino @@ -1,37 +1,39 @@ /* * ESP32-S3 CAN FD Logger with Web Interface - * Version: 3.0 - MCP2517FD CAN FD Support - * - * Features: - * - CAN FD (up to 8Mbps data rate, 64-byte frames) - * - PSRAM optimized for high-speed logging - * - SDIO 4-bit SD card (40MB/s write speed) - * - RTC timestamp synchronization - * - Web interface for configuration - * - Dual Serial logger support - * - * Hardware: - * - ESP32-S3 with OPI PSRAM - * - MCP2517FD CAN FD Controller - * - DS3231 RTC - * - SD Card (SDIO 4-bit) - * - * Arduino IDE Settings: - * - Board: ESP32S3 Dev Module - * - PSRAM: OPI PSRAM ⭐ Required! - * - Flash Size: 16MB (128Mb) - * - Partition: 16MB Flash (3MB APP/9.9MB FATFS) + * Version: 4.0 - Seeed_Arduino_CAN (mcp2518fd) Library + * + * ★ 라이브러리 변경: ACAN2517FD → Seeed_Arduino_CAN + * - SPI 속도 4MHz 고정 (브레드보드 안정적) + * - 0x1000 OSC readback 에러 해결 + * + * 필요 라이브러리 (Arduino IDE 라이브러리 매니저): + * - Seeed_Arduino_CAN (검색: "Seeed CAN") + * - WebSockets (by Markus Sattler) + * - ArduinoJson + * - SoftWire (I2C for RTC) + * + * 하드웨어: + * - ESP32-S3 N16R8 (OPI PSRAM) + * - MCP2518FD (ATA6560 트랜시버 내장 모듈) + * - DS3231 RTC + * - MicroSD (SDIO 4-bit, Adafruit 4682) + * + * Arduino IDE 설정: + * - Board: ESP32S3 Dev Module + * - PSRAM: OPI PSRAM ★ 필수! + * - Flash Size: 16MB (128Mb) + * - Partition Scheme: 16MB Flash (3MB APP/9.9MB FATFS) + * - USB Mode: Hardware CDC and JTAG */ #include #include -#include // ⭐ CAN FD Library +#include // ★ Seeed_Arduino_CAN 라이브러리 #include #include #include #include #include -#include #include #include #include @@ -42,137 +44,99 @@ #include #include #include -#include "canfd_index.h" // ⭐ Web interface -#include "canfd_settings.h" // ⭐ Settings page -#include "canfd_graph.h" // ⭐ Graph page -#include "canfd_graph_viewer.h" // ⭐ Graph viewer -// ======================================== -// GPIO Pin Definitions -// ======================================== -// HSPI Pins (CAN FD - MCP2517FD) -#define HSPI_MISO 13 -#define HSPI_MOSI 11 -#define HSPI_SCLK 12 -#define HSPI_CS 10 -#define CAN_INT_PIN 3 -// STBY connected to GND +#include "canfd_index.h" +#include "canfd_settings.h" +#include "canfd_graph.h" +#include "canfd_graph_viewer.h" -// SDIO 4-bit Pins (ESP32-S3) -#define SDIO_CLK 39 -#define SDIO_CMD 38 -#define SDIO_D0 40 -#define SDIO_D1 41 -#define SDIO_D2 42 -#define SDIO_D3 21 +// ============================================================ +// 핀 정의 +// ============================================================ +#define HSPI_MISO 13 +#define HSPI_MOSI 11 +#define HSPI_SCLK 12 +#define HSPI_CS 10 +#define CAN_INT_PIN 4 // ★ GPIO3 스트래핑 핀 충돌 방지 → GPIO4 -// I2C Pins (RTC DS3231) -#define RTC_SDA 8 -#define RTC_SCL 9 +#define SDIO_CLK 39 +#define SDIO_CMD 38 +#define SDIO_D0 40 +#define SDIO_D1 41 +#define SDIO_D2 42 +#define SDIO_D3 21 // OPI PSRAM 호환 + +#define RTC_SDA 8 +#define RTC_SCL 9 #define DS3231_ADDRESS 0x68 -// Serial Pins -#define SERIAL_TX_PIN 17 -#define SERIAL_RX_PIN 18 -#define SERIAL2_TX_PIN 6 -#define SERIAL2_RX_PIN 7 +// ============================================================ +// 버퍼 / 큐 크기 +// ============================================================ +#define CAN_QUEUE_SIZE 10000 +#define FILE_BUFFER_SIZE 131072 // 128 KB +#define RECENT_MSG_COUNT 100 +#define MAX_TX_MESSAGES 20 +#define MAX_SEQUENCES 10 +#define MAX_FILE_COMMENTS 50 +#define MAX_FILENAME_LEN 64 +#define MAX_COMMENT_LEN 128 -// ======================================== -// CAN FD Configuration -// ======================================== -#define CAN_FD_ENABLED true // ⭐ CAN FD 모드 활성화 - -// CAN FD Buffer Sizes (PSRAM optimized) -#define CAN_QUEUE_SIZE 10000 // ⭐ CAN FD는 더 큰 버퍼 필요 -#define FILE_BUFFER_SIZE 131072 // 128KB (CAN FD 64바이트 프레임 대응) -#define SERIAL_QUEUE_SIZE 1200 -#define SERIAL_CSV_BUFFER_SIZE 32768 -#define SERIAL2_QUEUE_SIZE 1200 -#define SERIAL2_CSV_BUFFER_SIZE 32768 - -#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 - -// ======================================== -// CAN FD Data Structures -// ======================================== -// ⭐ 로깅용 CAN FD 프레임 (라이브러리 CANFDMessage와 분리) +// ============================================================ +// 데이터 구조 +// ============================================================ struct LoggedCANFrame { uint64_t timestamp_us; uint32_t id; - uint8_t len; // 0-64 (actual data length for CAN FD) - uint8_t data[64]; // ⭐ CAN FD max 64 bytes - bool fd; // FD frame flag - bool brs; // Bit Rate Switch flag - bool esi; // Error State Indicator - bool ext; // Extended ID flag + uint8_t len; // 실제 데이터 길이 (CAN FD: 최대 64) + uint8_t data[64]; + bool fd; // FD 프레임 여부 + bool brs; // Bit Rate Switch + bool esi; // Error State Indicator + bool ext; // Extended ID (29-bit) } __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 { LoggedCANFrame msg; - uint32_t count; + uint32_t count; }; struct TxMessage { uint32_t id; - bool extended; - uint8_t dlc; - uint8_t data[64]; // ⭐ CAN FD 지원 + bool extended; + uint8_t dlc; + uint8_t data[64]; uint32_t interval; uint32_t lastSent; - bool active; - bool fd; // ⭐ FD frame flag - bool brs; // ⭐ Bit Rate Switch + bool active; + bool fd; + bool brs; }; struct SequenceStep { uint32_t canId; - bool extended; - uint8_t dlc; - uint8_t data[64]; // ⭐ CAN FD 지원 + bool extended; + uint8_t dlc; + uint8_t data[64]; uint32_t delayMs; - bool fd; // ⭐ FD frame flag - bool brs; // ⭐ Bit Rate Switch + bool fd; + bool brs; }; struct CANSequence { - char name[32]; + char name[32]; SequenceStep steps[20]; - uint8_t stepCount; - uint8_t repeatMode; - uint32_t repeatCount; + uint8_t stepCount; + uint8_t repeatMode; // 0=1회, 1=N회, 2=무한 + uint32_t repeatCount; }; struct SequenceRuntime { - bool running; + bool running; uint8_t currentStep; uint32_t currentRepeat; uint32_t lastStepTime; - int8_t activeSequenceIndex; + int8_t activeSequenceIndex; }; struct FileComment { @@ -181,642 +145,538 @@ struct FileComment { }; struct TimeSyncStatus { - bool synchronized; + bool synchronized; uint64_t lastSyncTime; - int32_t offsetUs; + int32_t offsetUs; uint32_t syncCount; - bool rtcAvailable; + bool rtcAvailable; uint32_t rtcSyncCount; -} timeSyncStatus = {false, 0, 0, 0, false, 0}; +} timeSyncStatus = {}; -struct PowerStatus { - float voltage; - float minVoltage; - bool lowVoltage; - uint32_t lastCheck; - uint32_t lastMinReset; -} powerStatus = {0.0, 999.9, false, 0, 0}; - -// ⭐ CAN FD Mode Settings +// ★ CAN 설정 (Seeed 라이브러리 기반) struct CANFDSettings { - uint32_t arbitrationBitRate; // Arbitration phase (typically 500k or 1M) - uint8_t dataBitRateFactor; // Data phase multiplier (x1, x2, x4, x8) - bool fdMode; // CAN FD enable - bool listenOnly; // Listen-only mode - bool loopback; // Loopback mode -} canfdSettings = {500000, 4, true, false, false}; // Default: 500k, x4 (=2Mbps) + bool fdMode; // true=CAN FD, false=Classic CAN + uint8_t controllerMode; // 0=Normal, 1=ListenOnly, 2=Loopback + uint8_t logFormat; // 0=PCAP, 1=CSV + uint32_t arbRateKbps; // 중재 속도 (kbps) + uint32_t dataRateKbps; // 데이터 속도 (kbps, FD 전용) +} canSettings = { + .fdMode = true, + .controllerMode = 0, + .logFormat = 0, + .arbRateKbps = 500, + .dataRateKbps = 2000 +}; -// ======================================== -// PSRAM Allocated Variables -// ======================================== -uint8_t *fileBuffer = nullptr; -char *serialCsvBuffer = nullptr; -char *serial2CsvBuffer = nullptr; -RecentCANData *recentData = nullptr; -TxMessage *txMessages = nullptr; -CANSequence *sequences = nullptr; -FileComment *fileComments = nullptr; +// PCAP 구조체 +struct __attribute__((packed)) PCAPGlobalHeader { + uint32_t magic_number; + uint16_t version_major; + uint16_t version_minor; + int32_t thiszone; + uint32_t sigfigs; + uint32_t snaplen; + uint32_t network; +}; +struct __attribute__((packed)) PCAPPacketHeader { + uint32_t ts_sec; + uint32_t ts_usec; + uint32_t incl_len; + uint32_t orig_len; +}; +struct __attribute__((packed)) SocketCANFDFrame { + uint32_t can_id; + uint8_t len; + uint8_t flags; + uint8_t res0; + uint8_t res1; + uint8_t data[64]; +}; +struct __attribute__((packed)) SocketCANFrame { + uint32_t can_id; + uint8_t can_dlc; + uint8_t pad; + uint8_t res0; + uint8_t res1; + uint8_t data[8]; +}; -// Queue Storage (PSRAM) -StaticQueue_t *canQueueBuffer = nullptr; -StaticQueue_t *serialQueueBuffer = nullptr; -StaticQueue_t *serial2QueueBuffer = nullptr; -uint8_t *canQueueStorage = nullptr; -uint8_t *serialQueueStorage = nullptr; -uint8_t *serial2QueueStorage = nullptr; +#define CAN_EFF_FLAG 0x80000000U +#define CANFD_BRS 0x01 +#define CANFD_ESI 0x02 -// WiFi Settings -char wifiSSID[32] = "Byun_CANFD_Logger"; +// ============================================================ +// PSRAM 동적 할당 변수 +// ============================================================ +uint8_t *fileBuffer = nullptr; +RecentCANData *recentData = nullptr; +TxMessage *txMessages = nullptr; +CANSequence *sequences = nullptr; +FileComment *fileComments = nullptr; +StaticQueue_t *canQueueBuffer = nullptr; +uint8_t *canQueueStorage = nullptr; + +// ============================================================ +// WiFi 설정 +// ============================================================ +char wifiSSID[32] = "Byun_CANFD_Logger"; char wifiPassword[64] = "12345678"; -bool staEnable = false; -char staSSID[32] = ""; -char staPassword[64] = ""; +bool staEnable = false; +char staSSID[32] = ""; +char staPassword[64] = ""; IPAddress staIP; -// Serial Settings -SerialSettings serialSettings = {115200, 8, 0, 1}; -SerialSettings serial2Settings = {115200, 8, 0, 1}; +// ============================================================ +// 전역 객체 +// ============================================================ +SPIClass hspi(HSPI); +mcp2518fd CAN(HSPI_CS); // ★ Seeed 라이브러리 객체 +SoftWire rtcWire(RTC_SDA, RTC_SCL); +Preferences preferences; +WebServer server(80); +WebSocketsServer ws(81); -// ======================================== -// Global Objects -// ======================================== -SPIClass hspi(HSPI); -ACAN2517FD *canfd = nullptr; // ⭐ CAN FD Controller -SoftWire rtcWire(RTC_SDA, RTC_SCL); -Preferences preferences; -WebServer server(80); -WebSocketsServer ws(81); +// ============================================================ +// 전역 변수 +// ============================================================ +QueueHandle_t canQueue = nullptr; +SemaphoreHandle_t sdMutex = nullptr; +SemaphoreHandle_t i2cMutex = nullptr; +TaskHandle_t canRxTaskHandle = nullptr; +TaskHandle_t sdWriteTaskHandle= nullptr; -// ======================================== -// Global Variables -// ======================================== -QueueHandle_t canQueue = nullptr; -QueueHandle_t serialQueue = nullptr; -QueueHandle_t serial2Queue = nullptr; -SemaphoreHandle_t sdMutex = nullptr; -SemaphoreHandle_t i2cMutex = nullptr; - -TaskHandle_t canRxTaskHandle = nullptr; -TaskHandle_t sdWriteTaskHandle = nullptr; -TaskHandle_t sdFlushTaskHandle = nullptr; -TaskHandle_t webTaskHandle = nullptr; -TaskHandle_t serialRxTaskHandle = nullptr; -TaskHandle_t serial2RxTaskHandle = nullptr; -TaskHandle_t rtcTaskHandle = nullptr; - -bool loggingEnabled = false; -bool wifiEnabled = false; volatile bool canInterruptFlag = false; -uint32_t canFrameCount = 0; -uint32_t canErrorCount = 0; -uint32_t serial1FrameCount = 0; -uint32_t serial2FrameCount = 0; -uint32_t sdWriteErrorCount = 0; +bool loggingEnabled = false; +bool canInitialized = false; -File canLogFile; -File serial1LogFile; -File serial2LogFile; -char currentCanFilename[MAX_FILENAME_LEN] = ""; -char currentSerial1Filename[MAX_FILENAME_LEN] = ""; -char currentSerial2Filename[MAX_FILENAME_LEN] = ""; +uint32_t canFrameCount = 0; +uint32_t canErrorCount = 0; +uint32_t sdWriteErrorCount= 0; -uint32_t fileBufferPos = 0; -uint32_t serialCsvBufferPos = 0; -uint32_t serial2CsvBufferPos = 0; -uint32_t lastFlushTime = 0; +File canLogFile; +char currentCanFilename[MAX_FILENAME_LEN] = ""; +uint32_t fileBufferPos = 0; +uint8_t recentDataCount = 0; +SequenceRuntime seqRuntime= {false, 0, 0, 0, -1}; -uint8_t recentDataCount = 0; -SequenceRuntime seqRuntime = {false, 0, 0, 0, -1}; - -// ======================================== -// Function Declarations -// ======================================== -void canISR(); -bool initCANFD(); -bool readRTC(struct tm *timeinfo); -void updateSystemTime(const struct tm *timeinfo); -bool allocatePSRAM(); -bool createQueues(); -void initSDCard(); -void initWiFi(); -void initWebServer(); -void setupWebRoutes(); - -// Task Functions -void canRxTask(void *parameter); -void sdWriteTask(void *parameter); -void sdFlushTask(void *parameter); -void webUpdateTask(void *parameter); -void serialRxTask(void *parameter); -void serial2RxTask(void *parameter); -void rtcSyncTask(void *parameter); -void txTask(void *parameter); -void sequenceTask(void *parameter); -void sdMonitorTask(void *parameter); - -// Utility Functions +// ============================================================ +// 함수 선언 +// ============================================================ +void IRAM_ATTR canISR(); +bool initCANFD(); +bool readRTC(struct tm *t); +void updateSystemTime(const struct tm *t); +bool allocatePSRAM(); +void initSDCard(); +void setupWebRoutes(); uint64_t getMicros64(); -void writeCANToBuffer(const LoggedCANFrame *msg); -void flushBufferToSD(); -String formatCANFDMessage(const LoggedCANFrame *msg); -void updateRecentData(const LoggedCANFrame *msg); +void writeCANToBuffer(const LoggedCANFrame *msg); +void flushBufferToSD(); +void updateRecentData(const LoggedCANFrame *msg); +uint8_t calcSeeedBaudrate(uint32_t arbKbps, uint32_t dataKbps, bool fd); +void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length); -// ======================================== -// CAN FD Interrupt Handler -// ======================================== -void IRAM_ATTR canISR() { - canInterruptFlag = true; +void canRxTask (void *p); +void sdWriteTask (void *p); +void sdFlushTask (void *p); +void webUpdateTask(void *p); +void rtcSyncTask (void *p); +void txTask (void *p); +void sequenceTask (void *p); + +// ============================================================ +// 인터럽트 +// ============================================================ +void IRAM_ATTR canISR() { canInterruptFlag = true; } + +// ============================================================ +// ★ Seeed 라이브러리 속도 상수 매핑 +// ============================================================ +uint8_t calcSeeedBaudrate(uint32_t arbKbps, uint32_t dataKbps, bool fd) { + if (!fd) { + if (arbKbps >= 1000) return CAN_1000KBPS; + if (arbKbps >= 500) return CAN_500KBPS; + if (arbKbps >= 250) return CAN_250KBPS; + return CAN_125KBPS; + } + // CAN FD 속도 상수 (mcp2518fd_can_dfs.h에 정의됨) + if (arbKbps >= 1000) { + return CAN_1000K_4M; + } else if (arbKbps >= 500) { + if (dataKbps >= 4000) return CAN_500K_4M; + if (dataKbps >= 3000) return CAN_500K_3M; + if (dataKbps >= 2000) return CAN_500K_2M; + return CAN_500K_1M; + } else if (arbKbps >= 250) { + if (dataKbps >= 4000) return CAN_250K_4M; + if (dataKbps >= 2000) return CAN_250K_2M; + if (dataKbps >= 1000) return CAN_250K_1M; + return CAN_250K_500K; + } + return CAN_125K_500K; } -// ======================================== -// PSRAM Allocation -// ======================================== +// ============================================================ +// PSRAM 할당 +// ============================================================ bool allocatePSRAM() { Serial.println("\n========================================"); - Serial.println(" PSRAM Memory Allocation"); + Serial.println(" PSRAM 메모리 할당"); Serial.println("========================================"); - - size_t psramBefore = ESP.getFreePsram(); - Serial.printf("Available PSRAM: %d KB\n", psramBefore / 1024); - - // File Buffer (128KB for CAN FD) + + size_t before = ESP.getFreePsram(); + Serial.printf("가용 PSRAM: %u KB\n", before / 1024); + fileBuffer = (uint8_t*)ps_malloc(FILE_BUFFER_SIZE); - if (!fileBuffer) { - Serial.println("✗ File buffer allocation failed!"); - return false; - } - Serial.printf("✓ File Buffer: %d KB\n", FILE_BUFFER_SIZE / 1024); - - // Serial CSV Buffers - serialCsvBuffer = (char*)ps_malloc(SERIAL_CSV_BUFFER_SIZE); - serial2CsvBuffer = (char*)ps_malloc(SERIAL2_CSV_BUFFER_SIZE); - if (!serialCsvBuffer || !serial2CsvBuffer) { - Serial.println("✗ Serial buffer allocation failed!"); - return false; - } - Serial.printf("✓ Serial Buffers: %d KB each\n", SERIAL_CSV_BUFFER_SIZE / 1024); - - // Recent Data Array + if (!fileBuffer) { Serial.println("✗ fileBuffer 실패"); return false; } + recentData = (RecentCANData*)ps_malloc(sizeof(RecentCANData) * RECENT_MSG_COUNT); - if (!recentData) { - Serial.println("✗ Recent data allocation failed!"); - return false; - } + if (!recentData) { Serial.println("✗ recentData 실패"); return false; } memset(recentData, 0, sizeof(RecentCANData) * RECENT_MSG_COUNT); - Serial.printf("✓ Recent Data: %d entries\n", RECENT_MSG_COUNT); - - // TX Messages + txMessages = (TxMessage*)ps_malloc(sizeof(TxMessage) * MAX_TX_MESSAGES); - if (!txMessages) { - Serial.println("✗ TX messages allocation failed!"); - return false; - } + if (!txMessages) { Serial.println("✗ txMessages 실패"); return false; } memset(txMessages, 0, sizeof(TxMessage) * MAX_TX_MESSAGES); - Serial.printf("✓ TX Messages: %d slots\n", MAX_TX_MESSAGES); - - // Sequences + sequences = (CANSequence*)ps_malloc(sizeof(CANSequence) * MAX_SEQUENCES); - if (!sequences) { - Serial.println("✗ Sequences allocation failed!"); - return false; - } + if (!sequences) { Serial.println("✗ sequences 실패"); return false; } memset(sequences, 0, sizeof(CANSequence) * MAX_SEQUENCES); - Serial.printf("✓ Sequences: %d slots\n", MAX_SEQUENCES); - - // File Comments + fileComments = (FileComment*)ps_malloc(sizeof(FileComment) * MAX_FILE_COMMENTS); - if (!fileComments) { - Serial.println("✗ File comments allocation failed!"); - return false; - } + if (!fileComments) { Serial.println("✗ fileComments 실패"); return false; } memset(fileComments, 0, sizeof(FileComment) * MAX_FILE_COMMENTS); - Serial.printf("✓ File Comments: %d entries\n", MAX_FILE_COMMENTS); - - // Queue Buffers - canQueueBuffer = (StaticQueue_t*)ps_malloc(sizeof(StaticQueue_t)); - serialQueueBuffer = (StaticQueue_t*)ps_malloc(sizeof(StaticQueue_t)); - serial2QueueBuffer = (StaticQueue_t*)ps_malloc(sizeof(StaticQueue_t)); - + + canQueueBuffer = (StaticQueue_t*)ps_malloc(sizeof(StaticQueue_t)); canQueueStorage = (uint8_t*)ps_malloc(CAN_QUEUE_SIZE * sizeof(LoggedCANFrame)); - serialQueueStorage = (uint8_t*)ps_malloc(SERIAL_QUEUE_SIZE * sizeof(SerialMessage)); - serial2QueueStorage = (uint8_t*)ps_malloc(SERIAL2_QUEUE_SIZE * sizeof(SerialMessage)); - - if (!canQueueBuffer || !serialQueueBuffer || !serial2QueueBuffer || - !canQueueStorage || !serialQueueStorage || !serial2QueueStorage) { - Serial.println("✗ Queue storage allocation failed!"); - return false; - } - - size_t queueSize = (CAN_QUEUE_SIZE * sizeof(LoggedCANFrame) + - SERIAL_QUEUE_SIZE * sizeof(SerialMessage) + - SERIAL2_QUEUE_SIZE * sizeof(SerialMessage)) / 1024; - Serial.printf("✓ Queue Storage: %d KB\n", queueSize); - - size_t psramUsed = (psramBefore - ESP.getFreePsram()) / 1024; - Serial.printf("\nTotal PSRAM Used: %d KB\n", psramUsed); - Serial.printf("Remaining PSRAM: %d KB\n", ESP.getFreePsram() / 1024); + if (!canQueueBuffer || !canQueueStorage) { Serial.println("✗ Queue storage 실패"); return false; } + + Serial.printf("✓ 파일 버퍼: %d KB\n", FILE_BUFFER_SIZE / 1024); + Serial.printf("✓ CAN 큐: %d 프레임 예약\n", CAN_QUEUE_SIZE); + Serial.printf("✓ PSRAM 사용: %u KB / 잔여: %u KB\n", + (before - ESP.getFreePsram()) / 1024, ESP.getFreePsram() / 1024); Serial.println("========================================\n"); - return true; } -// ======================================== -// Queue Creation -// ======================================== -bool createQueues() { - Serial.println("Creating FreeRTOS Queues..."); - - canQueue = xQueueCreateStatic( - CAN_QUEUE_SIZE, - sizeof(LoggedCANFrame), - canQueueStorage, - canQueueBuffer - ); - - serialQueue = xQueueCreateStatic( - SERIAL_QUEUE_SIZE, - sizeof(SerialMessage), - serialQueueStorage, - serialQueueBuffer - ); - - serial2Queue = xQueueCreateStatic( - SERIAL2_QUEUE_SIZE, - sizeof(SerialMessage), - serial2QueueStorage, - serial2QueueBuffer - ); - - if (!canQueue || !serialQueue || !serial2Queue) { - Serial.println("✗ Queue creation failed!"); - return false; - } - - Serial.printf("✓ CAN FD Queue: %d messages\n", CAN_QUEUE_SIZE); - Serial.printf("✓ Serial1 Queue: %d messages\n", SERIAL_QUEUE_SIZE); - Serial.printf("✓ Serial2 Queue: %d messages\n", SERIAL2_QUEUE_SIZE); - - return true; -} - -// ======================================== -// CAN FD Initialization -// ======================================== +// ============================================================ +// ★ CAN FD 초기화 (Seeed_Arduino_CAN 라이브러리) +// ============================================================ bool initCANFD() { Serial.println("\n========================================"); - Serial.println(" CAN FD Controller Initialization"); + Serial.printf(" CAN 초기화 (%s)\n", canSettings.fdMode ? "CAN FD" : "Classic CAN"); Serial.println("========================================"); - - // Pin configuration check - Serial.println("\nPin Configuration:"); - Serial.printf(" SCLK: GPIO%d\n", HSPI_SCLK); - Serial.printf(" MISO: GPIO%d\n", HSPI_MISO); - Serial.printf(" MOSI: GPIO%d\n", HSPI_MOSI); - Serial.printf(" CS: GPIO%d\n", HSPI_CS); - Serial.printf(" INT: GPIO%d\n", CAN_INT_PIN); - - // Initialize CS pin manually first + Serial.printf(" SCLK:GPIO%d MISO:GPIO%d MOSI:GPIO%d CS:GPIO%d INT:GPIO%d\n", + HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS, CAN_INT_PIN); + + // CS 핀 초기 설정 pinMode(HSPI_CS, OUTPUT); - digitalWrite(HSPI_CS, HIGH); // Deselect - delay(10); - - // Initialize HSPI - hspi.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS); - hspi.setFrequency(1000000); // Start with 1MHz for testing - Serial.println("✓ HSPI initialized (1MHz for testing)"); - - // Test SPI communication - Serial.println("\nTesting SPI communication..."); - digitalWrite(HSPI_CS, LOW); - uint8_t testByte = hspi.transfer(0x00); digitalWrite(HSPI_CS, HIGH); - Serial.printf(" SPI test byte: 0x%02X\n", testByte); - - delay(100); - - // Create CAN FD object - Serial.println("\nCreating ACAN2517FD object..."); - canfd = new ACAN2517FD(HSPI_CS, hspi, CAN_INT_PIN); - Serial.println("✓ ACAN2517FD object created"); - - // Determine DataBitRateFactor from settings - DataBitRateFactor factor; - switch (canfdSettings.dataBitRateFactor) { - case 1: factor = DataBitRateFactor::x1; break; - case 2: factor = DataBitRateFactor::x2; break; - case 4: factor = DataBitRateFactor::x4; break; - case 8: factor = DataBitRateFactor::x8; break; - default: factor = DataBitRateFactor::x4; break; + delay(10); + + // HSPI 초기화 + hspi.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS); + Serial.println("✓ HSPI 초기화 완료"); + + // ★ Seeed 라이브러리: SPI 객체 설정 (내부 4MHz 고정 사용) + CAN.setSPI(&hspi); + + // 동작 모드 설정 + // ★ CAN_BUS_Shield 라이브러리 실제 상수명: + // CAN_NORMAL_MODE / CAN_LISTEN_ONLY_MODE (Loopback은 미지원) + switch (canSettings.controllerMode) { + case 1: + CAN.setMode(CAN_LISTEN_ONLY_MODE); + Serial.println(" 모드: Listen-Only (수신 전용)"); + break; + case 2: + // ★ CAN_BUS_Shield 라이브러리는 Loopback 미지원 → Normal로 대체 + CAN.setMode(CAN_NORMAL_MODE); + Serial.println(" 모드: Normal (Loopback 미지원, Normal로 동작)"); + break; + default: + CAN.setMode(CAN_NORMAL_MODE); + Serial.println(" 모드: Normal (정상)"); + break; } - - // Try 40MHz crystal first (most common) - Serial.println("\nTrying 40MHz crystal configuration..."); - ACAN2517FDSettings settings( - ACAN2517FDSettings::OSC_40MHz, // 40MHz crystal - canfdSettings.arbitrationBitRate, // Arbitration bit rate - factor // Data bit rate factor + + // 속도 상수 계산 + uint8_t baudConst = calcSeeedBaudrate( + canSettings.arbRateKbps, + canSettings.dataRateKbps, + canSettings.fdMode ); - - // If 40MHz fails, we'll try 20MHz later - bool try20MHz = false; - - // Set operating mode - if (canfdSettings.listenOnly) { - settings.mRequestedMode = ACAN2517FDSettings::ListenOnly; - } else if (canfdSettings.loopback) { - settings.mRequestedMode = ACAN2517FDSettings::InternalLoopBack; - } else { - settings.mRequestedMode = ACAN2517FDSettings::NormalFD; // ⭐ CAN FD Mode + + Serial.printf(" 속도: Arb %u Kbps / Data %u Kbps\n", + canSettings.arbRateKbps, canSettings.dataRateKbps); + Serial.printf(" Seeed 상수: 0x%02X\n", baudConst); + + // ★ begin() - SPI 4MHz로 MCP2518FD 초기화 (라이브러리 내부 고정) + int ret = CAN.begin(baudConst); + + if (ret != CAN_OK) { + Serial.printf("\n✗ CAN 초기화 실패 (오류코드: %d)\n", ret); + Serial.println("─── 확인 사항 ───────────────────────"); + Serial.println(" 1. MCP2518FD VDD = 3.3V 확인"); + Serial.println(" 2. SDO(MISO)→GPIO13, SDI(MOSI)→GPIO11"); + Serial.println(" 3. SCK→GPIO12, CS→GPIO10, INT→GPIO4"); + Serial.println(" 4. STBY = GND 연결 확인"); + Serial.println(" 5. 크리스탈 40MHz 탑재 확인"); + Serial.println(" 6. 브레드보드 접촉 불량 확인"); + Serial.println("─────────────────────────────────────"); + return false; } - - // Configure receive FIFO (maximize for high-speed logging) - settings.mDriverReceiveFIFOSize = 64; // Driver FIFO - settings.mControllerReceiveFIFOSize = 32; // Controller FIFO - - // Configure transmit FIFO - settings.mDriverTransmitFIFOSize = 16; - settings.mControllerTransmitFIFOSize = 8; - - // Initialize CAN FD controller - Serial.println("\nInitializing MCP2517FD controller..."); - const uint32_t errorCode = canfd->begin(settings, [] { canISR(); }); - - if (errorCode != 0) { - Serial.print("✗ 40MHz initialization failed! Error: 0x"); - Serial.println(errorCode, HEX); - - // Try 20MHz crystal - Serial.println("\nRetrying with 20MHz crystal configuration..."); - delete canfd; - canfd = new ACAN2517FD(HSPI_CS, hspi, CAN_INT_PIN); - - ACAN2517FDSettings settings20( - ACAN2517FDSettings::OSC_20MHz, - canfdSettings.arbitrationBitRate, - factor - ); - - settings20.mRequestedMode = settings.mRequestedMode; - settings20.mDriverReceiveFIFOSize = 64; - settings20.mControllerReceiveFIFOSize = 32; - settings20.mDriverTransmitFIFOSize = 16; - settings20.mControllerTransmitFIFOSize = 8; - - const uint32_t errorCode20 = canfd->begin(settings20, [] { canISR(); }); - - if (errorCode20 != 0) { - Serial.print("✗ 20MHz initialization also failed! Error: 0x"); - Serial.println(errorCode20, HEX); - - // Decode error code - Serial.println("\n❌ CAN FD Initialization Failed!"); - Serial.println("\nError Analysis:"); - if (errorCode & 0x0001) Serial.println(" - Arbitration bit rate error"); - if (errorCode & 0x0002) Serial.println(" - Data bit rate error"); - if (errorCode & 0x0004) Serial.println(" - Invalid oscillator frequency"); - if (errorCode & 0x0008) Serial.println(" - TDC configuration error"); - if (errorCode & 0x0010) Serial.println(" - Invalid mode"); - if (errorCode & 0x1000) Serial.println(" - ⚠️ SPI communication failure!"); - if (errorCode & 0x2000) Serial.println(" - Controller configuration failed"); - - Serial.println("\n🔧 Troubleshooting Steps:"); - Serial.println("1. ✓ Check MCP2517FD power supply (3.3V)"); - Serial.println("2. ✓ Verify all SPI connections:"); - Serial.println(" MISO: GPIO13 ↔ SDO (MCP2517FD)"); - Serial.println(" MOSI: GPIO11 ↔ SDI (MCP2517FD)"); - Serial.println(" SCK: GPIO12 ↔ SCK (MCP2517FD)"); - Serial.println(" CS: GPIO10 ↔ CS (MCP2517FD)"); - Serial.println("3. ✓ Check crystal frequency (40MHz or 20MHz)"); - Serial.println("4. ✓ Ensure STBY pin connected to GND"); - Serial.println("5. ✓ Test continuity of all wires"); - Serial.println("6. ✓ Try slower SPI speed (edit code)"); - - delete canfd; - canfd = nullptr; - return false; - } else { - Serial.println("✓ 20MHz crystal detected and initialized!"); - Serial.println("ℹ️ Your MCP2517FD uses 20MHz crystal"); - } - } else { - Serial.println("✓ 40MHz crystal detected and initialized!"); - } - - Serial.println("✓ MCP2517FD initialized successfully"); - Serial.printf(" Mode: %s\n", - canfdSettings.listenOnly ? "Listen-Only" : - canfdSettings.loopback ? "Loopback" : "Normal FD"); - Serial.printf(" Arbitration Rate: %d bps\n", canfdSettings.arbitrationBitRate); - Serial.printf(" Data Rate Factor: x%d (= %d bps)\n", - canfdSettings.dataBitRateFactor, - canfdSettings.arbitrationBitRate * canfdSettings.dataBitRateFactor); - Serial.printf(" CAN FD: %s\n", canfdSettings.fdMode ? "Enabled" : "Disabled"); - Serial.printf(" RX FIFO: Driver=%d, Controller=%d\n", - settings.mDriverReceiveFIFOSize, settings.mControllerReceiveFIFOSize); + + canInitialized = true; + Serial.println("✓ MCP2518FD 초기화 성공!"); + Serial.println(" SPI: 4MHz (Seeed 라이브러리 내부 고정값)"); Serial.println("========================================\n"); - return true; } -// ======================================== -// RTC Functions -// ======================================== -bool readRTC(struct tm *timeinfo) { +// ============================================================ +// RTC DS3231 (SoftWire) +// ============================================================ +bool readRTC(struct tm *t) { if (!timeSyncStatus.rtcAvailable) return false; - - if (xSemaphoreTake(i2cMutex, pdMS_TO_TICKS(100)) != pdTRUE) { - return false; - } - + if (xSemaphoreTake(i2cMutex, pdMS_TO_TICKS(100)) != pdTRUE) return false; + rtcWire.beginTransmission(DS3231_ADDRESS); rtcWire.write(0x00); - if (rtcWire.endTransmission() != 0) { + bool ok = (rtcWire.endTransmission() == 0); + + if (ok && rtcWire.requestFrom(DS3231_ADDRESS, 7) == 7) { + uint8_t s = rtcWire.read(); + uint8_t mn = rtcWire.read(); + uint8_t h = rtcWire.read(); + rtcWire.read(); // DOW + uint8_t d = rtcWire.read(); + uint8_t mo = rtcWire.read(); + uint8_t y = rtcWire.read(); xSemaphoreGive(i2cMutex); - return false; + + t->tm_sec = ((s >> 4) * 10) + (s & 0x0F); + t->tm_min = ((mn >> 4) * 10) + (mn & 0x0F); + t->tm_hour = (((h & 0x30) >> 4) * 10) + (h & 0x0F); + t->tm_mday = ((d >> 4) * 10) + (d & 0x0F); + t->tm_mon = (((mo & 0x1F) >> 4) * 10) + (mo & 0x0F) - 1; + t->tm_year = ((y >> 4) * 10) + (y & 0x0F) + 100; + return true; } - - if (rtcWire.requestFrom(DS3231_ADDRESS, 7) != 7) { - xSemaphoreGive(i2cMutex); - return false; - } - - uint8_t second = rtcWire.read(); - uint8_t minute = rtcWire.read(); - uint8_t hour = rtcWire.read(); - rtcWire.read(); // day of week - uint8_t day = rtcWire.read(); - uint8_t month = rtcWire.read(); - uint8_t year = rtcWire.read(); - xSemaphoreGive(i2cMutex); - - timeinfo->tm_sec = ((second >> 4) * 10) + (second & 0x0F); - timeinfo->tm_min = ((minute >> 4) * 10) + (minute & 0x0F); - timeinfo->tm_hour = (((hour & 0x30) >> 4) * 10) + (hour & 0x0F); - timeinfo->tm_mday = ((day >> 4) * 10) + (day & 0x0F); - timeinfo->tm_mon = (((month & 0x1F) >> 4) * 10) + (month & 0x0F) - 1; - timeinfo->tm_year = ((year >> 4) * 10) + (year & 0x0F) + 100; - - return true; + return false; } -void updateSystemTime(const struct tm *timeinfo) { +void updateSystemTime(const struct tm *t) { struct timeval tv; - tv.tv_sec = mktime((struct tm*)timeinfo); + tv.tv_sec = mktime((struct tm*)t); tv.tv_usec = 0; settimeofday(&tv, NULL); } -// ======================================== -// Microsecond Timer (64-bit) -// ======================================== +// ============================================================ +// 64비트 마이크로초 타이머 +// ============================================================ uint64_t getMicros64() { - static uint32_t lastMicros = 0; - static uint32_t overflowCount = 0; - uint32_t currentMicros = micros(); - - if (currentMicros < lastMicros) { - overflowCount++; - } - lastMicros = currentMicros; - - return ((uint64_t)overflowCount << 32) | currentMicros; + static uint32_t last = 0, ovf = 0; + uint32_t now = micros(); + if (now < last) ovf++; + last = now; + return ((uint64_t)ovf << 32) | now; } -// ======================================== -// CAN FD Buffer Management -// ======================================== -void writeCANToBuffer(const LoggedCANFrame *msg) { - if (fileBufferPos + 512 > FILE_BUFFER_SIZE) { - return; // Buffer full +// ============================================================ +// 버퍼 관리 (PCAP / CSV) +// ============================================================ +void writeCANToPCAPBuffer(const LoggedCANFrame *msg) { + uint32_t pktSz = msg->fd ? sizeof(SocketCANFDFrame) : sizeof(SocketCANFrame); + if (fileBufferPos + sizeof(PCAPPacketHeader) + pktSz > FILE_BUFFER_SIZE) return; + + PCAPPacketHeader ph; + ph.ts_sec = msg->timestamp_us / 1000000ULL; + ph.ts_usec = msg->timestamp_us % 1000000ULL; + ph.incl_len = pktSz; + ph.orig_len = pktSz; + memcpy(fileBuffer + fileBufferPos, &ph, sizeof(ph)); + fileBufferPos += sizeof(ph); + + if (msg->fd) { + SocketCANFDFrame f = {}; + f.can_id = msg->id | (msg->ext ? CAN_EFF_FLAG : 0); + f.len = msg->len; + f.flags = (msg->brs ? CANFD_BRS : 0) | (msg->esi ? CANFD_ESI : 0); + memcpy(f.data, msg->data, msg->len); + memcpy(fileBuffer + fileBufferPos, &f, pktSz); + } else { + SocketCANFrame f = {}; + f.can_id = msg->id | (msg->ext ? CAN_EFF_FLAG : 0); + f.can_dlc = msg->len; + memcpy(f.data, msg->data, min((int)msg->len, 8)); + memcpy(fileBuffer + fileBufferPos, &f, pktSz); } - - // Format: timestamp,id,len,fd,brs,data[0-63] + fileBufferPos += pktSz; +} + +void writeCANToCSVBuffer(const LoggedCANFrame *msg) { + if (fileBufferPos + 512 > FILE_BUFFER_SIZE) return; char line[512]; - int len = snprintf(line, sizeof(line), - "%llu,%08X,%d,%d,%d,", - msg->timestamp_us, - msg->id, - msg->len, - msg->fd ? 1 : 0, - msg->brs ? 1 : 0 - ); - - // Add data bytes - for (int i = 0; i < msg->len; i++) { - len += snprintf(line + len, sizeof(line) - len, "%02X", msg->data[i]); - } - len += snprintf(line + len, sizeof(line) - len, "\n"); - - if (fileBufferPos + len < FILE_BUFFER_SIZE) { - memcpy(fileBuffer + fileBufferPos, line, len); - fileBufferPos += len; - } + int n = snprintf(line, sizeof(line), "%llu,%08X,%d,%d,%d,", + msg->timestamp_us, msg->id, msg->len, + msg->fd ? 1 : 0, msg->brs ? 1 : 0); + for (int i = 0; i < msg->len; i++) + n += snprintf(line + n, sizeof(line) - n, "%02X", msg->data[i]); + line[n++] = '\n'; + memcpy(fileBuffer + fileBufferPos, line, n); + fileBufferPos += n; +} + +void writeCANToBuffer(const LoggedCANFrame *msg) { + if (canSettings.logFormat == 0) writeCANToPCAPBuffer(msg); + else writeCANToCSVBuffer(msg); } void flushBufferToSD() { if (fileBufferPos == 0) return; - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(50)) == pdTRUE) { if (canLogFile) { size_t written = canLogFile.write(fileBuffer, fileBufferPos); - if (written != fileBufferPos) { - sdWriteErrorCount++; - } + if (written != fileBufferPos) sdWriteErrorCount++; } fileBufferPos = 0; xSemaphoreGive(sdMutex); } } -// ======================================== -// CAN FD RX Task (Highest Priority) -// ======================================== -void canRxTask(void *parameter) { - LoggedCANFrame msg; - CANFDMessage frame; // ⭐ ACAN2517FD 라이브러리의 CANFDMessage 클래스 - - Serial.println("✓ CAN FD RX Task started"); - - while (1) { - if (canInterruptFlag && canfd != nullptr) { - canInterruptFlag = false; - - // Read all available messages - while (canfd->available()) { - if (canfd->receive(frame)) { - // Convert to LoggedCANFrame for queue - msg.timestamp_us = getMicros64(); - msg.id = frame.id; - msg.len = frame.len; - msg.ext = frame.ext; - - // Determine frame type - msg.fd = (frame.type == CANFDMessage::CANFD_WITH_BIT_RATE_SWITCH || - frame.type == CANFDMessage::CANFD_NO_BIT_RATE_SWITCH); - msg.brs = (frame.type == CANFDMessage::CANFD_WITH_BIT_RATE_SWITCH); - msg.esi = false; - - // Copy data (up to 64 bytes for CAN FD) - memcpy(msg.data, frame.data, msg.len); - - // Send to queue (non-blocking for high-speed logging) - if (xQueueSend(canQueue, &msg, 0) == pdTRUE) { - canFrameCount++; - updateRecentData(&msg); - - // Broadcast to WebSocket clients for real-time graphing - if (ws.connectedClients() > 0) { - StaticJsonDocument<512> doc; - doc["type"] = "canMessage"; - doc["timestamp"] = msg.timestamp_us; - doc["id"] = msg.id; - doc["len"] = msg.len; - doc["fd"] = msg.fd; - doc["brs"] = msg.brs; - - // Convert data to hex string - char dataStr[256]; - char *ptr = dataStr; - for (uint8_t i = 0; i < msg.len; i++) { - ptr += sprintf(ptr, "%02X", msg.data[i]); - } - doc["data"] = dataStr; - - String response; - serializeJson(doc, response); - ws.broadcastTXT(response); - } - } else { - canErrorCount++; // Queue overflow - } - } - } +void updateRecentData(const LoggedCANFrame *msg) { + for (int i = 0; i < recentDataCount; i++) { + if (recentData[i].msg.id == msg->id && recentData[i].msg.ext == msg->ext) { + recentData[i].msg = *msg; + recentData[i].count++; + return; } - - vTaskDelay(pdMS_TO_TICKS(1)); // 1ms polling + } + if (recentDataCount < RECENT_MSG_COUNT) { + recentData[recentDataCount].msg = *msg; + recentData[recentDataCount].count = 1; + recentDataCount++; } } -// ======================================== -// SD Write Task -// ======================================== -void sdWriteTask(void *parameter) { +// ============================================================ +// SD 카드 초기화 +// ============================================================ +void initSDCard() { + Serial.println("\nSD 카드 초기화 (SDIO 4-bit, 20MHz)..."); + SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0, SDIO_D1, SDIO_D2, SDIO_D3); + + // ★ 20MHz 고정: WiFi DMA와 SDIO DMA 충돌 방지 (40MHz→0x109 에러) + if (!SD_MMC.begin("/sdcard", false, false, 20000)) { + Serial.println("⚠ 4-bit 실패, 1-bit 재시도..."); + SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0); + if (!SD_MMC.begin("/sdcard", true, false, 20000)) { + Serial.println("✗ SD 초기화 실패!"); + while (1) delay(1000); + } + Serial.println("✓ SD 초기화 (1-bit 20MHz)"); + } else { + Serial.println("✓ SD 초기화 (4-bit 20MHz)"); + } + Serial.printf(" 카드: %llu MB / %s\n", + SD_MMC.cardSize()/(1024*1024), + SD_MMC.cardType()==CARD_SDHC ? "SDHC" : + SD_MMC.cardType()==CARD_SD ? "SD" : "기타"); +} + +// ============================================================ +// ★ CAN RX Task - Core 1, 최우선 (Priority 24) +// +// CAN_BUS_Shield 라이브러리 실제 API (컴파일 에러 수정): +// CAN.checkReceive() → CAN_MSGAVAIL / CAN_NOMSG +// CAN.readMsgBufID(&id, &len, buf) → 3인수 버전만 사용 +// CAN.isExtendedFrame() → Extended ID 여부 +// len > 8 → FD 프레임 감지 (FD=최대 64byte) +// ============================================================ +void canRxTask(void *p) { LoggedCANFrame msg; - uint32_t batchCount = 0; - const uint32_t BATCH_SIZE = 50; // Write in batches for efficiency - - Serial.println("✓ SD Write Task started"); - + Serial.println("✓ CAN RX Task 시작 (Core1, Priority 24)"); + while (1) { - if (loggingEnabled && xQueueReceive(canQueue, &msg, pdMS_TO_TICKS(10)) == pdTRUE) { + if (!canInitialized) { vTaskDelay(pdMS_TO_TICKS(10)); continue; } + + // 인터럽트 플래그 or 직접 폴링 (인터럽트 놓친 경우 대비) + if (canInterruptFlag || CAN.checkReceive() == CAN_MSGAVAIL) { + canInterruptFlag = false; + + // 수신 큐가 빌 때까지 연속 처리 + while (CAN.checkReceive() == CAN_MSGAVAIL) { + unsigned long rxId = 0; + byte rxLen = 0; + uint8_t rxBuf[64] = {}; + + // ★ CAN_BUS_Shield 3인수 readMsgBufID 사용 + byte res = CAN.readMsgBufID(&rxId, &rxLen, rxBuf); + if (res != CAN_OK) { canErrorCount++; break; } + + // Extended ID / FD 감지 + bool isExt = CAN.isExtendedFrame(); + bool isFD = (rxLen > 8); // FD 프레임: 데이터 9~64바이트 + + // LoggedCANFrame 구성 + msg.timestamp_us = getMicros64(); + msg.id = (uint32_t)rxId; + msg.len = rxLen; + msg.ext = isExt; + msg.fd = isFD; + msg.brs = isFD; // BRS는 FD와 동일하게 처리 + msg.esi = false; + memcpy(msg.data, rxBuf, rxLen); + + if (xQueueSend(canQueue, &msg, 0) == pdTRUE) { + canFrameCount++; + updateRecentData(&msg); + + // WebSocket 실시간 전송 (연결된 클라이언트 있을 때만) + if (ws.connectedClients() > 0) { + char dataBuf[130] = {}; + for (int i = 0; i < rxLen; i++) + sprintf(dataBuf + i*2, "%02X", rxBuf[i]); + + char json[320]; + snprintf(json, sizeof(json), + "{\"type\":\"can\",\"ts\":%llu,\"id\":\"%lX\"," + "\"len\":%d,\"fd\":%s,\"brs\":%s,\"ext\":%s,\"data\":\"%s\"}", + msg.timestamp_us, msg.id, msg.len, + msg.fd ? "true":"false", + msg.brs ? "true":"false", + msg.ext ? "true":"false", + dataBuf); + ws.broadcastTXT(json); + } + } else { + canErrorCount++; // 큐 오버플로우 + } + } + } + vTaskDelay(pdMS_TO_TICKS(1)); + } +} + +// ============================================================ +// SD Write Task - Core 0, Priority 8 +// ============================================================ +void sdWriteTask(void *p) { + LoggedCANFrame msg; + uint32_t batch = 0; + Serial.println("✓ SD Write Task 시작 (Core0, Priority 8)"); + + while (1) { + if (loggingEnabled && + xQueueReceive(canQueue, &msg, pdMS_TO_TICKS(10)) == pdTRUE) { writeCANToBuffer(&msg); - batchCount++; - - // Flush when buffer is 80% full or batch complete - if (fileBufferPos > (FILE_BUFFER_SIZE * 80 / 100) || batchCount >= BATCH_SIZE) { + batch++; + // 버퍼 80% 이상 또는 배치 완료 시 플러시 + if (fileBufferPos > FILE_BUFFER_SIZE * 80 / 100 || batch >= 50) { flushBufferToSD(); - batchCount = 0; + batch = 0; } } else { vTaskDelay(pdMS_TO_TICKS(5)); @@ -824,25 +684,20 @@ void sdWriteTask(void *parameter) { } } -// ======================================== -// SD Flush Task (Periodic) -// ======================================== -void sdFlushTask(void *parameter) { - Serial.println("✓ SD Flush Task started"); - +// ============================================================ +// SD Flush Task - Core 0, Priority 9 (1초마다 강제 플러시) +// ============================================================ +void sdFlushTask(void *p) { + Serial.println("✓ SD Flush Task 시작 (Core0, Priority 9)"); while (1) { - vTaskDelay(pdMS_TO_TICKS(1000)); // Flush every 1 second - + vTaskDelay(pdMS_TO_TICKS(1000)); if (loggingEnabled && fileBufferPos > 0) { flushBufferToSD(); - - // Sync to physical media every 10 seconds + // 10초마다 파일 sync static uint32_t lastSync = 0; if (millis() - lastSync > 10000) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(100)) == pdTRUE) { - if (canLogFile) { - canLogFile.flush(); - } + if (canLogFile) canLogFile.flush(); xSemaphoreGive(sdMutex); } lastSync = millis(); @@ -851,774 +706,782 @@ void sdFlushTask(void *parameter) { } } -// ======================================== -// Setup -// ======================================== -void setup() { - Serial.begin(115200); - delay(1000); - - Serial.println("\n\n========================================"); - Serial.println(" ESP32-S3 CAN FD Logger v3.0"); - Serial.println(" MCP2517FD + PSRAM + SDIO"); - Serial.println("========================================\n"); - - // Check PSRAM - if (!psramFound()) { - Serial.println("✗ PSRAM not found! Please enable OPI PSRAM in Arduino IDE."); - while (1) delay(1000); - } - Serial.printf("✓ PSRAM detected: %d KB\n", ESP.getPsramSize() / 1024); - - // Allocate PSRAM - if (!allocatePSRAM()) { - Serial.println("✗ PSRAM allocation failed!"); - while (1) delay(1000); - } - - // Initialize mutexes - sdMutex = xSemaphoreCreateMutex(); - i2cMutex = xSemaphoreCreateMutex(); - - // Initialize RTC - rtcWire.begin(); - rtcWire.setClock(400000); - - rtcWire.beginTransmission(DS3231_ADDRESS); - if (rtcWire.endTransmission() == 0) { - timeSyncStatus.rtcAvailable = true; - Serial.println("✓ RTC DS3231 detected"); - - struct tm timeinfo; - if (readRTC(&timeinfo)) { - updateSystemTime(&timeinfo); - Serial.println("✓ System time synchronized with RTC"); - } - } else { - Serial.println("⚠ RTC not detected"); - } - - // Initialize CAN FD - if (!initCANFD()) { - Serial.println("\n⚠️ CAN FD initialization failed!"); - Serial.println("⚠️ System will continue without CAN FD"); - Serial.println("⚠️ Please check hardware connections\n"); - // Don't halt - continue with other features - } - - // Initialize SD Card (SDIO 4-bit) - Serial.println("\nInitializing SD Card (SDIO 4-bit)..."); - SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0, SDIO_D1, SDIO_D2, SDIO_D3); - - if (!SD_MMC.begin("/sdcard", false)) { // false = 4-bit mode - Serial.println("⚠ 4-bit mode failed, trying 1-bit..."); - SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0); - if (!SD_MMC.begin("/sdcard", true)) { - Serial.println("✗ SD Card initialization failed!"); - while (1) delay(1000); - } - Serial.println("✓ SD Card initialized (1-bit mode)"); - } else { - Serial.println("✓ SD Card initialized (4-bit mode)"); - } - - uint64_t cardSize = SD_MMC.cardSize() / (1024 * 1024); - Serial.printf(" Card Size: %llu MB\n", cardSize); - Serial.printf(" Card Type: %s\n", - SD_MMC.cardType() == CARD_MMC ? "MMC" : - SD_MMC.cardType() == CARD_SD ? "SD" : - SD_MMC.cardType() == CARD_SDHC ? "SDHC" : "Unknown"); - - // Load preferences - preferences.begin("can-logger", false); - - // WiFi settings - preferences.getString("wifi_ssid", wifiSSID, sizeof(wifiSSID)); - if (strlen(wifiSSID) == 0) strcpy(wifiSSID, "Byun_CANFD_Logger"); - - preferences.getString("wifi_pass", wifiPassword, sizeof(wifiPassword)); - if (strlen(wifiPassword) == 0) strcpy(wifiPassword, "12345678"); - - staEnable = preferences.getBool("sta_enable", false); - preferences.getString("sta_ssid", staSSID, sizeof(staSSID)); - preferences.getString("sta_pass", staPassword, sizeof(staPassword)); - - // CAN settings - canfdSettings.arbitrationBitRate = preferences.getUInt("arb_rate", 500000); - canfdSettings.dataBitRateFactor = preferences.getUChar("data_factor", 4); // x4 - canfdSettings.fdMode = preferences.getBool("fd_mode", true); - canfdSettings.listenOnly = preferences.getBool("listen_only", false); - canfdSettings.loopback = preferences.getBool("loopback", false); - - preferences.end(); - - Serial.println("\n========================================"); - Serial.println(" Loaded Settings"); - Serial.println("========================================"); - Serial.printf("WiFi SSID: %s\n", wifiSSID); - Serial.printf("STA Enable: %s\n", staEnable ? "Yes" : "No"); - if (staEnable && strlen(staSSID) > 0) { - Serial.printf("STA SSID: %s\n", staSSID); - } - Serial.printf("CAN Mode: %s\n", canfdSettings.fdMode ? "CAN FD" : "Classic CAN"); - Serial.printf("Bit Rate: %d bps\n", canfdSettings.arbitrationBitRate); - if (canfdSettings.fdMode) { - Serial.printf("Data Factor: x%d\n", canfdSettings.dataBitRateFactor); - } - Serial.println("========================================\n"); - - // Initialize WiFi - Serial.println("\nInitializing WiFi..."); - - if (staEnable && strlen(staSSID) > 0) { - // APSTA Mode: AP + Station - Serial.println("Starting APSTA mode (AP + Station)"); - WiFi.mode(WIFI_AP_STA); - - // Start AP - WiFi.softAP(wifiSSID, wifiPassword); - Serial.printf("✓ WiFi AP Started\n"); - Serial.printf(" SSID: %s\n", wifiSSID); - Serial.printf(" AP IP: %s\n", WiFi.softAPIP().toString().c_str()); - - // Connect to external WiFi - Serial.printf("\nConnecting to WiFi: %s\n", staSSID); - WiFi.begin(staSSID, staPassword); - - int attempts = 0; - while (WiFi.status() != WL_CONNECTED && attempts < 20) { - delay(500); - Serial.print("."); - attempts++; - } - - if (WiFi.status() == WL_CONNECTED) { - staIP = WiFi.localIP(); - Serial.printf("\n✓ WiFi Connected!\n"); - Serial.printf(" Station IP: %s\n", staIP.toString().c_str()); - } else { - Serial.println("\n✗ WiFi Connection Failed"); - Serial.println(" AP mode still active"); - } - } else { - // AP Mode only - Serial.println("Starting AP mode"); - WiFi.mode(WIFI_AP); - WiFi.softAP(wifiSSID, wifiPassword); - Serial.printf("✓ WiFi AP Started\n"); - Serial.printf(" SSID: %s\n", wifiSSID); - Serial.printf(" IP: %s\n", WiFi.softAPIP().toString().c_str()); - } - - // Initialize Web Server - setupWebRoutes(); - server.begin(); - Serial.println("✓ Web Server started"); - - // Create Queues - if (!createQueues()) { - Serial.println("✗ Queue creation failed!"); - while (1) delay(1000); - } - - // Attach CAN interrupt - pinMode(CAN_INT_PIN, INPUT_PULLUP); - attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING); - Serial.println("✓ CAN interrupt attached"); - - // Create Tasks - Serial.println("\nCreating FreeRTOS Tasks..."); - - // Core 1: Real-time processing - xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 16384, NULL, 24, &canRxTaskHandle, 1); - xTaskCreatePinnedToCore(txTask, "TX", 4096, NULL, 3, NULL, 1); - xTaskCreatePinnedToCore(sequenceTask, "SEQ", 4096, NULL, 2, NULL, 1); - xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 1); - - // Core 0: I/O tasks - xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 20480, NULL, 8, &sdWriteTaskHandle, 0); - xTaskCreatePinnedToCore(sdFlushTask, "SD_FLUSH", 4096, NULL, 9, &sdFlushTaskHandle, 0); - xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 12288, NULL, 4, &webTaskHandle, 0); - xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", 6144, NULL, 6, &serialRxTaskHandle, 0); - xTaskCreatePinnedToCore(serial2RxTask, "SERIAL2_RX", 6144, NULL, 6, &serial2RxTaskHandle,0); - - if (timeSyncStatus.rtcAvailable) { - xTaskCreatePinnedToCore(rtcSyncTask, "RTC_SYNC", 3072, NULL, 0, &rtcTaskHandle, 0); - } - - Serial.println("✓ All tasks created"); - - Serial.println("\n========================================"); - Serial.println(" CAN FD Logger Ready!"); - Serial.println("========================================"); - Serial.printf(" Web Interface: http://%s\n", WiFi.softAPIP().toString().c_str()); - Serial.printf(" CAN FD Mode: %s\n", canfdSettings.fdMode ? "Enabled" : "Classic CAN"); - Serial.printf(" Arbitration: %d bps\n", canfdSettings.arbitrationBitRate); - Serial.printf(" Data Factor: x%d\n", canfdSettings.dataBitRateFactor); - Serial.printf(" Free PSRAM: %d KB\n", ESP.getFreePsram() / 1024); - Serial.println("========================================\n"); - - // Initialize WebSocket - ws.begin(); - ws.onEvent(webSocketEvent); - Serial.println("✓ WebSocket server started on port 81"); +// ============================================================ +// Web Update Task (placeholder) +// ============================================================ +void webUpdateTask(void *p) { + while (1) { vTaskDelay(pdMS_TO_TICKS(50)); } } -// ======================================== -// WebSocket Event Handler -// ======================================== -void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) { - switch(type) { - case WStype_DISCONNECTED: - Serial.printf("[%u] Disconnected!\n", num); - break; - - case WStype_CONNECTED: - { - IPAddress ip = ws.remoteIP(num); - Serial.printf("[%u] Connected from %d.%d.%d.%d\n", num, ip[0], ip[1], ip[2], ip[3]); - - // Send initial status - StaticJsonDocument<512> doc; - doc["type"] = "status"; - doc["canReady"] = (canfd != nullptr); - doc["sdReady"] = (SD_MMC.cardType() != CARD_NONE); - doc["canFrames"] = canFrameCount; - doc["queueUsage"] = uxQueueMessagesWaiting(canQueue); - doc["canErrors"] = canErrorCount; - doc["freePsram"] = ESP.getFreePsram() / 1024; - doc["logging"] = loggingEnabled; - - String response; - serializeJson(doc, response); - ws.sendTXT(num, response); +// ============================================================ +// RTC 동기화 Task (60초 간격) +// ============================================================ +void rtcSyncTask(void *p) { + Serial.println("✓ RTC Sync Task 시작"); + while (1) { + vTaskDelay(pdMS_TO_TICKS(60000)); + struct tm t; + if (readRTC(&t)) { + updateSystemTime(&t); + timeSyncStatus.rtcSyncCount++; + // 필요시 로그: Serial.printf("RTC 동기화 #%d\n", timeSyncStatus.rtcSyncCount); + } + } +} + +// ============================================================ +// ★ TX Task - CAN_BUS_Shield 전송 API +// CAN.sendMsgBuf(id, ext, len, buf) ← FD/Classic 통일 +// DLC > 8 이면 라이브러리가 FD 프레임으로 자동 처리 +// ============================================================ +void txTask(void *p) { + Serial.println("✓ TX Task 시작"); + while (1) { + if (canInitialized) { + uint32_t now = millis(); + for (int i = 0; i < MAX_TX_MESSAGES; i++) { + TxMessage &tx = txMessages[i]; + if (!tx.active) continue; + if (now - tx.lastSent < tx.interval) continue; + + int8_t ret; + // ★ CAN_BUS_Shield: sendMsgBufFD 없음 → sendMsgBuf로 통일 + // 라이브러리가 DLC>8이면 FD로 자동 처리 + ret = CAN.sendMsgBuf(tx.id, tx.extended ? 1 : 0, tx.dlc, tx.data); + + if (ret == CAN_OK) tx.lastSent = now; } + } + vTaskDelay(pdMS_TO_TICKS(5)); + } +} + +// ============================================================ +// Sequence Task +// ============================================================ +void sequenceTask(void *p) { + Serial.println("✓ Sequence Task 시작"); + while (1) { + if (canInitialized && seqRuntime.running && + seqRuntime.activeSequenceIndex >= 0) { + + CANSequence &seq = sequences[seqRuntime.activeSequenceIndex]; + SequenceStep &step = seq.steps[seqRuntime.currentStep]; + + if (millis() - seqRuntime.lastStepTime >= step.delayMs) { + // ★ CAN_BUS_Shield: sendMsgBuf로 통일 (FD 자동 처리) + CAN.sendMsgBuf(step.canId, step.extended?1:0, step.dlc, step.data); + + seqRuntime.lastStepTime = millis(); + seqRuntime.currentStep++; + + if (seqRuntime.currentStep >= seq.stepCount) { + seqRuntime.currentStep = 0; + seqRuntime.currentRepeat++; + if (seq.repeatMode == 0 || + (seq.repeatMode == 1 && seqRuntime.currentRepeat >= seq.repeatCount)) + seqRuntime.running = false; + } + } + } + vTaskDelay(pdMS_TO_TICKS(1)); + } +} + +// ============================================================ +// WebSocket 이벤트 +// ============================================================ +void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) { + switch (type) { + case WStype_DISCONNECTED: + Serial.printf("[WS%u] 연결 해제\n", num); break; - - case WStype_TEXT: - { - String message = String((char*)payload); - StaticJsonDocument<512> doc; - DeserializationError error = deserializeJson(doc, message); - - if (!error) { - const char* cmd = doc["cmd"]; - - if (strcmp(cmd, "getStatus") == 0) { - StaticJsonDocument<512> resp; - resp["type"] = "status"; - resp["canReady"] = (canfd != nullptr); - resp["sdReady"] = (SD_MMC.cardType() != CARD_NONE); - resp["canFrames"] = canFrameCount; - resp["queueUsage"] = uxQueueMessagesWaiting(canQueue); - resp["canErrors"] = canErrorCount; - resp["freePsram"] = ESP.getFreePsram() / 1024; - resp["logging"] = loggingEnabled; - - String response; - serializeJson(resp, response); - ws.sendTXT(num, response); - } + + case WStype_CONNECTED: { + Serial.printf("[WS%u] 연결: %s\n", num, ws.remoteIP(num).toString().c_str()); + char buf[300]; + snprintf(buf, sizeof(buf), + "{\"type\":\"status\",\"canReady\":%s,\"sdReady\":%s," + "\"frames\":%lu,\"errors\":%lu,\"psram\":%u,\"logging\":%s," + "\"mode\":\"%s\",\"arb\":%u,\"data\":%u}", + canInitialized ? "true":"false", + SD_MMC.cardType()!=CARD_NONE ? "true":"false", + canFrameCount, canErrorCount, + ESP.getFreePsram()/1024, + loggingEnabled ? "true":"false", + canSettings.fdMode ? "fd":"classic", + canSettings.arbRateKbps, + canSettings.dataRateKbps); + ws.sendTXT(num, buf); + break; + } + + case WStype_TEXT: { + StaticJsonDocument<256> doc; + if (!deserializeJson(doc, payload, length)) { + const char *cmd = doc["cmd"]; + if (!cmd) break; + if (strcmp(cmd, "getStatus") == 0) { + char buf[300]; + snprintf(buf, sizeof(buf), + "{\"type\":\"status\",\"canReady\":%s,\"sdReady\":%s," + "\"frames\":%lu,\"errors\":%lu,\"psram\":%u,\"logging\":%s}", + canInitialized ? "true":"false", + SD_MMC.cardType()!=CARD_NONE ? "true":"false", + canFrameCount, canErrorCount, + ESP.getFreePsram()/1024, + loggingEnabled ? "true":"false"); + ws.sendTXT(num, buf); } } break; + } + default: break; } } -// ======================================== -// Loop -// ======================================== +// ============================================================ +// Web Routes 설정 +// ============================================================ +void setupWebRoutes() { + // 페이지들 + server.on("/", HTTP_GET, []() { server.send_P(200, "text/html", canfd_index_html); }); + server.on("/settings", HTTP_GET, []() { server.send_P(200, "text/html", canfd_settings_html); }); + server.on("/graph", HTTP_GET, []() { server.send_P(200, "text/html", canfd_graph_html); }); + server.on("/graph-view", HTTP_GET, []() { server.send_P(200, "text/html", canfd_graph_viewer_html); }); + + // 상태 API + server.on("/status", HTTP_GET, []() { + char buf[512]; + snprintf(buf, sizeof(buf), + "{\"canReady\":%s,\"sdReady\":%s,\"frames\":%lu," + "\"queueUsed\":%d,\"errors\":%lu,\"psram\":%u," + "\"logging\":%s,\"mode\":\"%s\"," + "\"arb\":%u,\"data\":%u,\"sdErr\":%lu}", + canInitialized ? "true":"false", + SD_MMC.cardType()!=CARD_NONE ? "true":"false", + canFrameCount, + uxQueueMessagesWaiting(canQueue), + canErrorCount, + ESP.getFreePsram()/1024, + loggingEnabled ? "true":"false", + canSettings.fdMode ? "fd":"classic", + canSettings.arbRateKbps, + canSettings.dataRateKbps, + sdWriteErrorCount); + server.send(200, "application/json", buf); + }); + + // 설정 GET + server.on("/settings/get", HTTP_GET, []() { + StaticJsonDocument<1024> doc; + doc["wifiSSID"] = wifiSSID; + doc["wifiPassword"] = wifiPassword; + doc["staEnable"] = staEnable; + doc["staSSID"] = staSSID; + doc["staPassword"] = staPassword; + doc["staConnected"] = (WiFi.status() == WL_CONNECTED); + doc["staIP"] = staIP.toString(); + doc["fdMode"] = canSettings.fdMode; + doc["arbRate"] = canSettings.arbRateKbps; + doc["dataRate"] = canSettings.dataRateKbps; + doc["controllerMode"] = canSettings.controllerMode; + doc["logFormat"] = canSettings.logFormat; + String resp; serializeJson(doc, resp); + server.send(200, "application/json", resp); + }); + + // 설정 저장 + 재부팅 (설정 반영) + server.on("/settings/save", HTTP_POST, []() { + StaticJsonDocument<1024> doc; + bool ok = false; + if (server.hasArg("plain") && !deserializeJson(doc, server.arg("plain"))) { + preferences.begin("can-logger", false); + + String s; + s = doc["wifiSSID"] | "Byun_CANFD_Logger"; + strlcpy(wifiSSID, s.c_str(), sizeof(wifiSSID)); + preferences.putString("wifi_ssid", wifiSSID); + + s = doc["wifiPassword"] | "12345678"; + strlcpy(wifiPassword, s.c_str(), sizeof(wifiPassword)); + preferences.putString("wifi_pass", wifiPassword); + + staEnable = doc["staEnable"] | false; + preferences.putBool("sta_enable", staEnable); + + s = doc["staSSID"] | ""; + strlcpy(staSSID, s.c_str(), sizeof(staSSID)); + preferences.putString("sta_ssid", staSSID); + + s = doc["staPassword"] | ""; + strlcpy(staPassword, s.c_str(), sizeof(staPassword)); + preferences.putString("sta_pass", staPassword); + + canSettings.fdMode = doc["fdMode"] | true; + canSettings.arbRateKbps = doc["arbRate"] | 500; + canSettings.dataRateKbps = doc["dataRate"] | 2000; + canSettings.controllerMode = doc["controllerMode"] | 0; + canSettings.logFormat = doc["logFormat"] | 0; + + preferences.putBool("fd_mode", canSettings.fdMode); + preferences.putUInt("arb_rate", canSettings.arbRateKbps); + preferences.putUInt("data_rate", canSettings.dataRateKbps); + preferences.putUChar("ctrl_mode", canSettings.controllerMode); + preferences.putUChar("log_fmt", canSettings.logFormat); + preferences.end(); + + Serial.printf("[설정저장] FD:%d Arb:%ukbps Data:%ukbps Mode:%d Fmt:%d\n", + canSettings.fdMode, canSettings.arbRateKbps, + canSettings.dataRateKbps, canSettings.controllerMode, canSettings.logFormat); + ok = true; + } + server.send(200, "application/json", + ok ? "{\"success\":true,\"message\":\"저장 완료. 재초기화하려면 /can/reinit 호출\"}" + : "{\"success\":false}"); + }); + + // 파일 목록 + server.on("/files/list", HTTP_GET, []() { + String json = "{\"files\":["; + bool first = true; + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { + File root = SD_MMC.open("/"); + if (root) { + File f = root.openNextFile(); + while (f) { + if (!f.isDirectory()) { + if (!first) json += ","; + char cmt[MAX_COMMENT_LEN] = ""; + for (int i = 0; i < MAX_FILE_COMMENTS; i++) { + if (strcmp(fileComments[i].filename, f.name()) == 0) { + strlcpy(cmt, fileComments[i].comment, sizeof(cmt)); + break; + } + } + json += "{\"name\":\""; json += f.name(); + json += "\",\"size\":"; json += f.size(); + json += ",\"comment\":\""; json += cmt; + json += "\"}"; + first = false; + } + f = root.openNextFile(); + } + } + xSemaphoreGive(sdMutex); + } + json += "]}"; + server.send(200, "application/json", json); + }); + + // ★ 파일 다운로드 (SD DMA 충돌 방지 + 3회 재시도) + server.on("/download", HTTP_GET, []() { + if (!server.hasArg("file")) { server.send(400, "text/plain", "file 파라미터 필요"); return; } + + String filename = server.arg("file"); + String filepath = "/" + filename; + Serial.printf("다운로드: %s\n", filename.c_str()); + + bool wasLogging = loggingEnabled; + if (wasLogging) { loggingEnabled = false; vTaskDelay(pdMS_TO_TICKS(200)); } + if (fileBufferPos > 0) flushBufferToSD(); + + // 파일 크기 확인 + size_t fileSize = 0; + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(2000)) != pdTRUE) { + server.send(503, "text/plain", "SD 사용 중"); + if (wasLogging) loggingEnabled = true; + return; + } + { + File probe = SD_MMC.open(filepath.c_str(), FILE_READ); + if (!probe) { + xSemaphoreGive(sdMutex); + server.send(404, "text/plain", "파일 없음"); + if (wasLogging) loggingEnabled = true; + return; + } + fileSize = probe.size(); + probe.close(); + } + xSemaphoreGive(sdMutex); + + Serial.printf(" 크기: %u bytes\n", fileSize); + + WiFiClient client = server.client(); + client.setTimeout(30); + + // HTTP 헤더 수동 전송 (server.setContentLength 방식 대신) + String hdr = "HTTP/1.1 200 OK\r\n" + "Content-Type: application/octet-stream\r\n" + "Content-Disposition: attachment; filename=\"" + filename + "\"\r\n" + "Content-Length: " + String(fileSize) + "\r\n" + "Connection: close\r\n\r\n"; + client.print(hdr); + + // PSRAM에서 전송 버퍼 할당 + const size_t CHUNK = 8192; + uint8_t *buf = (uint8_t*)ps_malloc(CHUNK); + if (!buf) buf = (uint8_t*)malloc(CHUNK); + if (!buf) { if (wasLogging) loggingEnabled = true; return; } + + size_t totalSent = 0; + bool ok = true; + uint32_t lastLog = 0; + + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(2000)) == pdTRUE) { + File file = SD_MMC.open(filepath.c_str(), FILE_READ); + if (file) { + while (client.connected()) { + // ★ SD 읽기 3회 재시도 (DMA 오류 0x109 대응) + size_t bytesRead = 0; + for (int retry = 0; retry < 3; retry++) { + bytesRead = file.read(buf, CHUNK); + if (bytesRead > 0) break; + if (!file.available()) break; // 진짜 EOF + xSemaphoreGive(sdMutex); + vTaskDelay(pdMS_TO_TICKS(20)); + xSemaphoreTake(sdMutex, pdMS_TO_TICKS(500)); + Serial.printf(" SD 재시도 %d at %u\n", retry+1, totalSent); + } + + if (bytesRead == 0) { + if (totalSent < fileSize) { + ok = false; + Serial.printf("✗ SD 오류: %u/%u\n", totalSent, fileSize); + } + break; + } + + // TCP 전송 (부분 전송 재시도) + size_t written = 0; + uint32_t txStart = millis(); + while (written < bytesRead) { + if (!client.connected() || millis()-txStart > 15000) { ok=false; break; } + size_t sent = client.write(buf+written, bytesRead-written); + if (sent > 0) { written += sent; totalSent += sent; txStart = millis(); } + else { + // WiFi TCP 버퍼 꽉 참 → DMA 충돌 방지 yield + xSemaphoreGive(sdMutex); + vTaskDelay(pdMS_TO_TICKS(5)); + xSemaphoreTake(sdMutex, pdMS_TO_TICKS(200)); + } + esp_task_wdt_reset(); + } + if (!ok) break; + + // 진행 로그 (512KB마다) + if (totalSent - lastLog >= 512*1024) { + Serial.printf(" 진행: %u/%u (%.1f%%)\n", + totalSent, fileSize, 100.0f*totalSent/fileSize); + lastLog = totalSent; + } + + // ★ SD DMA ↔ WiFi DMA 충돌 방지: 청크 사이 2ms yield + xSemaphoreGive(sdMutex); + vTaskDelay(pdMS_TO_TICKS(2)); + xSemaphoreTake(sdMutex, pdMS_TO_TICKS(200)); + } + file.close(); + } else { ok = false; } + xSemaphoreGive(sdMutex); + } else { ok = false; } + + free(buf); + client.stop(); + Serial.printf("%s 다운로드: %u/%u bytes\n", ok?"✓":"✗", totalSent, fileSize); + if (wasLogging) loggingEnabled = true; + }); + + // 파일 삭제 + server.on("/files/delete", HTTP_POST, []() { + StaticJsonDocument<512> doc; + bool ok = false; + if (server.hasArg("plain") && !deserializeJson(doc, server.arg("plain"))) { + // 단일 또는 배열 삭제 지원 + if (doc["filenames"].is()) { + JsonArray arr = doc["filenames"].as(); + for (const char *fn : arr) { + if (loggingEnabled && String(fn) == String(currentCanFilename).substring(1)) continue; + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(500)) == pdTRUE) { + SD_MMC.remove(("/" + String(fn)).c_str()); + xSemaphoreGive(sdMutex); + } + } + ok = true; + } else { + String fn = doc["filename"] | ""; + if (fn.length() > 0) { + if (!loggingEnabled || fn != String(currentCanFilename).substring(1)) { + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { + ok = SD_MMC.remove(("/" + fn).c_str()); + xSemaphoreGive(sdMutex); + } + } + } + } + } + server.send(200, "application/json", + ok ? "{\"success\":true}" : "{\"success\":false}"); + }); + + // 코멘트 저장 + server.on("/files/comment", HTTP_POST, []() { + StaticJsonDocument<512> doc; + if (server.hasArg("plain") && !deserializeJson(doc, server.arg("plain"))) { + const char *fn = doc["filename"]; + const char *cmt = doc["comment"]; + if (fn && cmt) { + for (int i = 0; i < MAX_FILE_COMMENTS; i++) { + if (strcmp(fileComments[i].filename, fn) == 0 || + strlen(fileComments[i].filename) == 0) { + strlcpy(fileComments[i].filename, fn, MAX_FILENAME_LEN); + strlcpy(fileComments[i].comment, cmt, MAX_COMMENT_LEN); + break; + } + } + } + } + server.send(200, "application/json", "{\"success\":true}"); + }); + + // 로깅 시작 + server.on("/logging/start", HTTP_POST, []() { + if (loggingEnabled) { + server.send(200,"application/json","{\"success\":false,\"message\":\"이미 로깅 중\"}"); + return; + } + if (!canInitialized) { + server.send(200,"application/json","{\"success\":false,\"message\":\"CAN 초기화 안됨\"}"); + return; + } + + char fn[MAX_FILENAME_LEN]; + time_t now = time(nullptr); + struct tm ti; localtime_r(&now, &ti); + const char *ext = (canSettings.logFormat == 0) ? "pcap" : "csv"; + snprintf(fn, sizeof(fn), "/%s_%04d%02d%02d_%02d%02d%02d.%s", + canSettings.fdMode ? "canfd" : "can", + ti.tm_year+1900, ti.tm_mon+1, ti.tm_mday, + ti.tm_hour, ti.tm_min, ti.tm_sec, ext); + + bool ok = false; + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { + canLogFile = SD_MMC.open(fn, FILE_WRITE); + if (canLogFile) { + if (canSettings.logFormat == 0) { + // PCAP 글로벌 헤더 + PCAPGlobalHeader h = {0xa1b2c3d4,2,4,0,0,65535,227}; + canLogFile.write((uint8_t*)&h, sizeof(h)); + canLogFile.flush(); + } else { + canLogFile.println("timestamp_us,id,len,fd,brs,data"); + } + strlcpy(currentCanFilename, fn, sizeof(currentCanFilename)); + loggingEnabled = true; + ok = true; + Serial.printf("✓ 로깅 시작: %s (%s)\n", fn, + canSettings.logFormat==0?"PCAP":"CSV"); + } + xSemaphoreGive(sdMutex); + } + server.send(200, "application/json", + ok ? "{\"success\":true}" : "{\"success\":false,\"message\":\"파일 생성 실패\"}"); + }); + + // 로깅 중지 + server.on("/logging/stop", HTTP_POST, []() { + if (!loggingEnabled) { + server.send(200,"application/json","{\"success\":false,\"message\":\"로깅 중 아님\"}"); + return; + } + loggingEnabled = false; + vTaskDelay(pdMS_TO_TICKS(200)); + flushBufferToSD(); + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { + if (canLogFile) { canLogFile.flush(); canLogFile.close(); } + xSemaphoreGive(sdMutex); + } + Serial.println("✓ 로깅 중지"); + server.send(200, "application/json", "{\"success\":true}"); + }); + + // 최근 CAN 메시지 조회 + server.on("/can/recent", HTTP_GET, []() { + String json = "{\"messages\":["; + for (int i = 0; i < recentDataCount; i++) { + if (i > 0) json += ","; + char dataBuf[130] = {}; + for (int j = 0; j < recentData[i].msg.len; j++) + sprintf(dataBuf + j*2, "%02X", recentData[i].msg.data[j]); + char entry[256]; + snprintf(entry, sizeof(entry), + "{\"id\":\"%lX\",\"ext\":%s,\"len\":%d,\"fd\":%s,\"count\":%lu,\"data\":\"%s\"}", + recentData[i].msg.id, + recentData[i].msg.ext ? "true":"false", + recentData[i].msg.len, + recentData[i].msg.fd ? "true":"false", + recentData[i].count, dataBuf); + json += entry; + } + json += "]}"; + server.send(200, "application/json", json); + }); + + // TX 메시지 설정 + server.on("/tx/set", HTTP_POST, []() { + StaticJsonDocument<1024> doc; + if (server.hasArg("plain") && !deserializeJson(doc, server.arg("plain"))) { + int idx = doc["index"] | -1; + if (idx >= 0 && idx < MAX_TX_MESSAGES) { + TxMessage &tx = txMessages[idx]; + String idStr = doc["id"] | "0"; + tx.id = strtoul(idStr.c_str(), nullptr, 16); + tx.extended = doc["extended"] | false; + tx.dlc = doc["dlc"] | 8; + tx.interval = doc["interval"] | 100; + tx.active = doc["active"] | false; + tx.fd = doc["fd"] | false; + tx.brs = doc["brs"] | false; + tx.lastSent = 0; + String dataStr = doc["data"] | "0000000000000000"; + for (int i = 0; i < tx.dlc && (i*2+1) < (int)dataStr.length(); i++) + tx.data[i] = strtoul(dataStr.substring(i*2, i*2+2).c_str(), nullptr, 16); + } + } + server.send(200, "application/json", "{\"success\":true}"); + }); + + // TX 목록 조회 + server.on("/tx/list", HTTP_GET, []() { + String json = "{\"messages\":["; + for (int i = 0; i < MAX_TX_MESSAGES; i++) { + if (i > 0) json += ","; + char dataBuf[130] = {}; + for (int j = 0; j < txMessages[i].dlc; j++) + sprintf(dataBuf + j*2, "%02X", txMessages[i].data[j]); + char entry[256]; + snprintf(entry, sizeof(entry), + "{\"index\":%d,\"id\":\"%lX\",\"dlc\":%d,\"interval\":%lu," + "\"active\":%s,\"fd\":%s,\"data\":\"%s\"}", + i, txMessages[i].id, txMessages[i].dlc, txMessages[i].interval, + txMessages[i].active ? "true":"false", + txMessages[i].fd ? "true":"false", dataBuf); + json += entry; + } + json += "]}"; + server.send(200, "application/json", json); + }); + + // 시간 동기화 (웹 클라이언트로부터) + server.on("/time/sync", HTTP_POST, []() { + StaticJsonDocument<128> doc; + if (server.hasArg("plain") && !deserializeJson(doc, server.arg("plain"))) { + uint64_t epochMs = doc["epochMs"] | 0; + if (epochMs > 0) { + struct timeval tv; + tv.tv_sec = epochMs / 1000; + tv.tv_usec = (epochMs % 1000) * 1000; + settimeofday(&tv, NULL); + timeSyncStatus.synchronized = true; + Serial.printf("✓ 시간 동기화: %llu ms (웹)\n", epochMs); + } + } + server.send(200, "application/json", "{\"success\":true}"); + }); + + // ★ CAN 재초기화 (설정 변경 후 호출) + server.on("/can/reinit", HTTP_POST, []() { + bool wasLogging = loggingEnabled; + if (wasLogging) { loggingEnabled = false; vTaskDelay(pdMS_TO_TICKS(300)); } + canInitialized = false; + detachInterrupt(digitalPinToInterrupt(CAN_INT_PIN)); + delay(100); + + bool ok = initCANFD(); + if (ok) attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING); + if (wasLogging && ok) loggingEnabled = true; + + server.send(200, "application/json", + ok ? "{\"success\":true,\"message\":\"CAN 재초기화 성공\"}" + : "{\"success\":false,\"message\":\"재초기화 실패\"}"); + }); + + server.onNotFound([]() { server.send(404, "text/plain", "Not Found"); }); +} + +// ============================================================ +// Setup +// ============================================================ +void setup() { + Serial.begin(115200); + delay(1000); + + Serial.println("\n\n========================================"); + Serial.println(" ESP32-S3 CAN FD Logger v4.0"); + Serial.println(" ★ Seeed_Arduino_CAN (MCP2518FD)"); + Serial.println(" SPI 4MHz 고정 - 브레드보드 안정적"); + Serial.println("========================================\n"); + + // PSRAM 확인 + if (!psramFound()) { + Serial.println("✗ PSRAM 없음! Arduino IDE → PSRAM: OPI PSRAM 선택 필요"); + while (1) delay(1000); + } + Serial.printf("✓ PSRAM: %u KB\n", ESP.getPsramSize()/1024); + + // PSRAM 할당 + if (!allocatePSRAM()) { Serial.println("✗ PSRAM 할당 실패"); while(1) delay(1000); } + + // 뮤텍스 생성 + sdMutex = xSemaphoreCreateMutex(); + i2cMutex = xSemaphoreCreateMutex(); + + // RTC DS3231 초기화 + rtcWire.begin(); + rtcWire.setClock(400000); + rtcWire.beginTransmission(DS3231_ADDRESS); + if (rtcWire.endTransmission() == 0) { + timeSyncStatus.rtcAvailable = true; + Serial.println("✓ RTC DS3231 감지"); + struct tm t; + if (readRTC(&t)) { + updateSystemTime(&t); + Serial.println("✓ RTC → 시스템 시간 동기화 완료"); + } + } else { + Serial.println("⚠ RTC 없음 → 웹 연결 시 시간 자동 동기화"); + } + + // 설정 로드 + preferences.begin("can-logger", true); + preferences.getString("wifi_ssid", wifiSSID, sizeof(wifiSSID)); + if (strlen(wifiSSID)==0) strlcpy(wifiSSID, "Byun_CANFD_Logger", sizeof(wifiSSID)); + preferences.getString("wifi_pass", wifiPassword, sizeof(wifiPassword)); + if (strlen(wifiPassword)==0) strlcpy(wifiPassword, "12345678", sizeof(wifiPassword)); + staEnable = preferences.getBool("sta_enable", false); + preferences.getString("sta_ssid", staSSID, sizeof(staSSID)); + preferences.getString("sta_pass", staPassword, sizeof(staPassword)); + canSettings.fdMode = preferences.getBool("fd_mode", true); + canSettings.arbRateKbps = preferences.getUInt("arb_rate", 500); + canSettings.dataRateKbps = preferences.getUInt("data_rate", 2000); + canSettings.controllerMode = preferences.getUChar("ctrl_mode", 0); + canSettings.logFormat = preferences.getUChar("log_fmt", 0); + preferences.end(); + + Serial.println("\n[로드된 설정]"); + Serial.printf(" WiFi AP: %s\n", wifiSSID); + Serial.printf(" STA: %s\n", staEnable ? staSSID : "비활성"); + Serial.printf(" CAN: %s | Arb %u kbps | Data %u kbps\n", + canSettings.fdMode ? "FD":"Classic", + canSettings.arbRateKbps, canSettings.dataRateKbps); + Serial.printf(" 로그: %s | 컨트롤러모드: %d\n", + canSettings.logFormat==0 ? "PCAP":"CSV", canSettings.controllerMode); + + // SD 카드 초기화 + initSDCard(); + + // ★ CAN FD 초기화 (Seeed 라이브러리) + if (!initCANFD()) { + Serial.println("⚠ CAN 없이 계속 동작"); + Serial.println(" → http://192.168.4.1/can/reinit 으로 재시도 가능"); + } + + // WiFi 초기화 + Serial.println("\nWiFi 초기화..."); + if (staEnable && strlen(staSSID) > 0) { + WiFi.mode(WIFI_AP_STA); + WiFi.softAP(wifiSSID, wifiPassword); + Serial.printf("✓ AP: %s (%s)\n", wifiSSID, WiFi.softAPIP().toString().c_str()); + WiFi.begin(staSSID, staPassword); + Serial.printf("STA 연결: %s", staSSID); + for (int i = 0; i < 20 && WiFi.status()!=WL_CONNECTED; i++) { + delay(500); Serial.print("."); + } + if (WiFi.status()==WL_CONNECTED) { + staIP = WiFi.localIP(); + Serial.printf("\n✓ STA IP: %s\n", staIP.toString().c_str()); + } else { Serial.println("\n✗ STA 실패 (AP만 동작)"); } + } else { + WiFi.mode(WIFI_AP); + WiFi.softAP(wifiSSID, wifiPassword); + Serial.printf("✓ AP: %s | IP: %s\n", wifiSSID, WiFi.softAPIP().toString().c_str()); + } + + // 웹서버 & WebSocket + setupWebRoutes(); + server.begin(); + ws.begin(); + ws.onEvent(webSocketEvent); + Serial.println("✓ WebServer:80 / WebSocket:81 시작"); + + // CAN 인터럽트 연결 + pinMode(CAN_INT_PIN, INPUT_PULLUP); + if (canInitialized) { + attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING); + Serial.printf("✓ CAN 인터럽트 → GPIO%d (FALLING)\n", CAN_INT_PIN); + } + + // CAN 큐 생성 + canQueue = xQueueCreateStatic( + CAN_QUEUE_SIZE, sizeof(LoggedCANFrame), + canQueueStorage, canQueueBuffer); + if (!canQueue) { Serial.println("✗ CAN 큐 생성 실패"); while(1) delay(1000); } + Serial.printf("✓ CAN 큐: %d 슬롯\n", CAN_QUEUE_SIZE); + + // ───────────────────────────────────── + // FreeRTOS Task 배분 + // + // Core 1 (CAN 실시간): + // canRxTask Priority 24 ← 최우선, 인터럽트 처리 + // txTask Priority 3 + // sequenceTask Priority 2 + // + // Core 0 (I/O / WiFi): + // sdFlushTask Priority 9 ← SD 먼저 플러시 + // sdWriteTask Priority 8 + // webUpdateTask Priority 4 + // rtcSyncTask Priority 1 (RTC 있을 때만) + // ───────────────────────────────────── + xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 16384, NULL, 24, &canRxTaskHandle, 1); + xTaskCreatePinnedToCore(txTask, "TX", 4096, NULL, 3, NULL, 1); + xTaskCreatePinnedToCore(sequenceTask, "SEQ", 4096, NULL, 2, NULL, 1); + + xTaskCreatePinnedToCore(sdFlushTask, "SD_F", 4096, NULL, 9, NULL, 0); + xTaskCreatePinnedToCore(sdWriteTask, "SD_W", 20480, NULL, 8, &sdWriteTaskHandle, 0); + xTaskCreatePinnedToCore(webUpdateTask,"WEB", 12288, NULL, 4, NULL, 0); + + if (timeSyncStatus.rtcAvailable) + xTaskCreatePinnedToCore(rtcSyncTask, "RTC", 3072, NULL, 1, NULL, 0); + + Serial.println("\n========================================"); + Serial.println(" ★ CAN FD Logger 준비 완료!"); + Serial.printf(" 웹 인터페이스: http://%s\n", WiFi.softAPIP().toString().c_str()); + Serial.printf(" CAN: %s / Arb %u kbps / Data %u kbps\n", + canSettings.fdMode ? "FD":"Classic", + canSettings.arbRateKbps, canSettings.dataRateKbps); + Serial.printf(" 여유 PSRAM: %u KB\n", ESP.getFreePsram()/1024); + Serial.println("========================================\n"); +} + +// ============================================================ +// Loop (서버 처리만 담당, 모든 로직은 Task에서 처리) +// ============================================================ void loop() { server.handleClient(); - ws.loop(); // WebSocket 처리 + ws.loop(); vTaskDelay(pdMS_TO_TICKS(10)); - - // Status output every 30 seconds + + // 30초 상태 출력 static uint32_t lastPrint = 0; if (millis() - lastPrint > 30000) { - Serial.printf("[Status] CAN: %lu frames | Queue: %d/%d | Errors: %lu | PSRAM: %d KB\n", + Serial.printf("[상태] CAN:%lu 프레임 | 큐:%d/%d | 오류:%lu | SD오류:%lu | PSRAM:%u KB\n", canFrameCount, uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE, canErrorCount, - ESP.getFreePsram() / 1024); + sdWriteErrorCount, + ESP.getFreePsram()/1024); lastPrint = millis(); } } - -// ======================================== -// Placeholder Functions (to be implemented) -// ======================================== -void webUpdateTask(void *parameter) { - while (1) { - vTaskDelay(pdMS_TO_TICKS(100)); - } -} - -void serialRxTask(void *parameter) { - while (1) { - vTaskDelay(pdMS_TO_TICKS(100)); - } -} - -void serial2RxTask(void *parameter) { - while (1) { - vTaskDelay(pdMS_TO_TICKS(100)); - } -} - -void rtcSyncTask(void *parameter) { - while (1) { - vTaskDelay(pdMS_TO_TICKS(60000)); - } -} - -void txTask(void *parameter) { - while (1) { - vTaskDelay(pdMS_TO_TICKS(100)); - } -} - -void sequenceTask(void *parameter) { - while (1) { - vTaskDelay(pdMS_TO_TICKS(100)); - } -} - -void sdMonitorTask(void *parameter) { - while (1) { - vTaskDelay(pdMS_TO_TICKS(5000)); - } -} - -void setupWebRoutes() { - // Main page - server.on("/", HTTP_GET, []() { - server.send_P(200, "text/html", canfd_index_html); - }); - - // Settings page - server.on("/settings", HTTP_GET, []() { - server.send_P(200, "text/html", canfd_settings_html); - }); - - // Graph page - server.on("/graph", HTTP_GET, []() { - server.send_P(200, "text/html", canfd_graph_html); - }); - - // Graph viewer page - server.on("/graph-view", HTTP_GET, []() { - server.send_P(200, "text/html", canfd_graph_viewer_html); - }); - - // Get settings - server.on("/settings/get", HTTP_GET, []() { - StaticJsonDocument<1024> doc; - - // WiFi settings - doc["wifiSSID"] = wifiSSID; - doc["wifiPassword"] = wifiPassword; - doc["staEnable"] = staEnable; - doc["staSSID"] = staSSID; - doc["staPassword"] = staPassword; - doc["staConnected"] = (WiFi.status() == WL_CONNECTED); - doc["staIP"] = staIP.toString(); - - // CAN settings - doc["canMode"] = canfdSettings.fdMode ? "fd" : "classic"; - doc["bitRate"] = canfdSettings.arbitrationBitRate; - doc["dataRate"] = canfdSettings.dataBitRateFactor; - - uint8_t mode = 0; - if (canfdSettings.listenOnly) mode = 1; - else if (canfdSettings.loopback) mode = 2; - doc["controllerMode"] = mode; - - String response; - serializeJson(doc, response); - server.send(200, "application/json", response); - }); - - // Save settings - server.on("/settings/save", HTTP_POST, []() { - StaticJsonDocument<1024> doc; - - if (server.hasArg("plain")) { - DeserializationError error = deserializeJson(doc, server.arg("plain")); - - if (!error) { - // WiFi settings - String newSSID = doc["wifiSSID"].as(); - String newPassword = doc["wifiPassword"].as(); - bool newStaEnable = doc["staEnable"]; - String newStaSSID = doc["staSSID"].as(); - String newStaPassword = doc["staPassword"].as(); - - // CAN settings - String canMode = doc["canMode"].as(); - uint32_t bitRate = doc["bitRate"]; - uint8_t dataRate = doc["dataRate"]; - uint8_t controllerMode = doc["controllerMode"]; - - // Save to preferences - preferences.begin("can-logger", false); - - // WiFi - preferences.putString("wifi_ssid", newSSID); - preferences.putString("wifi_pass", newPassword); - preferences.putBool("sta_enable", newStaEnable); - preferences.putString("sta_ssid", newStaSSID); - preferences.putString("sta_pass", newStaPassword); - - // CAN - preferences.putBool("fd_mode", canMode == "fd"); - preferences.putUInt("arb_rate", bitRate); - preferences.putUChar("data_factor", dataRate); - preferences.putBool("listen_only", controllerMode == 1); - preferences.putBool("loopback", controllerMode == 2); - - preferences.end(); - - doc.clear(); - doc["success"] = true; - doc["message"] = "Settings saved successfully"; - - Serial.println("\n========== Settings Saved =========="); - Serial.printf("WiFi SSID: %s\n", newSSID.c_str()); - Serial.printf("STA Enable: %s\n", newStaEnable ? "Yes" : "No"); - if (newStaEnable) { - Serial.printf("STA SSID: %s\n", newStaSSID.c_str()); - } - Serial.printf("CAN Mode: %s\n", canMode.c_str()); - Serial.printf("Bit Rate: %d\n", bitRate); - Serial.println("====================================\n"); - } else { - doc.clear(); - doc["success"] = false; - doc["message"] = "Invalid JSON"; - } - } else { - doc["success"] = false; - doc["message"] = "No data received"; - } - - String response; - serializeJson(doc, response); - server.send(200, "application/json", response); - }); - - // Status API - server.on("/status", HTTP_GET, []() { - StaticJsonDocument<512> doc; - - doc["canReady"] = (canfd != nullptr); - doc["sdReady"] = (SD_MMC.cardType() != CARD_NONE); - doc["canFrames"] = canFrameCount; - doc["queueUsage"] = uxQueueMessagesWaiting(canQueue); - doc["canErrors"] = canErrorCount; - doc["freePsram"] = ESP.getFreePsram() / 1024; - doc["logging"] = loggingEnabled; - doc["mode"] = canfdSettings.fdMode ? "fd" : "classic"; - - String response; - serializeJson(doc, response); - server.send(200, "application/json", response); - }); - - // File list API - server.on("/files/list", HTTP_GET, []() { - StaticJsonDocument<8192> doc; - JsonArray filesArray = doc.createNestedArray("files"); - - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - File root = SD_MMC.open("/"); - if (root) { - File file = root.openNextFile(); - while (file) { - if (!file.isDirectory()) { - JsonObject fileObj = filesArray.createNestedObject(); - fileObj["name"] = String(file.name()); - fileObj["size"] = file.size(); - } - file = root.openNextFile(); - } - } - xSemaphoreGive(sdMutex); - } - - String response; - serializeJson(doc, response); - server.send(200, "application/json", response); - }); - - // File download - server.on("/download", HTTP_GET, []() { - if (!server.hasArg("file")) { - server.send(400, "text/plain", "Missing file parameter"); - return; - } - - String filename = server.arg("file"); - Serial.printf("Download request: %s\n", filename.c_str()); - - // Pause logging temporarily - bool wasLogging = loggingEnabled; - if (wasLogging) { - loggingEnabled = false; - vTaskDelay(pdMS_TO_TICKS(100)); // Wait for tasks to stop - } - - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(2000)) == pdTRUE) { - String filepath = "/" + filename; - File file = SD_MMC.open(filepath.c_str(), FILE_READ); - - if (!file) { - xSemaphoreGive(sdMutex); - server.send(404, "text/plain", "File not found"); - if (wasLogging) loggingEnabled = true; - return; - } - - size_t fileSize = file.size(); - - // Send headers - server.sendHeader("Content-Type", "application/octet-stream"); - server.sendHeader("Content-Disposition", "attachment; filename=" + filename); - server.sendHeader("Content-Length", String(fileSize)); - server.setContentLength(fileSize); - server.send(200, "application/octet-stream", ""); - - // Stream file in chunks - const size_t CHUNK_SIZE = 4096; - uint8_t *buffer = (uint8_t*)malloc(CHUNK_SIZE); - - if (buffer) { - WiFiClient client = server.client(); - size_t totalSent = 0; - - while (file.available() && client.connected()) { - size_t bytesRead = file.read(buffer, CHUNK_SIZE); - if (bytesRead > 0) { - size_t sent = client.write(buffer, bytesRead); - totalSent += sent; - } - yield(); - } - - free(buffer); - Serial.printf("Download complete: %u / %u bytes\n", totalSent, fileSize); - } - - file.close(); - xSemaphoreGive(sdMutex); - } - - // Resume logging - if (wasLogging) { - loggingEnabled = true; - } - }); - - // File delete - server.on("/files/delete", HTTP_POST, []() { - StaticJsonDocument<256> doc; - - if (server.hasArg("plain")) { - DeserializationError error = deserializeJson(doc, server.arg("plain")); - - if (!error) { - String filename = doc["filename"].as(); - - // Check if file is being logged - if (loggingEnabled && filename == String(currentCanFilename).substring(1)) { - doc.clear(); - doc["success"] = false; - doc["message"] = "Cannot delete file being logged"; - } else { - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - String filepath = "/" + filename; - bool deleted = SD_MMC.remove(filepath.c_str()); - xSemaphoreGive(sdMutex); - - doc.clear(); - doc["success"] = deleted; - doc["message"] = deleted ? "File deleted" : "Delete failed"; - } else { - doc.clear(); - doc["success"] = false; - doc["message"] = "SD card busy"; - } - } - } else { - doc.clear(); - doc["success"] = false; - doc["message"] = "Invalid JSON"; - } - } else { - doc["success"] = false; - doc["message"] = "No data received"; - } - - String response; - serializeJson(doc, response); - server.send(200, "application/json", response); - }); - - // Start logging - server.on("/logging/start", HTTP_POST, []() { - StaticJsonDocument<256> doc; - - if (!loggingEnabled) { - // Create new log file - char filename[64]; - time_t now = time(nullptr); - struct tm timeinfo; - localtime_r(&now, &timeinfo); - - // Use mode prefix - const char* prefix = canfdSettings.fdMode ? "canfd" : "can"; - snprintf(filename, sizeof(filename), - "/%s_%04d%02d%02d_%02d%02d%02d.csv", - prefix, - timeinfo.tm_year + 1900, - timeinfo.tm_mon + 1, - timeinfo.tm_mday, - timeinfo.tm_hour, - timeinfo.tm_min, - timeinfo.tm_sec); - - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - canLogFile = SD_MMC.open(filename, FILE_WRITE); - if (canLogFile) { - // Write CSV header - if (canfdSettings.fdMode) { - canLogFile.println("timestamp_us,id,len,fd,brs,data"); - } else { - canLogFile.println("timestamp_us,id,dlc,data"); - } - strncpy(currentCanFilename, filename, sizeof(currentCanFilename)); - loggingEnabled = true; - doc["success"] = true; - doc["message"] = String("Logging started: ") + filename; - Serial.printf("✓ Logging started: %s\n", filename); - } else { - doc["success"] = false; - doc["message"] = "Failed to create log file"; - Serial.println("✗ Failed to create log file"); - } - xSemaphoreGive(sdMutex); - } else { - doc["success"] = false; - doc["message"] = "SD card busy"; - } - } else { - doc["success"] = false; - doc["message"] = "Logging already active"; - } - - String response; - serializeJson(doc, response); - server.send(200, "application/json", response); - }); - - // Stop logging - server.on("/logging/stop", HTTP_POST, []() { - StaticJsonDocument<128> doc; - - if (loggingEnabled) { - loggingEnabled = false; - - // Flush any remaining data - if (fileBufferPos > 0) { - flushBufferToSD(); - } - - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - if (canLogFile) { - canLogFile.close(); - Serial.println("✓ Logging stopped"); - } - xSemaphoreGive(sdMutex); - } - - doc["success"] = true; - doc["message"] = "Logging stopped"; - } else { - doc["success"] = false; - doc["message"] = "Logging not active"; - } - - String response; - serializeJson(doc, response); - server.send(200, "application/json", response); - }); - - // Settings apply - server.on("/settings/apply", HTTP_POST, []() { - StaticJsonDocument<512> doc; - - if (server.hasArg("plain")) { - DeserializationError error = deserializeJson(doc, server.arg("plain")); - - if (!error) { - String mode = doc["mode"].as(); - uint32_t bitRate = doc["bitRate"]; - uint8_t dataRate = doc["dataRate"]; - uint8_t controllerMode = doc["controllerMode"]; - - // Update settings - canfdSettings.fdMode = (mode == "fd"); - canfdSettings.arbitrationBitRate = bitRate; - canfdSettings.dataBitRateFactor = dataRate; - canfdSettings.listenOnly = (controllerMode == 1); - canfdSettings.loopback = (controllerMode == 2); - - // Save to preferences - preferences.begin("can-logger", false); - preferences.putBool("fd_mode", canfdSettings.fdMode); - preferences.putUInt("arb_rate", canfdSettings.arbitrationBitRate); - preferences.putUChar("data_factor", canfdSettings.dataBitRateFactor); - preferences.putBool("listen_only", canfdSettings.listenOnly); - preferences.putBool("loopback", canfdSettings.loopback); - preferences.end(); - - doc.clear(); - doc["success"] = true; - doc["message"] = "Settings saved successfully"; - - Serial.println("\n========== Settings Updated =========="); - Serial.printf("Mode: %s\n", canfdSettings.fdMode ? "CAN FD" : "Classic CAN"); - Serial.printf("Bit Rate: %d bps\n", canfdSettings.arbitrationBitRate); - if (canfdSettings.fdMode) { - Serial.printf("Data Factor: x%d\n", canfdSettings.dataBitRateFactor); - } - Serial.println("======================================\n"); - } else { - doc.clear(); - doc["success"] = false; - doc["message"] = "Invalid JSON"; - } - } else { - doc["success"] = false; - doc["message"] = "No data received"; - } - - String response; - serializeJson(doc, response); - server.send(200, "application/json", response); - }); - - // 404 handler - server.onNotFound([]() { - server.send(404, "text/plain", "404: Not Found"); - }); -} - -void updateRecentData(const LoggedCANFrame *msg) { - // Update recent data array -} - -String formatCANFDMessage(const LoggedCANFrame *msg) { - return ""; -} diff --git a/canfd_graph.h b/canfd_graph.h index 9ad5edc..4767ea3 100644 --- a/canfd_graph.h +++ b/canfd_graph.h @@ -885,4 +885,4 @@ const char canfd_graph_html[] PROGMEM = R"rawliteral( )rawliteral"; -#endif +#endif \ No newline at end of file diff --git a/canfd_graph_viewer.h b/canfd_graph_viewer.h index c2017d0..441fd65 100644 --- a/canfd_graph_viewer.h +++ b/canfd_graph_viewer.h @@ -975,4 +975,4 @@ const char canfd_graph_viewer_html[] PROGMEM = R"rawliteral( )rawliteral"; -#endif +#endif \ No newline at end of file diff --git a/canfd_index.h b/canfd_index.h index 3353a71..f52ff14 100644 --- a/canfd_index.h +++ b/canfd_index.h @@ -250,6 +250,7 @@ const char canfd_index_html[] PROGMEM = R"rawliteral(
diff --git a/canfd_settings.h b/canfd_settings.h index 986280b..c2808fd 100644 --- a/canfd_settings.h +++ b/canfd_settings.h @@ -291,13 +291,13 @@ const char canfd_settings_html[] PROGMEM = R"rawliteral(
- +
ESP32가 생성할 WiFi 네트워크 이름 (최대 31자)
- +
WiFi 접속 시 필요한 비밀번호 (8-63자)
@@ -379,6 +379,15 @@ const char canfd_settings_html[] PROGMEM = R"rawliteral(
Normal: 일반 통신, Listen Only: 수신 전용
+ +
+ + +
CSV: 텍스트 형식 (분석 용이), PCAP: Wireshark로 분석 가능
+
@@ -409,7 +418,7 @@ const char canfd_settings_html[] PROGMEM = R"rawliteral( .then(data => { // WiFi settings document.getElementById('wifi-ssid').value = data.wifiSSID || 'Byun_CANFD_Logger'; - document.getElementById('wifi-password').value = data.wifiPassword || ''; + document.getElementById('wifi-password').value = data.wifiPassword || '12345678'; // STA settings document.getElementById('sta-enable').checked = data.staEnable || false; @@ -421,6 +430,7 @@ const char canfd_settings_html[] PROGMEM = R"rawliteral( document.getElementById('bit-rate').value = data.bitRate || 500000; document.getElementById('data-rate').value = data.dataRate || 4; document.getElementById('controller-mode').value = data.controllerMode || 0; + document.getElementById('log-format').value = data.logFormat || 0; // Update UI toggleSTASettings(); @@ -452,13 +462,16 @@ const char canfd_settings_html[] PROGMEM = R"rawliteral( canMode: document.getElementById('can-mode').value, bitRate: parseInt(document.getElementById('bit-rate').value), dataRate: parseInt(document.getElementById('data-rate').value), - controllerMode: parseInt(document.getElementById('controller-mode').value) + controllerMode: parseInt(document.getElementById('controller-mode').value), + logFormat: parseInt(document.getElementById('log-format').value) }; + + console.log('Saving settings:', settings); - // Validation + // Validation - AP SSID가 비어있으면 기본값 사용 if (settings.wifiSSID.length === 0) { - alert('WiFi SSID를 입력하세요.'); - return; + settings.wifiSSID = 'Byun_CANFD_Logger'; + console.log('Using default SSID:', settings.wifiSSID); } if (settings.wifiPassword.length > 0 && settings.wifiPassword.length < 8) { @@ -485,6 +498,7 @@ const char canfd_settings_html[] PROGMEM = R"rawliteral( } }) .catch(err => { + console.error('Save error:', err); alert('설정 저장 실패: ' + err); }); } @@ -547,4 +561,4 @@ const char canfd_settings_html[] PROGMEM = R"rawliteral( )rawliteral"; -#endif +#endif \ No newline at end of file