From 55037605558682761b172cab55ae814b50a8429b Mon Sep 17 00:00:00 2001 From: byun Date: Fri, 13 Feb 2026 19:38:00 +0000 Subject: [PATCH] =?UTF-8?q?=EC=B2=AB=20=EC=97=85=EB=A1=9C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ESP32_CANFD_Logger.ino | 1624 ++++++++++++++++++++++++++++++++++++++++ canfd_graph.h | 888 ++++++++++++++++++++++ canfd_graph_viewer.h | 978 ++++++++++++++++++++++++ canfd_index.h | 581 ++++++++++++++ transmit.h | 550 ++++++++++++++ 5 files changed, 4621 insertions(+) create mode 100644 ESP32_CANFD_Logger.ino create mode 100644 canfd_graph.h create mode 100644 canfd_graph_viewer.h create mode 100644 canfd_index.h create mode 100644 transmit.h diff --git a/ESP32_CANFD_Logger.ino b/ESP32_CANFD_Logger.ino new file mode 100644 index 0000000..4427e80 --- /dev/null +++ b/ESP32_CANFD_Logger.ino @@ -0,0 +1,1624 @@ +/* + * 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 ""; +} \ No newline at end of file diff --git a/canfd_graph.h b/canfd_graph.h new file mode 100644 index 0000000..9ad5edc --- /dev/null +++ b/canfd_graph.h @@ -0,0 +1,888 @@ +#ifndef CANFD_GRAPH_H +#define CANFD_GRAPH_H + +const char canfd_graph_html[] PROGMEM = R"rawliteral( + + + + + + CAN FD Signal Graph + + + +
+
+

🚀 CAN FD Signal Graph

+

Real-time Signal Visualization (64-byte Support)

+
+ + + +
+ + +

Upload DBC File

+
+
+ +

Click to upload DBC (CAN FD supported)

+

No file loaded

+
+
+ + +
+
+ + + + +)rawliteral"; + +#endif diff --git a/canfd_graph_viewer.h b/canfd_graph_viewer.h new file mode 100644 index 0000000..c2017d0 --- /dev/null +++ b/canfd_graph_viewer.h @@ -0,0 +1,978 @@ +#ifndef CANFD_GRAPH_VIEWER_H +#define CANFD_GRAPH_VIEWER_H + +const char canfd_graph_viewer_html[] PROGMEM = R"rawliteral( + + + + + + CAN FD Signal Graph Viewer + + + +
+

🚀 CAN FD Real-time Signal Graphs

+

Viewing 0 signals (64-byte support)

+
+ + + +
+
+ + + +
+
+ X-Axis Scale: + + +
+
+ X-Axis Range: + + +
+
+ Sort by: + + + +
+
+ + +
+
+ +
Connecting...
+ +
+ Data Points: 0 + Recording Time: 0s + Messages Received: 0 +
+ +
+ + + + +)rawliteral"; + +#endif diff --git a/canfd_index.h b/canfd_index.h new file mode 100644 index 0000000..3353a71 --- /dev/null +++ b/canfd_index.h @@ -0,0 +1,581 @@ +#ifndef CANFD_INDEX_H +#define CANFD_INDEX_H + +const char canfd_index_html[] PROGMEM = R"rawliteral( + + + + + + ESP32-S3 CAN FD Logger + + + +
+
+

🚗 ESP32-S3 CAN FD Logger

+

MCP2517FD High-Speed Data Logger with Web Interface

+
+ + + +
+ +
+
+

CAN Controller

+
+ Loading... +
+
+
+

SD Card

+
+ Loading... +
+
+
+

Total Frames

+
0
+
+
+

Queue Usage

+
0 / 10000
+
+
+

Errors

+
0
+
+
+

Free PSRAM

+
- KB
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+

🎬 Logging Control

+
+ + + + +
+
+ + +
+

📁 Log Files on SD Card

+
+ + +
+
+

Loading files...

+
+
+ + +
+

📺 Real-time CAN Monitor

+
+
Waiting for CAN messages...
+
+
+
+
+ + + + +)rawliteral"; + +#endif \ No newline at end of file diff --git a/transmit.h b/transmit.h new file mode 100644 index 0000000..ce3a6a9 --- /dev/null +++ b/transmit.h @@ -0,0 +1,550 @@ +#ifndef CANFD_SETTINGS_H +#define CANFD_SETTINGS_H + +const char canfd_settings_html[] PROGMEM = R"rawliteral( + + + + + + Settings - ESP32-S3 CAN FD Logger + + + +
+
+

⚙️ Settings

+

Configure WiFi and CAN Settings

+
+ + + +
+
+ + Settings saved successfully! +
+ +
+ + Loading settings... +
+ + +
+
+ 📶 + WiFi Configuration +
+ +

AP Mode (Access Point)

+ +
+ + +
ESP32가 생성할 WiFi 네트워크 이름 (최대 31자)
+
+ +
+ + +
WiFi 접속 시 필요한 비밀번호 (8-63자)
+
+ +
+ +

APSTA Mode (AP + Station)

+ +
+ + +
+
+ AP와 Station을 동시에 사용하여 인터넷 접속 가능 +
+ +
+
+ + +
연결할 외부 WiFi 네트워크 이름
+
+ +
+ + +
외부 WiFi 비밀번호
+
+ +
+ ✓ WiFi 연결됨: +
+
+
+ + +
+
+ 🚗 + CAN Configuration +
+ +
+ + +
CAN FD: 최대 64바이트, Classic CAN: 최대 8바이트
+
+ +
+ + +
CAN 버스 속도
+
+ +
+ + +
CAN FD 데이터 구간 속도 배율
+
+ +
+ + +
Normal: 일반 통신, Listen Only: 수신 전용
+
+
+ +
+ + +
+ +
+
+ ⚠️ 중요 안내 +
+
+ • WiFi 설정을 변경한 경우, ESP32를 재부팅해야 새 SSID/비밀번호가 적용됩니다.
+ • APSTA 모드: Station 모드를 활성화하면 ESP32가 AP와 Station을 동시에 사용합니다.
+ • Station 모드로 외부 WiFi에 연결하면 인터넷 접속이 가능해집니다.
+ • Station 연결 실패 시에도 AP 모드는 정상 동작합니다.
+ • CAN 설정 변경 후에도 ESP32 재부팅이 필요합니다.
+ • 설정 저장 후 ESP32의 RST 버튼을 눌러 재부팅하세요. +
+
+
+
+ + + + +)rawliteral"; + +#endif \ No newline at end of file