/* * 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) */ #include #include #include // ⭐ CAN FD Library #include #include #include #include #include #include #include #include #include #include #include #include #include #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 // 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 // I2C Pins (RTC DS3231) #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 // ======================================== // 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 } __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; }; struct TxMessage { uint32_t id; bool extended; uint8_t dlc; uint8_t data[64]; // ⭐ CAN FD 지원 uint32_t interval; uint32_t lastSent; bool active; bool fd; // ⭐ FD frame flag bool brs; // ⭐ Bit Rate Switch }; struct SequenceStep { uint32_t canId; bool extended; uint8_t dlc; uint8_t data[64]; // ⭐ CAN FD 지원 uint32_t delayMs; bool fd; // ⭐ FD frame flag bool brs; // ⭐ Bit Rate Switch }; struct CANSequence { char name[32]; SequenceStep steps[20]; uint8_t stepCount; uint8_t repeatMode; uint32_t repeatCount; }; struct SequenceRuntime { bool running; uint8_t currentStep; uint32_t currentRepeat; uint32_t lastStepTime; int8_t activeSequenceIndex; }; struct FileComment { char filename[MAX_FILENAME_LEN]; char comment[MAX_COMMENT_LEN]; }; struct TimeSyncStatus { bool synchronized; uint64_t lastSyncTime; int32_t offsetUs; uint32_t syncCount; bool rtcAvailable; uint32_t rtcSyncCount; } timeSyncStatus = {false, 0, 0, 0, false, 0}; struct PowerStatus { float voltage; float minVoltage; bool lowVoltage; uint32_t lastCheck; uint32_t lastMinReset; } powerStatus = {0.0, 999.9, false, 0, 0}; // ⭐ CAN FD Mode Settings 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) // ======================================== // 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; // 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; // WiFi Settings char wifiSSID[32] = "Byun_CANFD_Logger"; char wifiPassword[64] = "12345678"; 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}; // ======================================== // 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); // ======================================== // 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; File canLogFile; File serial1LogFile; File serial2LogFile; char currentCanFilename[MAX_FILENAME_LEN] = ""; char currentSerial1Filename[MAX_FILENAME_LEN] = ""; char currentSerial2Filename[MAX_FILENAME_LEN] = ""; uint32_t fileBufferPos = 0; uint32_t serialCsvBufferPos = 0; uint32_t serial2CsvBufferPos = 0; uint32_t lastFlushTime = 0; 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 uint64_t getMicros64(); void writeCANToBuffer(const LoggedCANFrame *msg); void flushBufferToSD(); String formatCANFDMessage(const LoggedCANFrame *msg); void updateRecentData(const LoggedCANFrame *msg); // ======================================== // CAN FD Interrupt Handler // ======================================== void IRAM_ATTR canISR() { canInterruptFlag = true; } // ======================================== // PSRAM Allocation // ======================================== bool allocatePSRAM() { Serial.println("\n========================================"); Serial.println(" PSRAM Memory Allocation"); Serial.println("========================================"); size_t psramBefore = ESP.getFreePsram(); Serial.printf("Available PSRAM: %d KB\n", psramBefore / 1024); // File Buffer (128KB for CAN FD) 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 recentData = (RecentCANData*)ps_malloc(sizeof(RecentCANData) * RECENT_MSG_COUNT); if (!recentData) { Serial.println("✗ Recent data allocation failed!"); 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; } 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; } 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; } 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)); 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); 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 // ======================================== bool initCANFD() { Serial.println("\n========================================"); Serial.println(" CAN FD Controller Initialization"); 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 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; } // 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 ); // 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 } // 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); Serial.println("========================================\n"); return true; } // ======================================== // RTC Functions // ======================================== bool readRTC(struct tm *timeinfo) { if (!timeSyncStatus.rtcAvailable) return false; if (xSemaphoreTake(i2cMutex, pdMS_TO_TICKS(100)) != pdTRUE) { return false; } rtcWire.beginTransmission(DS3231_ADDRESS); rtcWire.write(0x00); if (rtcWire.endTransmission() != 0) { xSemaphoreGive(i2cMutex); return false; } 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; } void updateSystemTime(const struct tm *timeinfo) { struct timeval tv; tv.tv_sec = mktime((struct tm*)timeinfo); tv.tv_usec = 0; settimeofday(&tv, NULL); } // ======================================== // Microsecond Timer (64-bit) // ======================================== 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; } // ======================================== // CAN FD Buffer Management // ======================================== void writeCANToBuffer(const LoggedCANFrame *msg) { if (fileBufferPos + 512 > FILE_BUFFER_SIZE) { return; // Buffer full } // Format: timestamp,id,len,fd,brs,data[0-63] 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; } } 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++; } } 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 } } } } vTaskDelay(pdMS_TO_TICKS(1)); // 1ms polling } } // ======================================== // SD Write Task // ======================================== void sdWriteTask(void *parameter) { LoggedCANFrame msg; uint32_t batchCount = 0; const uint32_t BATCH_SIZE = 50; // Write in batches for efficiency Serial.println("✓ SD Write Task started"); 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) { flushBufferToSD(); batchCount = 0; } } else { vTaskDelay(pdMS_TO_TICKS(5)); } } } // ======================================== // SD Flush Task (Periodic) // ======================================== void sdFlushTask(void *parameter) { Serial.println("✓ SD Flush Task started"); while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); // Flush every 1 second if (loggingEnabled && fileBufferPos > 0) { flushBufferToSD(); // Sync to physical media every 10 seconds static uint32_t lastSync = 0; if (millis() - lastSync > 10000) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(100)) == pdTRUE) { if (canLogFile) { canLogFile.flush(); } xSemaphoreGive(sdMutex); } lastSync = millis(); } } } } // ======================================== // 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"); } // ======================================== // 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); } 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); } } } break; } } // ======================================== // Loop // ======================================== void loop() { server.handleClient(); ws.loop(); // WebSocket 처리 vTaskDelay(pdMS_TO_TICKS(10)); // Status output every 30 seconds static uint32_t lastPrint = 0; if (millis() - lastPrint > 30000) { Serial.printf("[Status] CAN: %lu frames | Queue: %d/%d | Errors: %lu | PSRAM: %d KB\n", canFrameCount, uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE, canErrorCount, 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 ""; }