Files
esp32s3_canlogger_mcp2515/aa.ino
2025-12-03 08:30:34 +00:00

1972 lines
68 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.5 - ESP32-S3 PSRAM Full Optimized Edition
* Complete PSRAM allocation with memory verification
*/
#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"
// ========================================
// ESP32-S3 PSRAM 최적화 설정
// ========================================
// PSRAM 활용 - 대용량 버퍼 (8MB PSRAM 기준)
#define CAN_QUEUE_SIZE 10000 // 10,000개 (210KB PSRAM)
#define SERIAL_QUEUE_SIZE 2000 // 2,000개 (150KB PSRAM)
#define FILE_BUFFER_SIZE 131072 // 128KB (PSRAM)
#define SERIAL_CSV_BUFFER_SIZE 65536 // 64KB (PSRAM)
// webUpdateTask에서 전송
doc["psramFree"] = ESP.getFreePsram(); // ← 추가
doc["serialQueueUsed"] = uxQueueMessagesWaiting(serialQueue); // ← 추가
doc["serialQueueSize"] = SERIAL_QUEUE_SIZE; // ← 추가
// 기타 상수
#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
// GPIO 핀 정의
#define CAN_INT_PIN 4
#define SERIAL_TX_PIN 17
#define SERIAL_RX_PIN 18
#define HSPI_MISO 13
#define HSPI_MOSI 11
#define HSPI_SCLK 12
#define HSPI_CS 10
#define VSPI_MISO 41
#define VSPI_MOSI 40
#define VSPI_SCLK 39
#define VSPI_CS 42
#define RTC_SDA 8
#define RTC_SCL 9
#define DS3231_ADDRESS 0x68
// 동기화 및 모니터링 설정
#define RTC_SYNC_INTERVAL_MS 60000
#define VOLTAGE_CHECK_INTERVAL_MS 5000
#define LOW_VOLTAGE_THRESHOLD 3.0
#define MONITORING_VOLT 5
// ========================================
// RTOS 우선순위 정의
// ========================================
#define PRIORITY_CAN_RX 24 // 최고 우선순위
#define PRIORITY_SD_WRITE 20 // 매우 높음
#define PRIORITY_SERIAL_RX 18 // 높음
#define PRIORITY_TX_TASK 15 // 중간-높음
#define PRIORITY_SEQUENCE 12 // 중간
#define PRIORITY_WEB_UPDATE 8 // 중간-낮음
#define PRIORITY_SD_MONITOR 5 // 낮음
#define PRIORITY_RTC_SYNC 2 // 최저
// ========================================
// Stack 크기 정의
// ========================================
#define STACK_CAN_RX 6144
#define STACK_SD_WRITE 32768
#define STACK_SERIAL_RX 8192
#define STACK_TX_TASK 6144
#define STACK_SEQUENCE 6144
#define STACK_WEB_UPDATE 16384
#define STACK_SD_MONITOR 4096
#define STACK_RTC_SYNC 3072
// ========================================
// 구조체 정의
// ========================================
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; // 128KB PSRAM
char *serialCsvBuffer = nullptr; // 64KB PSRAM
uint8_t *canQueueStorage = nullptr; // 210KB PSRAM
uint8_t *serialQueueStorage = nullptr; // 150KB PSRAM
// FreeRTOS 정적 Queue 구조체 (DRAM - 작은 크기)
StaticQueue_t canQueueBuffer;
StaticQueue_t serialQueueBuffer;
// ========================================
// 전역 변수
// ========================================
// 하드웨어 객체
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;
QueueHandle_t serialQueue;
SemaphoreHandle_t sdMutex;
SemaphoreHandle_t rtcMutex;
SemaphoreHandle_t serialMutex;
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;
// CAN 설정
MCP2515Mode currentMcpMode = MCP_MODE_NORMAL;
CAN_SPEED currentCanSpeed = CAN_1000KBPS;
const char* canSpeedNames[] = {"125K", "250K", "500K", "1M"};
CAN_SPEED canSpeedValues[] = {CAN_125KBPS, CAN_250KBPS, CAN_500KBPS, CAN_1000KBPS};
// 통계
RecentCANData recentData[RECENT_MSG_COUNT];
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;
// CAN 송신 및 시퀀스
TxMessage txMessages[MAX_TX_MESSAGES];
#define MAX_SEQUENCES 10
CANSequence sequences[MAX_SEQUENCES];
uint8_t sequenceCount = 0;
SequenceRuntime seqRuntime = {false, 0, 0, 0, -1};
// 파일 커멘트
#define MAX_FILE_COMMENTS 50
FileComment fileComments[MAX_FILE_COMMENTS];
int commentCount = 0;
// WiFi 설정
char wifiSSID[32] = "Byun_CAN_Logger";
char wifiPassword[64] = "12345678";
bool enableSTAMode = false;
char staSSID[32] = "";
char staPassword[64] = "";
// RTC
SoftWire rtcWire(RTC_SDA, RTC_SCL);
char rtcSyncBuffer[20];
// Forward declarations
void IRAM_ATTR canISR();
// ========================================
// 메모리 정보 출력 함수
// ========================================
void printMemoryInfo(const char* name, void* ptr) {
if (ptr == nullptr) {
Serial.printf(" %-25s: NULL\n", name);
return;
}
bool isPSRAM = esp_ptr_external_ram(ptr);
Serial.printf(" %-25s: %s @ 0x%08X\n",
name,
isPSRAM ? "PSRAM" : "DRAM ",
(uint32_t)ptr);
}
// ========================================
// PSRAM 버퍼 할당
// ========================================
bool allocatePSRAMBuffers() {
Serial.println("\n========================================");
Serial.println("PSRAM 버퍼 할당 중...");
Serial.println("========================================");
// 1. 파일 버퍼 (128KB)
fileBuffer = (uint8_t*)heap_caps_malloc(FILE_BUFFER_SIZE,
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (fileBuffer == nullptr) {
Serial.printf("✗ 파일 버퍼 할당 실패 (%d KB)\n", FILE_BUFFER_SIZE / 1024);
return false;
}
memset(fileBuffer, 0, FILE_BUFFER_SIZE);
// 2. Serial CSV 버퍼 (64KB)
serialCsvBuffer = (char*)heap_caps_malloc(SERIAL_CSV_BUFFER_SIZE,
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (serialCsvBuffer == nullptr) {
Serial.printf("✗ Serial CSV 버퍼 할당 실패 (%d KB)\n", SERIAL_CSV_BUFFER_SIZE / 1024);
heap_caps_free(fileBuffer);
return false;
}
memset(serialCsvBuffer, 0, SERIAL_CSV_BUFFER_SIZE);
// 3. CAN Queue 버퍼 (210KB)
size_t canQueueSize = CAN_QUEUE_SIZE * sizeof(CANMessage);
canQueueStorage = (uint8_t*)heap_caps_malloc(canQueueSize,
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (canQueueStorage == nullptr) {
Serial.printf("✗ CAN Queue 버퍼 할당 실패 (%.1f KB)\n", canQueueSize / 1024.0);
heap_caps_free(fileBuffer);
heap_caps_free(serialCsvBuffer);
return false;
}
memset(canQueueStorage, 0, canQueueSize);
// 4. Serial Queue 버퍼 (150KB)
size_t serialQueueSize = SERIAL_QUEUE_SIZE * sizeof(SerialMessage);
serialQueueStorage = (uint8_t*)heap_caps_malloc(serialQueueSize,
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (serialQueueStorage == nullptr) {
Serial.printf("✗ Serial Queue 버퍼 할당 실패 (%.1f KB)\n", serialQueueSize / 1024.0);
heap_caps_free(fileBuffer);
heap_caps_free(serialCsvBuffer);
heap_caps_free(canQueueStorage);
return false;
}
memset(serialQueueStorage, 0, serialQueueSize);
// 메모리 정보 출력
Serial.println("\n✓ PSRAM 버퍼 할당 완료:");
printMemoryInfo("파일 버퍼 (128KB)", fileBuffer);
printMemoryInfo("Serial CSV 버퍼 (64KB)", serialCsvBuffer);
printMemoryInfo("CAN Queue 버퍼 (210KB)", canQueueStorage);
printMemoryInfo("Serial Queue 버퍼 (150KB)", serialQueueStorage);
float totalPSRAM = (FILE_BUFFER_SIZE + SERIAL_CSV_BUFFER_SIZE + canQueueSize + serialQueueSize) / 1024.0;
Serial.printf("\n총 PSRAM 사용: %.1f KB\n", totalPSRAM);
Serial.printf("PSRAM 여유: %u KB\n\n", ESP.getFreePsram() / 1024);
return true;
}
// ========================================
// Queue 생성 (PSRAM 버퍼 사용)
// ========================================
bool createQueues() {
Serial.println("========================================");
Serial.println("Queue 생성 중 (PSRAM 버퍼 사용)...");
Serial.println("========================================");
// CAN Queue (PSRAM 버퍼 사용)
canQueue = xQueueCreateStatic(
CAN_QUEUE_SIZE,
sizeof(CANMessage),
canQueueStorage,
&canQueueBuffer
);
if (canQueue == NULL) {
Serial.println("✗ CAN Queue 생성 실패!");
return false;
}
// Serial Queue (PSRAM 버퍼 사용)
serialQueue = xQueueCreateStatic(
SERIAL_QUEUE_SIZE,
sizeof(SerialMessage),
serialQueueStorage,
&serialQueueBuffer
);
if (serialQueue == NULL) {
Serial.println("✗ Serial Queue 생성 실패!");
return false;
}
Serial.println("\n✓ Queue 생성 완료:");
Serial.printf(" - CAN Queue : %d개 × %d bytes = %.1f KB\n",
CAN_QUEUE_SIZE, sizeof(CANMessage),
(CAN_QUEUE_SIZE * sizeof(CANMessage)) / 1024.0);
Serial.printf(" - Serial Queue : %d개 × %d bytes = %.1f KB\n",
SERIAL_QUEUE_SIZE, sizeof(SerialMessage),
(SERIAL_QUEUE_SIZE * sizeof(SerialMessage)) / 1024.0);
Serial.println("\n메모리 위치 확인:");
printMemoryInfo("CAN Queue 버퍼", canQueueStorage);
printMemoryInfo("CAN Queue 핸들", canQueue);
printMemoryInfo("Serial Queue 버퍼", serialQueueStorage);
printMemoryInfo("Serial Queue 핸들", serialQueue);
Serial.println();
return true;
}
// ========================================
// Serial 설정 함수
// ========================================
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(4096);
Serial.printf("✓ Serial 설정: %u-%u-%u-%u\n",
serialSettings.baudRate, serialSettings.dataBits,
serialSettings.parity, serialSettings.stopBits);
}
// ========================================
// 설정 저장/로드
// ========================================
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(50)) != 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(buffer0 & 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(50)) != 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", "time.google.com");
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;
}
// ========================================
// CAN 인터럽트 핸들러
// ========================================
void IRAM_ATTR canISR() {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (canRxTaskHandle != NULL) {
vTaskNotifyGiveFromISR(canRxTaskHandle, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
// ========================================
// Serial RX Task (Priority 18)
// ========================================
void serialRxTask(void *parameter) {
SerialMessage serialMsg;
uint8_t lineBuffer[MAX_SERIAL_LINE_LEN];
uint16_t lineIndex = 0;
uint32_t lastActivity = millis();
Serial.println("✓ Serial RX Task 시작 (Priority 18)");
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, 0) == 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, 0) == pdTRUE) {
totalSerialRxCount++;
}
lineIndex = 0;
}
vTaskDelay(1);
}
}
// ========================================
// CAN RX Task (Priority 24 - 최고)
// ========================================
void canRxTask(void *parameter) {
struct can_frame frame;
CANMessage msg;
Serial.println("✓ CAN RX Task 시작 (Priority 24 - 최고)");
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 (Priority 20 - 매우 높음)
// ========================================
void sdWriteTask(void *parameter) {
CANMessage canMsg;
uint32_t csvFlushCounter = 0;
Serial.println("✓ SD Write Task 시작 (Priority 20)");
while (1) {
bool hasWork = false;
int batchCount = 0;
while (batchCount < 100 && xQueueReceive(canQueue, &canMsg, 0) == pdTRUE) {
hasWork = true;
batchCount++;
// 실시간 모니터링 업데이트
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(10)) == 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;
if (++csvFlushCounter >= 500) {
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 * 0.9) {
if (logFile) {
logFile.write(fileBuffer, bufferIndex);
logFile.flush();
bufferIndex = 0;
}
}
}
xSemaphoreGive(sdMutex);
}
}
}
if (!hasWork) {
vTaskDelay(1);
} else {
taskYIELD();
}
}
}
// ========================================
// SD Monitor Task (Priority 5)
// ========================================
void sdMonitorTask(void *parameter) {
const TickType_t xDelay = pdMS_TO_TICKS(1000);
uint32_t lastStatusPrint = 0;
Serial.println("✓ SD Monitor Task 시작 (Priority 5)");
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;
}
// 10초마다 상태 출력
if (currentTime - lastStatusPrint >= 10000) {
uint32_t canQueueUsed = uxQueueMessagesWaiting(canQueue);
uint32_t serialQueueUsed = uxQueueMessagesWaiting(serialQueue);
float canQueuePercent = (float)canQueueUsed / CAN_QUEUE_SIZE * 100.0;
float serialQueuePercent = (float)serialQueueUsed / SERIAL_QUEUE_SIZE * 100.0;
Serial.printf("[상태] CAN: %u msg/s | CAN큐: %u/%u (%.1f%%) | Serial큐: %u/%u (%.1f%%) | PSRAM: %u KB\n",
msgPerSecond,
canQueueUsed, CAN_QUEUE_SIZE, canQueuePercent,
serialQueueUsed, SERIAL_QUEUE_SIZE, serialQueuePercent,
ESP.getFreePsram() / 1024);
if (canQueuePercent >= 80.0) {
Serial.printf("⚠️ 경고: CAN Queue 사용률 %.1f%%\n", canQueuePercent);
}
if (serialQueuePercent >= 80.0) {
Serial.printf("⚠️ 경고: Serial Queue 사용률 %.1f%%\n", serialQueuePercent);
}
lastStatusPrint = currentTime;
}
vTaskDelay(xDelay);
}
}
// ========================================
// TX Task (Priority 15)
// ========================================
void txTask(void *parameter) {
struct can_frame frame;
Serial.println("✓ TX Task 시작 (Priority 15)");
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(pdMS_TO_TICKS(anyActive ? 1 : 10));
}
}
// ========================================
// Sequence Task (Priority 12)
// ========================================
void sequenceTask(void *parameter) {
Serial.println("✓ Sequence Task 시작 (Priority 12)");
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));
}
}
}
// ========================================
// 파일 커멘트 관리
// ========================================
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);
}
}
// ========================================
// 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();
Serial.printf("✓ CAN 로깅 시작: %s (%s)\n",
currentFilename, canLogFormatCSV ? "CSV" : "BIN");
}
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;
Serial.printf("✓ CAN 로깅 종료: %u bytes\n", currentFileSize);
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();
Serial.printf("✓ Serial 로깅 시작: %s\n", currentSerialFilename);
}
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;
Serial.printf("✓ Serial 로깅 종료: %u bytes\n", currentSerialFileSize);
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) {
serialSettings.baudRate = doc["baudRate"] | 115200;
serialSettings.dataBits = doc["dataBits"] | 8;
serialSettings.parity = doc["parity"] | 0;
serialSettings.stopBits = doc["stopBits"] | 1;
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, "syncTime") == 0) {
uint64_t phoneTime = doc["time"];
if (phoneTime > 0) {
time_t seconds = phoneTime / 1000;
suseconds_t microseconds = (phoneTime % 1000) * 1000;
struct timeval tv = {seconds, microseconds};
settimeofday(&tv, NULL);
timeSyncStatus.synchronized = true;
timeSyncStatus.lastSyncTime = phoneTime * 1000;
timeSyncStatus.syncCount++;
if (timeSyncStatus.rtcAvailable) {
struct tm timeinfo;
localtime_r(&seconds, &timeinfo);
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, "addTx") == 0) {
for (int i = 0; i < MAX_TX_MESSAGES; i++) {
if (!txMessages[i].active) {
txMessages[i].id = strtoul(doc["id"], NULL, 16);
txMessages[i].extended = doc["ext"] | false;
txMessages[i].dlc = doc["dlc"] | 8;
JsonArray dataArray = doc["data"];
for (int j = 0; j < 8; j++) {
txMessages[i].data[j] = dataArray[j] | 0;
}
txMessages[i].interval = doc["interval"] | 1000;
txMessages[i].active = true;
txMessages[i].lastSent = 0;
break;
}
}
}
else if (strcmp(cmd, "removeTx") == 0) {
uint32_t id = strtoul(doc["id"], NULL, 16);
for (int i = 0; i < MAX_TX_MESSAGES; i++) {
if (txMessages[i].active && txMessages[i].id == id) {
txMessages[i].active = false;
break;
}
}
}
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();
}
}
}
}
// ========================================
// Web Update Task (Priority 8)
// ========================================
void webUpdateTask(void *parameter) {
const TickType_t xDelay = pdMS_TO_TICKS(100);
Serial.println("✓ Web Update Task 시작 (Priority 8)");
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["voltage"] = powerStatus.voltage;
doc["mcpMode"] = (int)currentMcpMode;
if (loggingEnabled && currentFilename[0] != '\0') {
doc["currentFile"] = String(currentFilename);
}
if (serialLoggingEnabled && currentSerialFilename[0] != '\0') {
doc["currentSerialFile"] = String(currentSerialFilename);
}
time_t now;
time(&now);
doc["timestamp"] = (uint64_t)now;
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]);
}
}
}
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++;
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 * 0.9) {
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 - PSRAM Edition");
Serial.println(" Version 2.5 - Full Optimization");
Serial.println("========================================\n");
// PSRAM 확인
if (!psramFound()) {
Serial.println("✗ PSRAM 없음!");
Serial.println(" Arduino IDE → Tools → PSRAM → OPI PSRAM");
while (1) delay(1000);
}
Serial.printf("✓ PSRAM 감지: %d MB\n", ESP.getPsramSize() / 1024 / 1024);
Serial.printf(" 초기 여유: %u KB\n", ESP.getFreePsram() / 1024);
// PSRAM 버퍼 할당
if (!allocatePSRAMBuffers()) {
Serial.println("\n✗ PSRAM 버퍼 할당 실패!");
while (1) delay(1000);
}
// 설정 로드
loadSettings();
// GPIO 초기화
pinMode(CAN_INT_PIN, INPUT_PULLUP);
analogSetPinAttenuation(MONITORING_VOLT, ADC_11db);
analogSetAttenuation(ADC_11db);
memset(recentData, 0, sizeof(recentData));
memset(txMessages, 0, sizeof(txMessages));
memset(fileComments, 0, sizeof(fileComments));
// SPI 초기화
Serial.println("========================================");
Serial.println("SPI 초기화...");
Serial.println("========================================");
hspi.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS);
hspi.setFrequency(20000000);
pinMode(VSPI_CS, OUTPUT);
digitalWrite(VSPI_CS, HIGH);
vspi.begin(VSPI_SCLK, VSPI_MISO, VSPI_MOSI, VSPI_CS);
vspi.setFrequency(40000000);
Serial.println("✓ SPI 초기화 완료\n");
// Watchdog 비활성화
esp_task_wdt_deinit();
// MCP2515 초기화
Serial.println("========================================");
Serial.println("MCP2515 초기화...");
Serial.println("========================================");
mcp2515.reset();
delay(50);
mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ);
setMCP2515Mode(currentMcpMode);
Serial.println("✓ MCP2515 초기화 완료\n");
// Serial 통신 초기화
applySerialSettings();
// Mutex 생성
Serial.println("========================================");
Serial.println("Mutex 생성...");
Serial.println("========================================");
sdMutex = xSemaphoreCreateMutex();
rtcMutex = xSemaphoreCreateMutex();
serialMutex = xSemaphoreCreateMutex();
if (!sdMutex || !rtcMutex || !serialMutex) {
Serial.println("✗ Mutex 생성 실패!");
while (1) delay(1000);
}
Serial.println("✓ Mutex 생성 완료 (DRAM)\n");
// Queue 생성
if (!createQueues()) {
Serial.println("✗ Queue 생성 실패!");
while (1) delay(1000);
}
// RTC 초기화
Serial.println("========================================");
Serial.println("RTC 초기화...");
Serial.println("========================================");
initRTC();
Serial.println();
// SD 카드 초기화
Serial.println("========================================");
Serial.println("SD 카드 초기화...");
Serial.println("========================================");
if (SD.begin(VSPI_CS, vspi)) {
sdCardReady = true;
Serial.println("✓ SD 카드 초기화 완료");
loadFileComments();
loadSequences();
} else {
Serial.println("✗ SD 카드 초기화 실패");
}
Serial.println();
// WiFi 설정
Serial.println("========================================");
Serial.println("WiFi 초기화...");
Serial.println("========================================");
if (enableSTAMode && strlen(staSSID) > 0) {
WiFi.mode(WIFI_AP_STA);
WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4);
WiFi.begin(staSSID, staPassword);
Serial.printf("AP SSID: %s\n", wifiSSID);
Serial.printf("AP IP: %s\n", WiFi.softAPIP().toString().c_str());
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 연결: %s\n", WiFi.localIP().toString().c_str());
initNTP();
} else {
Serial.println("! STA 연결 실패 (AP 모드는 정상)");
}
} else {
WiFi.mode(WIFI_AP);
WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4);
Serial.printf("AP SSID: %s\n", wifiSSID);
Serial.printf("AP IP: %s\n", WiFi.softAPIP().toString().c_str());
}
WiFi.setSleep(false);
esp_wifi_set_max_tx_power(84);
Serial.println();
// WebSocket & Server
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) {
server.sendHeader("Content-Disposition",
"attachment; filename=\"" + server.arg("file") + "\"");
server.streamFile(file, "application/octet-stream");
file.close();
}
}
}
});
server.begin();
// CAN 인터럽트
attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING);
// Task 생성
Serial.println("========================================");
Serial.println("Task 생성 (우선순위 최적화)...");
Serial.println("========================================");
xTaskCreatePinnedToCore(canRxTask, "CAN_RX", STACK_CAN_RX, NULL,
PRIORITY_CAN_RX, &canRxTaskHandle, 1);
xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", STACK_SD_WRITE, NULL,
PRIORITY_SD_WRITE, &sdWriteTaskHandle, 1);
xTaskCreatePinnedToCore(sequenceTask, "SEQ_TASK", STACK_SEQUENCE, NULL,
PRIORITY_SEQUENCE, NULL, 1);
xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", STACK_SERIAL_RX, NULL,
PRIORITY_SERIAL_RX, &serialRxTaskHandle, 0);
xTaskCreatePinnedToCore(txTask, "TX_TASK", STACK_TX_TASK, NULL,
PRIORITY_TX_TASK, NULL, 0);
xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", STACK_WEB_UPDATE, NULL,
PRIORITY_WEB_UPDATE, &webTaskHandle, 0);
xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", STACK_SD_MONITOR, NULL,
PRIORITY_SD_MONITOR, NULL, 0);
if (timeSyncStatus.rtcAvailable) {
xTaskCreatePinnedToCore(rtcSyncTask, "RTC_SYNC", STACK_RTC_SYNC, NULL,
PRIORITY_RTC_SYNC, &rtcTaskHandle, 0);
}
Serial.println("✓ 모든 Task 생성 완료\n");
// 최종 메모리 상태
Serial.println("========================================");
Serial.println("최종 메모리 상태");
Serial.println("========================================");
Serial.printf("PSRAM 사용: %u KB\n",
(ESP.getPsramSize() - ESP.getFreePsram()) / 1024);
Serial.printf("PSRAM 여유: %u KB\n", ESP.getFreePsram() / 1024);
Serial.printf("DRAM 여유: %u KB\n", ESP.getFreeHeap() / 1024);
Serial.println();
Serial.println("========================================");
Serial.println("시스템 준비 완료!");
Serial.println("========================================");
Serial.printf("접속: http://%s\n", WiFi.softAPIP().toString().c_str());
Serial.println("페이지:");
Serial.println(" - Monitor : /");
Serial.println(" - Transmit : /transmit");
Serial.println(" - Graph : /graph");
Serial.println(" - Settings : /settings");
Serial.println(" - Serial : /serial");
Serial.println("========================================\n");
}
void loop() {
server.handleClient();
vTaskDelay(pdMS_TO_TICKS(10));
static uint32_t lastPrint = 0;
if (millis() - lastPrint > 30000) {
Serial.printf("[30초 통계] CAN: %lu msg/s | Queue: %d/%d (%.1f%%) | PSRAM: %u KB\n",
msgPerSecond,
uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE,
(float)uxQueueMessagesWaiting(canQueue) / CAN_QUEUE_SIZE * 100.0,
ESP.getFreePsram() / 1024);
lastPrint = millis();
}
}