Files
esp32s3_canlogger_mcp2515/ESP32_CAN_Logger-a.ino
2025-12-06 00:32:26 +00:00

1782 lines
64 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

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.

/*
* Byun CAN Logger with Web Interface + Serial Terminal
* Version: 2.3 - PSRAM Optimized (Complete Version)
*
* PSRAM 최적화 완전판:
* - 원본 기능 100% 유지
* - 대용량 버퍼/Queue를 PSRAM에 할당
* - 웹서버, WebSocket, 모든 Task 포함
*
* Arduino IDE 설정:
* - Board: ESP32S3 Dev Module
* - PSRAM: OPI PSRAM ⭐ 필수!
* - Flash Size: 16MB (128Mb)
* - Partition: 16MB Flash (3MB APP/9.9MB FATFS)
*/
#include <Arduino.h>
#include <SPI.h>
#include <mcp2515.h>
#include <SoftWire.h>
#include <SD.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 "index.h"
#include "transmit.h"
#include "graph.h"
#include "graph_viewer.h"
#include "settings.h"
#include "serial_terminal.h"
// GPIO 핀 정의
#define CAN_INT_PIN 4
#define SERIAL_TX_PIN 17
#define SERIAL_RX_PIN 18
// HSPI 핀 (CAN)
#define HSPI_MISO 13
#define HSPI_MOSI 11
#define HSPI_SCLK 12
#define HSPI_CS 10
// VSPI 핀 (SD Card)
#define VSPI_MISO 41
#define VSPI_MOSI 40
#define VSPI_SCLK 39
#define VSPI_CS 42
// I2C2 핀 (RTC DS3231)
#define RTC_SDA 8
#define RTC_SCL 9
#define DS3231_ADDRESS 0x68
// ========================================
// PSRAM 최적화 설정
// ========================================
#define CAN_QUEUE_SIZE 6000 // 1000 → 6000 (PSRAM 사용)
#define FILE_BUFFER_SIZE 65536 // 16KB → 64KB (PSRAM 사용)
#define SERIAL_QUEUE_SIZE 1200 // 200 → 1200 (PSRAM 사용)
#define SERIAL_CSV_BUFFER_SIZE 32768 // 8KB → 32KB (PSRAM 사용)
#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
// ========================================
// 구조체 정의
// ========================================
struct CANMessage {
uint64_t timestamp_us;
uint32_t id;
uint8_t dlc;
uint8_t data[8];
} __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;
} serialSettings = {115200, 8, 0, 1};
struct RecentCANData {
CANMessage msg;
uint32_t count;
};
struct TxMessage {
uint32_t id;
bool extended;
uint8_t dlc;
uint8_t data[8];
uint32_t interval;
uint32_t lastSent;
bool active;
};
struct SequenceStep {
uint32_t canId;
bool extended;
uint8_t dlc;
uint8_t data[8];
uint32_t delayMs;
};
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};
enum MCP2515Mode {
MCP_MODE_NORMAL = 0,
MCP_MODE_LISTEN_ONLY = 1,
MCP_MODE_LOOPBACK = 2,
MCP_MODE_TRANSMIT = 3
};
// ========================================
// PSRAM 할당 변수 (포인터로 선언)
// ========================================
uint8_t *fileBuffer = nullptr;
char *serialCsvBuffer = nullptr;
RecentCANData *recentData = nullptr;
TxMessage *txMessages = nullptr;
CANSequence *sequences = nullptr;
FileComment *fileComments = nullptr;
// Queue 저장소 (PSRAM)
StaticQueue_t *canQueueBuffer = nullptr;
StaticQueue_t *serialQueueBuffer = nullptr;
uint8_t *canQueueStorage = nullptr;
uint8_t *serialQueueStorage = nullptr;
// WiFi 설정 (내부 SRAM)
char wifiSSID[32] = "Byun_CAN_Logger";
char wifiPassword[64] = "12345678";
bool enableSTAMode = false;
char staSSID[32] = "";
char staPassword[64] = "";
// 전역 객체 (내부 SRAM)
SPIClass hspi(HSPI);
SPIClass vspi(FSPI);
MCP2515 mcp2515(HSPI_CS, 20000000, &hspi);
HardwareSerial SerialComm(1);
WebServer server(80);
WebSocketsServer webSocket = WebSocketsServer(81);
Preferences preferences;
// FreeRTOS 핸들
QueueHandle_t canQueue = NULL;
QueueHandle_t serialQueue = NULL;
SemaphoreHandle_t sdMutex = NULL;
SemaphoreHandle_t rtcMutex = NULL;
SemaphoreHandle_t serialMutex = NULL;
TaskHandle_t canRxTaskHandle = NULL;
TaskHandle_t sdWriteTaskHandle = NULL;
TaskHandle_t webTaskHandle = NULL;
TaskHandle_t rtcTaskHandle = NULL;
TaskHandle_t serialRxTaskHandle = NULL;
// 로깅 변수
volatile bool loggingEnabled = false;
volatile bool serialLoggingEnabled = false;
volatile bool sdCardReady = false;
File logFile;
File serialLogFile;
char currentFilename[MAX_FILENAME_LEN];
char currentSerialFilename[MAX_FILENAME_LEN];
uint16_t bufferIndex = 0;
uint16_t serialCsvIndex = 0;
volatile uint32_t currentFileSize = 0;
volatile uint32_t currentSerialFileSize = 0;
volatile bool canLogFormatCSV = false;
volatile bool serialLogFormatCSV = true;
volatile uint64_t canLogStartTime = 0;
volatile uint64_t serialLogStartTime = 0;
// 기타 전역 변수
MCP2515Mode currentMcpMode = MCP_MODE_NORMAL;
SoftWire rtcWire(RTC_SDA, RTC_SCL);
char rtcSyncBuffer[20];
CAN_SPEED currentCanSpeed = CAN_1000KBPS;
const char* canSpeedNames[] = {"125K", "250K", "500K", "1M"};
CAN_SPEED canSpeedValues[] = {CAN_125KBPS, CAN_250KBPS, CAN_500KBPS, CAN_1000KBPS};
uint32_t totalMsgCount = 0;
uint32_t msgPerSecond = 0;
uint32_t lastMsgCountTime = 0;
uint32_t lastMsgCount = 0;
volatile uint32_t totalSerialRxCount = 0;
volatile uint32_t totalSerialTxCount = 0;
uint32_t totalTxCount = 0;
uint8_t sequenceCount = 0;
SequenceRuntime seqRuntime = {false, 0, 0, 0, -1};
int commentCount = 0;
// Forward declarations
void IRAM_ATTR canISR();
void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length);
// ========================================
// PSRAM 초기화 함수
// ========================================
bool initPSRAM() {
Serial.println("\n========================================");
Serial.println(" PSRAM 메모리 할당");
Serial.println("========================================");
if (!psramFound()) {
Serial.println("✗ PSRAM을 찾을 수 없습니다!");
Serial.println("✗ Arduino IDE 설정:");
Serial.println(" Tools → PSRAM → OPI PSRAM");
return false;
}
Serial.printf("✓ PSRAM 총 용량: %d MB\n", ESP.getPsramSize() / 1024 / 1024);
Serial.printf("✓ PSRAM 여유: %d KB\n\n", ESP.getFreePsram() / 1024);
// 버퍼 할당
fileBuffer = (uint8_t*)ps_malloc(FILE_BUFFER_SIZE);
if (!fileBuffer) {
Serial.println("✗ fileBuffer 할당 실패");
return false;
}
Serial.printf("✓ fileBuffer: %d KB\n", FILE_BUFFER_SIZE / 1024);
serialCsvBuffer = (char*)ps_malloc(SERIAL_CSV_BUFFER_SIZE);
if (!serialCsvBuffer) {
Serial.println("✗ serialCsvBuffer 할당 실패");
return false;
}
Serial.printf("✓ serialCsvBuffer: %d KB\n", SERIAL_CSV_BUFFER_SIZE / 1024);
recentData = (RecentCANData*)ps_calloc(RECENT_MSG_COUNT, sizeof(RecentCANData));
if (!recentData) {
Serial.println("✗ recentData 할당 실패");
return false;
}
Serial.printf("✓ recentData: %.2f KB\n", (float)(RECENT_MSG_COUNT * sizeof(RecentCANData)) / 1024.0);
txMessages = (TxMessage*)ps_calloc(MAX_TX_MESSAGES, sizeof(TxMessage));
if (!txMessages) {
Serial.println("✗ txMessages 할당 실패");
return false;
}
Serial.printf("✓ txMessages: %.2f KB\n", (float)(MAX_TX_MESSAGES * sizeof(TxMessage)) / 1024.0);
sequences = (CANSequence*)ps_calloc(MAX_SEQUENCES, sizeof(CANSequence));
if (!sequences) {
Serial.println("✗ sequences 할당 실패");
return false;
}
Serial.printf("✓ sequences: %.2f KB\n", (float)(MAX_SEQUENCES * sizeof(CANSequence)) / 1024.0);
fileComments = (FileComment*)ps_calloc(MAX_FILE_COMMENTS, sizeof(FileComment));
if (!fileComments) {
Serial.println("✗ fileComments 할당 실패");
return false;
}
Serial.printf("✓ fileComments: %.2f KB\n", (float)(MAX_FILE_COMMENTS * sizeof(FileComment)) / 1024.0);
// Queue 저장소 할당
Serial.println("\n📦 Queue 저장소 할당...");
canQueueBuffer = (StaticQueue_t*)ps_malloc(sizeof(StaticQueue_t));
canQueueStorage = (uint8_t*)ps_malloc(CAN_QUEUE_SIZE * sizeof(CANMessage));
if (!canQueueBuffer || !canQueueStorage) {
Serial.println("✗ CAN Queue 저장소 할당 실패");
return false;
}
Serial.printf("✓ CAN Queue: %d 개 × %d bytes = %.2f KB\n",
CAN_QUEUE_SIZE, sizeof(CANMessage),
(float)(CAN_QUEUE_SIZE * sizeof(CANMessage)) / 1024.0);
serialQueueBuffer = (StaticQueue_t*)ps_malloc(sizeof(StaticQueue_t));
serialQueueStorage = (uint8_t*)ps_malloc(SERIAL_QUEUE_SIZE * sizeof(SerialMessage));
if (!serialQueueBuffer || !serialQueueStorage) {
Serial.println("✗ Serial Queue 저장소 할당 실패");
return false;
}
Serial.printf("✓ Serial Queue: %d 개 × %d bytes = %.2f KB\n",
SERIAL_QUEUE_SIZE, sizeof(SerialMessage),
(float)(SERIAL_QUEUE_SIZE * sizeof(SerialMessage)) / 1024.0);
Serial.println("========================================");
Serial.printf("✓ PSRAM 남은 용량: %.2f KB\n", (float)ESP.getFreePsram() / 1024.0);
Serial.println("========================================\n");
return true;
}
bool createQueues() {
Serial.println("Queue 생성 (PSRAM 사용)...");
canQueue = xQueueCreateStatic(
CAN_QUEUE_SIZE,
sizeof(CANMessage),
canQueueStorage,
canQueueBuffer
);
if (canQueue == NULL) {
Serial.println("✗ CAN Queue 생성 실패");
return false;
}
Serial.printf("✓ CAN Queue: %d 개\n", CAN_QUEUE_SIZE);
serialQueue = xQueueCreateStatic(
SERIAL_QUEUE_SIZE,
sizeof(SerialMessage),
serialQueueStorage,
serialQueueBuffer
);
if (serialQueue == NULL) {
Serial.println("✗ Serial Queue 생성 실패");
return false;
}
Serial.printf("✓ Serial Queue: %d 개\n\n", SERIAL_QUEUE_SIZE);
return true;
}
// ========================================
// 설정 저장/로드 함수
// ========================================
void loadSerialSettings() {
serialSettings.baudRate = preferences.getUInt("ser_baud", 115200);
serialSettings.dataBits = preferences.getUChar("ser_data", 8);
serialSettings.parity = preferences.getUChar("ser_parity", 0);
serialSettings.stopBits = preferences.getUChar("ser_stop", 1);
}
void saveSerialSettings() {
preferences.putUInt("ser_baud", serialSettings.baudRate);
preferences.putUChar("ser_data", serialSettings.dataBits);
preferences.putUChar("ser_parity", serialSettings.parity);
preferences.putUChar("ser_stop", serialSettings.stopBits);
}
void applySerialSettings() {
uint32_t config = SERIAL_8N1;
if (serialSettings.dataBits == 5) {
if (serialSettings.parity == 0) config = SERIAL_5N1;
else if (serialSettings.parity == 1) config = SERIAL_5E1;
else if (serialSettings.parity == 2) config = SERIAL_5O1;
} else if (serialSettings.dataBits == 6) {
if (serialSettings.parity == 0) config = SERIAL_6N1;
else if (serialSettings.parity == 1) config = SERIAL_6E1;
else if (serialSettings.parity == 2) config = SERIAL_6O1;
} else if (serialSettings.dataBits == 7) {
if (serialSettings.parity == 0) config = SERIAL_7N1;
else if (serialSettings.parity == 1) config = SERIAL_7E1;
else if (serialSettings.parity == 2) config = SERIAL_7O1;
} else {
if (serialSettings.parity == 0) config = SERIAL_8N1;
else if (serialSettings.parity == 1) config = SERIAL_8E1;
else if (serialSettings.parity == 2) config = SERIAL_8O1;
}
if (serialSettings.stopBits == 2) {
config |= 0x3000;
}
SerialComm.begin(serialSettings.baudRate, config, SERIAL_RX_PIN, SERIAL_TX_PIN);
SerialComm.setRxBufferSize(2048);
}
void loadSettings() {
preferences.begin("can-logger", false);
preferences.getString("wifi_ssid", wifiSSID, sizeof(wifiSSID));
preferences.getString("wifi_pass", wifiPassword, sizeof(wifiPassword));
enableSTAMode = preferences.getBool("sta_enable", false);
preferences.getString("sta_ssid", staSSID, sizeof(staSSID));
preferences.getString("sta_pass", staPassword, sizeof(staPassword));
if (strlen(wifiSSID) == 0) strcpy(wifiSSID, "Byun_CAN_Logger");
if (strlen(wifiPassword) == 0) strcpy(wifiPassword, "12345678");
int speedIndex = preferences.getInt("can_speed", 3);
if (speedIndex >= 0 && speedIndex < 4) {
currentCanSpeed = canSpeedValues[speedIndex];
}
int savedMode = preferences.getInt("mcp_mode", 0);
if (savedMode >= 0 && savedMode <= 3) {
currentMcpMode = (MCP2515Mode)savedMode;
}
loadSerialSettings();
preferences.end();
}
void saveSettings() {
preferences.begin("can-logger", false);
preferences.putString("wifi_ssid", wifiSSID);
preferences.putString("wifi_pass", wifiPassword);
preferences.putBool("sta_enable", enableSTAMode);
preferences.putString("sta_ssid", staSSID);
preferences.putString("sta_pass", staPassword);
for (int i = 0; i < 4; i++) {
if (canSpeedValues[i] == currentCanSpeed) {
preferences.putInt("can_speed", i);
break;
}
}
preferences.putInt("mcp_mode", (int)currentMcpMode);
saveSerialSettings();
preferences.end();
}
// ========================================
// RTC 함수
// ========================================
void initRTC() {
rtcWire.begin();
rtcWire.setClock(100000);
rtcWire.beginTransmission(DS3231_ADDRESS);
if (rtcWire.endTransmission() == 0) {
timeSyncStatus.rtcAvailable = true;
Serial.println("✓ RTC(DS3231) 감지됨");
} else {
timeSyncStatus.rtcAvailable = false;
Serial.println("! RTC(DS3231) 없음");
}
}
uint8_t bcdToDec(uint8_t val) {
return (val >> 4) * 10 + (val & 0x0F);
}
uint8_t decToBcd(uint8_t val) {
return ((val / 10) << 4) | (val % 10);
}
bool readRTC(struct tm *timeinfo) {
if (!timeSyncStatus.rtcAvailable) return false;
if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(100)) != pdTRUE) return false;
rtcWire.beginTransmission(DS3231_ADDRESS);
rtcWire.write(0x00);
if (rtcWire.endTransmission() != 0) {
xSemaphoreGive(rtcMutex);
return false;
}
if (rtcWire.requestFrom(DS3231_ADDRESS, 7) != 7) {
xSemaphoreGive(rtcMutex);
return false;
}
uint8_t buffer[7];
for (int i = 0; i < 7; i++) buffer[i] = rtcWire.read();
xSemaphoreGive(rtcMutex);
timeinfo->tm_sec = bcdToDec(buffer[0] & 0x7F);
timeinfo->tm_min = bcdToDec(buffer[1] & 0x7F);
timeinfo->tm_hour = bcdToDec(buffer[2] & 0x3F);
timeinfo->tm_wday = bcdToDec(buffer[3] & 0x07) - 1;
timeinfo->tm_mday = bcdToDec(buffer[4] & 0x3F);
timeinfo->tm_mon = bcdToDec(buffer[5] & 0x1F) - 1;
timeinfo->tm_year = bcdToDec(buffer[6]) + 100;
return true;
}
bool writeRTC(const struct tm *timeinfo) {
if (!timeSyncStatus.rtcAvailable) return false;
if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(100)) != pdTRUE) return false;
rtcWire.beginTransmission(DS3231_ADDRESS);
rtcWire.write(0x00);
rtcWire.write(decToBcd(timeinfo->tm_sec));
rtcWire.write(decToBcd(timeinfo->tm_min));
rtcWire.write(decToBcd(timeinfo->tm_hour));
rtcWire.write(decToBcd(timeinfo->tm_wday + 1));
rtcWire.write(decToBcd(timeinfo->tm_mday));
rtcWire.write(decToBcd(timeinfo->tm_mon + 1));
rtcWire.write(decToBcd(timeinfo->tm_year - 100));
bool success = (rtcWire.endTransmission() == 0);
xSemaphoreGive(rtcMutex);
return success;
}
void timeSyncCallback(struct timeval *tv) {
Serial.println("✓ NTP 시간 동기화 완료");
timeSyncStatus.synchronized = true;
timeSyncStatus.lastSyncTime = (uint64_t)tv->tv_sec * 1000000ULL + tv->tv_usec;
timeSyncStatus.syncCount++;
if (timeSyncStatus.rtcAvailable) {
struct tm timeinfo;
time_t now = tv->tv_sec;
localtime_r(&now, &timeinfo);
if (writeRTC(&timeinfo)) {
timeSyncStatus.rtcSyncCount++;
}
}
}
void initNTP() {
configTime(9 * 3600, 0, "pool.ntp.org", "time.nist.gov");
sntp_set_time_sync_notification_cb(timeSyncCallback);
}
void rtcSyncTask(void *parameter) {
const TickType_t xDelay = pdMS_TO_TICKS(RTC_SYNC_INTERVAL_MS);
while (1) {
if (timeSyncStatus.rtcAvailable) {
struct tm timeinfo;
if (readRTC(&timeinfo)) {
time_t now = mktime(&timeinfo);
struct timeval tv = { .tv_sec = now, .tv_usec = 0 };
settimeofday(&tv, NULL);
timeSyncStatus.synchronized = true;
timeSyncStatus.lastSyncTime = (uint64_t)now * 1000000ULL;
timeSyncStatus.rtcSyncCount++;
}
}
vTaskDelay(xDelay);
}
}
// ========================================
// MCP2515 모드
// ========================================
bool setMCP2515Mode(MCP2515Mode mode) {
const char* modeName;
MCP2515::ERROR result;
switch (mode) {
case MCP_MODE_NORMAL:
result = mcp2515.setNormalMode();
modeName = "Normal";
break;
case MCP_MODE_LISTEN_ONLY:
result = mcp2515.setListenOnlyMode();
modeName = "Listen-Only";
break;
case MCP_MODE_LOOPBACK:
result = mcp2515.setLoopbackMode();
modeName = "Loopback";
break;
case MCP_MODE_TRANSMIT:
result = mcp2515.setListenOnlyMode();
modeName = "Transmit-Only";
break;
default:
return false;
}
if (result == MCP2515::ERROR_OK) {
currentMcpMode = mode;
Serial.printf("✓ MCP2515 모드: %s\n", modeName);
return true;
}
return false;
}
// ========================================
// 인터럽트 및 Task
// ========================================
void IRAM_ATTR canISR() {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (canRxTaskHandle != NULL) {
vTaskNotifyGiveFromISR(canRxTaskHandle, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
void serialRxTask(void *parameter) {
SerialMessage serialMsg;
uint8_t lineBuffer[MAX_SERIAL_LINE_LEN];
uint16_t lineIndex = 0;
uint32_t lastActivity = millis();
while (1) {
while (SerialComm.available()) {
uint8_t c = SerialComm.read();
lineBuffer[lineIndex++] = c;
lastActivity = millis();
if (c == '\n' || c == '\r' || lineIndex >= MAX_SERIAL_LINE_LEN - 1) {
if (lineIndex > 0) {
struct timeval tv;
gettimeofday(&tv, NULL);
serialMsg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
serialMsg.length = lineIndex;
memcpy(serialMsg.data, lineBuffer, lineIndex);
serialMsg.isTx = false;
if (xQueueSend(serialQueue, &serialMsg, pdMS_TO_TICKS(10)) == pdTRUE) {
totalSerialRxCount++;
}
lineIndex = 0;
}
}
if (lineIndex >= MAX_SERIAL_LINE_LEN - 1) lineIndex = 0;
}
if (lineIndex > 0 && (millis() - lastActivity > 100)) {
struct timeval tv;
gettimeofday(&tv, NULL);
serialMsg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
serialMsg.length = lineIndex;
memcpy(serialMsg.data, lineBuffer, lineIndex);
serialMsg.isTx = false;
if (xQueueSend(serialQueue, &serialMsg, pdMS_TO_TICKS(10)) == pdTRUE) {
totalSerialRxCount++;
}
lineIndex = 0;
}
vTaskDelay(pdMS_TO_TICKS(1));
}
}
void canRxTask(void *parameter) {
struct can_frame frame;
CANMessage msg;
while (1) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {
struct timeval tv;
gettimeofday(&tv, NULL);
msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
msg.id = frame.can_id & 0x1FFFFFFF;
msg.dlc = frame.can_dlc;
memcpy(msg.data, frame.data, 8);
if (xQueueSend(canQueue, &msg, 0) == pdTRUE) {
totalMsgCount++;
}
}
}
}
// ========================================
// SD Write Task
// ========================================
void sdWriteTask(void *parameter) {
CANMessage canMsg;
SerialMessage serialMsg;
while (1) {
bool hasWork = false;
// CAN 메시지 처리
if (xQueueReceive(canQueue, &canMsg, 0) == pdTRUE) {
hasWork = true;
// 실시간 모니터링 업데이트
bool found = false;
for (int i = 0; i < RECENT_MSG_COUNT; i++) {
if (recentData[i].msg.id == canMsg.id) {
recentData[i].msg = canMsg;
recentData[i].count++;
found = true;
break;
}
}
if (!found) {
for (int i = 0; i < RECENT_MSG_COUNT; i++) {
if (recentData[i].count == 0) {
recentData[i].msg = canMsg;
recentData[i].count = 1;
break;
}
}
}
// CAN 로깅
if (loggingEnabled && sdCardReady) {
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
if (canLogFormatCSV) {
char csvLine[128];
uint64_t relativeTime = canMsg.timestamp_us - canLogStartTime;
char dataStr[32];
int dataLen = 0;
for (int i = 0; i < canMsg.dlc; i++) {
dataLen += sprintf(&dataStr[dataLen], "%02X", canMsg.data[i]);
if (i < canMsg.dlc - 1) dataStr[dataLen++] = ' ';
}
dataStr[dataLen] = '\0';
int lineLen = snprintf(csvLine, sizeof(csvLine),
"%llu,0x%X,%d,%s\n",
relativeTime, canMsg.id, canMsg.dlc, dataStr);
if (logFile) {
logFile.write((uint8_t*)csvLine, lineLen);
currentFileSize += lineLen;
static int csvFlushCounter = 0;
if (++csvFlushCounter >= 100) {
logFile.flush();
csvFlushCounter = 0;
}
}
} else {
// BIN 형식
if (bufferIndex + sizeof(CANMessage) <= FILE_BUFFER_SIZE) {
memcpy(&fileBuffer[bufferIndex], &canMsg, sizeof(CANMessage));
bufferIndex += sizeof(CANMessage);
currentFileSize += sizeof(CANMessage);
}
if (bufferIndex >= FILE_BUFFER_SIZE - sizeof(CANMessage)) {
if (logFile) {
logFile.write(fileBuffer, bufferIndex);
logFile.flush();
bufferIndex = 0;
}
}
}
xSemaphoreGive(sdMutex);
}
}
}
if (!hasWork) {
vTaskDelay(pdMS_TO_TICKS(1));
}
}
}
void sdMonitorTask(void *parameter) {
const TickType_t xDelay = pdMS_TO_TICKS(1000);
while (1) {
uint32_t currentTime = millis();
// 메시지/초 계산
if (currentTime - lastMsgCountTime >= 1000) {
msgPerSecond = totalMsgCount - lastMsgCount;
lastMsgCount = totalMsgCount;
lastMsgCountTime = currentTime;
}
// 전압 체크
if (currentTime - powerStatus.lastCheck >= VOLTAGE_CHECK_INTERVAL_MS) {
float rawVoltage = analogRead(MONITORING_VOLT) * (3.3 / 4095.0);
powerStatus.voltage = rawVoltage * 1.0;
if (currentTime - powerStatus.lastMinReset >= 1000) {
powerStatus.minVoltage = powerStatus.voltage;
powerStatus.lastMinReset = currentTime;
} else {
if (powerStatus.voltage < powerStatus.minVoltage) {
powerStatus.minVoltage = powerStatus.voltage;
}
}
powerStatus.lowVoltage = (powerStatus.voltage < LOW_VOLTAGE_THRESHOLD);
powerStatus.lastCheck = currentTime;
}
vTaskDelay(xDelay);
}
}
// ========================================
// 파일 커멘트 관리
// ========================================
void saveFileComments() {
if (!sdCardReady) return;
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
File commentFile = SD.open("/comments.dat", FILE_WRITE);
if (commentFile) {
commentFile.write((uint8_t*)&commentCount, sizeof(commentCount));
commentFile.write((uint8_t*)fileComments, sizeof(FileComment) * commentCount);
commentFile.close();
}
xSemaphoreGive(sdMutex);
}
}
void loadFileComments() {
if (!sdCardReady) return;
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
if (SD.exists("/comments.dat")) {
File commentFile = SD.open("/comments.dat", FILE_READ);
if (commentFile) {
commentFile.read((uint8_t*)&commentCount, sizeof(commentCount));
if (commentCount > MAX_FILE_COMMENTS) commentCount = MAX_FILE_COMMENTS;
commentFile.read((uint8_t*)fileComments, sizeof(FileComment) * commentCount);
commentFile.close();
}
}
xSemaphoreGive(sdMutex);
}
}
const char* getFileComment(const char* filename) {
for (int i = 0; i < commentCount; i++) {
if (strcmp(fileComments[i].filename, filename) == 0) {
return fileComments[i].comment;
}
}
return "";
}
void addFileComment(const char* filename, const char* comment) {
for (int i = 0; i < commentCount; i++) {
if (strcmp(fileComments[i].filename, filename) == 0) {
strncpy(fileComments[i].comment, comment, MAX_COMMENT_LEN - 1);
fileComments[i].comment[MAX_COMMENT_LEN - 1] = '\0';
saveFileComments();
return;
}
}
if (commentCount < MAX_FILE_COMMENTS) {
strncpy(fileComments[commentCount].filename, filename, MAX_FILENAME_LEN - 1);
fileComments[commentCount].filename[MAX_FILENAME_LEN - 1] = '\0';
strncpy(fileComments[commentCount].comment, comment, MAX_COMMENT_LEN - 1);
fileComments[commentCount].comment[MAX_COMMENT_LEN - 1] = '\0';
commentCount++;
saveFileComments();
}
}
// ========================================
// 시퀀스 관리
// ========================================
void saveSequences() {
if (!sdCardReady) return;
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
File seqFile = SD.open("/sequences.dat", FILE_WRITE);
if (seqFile) {
seqFile.write((uint8_t*)&sequenceCount, sizeof(sequenceCount));
seqFile.write((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount);
seqFile.close();
}
xSemaphoreGive(sdMutex);
}
}
void loadSequences() {
if (!sdCardReady) return;
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
if (SD.exists("/sequences.dat")) {
File seqFile = SD.open("/sequences.dat", FILE_READ);
if (seqFile) {
seqFile.read((uint8_t*)&sequenceCount, sizeof(sequenceCount));
if (sequenceCount > MAX_SEQUENCES) sequenceCount = MAX_SEQUENCES;
seqFile.read((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount);
seqFile.close();
}
}
xSemaphoreGive(sdMutex);
}
}
// ========================================
// TX Task
// ========================================
void txTask(void *parameter) {
struct can_frame frame;
while (1) {
uint32_t now = millis();
bool anyActive = false;
for (int i = 0; i < MAX_TX_MESSAGES; i++) {
if (txMessages[i].active && txMessages[i].interval > 0) {
anyActive = true;
if (now - txMessages[i].lastSent >= txMessages[i].interval) {
if (currentMcpMode == MCP_MODE_TRANSMIT) {
mcp2515.setNormalMode();
}
frame.can_id = txMessages[i].id;
if (txMessages[i].extended) {
frame.can_id |= CAN_EFF_FLAG;
}
frame.can_dlc = txMessages[i].dlc;
memcpy(frame.data, txMessages[i].data, 8);
if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) {
totalTxCount++;
txMessages[i].lastSent = now;
}
if (currentMcpMode == MCP_MODE_TRANSMIT) {
mcp2515.setListenOnlyMode();
}
}
}
}
vTaskDelay(anyActive ? pdMS_TO_TICKS(1) : pdMS_TO_TICKS(10));
}
}
// ========================================
// Sequence Task
// ========================================
void sequenceTask(void *parameter) {
while (1) {
if (seqRuntime.running && seqRuntime.activeSequenceIndex >= 0 &&
seqRuntime.activeSequenceIndex < sequenceCount) {
CANSequence* seq = &sequences[seqRuntime.activeSequenceIndex];
uint32_t now = millis();
if (seqRuntime.currentStep < seq->stepCount) {
SequenceStep* step = &seq->steps[seqRuntime.currentStep];
if (now - seqRuntime.lastStepTime >= step->delayMs) {
if (currentMcpMode == MCP_MODE_TRANSMIT) {
mcp2515.setNormalMode();
}
struct can_frame frame;
frame.can_id = step->canId;
if (step->extended) {
frame.can_id |= CAN_EFF_FLAG;
}
frame.can_dlc = step->dlc;
memcpy(frame.data, step->data, 8);
if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) {
totalTxCount++;
}
if (currentMcpMode == MCP_MODE_TRANSMIT) {
mcp2515.setListenOnlyMode();
}
seqRuntime.currentStep++;
seqRuntime.lastStepTime = now;
}
} else {
if (seq->repeatMode == 0) {
seqRuntime.running = false;
} else if (seq->repeatMode == 1) {
seqRuntime.currentRepeat++;
if (seqRuntime.currentRepeat >= seq->repeatCount) {
seqRuntime.running = false;
} else {
seqRuntime.currentStep = 0;
seqRuntime.lastStepTime = now;
}
} else if (seq->repeatMode == 2) {
seqRuntime.currentStep = 0;
seqRuntime.lastStepTime = now;
}
}
vTaskDelay(pdMS_TO_TICKS(1));
} else {
vTaskDelay(pdMS_TO_TICKS(10));
}
}
}
// ========================================
// WebSocket 이벤트 처리 (중요!)
// ========================================
void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) {
if (type == WStype_TEXT) {
DynamicJsonDocument doc(2048);
DeserializationError error = deserializeJson(doc, payload);
if (error) return;
const char* cmd = doc["cmd"];
if (strcmp(cmd, "getSettings") == 0) {
DynamicJsonDocument response(1024);
response["type"] = "settings";
response["ssid"] = wifiSSID;
response["password"] = wifiPassword;
response["staEnable"] = enableSTAMode;
response["staSSID"] = staSSID;
response["staPassword"] = staPassword;
response["staConnected"] = (WiFi.status() == WL_CONNECTED);
response["staIP"] = WiFi.localIP().toString();
String json;
serializeJson(response, json);
webSocket.sendTXT(num, json);
}
else if (strcmp(cmd, "saveSettings") == 0) {
const char* newSSID = doc["ssid"];
const char* newPassword = doc["password"];
bool newSTAEnable = doc["staEnable"];
const char* newSTASSID = doc["staSSID"];
const char* newSTAPassword = doc["staPassword"];
if (newSSID && strlen(newSSID) > 0) {
strncpy(wifiSSID, newSSID, sizeof(wifiSSID) - 1);
wifiSSID[sizeof(wifiSSID) - 1] = '\0';
}
if (newPassword) {
strncpy(wifiPassword, newPassword, sizeof(wifiPassword) - 1);
wifiPassword[sizeof(wifiPassword) - 1] = '\0';
}
enableSTAMode = newSTAEnable;
if (newSTASSID) {
strncpy(staSSID, newSTASSID, sizeof(staSSID) - 1);
staSSID[sizeof(staSSID) - 1] = '\0';
}
if (newSTAPassword) {
strncpy(staPassword, newSTAPassword, sizeof(staPassword) - 1);
staPassword[sizeof(staPassword) - 1] = '\0';
}
saveSettings();
DynamicJsonDocument response(256);
response["type"] = "settingsSaved";
response["success"] = true;
String json;
serializeJson(response, json);
webSocket.sendTXT(num, json);
}
else if (strcmp(cmd, "startLogging") == 0) {
if (!loggingEnabled && sdCardReady) {
const char* format = doc["format"];
if (format && strcmp(format, "csv") == 0) {
canLogFormatCSV = true;
} else {
canLogFormatCSV = false;
}
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
struct tm timeinfo;
time_t now;
time(&now);
localtime_r(&now, &timeinfo);
struct timeval tv;
gettimeofday(&tv, NULL);
canLogStartTime = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
const char* ext = canLogFormatCSV ? "csv" : "bin";
snprintf(currentFilename, sizeof(currentFilename),
"/CAN_%04d%02d%02d_%02d%02d%02d.%s",
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, ext);
logFile = SD.open(currentFilename, FILE_WRITE);
if (logFile) {
if (canLogFormatCSV) {
logFile.println("Time_us,CAN_ID,DLC,Data");
}
loggingEnabled = true;
bufferIndex = 0;
currentFileSize = logFile.size();
}
xSemaphoreGive(sdMutex);
}
}
}
else if (strcmp(cmd, "stopLogging") == 0) {
if (loggingEnabled) {
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
if (bufferIndex > 0 && logFile) {
logFile.write(fileBuffer, bufferIndex);
bufferIndex = 0;
}
if (logFile) {
logFile.close();
}
loggingEnabled = false;
xSemaphoreGive(sdMutex);
}
}
}
else if (strcmp(cmd, "startSerialLogging") == 0) {
if (!serialLoggingEnabled && sdCardReady) {
const char* format = doc["format"];
if (format && strcmp(format, "bin") == 0) {
serialLogFormatCSV = false;
} else {
serialLogFormatCSV = true;
}
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
struct tm timeinfo;
time_t now;
time(&now);
localtime_r(&now, &timeinfo);
struct timeval tv;
gettimeofday(&tv, NULL);
serialLogStartTime = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
const char* ext = serialLogFormatCSV ? "csv" : "bin";
snprintf(currentSerialFilename, sizeof(currentSerialFilename),
"/SER_%04d%02d%02d_%02d%02d%02d.%s",
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, ext);
serialLogFile = SD.open(currentSerialFilename, FILE_WRITE);
if (serialLogFile) {
if (serialLogFormatCSV) {
serialLogFile.println("Time_us,Direction,Data");
}
serialLoggingEnabled = true;
serialCsvIndex = 0;
currentSerialFileSize = serialLogFile.size();
}
xSemaphoreGive(sdMutex);
}
}
}
else if (strcmp(cmd, "stopSerialLogging") == 0) {
if (serialLoggingEnabled) {
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
if (serialCsvIndex > 0 && serialLogFile) {
serialLogFile.write((uint8_t*)serialCsvBuffer, serialCsvIndex);
serialCsvIndex = 0;
}
if (serialLogFile) {
serialLogFile.close();
}
serialLoggingEnabled = false;
xSemaphoreGive(sdMutex);
}
}
}
else if (strcmp(cmd, "sendSerial") == 0) {
const char* data = doc["data"];
if (data && strlen(data) > 0) {
SerialComm.println(data);
SerialMessage serialMsg;
struct timeval tv;
gettimeofday(&tv, NULL);
serialMsg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
serialMsg.length = strlen(data) + 2;
if (serialMsg.length > MAX_SERIAL_LINE_LEN - 1) {
serialMsg.length = MAX_SERIAL_LINE_LEN - 1;
}
snprintf((char*)serialMsg.data, MAX_SERIAL_LINE_LEN, "%s\r\n", data);
serialMsg.isTx = true;
if (xQueueSend(serialQueue, &serialMsg, pdMS_TO_TICKS(10)) == pdTRUE) {
totalSerialTxCount++;
}
}
}
else if (strcmp(cmd, "setSerialConfig") == 0) {
uint32_t baud = doc["baudRate"] | 115200;
uint8_t data = doc["dataBits"] | 8;
uint8_t parity = doc["parity"] | 0;
uint8_t stop = doc["stopBits"] | 1;
serialSettings.baudRate = baud;
serialSettings.dataBits = data;
serialSettings.parity = parity;
serialSettings.stopBits = stop;
saveSerialSettings();
applySerialSettings();
}
else if (strcmp(cmd, "getSerialConfig") == 0) {
DynamicJsonDocument response(512);
response["type"] = "serialConfig";
response["baudRate"] = serialSettings.baudRate;
response["dataBits"] = serialSettings.dataBits;
response["parity"] = serialSettings.parity;
response["stopBits"] = serialSettings.stopBits;
String json;
serializeJson(response, json);
webSocket.sendTXT(num, json);
}
else if (strcmp(cmd, "setSpeed") == 0) {
int speedIndex = doc["speed"];
if (speedIndex >= 0 && speedIndex < 4) {
currentCanSpeed = canSpeedValues[speedIndex];
mcp2515.reset();
mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ);
setMCP2515Mode(currentMcpMode);
saveSettings();
}
}
else if (strcmp(cmd, "setMcpMode") == 0) {
int mode = doc["mode"];
if (mode >= 0 && mode <= 3) {
setMCP2515Mode((MCP2515Mode)mode);
saveSettings();
}
}
else if (strcmp(cmd, "syncTimeFromPhone") == 0) {
int year = doc["year"] | 2024;
int month = doc["month"] | 1;
int day = doc["day"] | 1;
int hour = doc["hour"] | 0;
int minute = doc["minute"] | 0;
int second = doc["second"] | 0;
struct tm timeinfo;
timeinfo.tm_year = year - 1900;
timeinfo.tm_mon = month - 1;
timeinfo.tm_mday = day;
timeinfo.tm_hour = hour;
timeinfo.tm_min = minute;
timeinfo.tm_sec = second;
time_t t = mktime(&timeinfo);
struct timeval tv = {t, 0};
settimeofday(&tv, NULL);
timeSyncStatus.synchronized = true;
timeSyncStatus.lastSyncTime = (uint64_t)t * 1000000ULL;
timeSyncStatus.syncCount++;
if (timeSyncStatus.rtcAvailable) {
writeRTC(&timeinfo);
}
}
else if (strcmp(cmd, "getFiles") == 0) {
if (sdCardReady) {
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
DynamicJsonDocument response(6144);
response["type"] = "files";
JsonArray files = response.createNestedArray("list");
File root = SD.open("/");
File file = root.openNextFile();
while (file) {
if (!file.isDirectory()) {
const char* filename = file.name();
if (filename[0] != '.' &&
strcmp(filename, "System Volume Information") != 0) {
JsonObject fileObj = files.createNestedObject();
fileObj["name"] = filename;
fileObj["size"] = file.size();
const char* comment = getFileComment(filename);
if (strlen(comment) > 0) {
fileObj["comment"] = comment;
}
}
}
file = root.openNextFile();
}
xSemaphoreGive(sdMutex);
String json;
serializeJson(response, json);
webSocket.sendTXT(num, json);
}
}
}
else if (strcmp(cmd, "deleteFile") == 0) {
const char* filename = doc["filename"];
if (filename && strlen(filename) > 0) {
String fullPath = "/" + String(filename);
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
bool success = false;
if (SD.exists(fullPath)) {
if (SD.remove(fullPath)) {
success = true;
}
}
xSemaphoreGive(sdMutex);
DynamicJsonDocument response(256);
response["type"] = "deleteResult";
response["success"] = success;
String json;
serializeJson(response, json);
webSocket.sendTXT(num, json);
}
}
}
else if (strcmp(cmd, "addComment") == 0) {
const char* filename = doc["filename"];
const char* comment = doc["comment"];
if (filename && comment) {
addFileComment(filename, comment);
}
}
else if (strcmp(cmd, "sendOnce") == 0) {
if (currentMcpMode == MCP_MODE_TRANSMIT) {
mcp2515.setNormalMode();
}
struct can_frame frame;
frame.can_id = strtoul(doc["id"], NULL, 16);
if (doc["ext"] | false) {
frame.can_id |= CAN_EFF_FLAG;
}
frame.can_dlc = doc["dlc"] | 8;
JsonArray dataArray = doc["data"];
for (int i = 0; i < 8; i++) {
frame.data[i] = dataArray[i] | 0;
}
if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) {
totalTxCount++;
}
if (currentMcpMode == MCP_MODE_TRANSMIT) {
mcp2515.setListenOnlyMode();
}
}
else if (strcmp(cmd, "getSequences") == 0) {
DynamicJsonDocument response(3072);
response["type"] = "sequences";
JsonArray seqArray = response.createNestedArray("list");
for (int i = 0; i < sequenceCount; i++) {
JsonObject seqObj = seqArray.createNestedObject();
seqObj["name"] = sequences[i].name;
seqObj["steps"] = sequences[i].stepCount;
seqObj["repeatMode"] = sequences[i].repeatMode;
seqObj["repeatCount"] = sequences[i].repeatCount;
}
String json;
serializeJson(response, json);
webSocket.sendTXT(num, json);
}
}
}
// ========================================
// Web Update Task
// ========================================
void webUpdateTask(void *parameter) {
const TickType_t xDelay = pdMS_TO_TICKS(100);
while (1) {
webSocket.loop();
if (webSocket.connectedClients() > 0) {
DynamicJsonDocument doc(4096);
doc["type"] = "update";
doc["logging"] = loggingEnabled;
doc["serialLogging"] = serialLoggingEnabled;
doc["sdReady"] = sdCardReady;
doc["totalMsg"] = totalMsgCount;
doc["msgPerSec"] = msgPerSecond;
doc["totalTx"] = totalTxCount;
doc["totalSerialRx"] = totalSerialRxCount;
doc["totalSerialTx"] = totalSerialTxCount;
doc["fileSize"] = currentFileSize;
doc["serialFileSize"] = currentSerialFileSize;
doc["queueUsed"] = uxQueueMessagesWaiting(canQueue);
doc["queueSize"] = CAN_QUEUE_SIZE;
doc["serialQueueUsed"] = uxQueueMessagesWaiting(serialQueue);
doc["serialQueueSize"] = SERIAL_QUEUE_SIZE;
doc["timeSync"] = timeSyncStatus.synchronized;
doc["rtcAvail"] = timeSyncStatus.rtcAvailable;
doc["rtcSyncCnt"] = timeSyncStatus.rtcSyncCount;
doc["syncCount"] = timeSyncStatus.syncCount;
doc["voltage"] = powerStatus.voltage;
doc["minVoltage"] = powerStatus.minVoltage;
doc["lowVoltage"] = powerStatus.lowVoltage;
doc["mcpMode"] = (int)currentMcpMode;
if (loggingEnabled && currentFilename[0] != '\0') {
doc["currentFile"] = String(currentFilename);
} else {
doc["currentFile"] = "";
}
if (serialLoggingEnabled && currentSerialFilename[0] != '\0') {
doc["currentSerialFile"] = String(currentSerialFilename);
} else {
doc["currentSerialFile"] = "";
}
time_t now;
time(&now);
doc["timestamp"] = (uint64_t)now;
// CAN 메시지 배열
JsonArray messages = doc.createNestedArray("messages");
for (int i = 0; i < RECENT_MSG_COUNT; i++) {
if (recentData[i].count > 0) {
JsonObject msgObj = messages.createNestedObject();
msgObj["id"] = recentData[i].msg.id;
msgObj["dlc"] = recentData[i].msg.dlc;
msgObj["count"] = recentData[i].count;
JsonArray dataArray = msgObj.createNestedArray("data");
for (int j = 0; j < recentData[i].msg.dlc; j++) {
dataArray.add(recentData[i].msg.data[j]);
}
}
}
// Serial 메시지 배열
SerialMessage serialMsg;
JsonArray serialMessages = doc.createNestedArray("serialMessages");
int serialCount = 0;
while (serialCount < 10 && xQueueReceive(serialQueue, &serialMsg, 0) == pdTRUE) {
JsonObject serMsgObj = serialMessages.createNestedObject();
serMsgObj["timestamp"] = serialMsg.timestamp_us;
serMsgObj["isTx"] = serialMsg.isTx;
char dataStr[MAX_SERIAL_LINE_LEN + 1];
memcpy(dataStr, serialMsg.data, serialMsg.length);
dataStr[serialMsg.length] = '\0';
serMsgObj["data"] = dataStr;
serialCount++;
// Serial 로깅
if (serialLoggingEnabled && sdCardReady) {
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
if (serialLogFormatCSV) {
uint64_t relativeTime = serialMsg.timestamp_us - serialLogStartTime;
char csvLine[256];
int lineLen = snprintf(csvLine, sizeof(csvLine),
"%llu,%s,\"%s\"\n",
relativeTime,
serialMsg.isTx ? "TX" : "RX",
dataStr);
if (serialCsvIndex + lineLen < SERIAL_CSV_BUFFER_SIZE) {
memcpy(&serialCsvBuffer[serialCsvIndex], csvLine, lineLen);
serialCsvIndex += lineLen;
currentSerialFileSize += lineLen;
}
if (serialCsvIndex >= SERIAL_CSV_BUFFER_SIZE - 256) {
if (serialLogFile) {
serialLogFile.write((uint8_t*)serialCsvBuffer, serialCsvIndex);
serialLogFile.flush();
serialCsvIndex = 0;
}
}
} else {
if (serialLogFile) {
serialLogFile.write((uint8_t*)&serialMsg, sizeof(SerialMessage));
currentSerialFileSize += sizeof(SerialMessage);
static int binFlushCounter = 0;
if (++binFlushCounter >= 50) {
serialLogFile.flush();
binFlushCounter = 0;
}
}
}
xSemaphoreGive(sdMutex);
}
}
}
String json;
serializeJson(doc, json);
webSocket.broadcastTXT(json);
}
vTaskDelay(xDelay);
}
}
// ========================================
// Setup
// ========================================
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n========================================");
Serial.println(" Byun CAN Logger + Serial Terminal");
Serial.println(" Version 2.3 - PSRAM Optimized");
Serial.println(" ESP32-S3 Complete Edition");
Serial.println("========================================\n");
// ★★★ PSRAM 초기화 (가장 먼저!) ★★★
if (!initPSRAM()) {
Serial.println("\n✗ PSRAM 초기화 실패!");
Serial.println("✗ Arduino IDE 설정:");
Serial.println(" Tools → PSRAM → OPI PSRAM");
while (1) {
delay(1000);
Serial.println("✗ 설정 후 재업로드 필요!");
}
}
loadSettings();
analogSetPinAttenuation(MONITORING_VOLT, ADC_11db);
pinMode(CAN_INT_PIN, INPUT_PULLUP);
analogSetAttenuation(ADC_11db);
// SPI 초기화
Serial.println("SPI 초기화...");
hspi.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS);
hspi.beginTransaction(SPISettings(40000000, MSBFIRST, SPI_MODE0));
hspi.endTransaction();
pinMode(VSPI_CS, OUTPUT);
digitalWrite(VSPI_CS, HIGH);
delay(100);
vspi.begin(VSPI_SCLK, VSPI_MISO, VSPI_MOSI, VSPI_CS);
vspi.setFrequency(40000000);
Serial.println("✓ SPI 초기화 완료");
// Watchdog 비활성화
esp_task_wdt_deinit();
// MCP2515 초기화
Serial.println("MCP2515 초기화...");
mcp2515.reset();
delay(50);
mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ);
mcp2515.setNormalMode();
Serial.println("✓ MCP2515 초기화 완료");
// Serial 통신 초기화
applySerialSettings();
Serial.println("✓ Serial 통신 초기화 완료");
// Mutex 생성
sdMutex = xSemaphoreCreateMutex();
rtcMutex = xSemaphoreCreateMutex();
serialMutex = xSemaphoreCreateMutex();
if (!sdMutex || !rtcMutex || !serialMutex) {
Serial.println("✗ Mutex 생성 실패!");
while (1) delay(1000);
}
// RTC 초기화
initRTC();
// SD 카드 초기화
if (SD.begin(VSPI_CS, vspi)) {
sdCardReady = true;
Serial.println("✓ SD 카드 초기화 완료");
loadFileComments();
loadSequences();
} else {
Serial.println("✗ SD 카드 초기화 실패");
}
// WiFi 설정
if (enableSTAMode && strlen(staSSID) > 0) {
Serial.println("\n📶 WiFi APSTA 모드...");
WiFi.mode(WIFI_AP_STA);
WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4);
Serial.printf("✓ AP: %s\n", wifiSSID);
Serial.printf("✓ AP IP: %s\n", WiFi.softAPIP().toString().c_str());
WiFi.begin(staSSID, staPassword);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("✓ STA IP: %s\n", WiFi.localIP().toString().c_str());
initNTP();
}
} else {
Serial.println("\n📶 WiFi AP 모드...");
WiFi.mode(WIFI_AP);
WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4);
Serial.printf("✓ AP: %s\n", wifiSSID);
Serial.printf("✓ AP IP: %s\n", WiFi.softAPIP().toString().c_str());
}
WiFi.setSleep(false);
esp_wifi_set_max_tx_power(84);
// WebSocket 시작
webSocket.begin();
webSocket.onEvent(webSocketEvent);
// ★★★ 웹 서버 라우팅 (중요!) ★★★
server.on("/", HTTP_GET, []() {
server.send_P(200, "text/html", index_html);
});
server.on("/transmit", HTTP_GET, []() {
server.send_P(200, "text/html", transmit_html);
});
server.on("/graph", HTTP_GET, []() {
server.send_P(200, "text/html", graph_html);
});
server.on("/graph-view", HTTP_GET, []() {
server.send_P(200, "text/html", graph_viewer_html);
});
server.on("/settings", HTTP_GET, []() {
server.send_P(200, "text/html", settings_html);
});
server.on("/serial", HTTP_GET, []() {
server.send_P(200, "text/html", serial_terminal_html);
});
server.on("/download", HTTP_GET, []() {
if (server.hasArg("file")) {
String filename = "/" + server.arg("file");
if (SD.exists(filename)) {
File file = SD.open(filename, FILE_READ);
if (file) {
String displayName = server.arg("file");
server.sendHeader("Content-Disposition",
"attachment; filename=\"" + displayName + "\"");
server.sendHeader("Content-Type", "application/octet-stream");
server.streamFile(file, "application/octet-stream");
file.close();
} else {
server.send(500, "text/plain", "Failed to open file");
}
} else {
server.send(404, "text/plain", "File not found");
}
} else {
server.send(400, "text/plain", "Bad request");
}
});
server.begin();
Serial.println("✓ 웹 서버 시작 완료");
// ★★★ Queue 생성 (PSRAM 사용) ★★★
if (!createQueues()) {
Serial.println("✗ Queue 생성 실패!");
while (1) delay(1000);
}
// CAN 인터럽트 활성화
attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING);
// Task 생성
xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 4096, NULL, 6, &canRxTaskHandle, 1);
xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 24576, NULL, 4, &sdWriteTaskHandle, 1);
xTaskCreatePinnedToCore(sequenceTask, "SEQ", 4096, NULL, 2, NULL, 1);
xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", 6144, NULL, 5, &serialRxTaskHandle, 0);
xTaskCreatePinnedToCore(txTask, "TX", 4096, NULL, 3, NULL, 0);
xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 10240, NULL, 2, &webTaskHandle, 0);
xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 0);
if (timeSyncStatus.rtcAvailable) {
xTaskCreatePinnedToCore(rtcSyncTask, "RTC_SYNC", 3072, NULL, 0, &rtcTaskHandle, 0);
}
Serial.println("✓ 모든 Task 시작 완료");
Serial.println("\n========================================");
Serial.println(" 접속 방법");
Serial.println("========================================");
Serial.printf(" WiFi SSID: %s\n", wifiSSID);
Serial.printf(" URL: http://%s\n", WiFi.softAPIP().toString().c_str());
Serial.println("========================================");
Serial.println(" PSRAM 상태");
Serial.println("========================================");
Serial.printf(" 여유 PSRAM: %d KB\n", ESP.getFreePsram() / 1024);
Serial.println("========================================\n");
}
// ========================================
// Loop
// ========================================
void loop() {
server.handleClient();
vTaskDelay(pdMS_TO_TICKS(10));
static uint32_t lastPrint = 0;
if (millis() - lastPrint > 30000) {
Serial.printf("[상태] CAN큐: %d/%d | Serial큐: %d/%d | PSRAM: %d KB\n",
uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE,
uxQueueMessagesWaiting(serialQueue), SERIAL_QUEUE_SIZE,
ESP.getFreePsram() / 1024);
lastPrint = millis();
}
}