Files
esp32s3-mcp2518FD-logger/ESP32_CANFD_Logger.ino
2026-02-13 19:38:00 +00:00

1624 lines
56 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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 <Arduino.h>
#include <SPI.h>
#include <ACAN2517FD.h> // ⭐ CAN FD Library
#include <SoftWire.h>
#include <SD_MMC.h>
#include <WiFi.h>
#include <esp_wifi.h>
#include <esp_task_wdt.h>
#include <esp_sntp.h>
#include <WebServer.h>
#include <WebSocketsServer.h>
#include <ArduinoJson.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/queue.h>
#include <freertos/semphr.h>
#include <sys/time.h>
#include <time.h>
#include <Preferences.h>
#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>();
String newPassword = doc["wifiPassword"].as<String>();
bool newStaEnable = doc["staEnable"];
String newStaSSID = doc["staSSID"].as<String>();
String newStaPassword = doc["staPassword"].as<String>();
// CAN settings
String canMode = doc["canMode"].as<String>();
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<String>();
// 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<String>();
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 "";
}