1624 lines
56 KiB
C++
1624 lines
56 KiB
C++
/*
|
||
* 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 "";
|
||
} |