diff --git a/ESP32_CAN_Logger-a.ino b/ESP32_CAN_Logger-a.ino index d37de86..edb35aa 100644 --- a/ESP32_CAN_Logger-a.ino +++ b/ESP32_CAN_Logger-a.ino @@ -1,7 +1,17 @@ /* * Byun CAN Logger with Web Interface + Serial Terminal - * Version: 2.2 - RTC Time Sync & Settings Page Fixed - * Fixed: RTC synchronization with WiFi time + Settings page loading + * 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 @@ -32,8 +42,6 @@ // GPIO 핀 정의 #define CAN_INT_PIN 4 - -// Serial 통신 핀 (추가) #define SERIAL_TX_PIN 17 #define SERIAL_RX_PIN 18 @@ -49,34 +57,35 @@ #define VSPI_SCLK 39 #define VSPI_CS 42 -// I2C2 핀 (RTC DS3231) - SoftWire 사용 +// I2C2 핀 (RTC DS3231) #define RTC_SDA 8 #define RTC_SCL 9 #define DS3231_ADDRESS 0x68 -// 버퍼 설정 (ESP32-S3 N16R8 - DRAM 제한 회피) -// 주의: PSRAM 할당이 안 되는 경우 크기 축소 -#define CAN_QUEUE_SIZE 1000 // 6000 → 1000 (DRAM 절약) -#define FILE_BUFFER_SIZE 16384 // 32768 → 16384 (16KB) +// ======================================== +// 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 - -// Serial 버퍼 설정 (추가) -#define SERIAL_QUEUE_SIZE 200 // 1200 → 200 (DRAM 절약) -#define SERIAL_CSV_BUFFER_SIZE 8192 // 20480 → 8192 (8KB) -#define MAX_SERIAL_LINE_LEN 64 // 128 → 64 (FreeRTOS Queue 제한 회피) - -// RTC 동기화 설정 +#define MAX_SERIAL_LINE_LEN 64 #define RTC_SYNC_INTERVAL_MS 60000 - -// 전력 모니터링 설정 #define VOLTAGE_CHECK_INTERVAL_MS 5000 #define LOW_VOLTAGE_THRESHOLD 3.0 #define MONITORING_VOLT 5 +#define MAX_GRAPH_SIGNALS 20 +#define MAX_SEQUENCES 10 +#define MAX_FILE_COMMENTS 50 -// CAN 메시지 구조체 +// ======================================== +// 구조체 정의 +// ======================================== struct CANMessage { uint64_t timestamp_us; uint32_t id; @@ -84,29 +93,25 @@ struct CANMessage { uint8_t data[8]; } __attribute__((packed)); -// Serial 메시지 구조체 (추가) struct SerialMessage { uint64_t timestamp_us; uint16_t length; uint8_t data[MAX_SERIAL_LINE_LEN]; - bool isTx; // true=송신, false=수신 + bool isTx; } __attribute__((packed)); -// Serial 설정 구조체 (추가) struct SerialSettings { uint32_t baudRate; - uint8_t dataBits; // 5, 6, 7, 8 - uint8_t parity; // 0=None, 1=Even, 2=Odd - uint8_t stopBits; // 1, 2 + uint8_t dataBits; + uint8_t parity; + uint8_t stopBits; } serialSettings = {115200, 8, 0, 1}; -// 실시간 모니터링용 구조체 struct RecentCANData { CANMessage msg; uint32_t count; }; -// CAN 송신용 구조체 struct TxMessage { uint32_t id; bool extended; @@ -117,7 +122,6 @@ struct TxMessage { bool active; }; -// CAN 시퀀스 스텝 구조체 struct SequenceStep { uint32_t canId; bool extended; @@ -126,7 +130,6 @@ struct SequenceStep { uint32_t delayMs; }; -// CAN 시퀀스 구조체 struct CANSequence { char name[32]; SequenceStep steps[20]; @@ -135,7 +138,6 @@ struct CANSequence { uint32_t repeatCount; }; -// 시퀀스 실행 상태 struct SequenceRuntime { bool running; uint8_t currentStep; @@ -144,13 +146,11 @@ struct SequenceRuntime { int8_t activeSequenceIndex; }; -// 파일 커멘트 구조체 struct FileComment { char filename[MAX_FILENAME_LEN]; char comment[MAX_COMMENT_LEN]; }; -// 시간 동기화 상태 struct TimeSyncStatus { bool synchronized; uint64_t lastSyncTime; @@ -160,7 +160,6 @@ struct TimeSyncStatus { uint32_t rtcSyncCount; } timeSyncStatus = {false, 0, 0, 0, false, 0}; -// 전력 모니터링 상태 struct PowerStatus { float voltage; float minVoltage; @@ -169,16 +168,6 @@ struct PowerStatus { uint32_t lastMinReset; } powerStatus = {0.0, 999.9, false, 0, 0}; -// MCP2515 레지스터 주소 정의 -#ifndef MCP_CANCTRL -#define MCP_CANCTRL 0x0F -#endif - -#ifndef MCP_CANSTAT -#define MCP_CANSTAT 0x0E -#endif - -// MCP2515 모드 정의 enum MCP2515Mode { MCP_MODE_NORMAL = 0, MCP_MODE_LISTEN_ONLY = 1, @@ -186,42 +175,51 @@ enum MCP2515Mode { MCP_MODE_TRANSMIT = 3 }; -// WiFi AP 기본 설정 +// ======================================== +// 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"; - -// WiFi Station 모드 설정 bool enableSTAMode = false; char staSSID[32] = ""; char staPassword[64] = ""; -// 전역 변수 +// 전역 객체 (내부 SRAM) SPIClass hspi(HSPI); SPIClass vspi(FSPI); MCP2515 mcp2515(HSPI_CS, 20000000, &hspi); - -// Serial 통신용 (추가) -HardwareSerial SerialComm(1); // UART1 사용 - +HardwareSerial SerialComm(1); WebServer server(80); WebSocketsServer webSocket = WebSocketsServer(81); Preferences preferences; -// Forward declaration -void IRAM_ATTR canISR(); - -// Queue 핸들 (FreeRTOS가 자동 할당) -QueueHandle_t canQueue; -QueueHandle_t serialQueue; -SemaphoreHandle_t sdMutex; -SemaphoreHandle_t rtcMutex; -SemaphoreHandle_t serialMutex; +// 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; @@ -229,77 +227,169 @@ File logFile; File serialLogFile; char currentFilename[MAX_FILENAME_LEN]; char currentSerialFilename[MAX_FILENAME_LEN]; -uint8_t fileBuffer[FILE_BUFFER_SIZE]; -char serialCsvBuffer[SERIAL_CSV_BUFFER_SIZE]; uint16_t bufferIndex = 0; uint16_t serialCsvIndex = 0; - -// 로깅 파일 크기 추적 volatile uint32_t currentFileSize = 0; volatile uint32_t currentSerialFileSize = 0; - -// 로깅 형식 선택 (false=BIN, true=CSV) volatile bool canLogFormatCSV = false; volatile bool serialLogFormatCSV = true; - -// 로깅 시작 타임스탬프 (상대시간 계산용) volatile uint64_t canLogStartTime = 0; volatile uint64_t serialLogStartTime = 0; -// 현재 MCP2515 모드 +// 기타 전역 변수 MCP2515Mode currentMcpMode = MCP_MODE_NORMAL; - -// RTC 관련 SoftWire rtcWire(RTC_SDA, RTC_SCL); char rtcSyncBuffer[20]; - -// CAN 속도 설정 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; - -// Serial 통신 카운터 (추가) volatile uint32_t totalSerialRxCount = 0; volatile uint32_t totalSerialTxCount = 0; - -// 그래프 최대 개수 -#define MAX_GRAPH_SIGNALS 20 - -// CAN 송신용 -TxMessage txMessages[MAX_TX_MESSAGES]; uint32_t totalTxCount = 0; - -// CAN 시퀀스 -#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; -// ======================================== -// Serial 설정 저장/로드 함수 (추가) -// ======================================== +// 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); - - Serial.printf("✓ Serial 설정 로드: %u-%u-%u-%u\n", - serialSettings.baudRate, serialSettings.dataBits, - serialSettings.parity, serialSettings.stopBits); } void saveSerialSettings() { @@ -307,16 +397,11 @@ void saveSerialSettings() { preferences.putUChar("ser_data", serialSettings.dataBits); preferences.putUChar("ser_parity", serialSettings.parity); preferences.putUChar("ser_stop", serialSettings.stopBits); - - Serial.printf("✓ Serial 설정 저장: %u-%u-%u-%u\n", - serialSettings.baudRate, serialSettings.dataBits, - serialSettings.parity, serialSettings.stopBits); } void applySerialSettings() { uint32_t config = SERIAL_8N1; - // Data bits + Parity 설정 if (serialSettings.dataBits == 5) { if (serialSettings.parity == 0) config = SERIAL_5N1; else if (serialSettings.parity == 1) config = SERIAL_5E1; @@ -329,100 +414,68 @@ void applySerialSettings() { if (serialSettings.parity == 0) config = SERIAL_7N1; else if (serialSettings.parity == 1) config = SERIAL_7E1; else if (serialSettings.parity == 2) config = SERIAL_7O1; - } else { // 8 bits + } else { if (serialSettings.parity == 0) config = SERIAL_8N1; else if (serialSettings.parity == 1) config = SERIAL_8E1; else if (serialSettings.parity == 2) config = SERIAL_8O1; } - // Stop bits 설정 if (serialSettings.stopBits == 2) { - config |= 0x3000; // 2 stop bits + config |= 0x3000; } SerialComm.begin(serialSettings.baudRate, config, SERIAL_RX_PIN, SERIAL_TX_PIN); SerialComm.setRxBufferSize(2048); - - Serial.printf("✓ Serial 설정 적용: %u baud, config=0x%X\n", - serialSettings.baudRate, config); } -// ======================================== -// 설정 저장/로드 함수 -// ======================================== - void loadSettings() { preferences.begin("can-logger", false); - - // WiFi AP 설정 로드 preferences.getString("wifi_ssid", wifiSSID, sizeof(wifiSSID)); preferences.getString("wifi_pass", wifiPassword, sizeof(wifiPassword)); - - // WiFi STA 모드 설정 로드 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"); - } + if (strlen(wifiSSID) == 0) strcpy(wifiSSID, "Byun_CAN_Logger"); + if (strlen(wifiPassword) == 0) strcpy(wifiPassword, "12345678"); - // CAN 속도 로드 int speedIndex = preferences.getInt("can_speed", 3); if (speedIndex >= 0 && speedIndex < 4) { currentCanSpeed = canSpeedValues[speedIndex]; - Serial.printf("✓ 저장된 CAN 속도 로드: %s\n", canSpeedNames[speedIndex]); } - // MCP2515 모드 로드 int savedMode = preferences.getInt("mcp_mode", 0); if (savedMode >= 0 && savedMode <= 3) { currentMcpMode = (MCP2515Mode)savedMode; - Serial.printf("✓ 저장된 MCP 모드 로드: %d\n", savedMode); } - // Serial 설정 로드 (추가) 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); - // CAN 속도 저장 for (int i = 0; i < 4; i++) { if (canSpeedValues[i] == currentCanSpeed) { preferences.putInt("can_speed", i); - Serial.printf("✓ CAN 속도 저장: %s (인덱스 %d)\n", canSpeedNames[i], i); break; } } - // MCP2515 모드 저장 preferences.putInt("mcp_mode", (int)currentMcpMode); - Serial.printf("✓ MCP 모드 저장: %d\n", (int)currentMcpMode); - - // Serial 설정 저장 (추가) saveSerialSettings(); - preferences.end(); } // ======================================== // RTC 함수 // ======================================== - void initRTC() { rtcWire.begin(); rtcWire.setClock(100000); @@ -433,7 +486,7 @@ void initRTC() { Serial.println("✓ RTC(DS3231) 감지됨"); } else { timeSyncStatus.rtcAvailable = false; - Serial.println("! RTC(DS3231) 없음 - 시간 동기화 필요"); + Serial.println("! RTC(DS3231) 없음"); } } @@ -447,10 +500,7 @@ uint8_t decToBcd(uint8_t val) { bool readRTC(struct tm *timeinfo) { if (!timeSyncStatus.rtcAvailable) return false; - - if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(100)) != pdTRUE) { - return false; - } + if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(100)) != pdTRUE) return false; rtcWire.beginTransmission(DS3231_ADDRESS); rtcWire.write(0x00); @@ -465,10 +515,7 @@ bool readRTC(struct tm *timeinfo) { } uint8_t buffer[7]; - for (int i = 0; i < 7; i++) { - buffer[i] = rtcWire.read(); - } - + for (int i = 0; i < 7; i++) buffer[i] = rtcWire.read(); xSemaphoreGive(rtcMutex); timeinfo->tm_sec = bcdToDec(buffer[0] & 0x7F); @@ -484,10 +531,7 @@ bool readRTC(struct tm *timeinfo) { bool writeRTC(const struct tm *timeinfo) { if (!timeSyncStatus.rtcAvailable) return false; - - if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(100)) != pdTRUE) { - return false; - } + if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(100)) != pdTRUE) return false; rtcWire.beginTransmission(DS3231_ADDRESS); rtcWire.write(0x00); @@ -500,48 +544,33 @@ bool writeRTC(const struct tm *timeinfo) { rtcWire.write(decToBcd(timeinfo->tm_year - 100)); bool success = (rtcWire.endTransmission() == 0); - xSemaphoreGive(rtcMutex); - return success; } -// NTP 시간 동기화 콜백 void timeSyncCallback(struct timeval *tv) { Serial.println("✓ NTP 시간 동기화 완료"); - timeSyncStatus.synchronized = true; timeSyncStatus.lastSyncTime = (uint64_t)tv->tv_sec * 1000000ULL + tv->tv_usec; timeSyncStatus.syncCount++; - // RTC에 시간 저장 if (timeSyncStatus.rtcAvailable) { struct tm timeinfo; time_t now = tv->tv_sec; localtime_r(&now, &timeinfo); - if (writeRTC(&timeinfo)) { - Serial.println("✓ NTP → RTC 시간 동기화 완료"); timeSyncStatus.rtcSyncCount++; - } else { - Serial.println("✗ RTC 쓰기 실패"); } } } void initNTP() { - // NTP 서버 설정 - configTime(9 * 3600, 0, "pool.ntp.org", "time.nist.gov", "time.google.com"); - - // NTP 동기화 콜백 등록 + configTime(9 * 3600, 0, "pool.ntp.org", "time.nist.gov"); sntp_set_time_sync_notification_cb(timeSyncCallback); - - Serial.println("✓ NTP 클라이언트 초기화 완료"); } void rtcSyncTask(void *parameter) { const TickType_t xDelay = pdMS_TO_TICKS(RTC_SYNC_INTERVAL_MS); - while (1) { if (timeSyncStatus.rtcAvailable) { struct tm timeinfo; @@ -549,21 +578,18 @@ void rtcSyncTask(void *parameter) { 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 모드 설정 +// MCP2515 모드 // ======================================== - bool setMCP2515Mode(MCP2515Mode mode) { const char* modeName; MCP2515::ERROR result; @@ -582,8 +608,8 @@ bool setMCP2515Mode(MCP2515Mode mode) { modeName = "Loopback"; break; case MCP_MODE_TRANSMIT: - result = mcp2515.setListenOnlyMode(); // Listen-Only 기본 상태 - modeName = "Transmit-Only (Listen base)"; + result = mcp2515.setListenOnlyMode(); + modeName = "Transmit-Only"; break; default: return false; @@ -591,18 +617,15 @@ bool setMCP2515Mode(MCP2515Mode mode) { if (result == MCP2515::ERROR_OK) { currentMcpMode = mode; - Serial.printf("✓ MCP2515 모드 변경: %s\n", modeName); + Serial.printf("✓ MCP2515 모드: %s\n", modeName); return true; - } else { - Serial.printf("✗ MCP2515 모드 변경 실패: %s (error=%d)\n", modeName, result); - return false; } + return false; } // ======================================== -// CAN 인터럽트 핸들러 +// 인터럽트 및 Task // ======================================== - void IRAM_ATTR canISR() { BaseType_t xHigherPriorityTaskWoken = pdFALSE; if (canRxTaskHandle != NULL) { @@ -611,61 +634,41 @@ void IRAM_ATTR canISR() { } } -// ======================================== -// Serial 수신 Task (추가) -// ======================================== - 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 시작"); - while (1) { - bool hasData = false; - while (SerialComm.available()) { - hasData = true; 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++; - } else { - Serial.println("! Serial Queue 전송 실패"); } - lineIndex = 0; } } - // 버퍼 오버플로우 방지 - if (lineIndex >= MAX_SERIAL_LINE_LEN - 1) { - lineIndex = 0; - } + if (lineIndex >= MAX_SERIAL_LINE_LEN - 1) lineIndex = 0; } - // 타임아웃: 100ms 동안 데이터가 없으면 버퍼 내용 전송 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; @@ -673,7 +676,6 @@ void serialRxTask(void *parameter) { if (xQueueSend(serialQueue, &serialMsg, pdMS_TO_TICKS(10)) == pdTRUE) { totalSerialRxCount++; } - lineIndex = 0; } @@ -681,16 +683,10 @@ void serialRxTask(void *parameter) { } } -// ======================================== -// CAN 수신 Task -// ======================================== - void canRxTask(void *parameter) { struct can_frame frame; CANMessage msg; - Serial.println("✓ CAN RX Task 시작"); - while (1) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); @@ -698,7 +694,6 @@ void canRxTask(void *parameter) { 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); @@ -711,15 +706,12 @@ void canRxTask(void *parameter) { } // ======================================== -// SD 쓰기 Task (CAN + Serial 동시 지원) +// SD Write Task // ======================================== - void sdWriteTask(void *parameter) { CANMessage canMsg; SerialMessage serialMsg; - Serial.println("✓ SD Write Task 시작"); - while (1) { bool hasWork = false; @@ -752,11 +744,8 @@ void sdWriteTask(void *parameter) { if (loggingEnabled && sdCardReady) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(100)) == pdTRUE) { if (canLogFormatCSV) { - // CSV 형식 로깅 char csvLine[128]; uint64_t relativeTime = canMsg.timestamp_us - canLogStartTime; - - // 데이터를 16진수 문자열로 변환 char dataStr[32]; int dataLen = 0; for (int i = 0; i < canMsg.dlc; i++) { @@ -773,7 +762,6 @@ void sdWriteTask(void *parameter) { logFile.write((uint8_t*)csvLine, lineLen); currentFileSize += lineLen; - // 주기적으로 플러시 (100개마다) static int csvFlushCounter = 0; if (++csvFlushCounter >= 100) { logFile.flush(); @@ -781,7 +769,7 @@ void sdWriteTask(void *parameter) { } } } else { - // BIN 형식 로깅 (기존 방식) + // BIN 형식 if (bufferIndex + sizeof(CANMessage) <= FILE_BUFFER_SIZE) { memcpy(&fileBuffer[bufferIndex], &canMsg, sizeof(CANMessage)); bufferIndex += sizeof(CANMessage); @@ -796,31 +784,19 @@ void sdWriteTask(void *parameter) { } } } - xSemaphoreGive(sdMutex); } } } - // Serial 메시지 처리 - Queue에서는 항상 빼내고, 로깅이 활성화된 경우에만 저장 - // 이렇게 해야 webUpdateTask에서 메시지를 받을 수 있음 - // NOTE: Serial Queue는 webUpdateTask에서도 읽으므로 여기서는 로깅만 처리 - if (!hasWork) { vTaskDelay(pdMS_TO_TICKS(1)); } } } -// ======================================== -// SD 모니터 Task -// ======================================== - void sdMonitorTask(void *parameter) { const TickType_t xDelay = pdMS_TO_TICKS(1000); - uint32_t lastStatusPrint = 0; - - Serial.println("✓ SD Monitor Task 시작"); while (1) { uint32_t currentTime = millis(); @@ -850,38 +826,6 @@ void sdMonitorTask(void *parameter) { powerStatus.lastCheck = currentTime; } - // 10초마다 상태 출력 (시간 동기화 및 Queue 사용률 확인용) - if (currentTime - lastStatusPrint >= 10000) { - time_t now; - time(&now); - struct tm timeinfo; - localtime_r(&now, &timeinfo); - - 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("[상태] %04d-%02d-%02d %02d:%02d:%02d | CAN: %u msg/s | CAN큐: %u/%u (%.1f%%) | Serial큐: %u/%u (%.1f%%) | TimeSync: %s | RTC동기: %u회\n", - timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, - timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, - msgPerSecond, - canQueueUsed, CAN_QUEUE_SIZE, canQueuePercent, - serialQueueUsed, SERIAL_QUEUE_SIZE, serialQueuePercent, - timeSyncStatus.synchronized ? "OK" : "NO", - timeSyncStatus.rtcSyncCount); - - // Queue 사용률 경고 (90% 이상) - if (canQueuePercent >= 90.0) { - Serial.printf("⚠️ 경고: CAN Queue 사용률 %.1f%% - SD 카드 속도 확인 필요!\n", canQueuePercent); - } - if (serialQueuePercent >= 90.0) { - Serial.printf("⚠️ 경고: Serial Queue 사용률 %.1f%% - SD 카드 속도 확인 필요!\n", serialQueuePercent); - } - - lastStatusPrint = currentTime; - } - vTaskDelay(xDelay); } } @@ -889,17 +833,14 @@ void sdMonitorTask(void *parameter) { // ======================================== // 파일 커멘트 관리 // ======================================== - 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(); - Serial.println("✓ 파일 커멘트 저장 완료"); } xSemaphoreGive(sdMutex); } @@ -907,7 +848,6 @@ void saveFileComments() { 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); @@ -916,7 +856,6 @@ void loadFileComments() { if (commentCount > MAX_FILE_COMMENTS) commentCount = MAX_FILE_COMMENTS; commentFile.read((uint8_t*)fileComments, sizeof(FileComment) * commentCount); commentFile.close(); - Serial.printf("✓ 파일 커멘트 %d개 로드 완료\n", commentCount); } } xSemaphoreGive(sdMutex); @@ -953,19 +892,16 @@ void addFileComment(const char* filename, const char* comment) { } // ======================================== -// CAN 시퀀스 관리 +// 시퀀스 관리 // ======================================== - 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(); - Serial.println("✓ 시퀀스 저장 완료"); } xSemaphoreGive(sdMutex); } @@ -973,7 +909,6 @@ void saveSequences() { 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); @@ -982,7 +917,6 @@ void loadSequences() { if (sequenceCount > MAX_SEQUENCES) sequenceCount = MAX_SEQUENCES; seqFile.read((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount); seqFile.close(); - Serial.printf("✓ 시퀀스 %d개 로드 완료\n", sequenceCount); } } xSemaphoreGive(sdMutex); @@ -990,14 +924,11 @@ void loadSequences() { } // ======================================== -// CAN TX Task +// TX Task // ======================================== - void txTask(void *parameter) { struct can_frame frame; - Serial.println("✓ TX Task 시작"); - while (1) { uint32_t now = millis(); bool anyActive = false; @@ -1007,7 +938,6 @@ void txTask(void *parameter) { anyActive = true; if (now - txMessages[i].lastSent >= txMessages[i].interval) { - // Transmit-Only 모드: 송신 전 Normal 모드로 전환 if (currentMcpMode == MCP_MODE_TRANSMIT) { mcp2515.setNormalMode(); } @@ -1024,7 +954,6 @@ void txTask(void *parameter) { txMessages[i].lastSent = now; } - // Transmit-Only 모드: 송신 후 Listen-Only로 복귀 if (currentMcpMode == MCP_MODE_TRANSMIT) { mcp2515.setListenOnlyMode(); } @@ -1032,21 +961,14 @@ void txTask(void *parameter) { } } - if (anyActive) { - vTaskDelay(pdMS_TO_TICKS(1)); - } else { - vTaskDelay(pdMS_TO_TICKS(10)); - } + vTaskDelay(anyActive ? pdMS_TO_TICKS(1) : pdMS_TO_TICKS(10)); } } // ======================================== -// 시퀀스 Task +// Sequence Task // ======================================== - void sequenceTask(void *parameter) { - Serial.println("✓ Sequence Task 시작"); - while (1) { if (seqRuntime.running && seqRuntime.activeSequenceIndex >= 0 && seqRuntime.activeSequenceIndex < sequenceCount) { @@ -1058,7 +980,6 @@ void sequenceTask(void *parameter) { SequenceStep* step = &seq->steps[seqRuntime.currentStep]; if (now - seqRuntime.lastStepTime >= step->delayMs) { - // Transmit-Only 모드: 송신 전 Normal 모드로 전환 if (currentMcpMode == MCP_MODE_TRANSMIT) { mcp2515.setNormalMode(); } @@ -1075,7 +996,6 @@ void sequenceTask(void *parameter) { totalTxCount++; } - // Transmit-Only 모드: 송신 후 Listen-Only로 복귀 if (currentMcpMode == MCP_MODE_TRANSMIT) { mcp2515.setListenOnlyMode(); } @@ -1108,25 +1028,17 @@ void sequenceTask(void *parameter) { } // ======================================== -// WebSocket 이벤트 처리 (Settings 명령 추가) +// 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) { - Serial.print("✗ JSON 파싱 실패: "); - Serial.println(error.c_str()); - return; - } + if (error) return; const char* cmd = doc["cmd"]; - // ======================================== - // Settings 페이지 명령 처리 (추가) - // ======================================== if (strcmp(cmd, "getSettings") == 0) { DynamicJsonDocument response(1024); response["type"] = "settings"; @@ -1141,11 +1053,8 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) String json; serializeJson(response, json); webSocket.sendTXT(num, json); - - Serial.println("✓ 설정 전송 완료"); } else if (strcmp(cmd, "saveSettings") == 0) { - // WiFi 설정 저장 const char* newSSID = doc["ssid"]; const char* newPassword = doc["password"]; bool newSTAEnable = doc["staEnable"]; @@ -1183,15 +1092,9 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) String json; serializeJson(response, json); webSocket.sendTXT(num, json); - - Serial.println("✓ 설정 저장 완료 (재부팅 필요)"); } - // ======================================== - // 기존 명령들 - // ======================================== else if (strcmp(cmd, "startLogging") == 0) { if (!loggingEnabled && sdCardReady) { - // 로깅 형식 선택 const char* format = doc["format"]; if (format && strcmp(format, "csv") == 0) { canLogFormatCSV = true; @@ -1205,12 +1108,10 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) 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", @@ -1220,7 +1121,6 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) logFile = SD.open(currentFilename, FILE_WRITE); if (logFile) { - // CSV 형식이면 헤더 작성 if (canLogFormatCSV) { logFile.println("Time_us,CAN_ID,DLC,Data"); } @@ -1228,10 +1128,6 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) loggingEnabled = true; bufferIndex = 0; currentFileSize = logFile.size(); - Serial.printf("✓ CAN 로깅 시작: %s (%s)\n", - currentFilename, canLogFormatCSV ? "CSV" : "BIN"); - } else { - Serial.println("✗ 파일 생성 실패"); } xSemaphoreGive(sdMutex); @@ -1251,20 +1147,17 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) } loggingEnabled = false; - Serial.printf("✓ CAN 로깅 종료: %s (%u bytes)\n", currentFilename, 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; // 기본값 CSV + serialLogFormatCSV = true; } if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { @@ -1273,12 +1166,10 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) 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", @@ -1288,17 +1179,12 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) serialLogFile = SD.open(currentSerialFilename, FILE_WRITE); if (serialLogFile) { - // CSV 형식이면 헤더 작성 if (serialLogFormatCSV) { serialLogFile.println("Time_us,Direction,Data"); } serialLoggingEnabled = true; serialCsvIndex = 0; currentSerialFileSize = serialLogFile.size(); - Serial.printf("✓ Serial 로깅 시작: %s (%s)\n", - currentSerialFilename, serialLogFormatCSV ? "CSV" : "BIN"); - } else { - Serial.println("✗ Serial 파일 생성 실패"); } xSemaphoreGive(sdMutex); @@ -1308,7 +1194,6 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) else if (strcmp(cmd, "stopSerialLogging") == 0) { if (serialLoggingEnabled) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - // 남은 CSV 버퍼 내용 쓰기 if (serialCsvIndex > 0 && serialLogFile) { serialLogFile.write((uint8_t*)serialCsvBuffer, serialCsvIndex); serialCsvIndex = 0; @@ -1319,9 +1204,6 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) } serialLoggingEnabled = false; - Serial.printf("✓ Serial 로깅 종료: %s (%u bytes)\n", - currentSerialFilename, currentSerialFileSize); - xSemaphoreGive(sdMutex); } } @@ -1329,28 +1211,22 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) else if (strcmp(cmd, "sendSerial") == 0) { const char* data = doc["data"]; if (data && strlen(data) > 0) { - // UART로 데이터 전송 SerialComm.println(data); - // Queue에 TX 메시지 추가 (모니터링용) 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; // \r\n 포함 + 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++; - Serial.printf("✓ Serial TX Queue 전송: %s\n", data); - } else { - Serial.printf("✗ Serial TX Queue 전송 실패: %s\n", data); } } } @@ -1367,8 +1243,6 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) saveSerialSettings(); applySerialSettings(); - - Serial.printf("✓ Serial 설정 변경: %u-%u-%u-%u\n", baud, data, parity, stop); } else if (strcmp(cmd, "getSerialConfig") == 0) { DynamicJsonDocument response(512); @@ -1389,10 +1263,7 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) mcp2515.reset(); mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ); setMCP2515Mode(currentMcpMode); - saveSettings(); - - Serial.printf("✓ CAN 속도 변경: %s\n", canSpeedNames[speedIndex]); } } else if (strcmp(cmd, "setMcpMode") == 0) { @@ -1402,31 +1273,7 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) 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); - Serial.println("✓ 시간 동기화 완료 (Phone → ESP32 → RTC)"); - } else { - Serial.println("✓ 시간 동기화 완료 (Phone → ESP32)"); - } - } - } else if (strcmp(cmd, "syncTimeFromPhone") == 0) { - // 개별 시간 값으로 동기화 (년/월/일/시/분/초) int year = doc["year"] | 2024; int month = doc["month"] | 1; int day = doc["day"] | 1; @@ -1452,11 +1299,6 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) if (timeSyncStatus.rtcAvailable) { writeRTC(&timeinfo); - Serial.printf("✓ 시간 동기화 완료: %04d-%02d-%02d %02d:%02d:%02d (Phone → ESP32 → RTC)\n", - year, month, day, hour, minute, second); - } else { - Serial.printf("✓ 시간 동기화 완료: %04d-%02d-%02d %02d:%02d:%02d (Phone → ESP32)\n", - year, month, day, hour, minute, second); } } else if (strcmp(cmd, "getFiles") == 0) { @@ -1501,39 +1343,15 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) const char* filename = doc["filename"]; if (filename && strlen(filename) > 0) { - if (loggingEnabled && currentFilename[0] != '\0') { - String currentFileStr = String(currentFilename); - if (currentFileStr.startsWith("/")) currentFileStr = currentFileStr.substring(1); - - if (strcmp(filename, currentFileStr.c_str()) == 0) { - DynamicJsonDocument response(256); - response["type"] = "deleteResult"; - response["success"] = false; - response["message"] = "Cannot delete file currently being logged"; - - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); - return; - } - } - String fullPath = "/" + String(filename); if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { bool success = false; - String message = ""; if (SD.exists(fullPath)) { if (SD.remove(fullPath)) { success = true; - message = "File deleted successfully"; - Serial.printf("✓ 파일 삭제: %s\n", filename); - } else { - message = "Failed to delete file"; } - } else { - message = "File not found"; } xSemaphoreGive(sdMutex); @@ -1541,50 +1359,10 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) DynamicJsonDocument response(256); response["type"] = "deleteResult"; response["success"] = success; - response["message"] = message; String json; serializeJson(response, json); webSocket.sendTXT(num, json); - - if (success) { - vTaskDelay(pdMS_TO_TICKS(100)); - - DynamicJsonDocument filesDoc(6144); - filesDoc["type"] = "files"; - JsonArray files = filesDoc.createNestedArray("list"); - - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - File root = SD.open("/"); - File file = root.openNextFile(); - - while (file) { - if (!file.isDirectory()) { - const char* fname = file.name(); - - if (fname[0] != '.' && - strcmp(fname, "System Volume Information") != 0) { - - JsonObject fileObj = files.createNestedObject(); - fileObj["name"] = fname; - fileObj["size"] = file.size(); - - const char* comment = getFileComment(fname); - if (strlen(comment) > 0) { - fileObj["comment"] = comment; - } - } - } - file = root.openNextFile(); - } - - xSemaphoreGive(sdMutex); - } - - String filesJson; - serializeJson(filesDoc, filesJson); - webSocket.sendTXT(num, filesJson); - } } } } @@ -1594,92 +1372,9 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) if (filename && comment) { addFileComment(filename, comment); - Serial.printf("✓ 커멘트 추가: %s\n", filename); - - vTaskDelay(pdMS_TO_TICKS(100)); - - 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* fname = file.name(); - - if (fname[0] != '.' && - strcmp(fname, "System Volume Information") != 0) { - - JsonObject fileObj = files.createNestedObject(); - fileObj["name"] = fname; - fileObj["size"] = file.size(); - - const char* fcomment = getFileComment(fname); - if (strlen(fcomment) > 0) { - fileObj["comment"] = fcomment; - } - } - } - file = root.openNextFile(); - } - - xSemaphoreGive(sdMutex); - - String json; - serializeJson(response, json); - webSocket.sendTXT(num, json); - } - } - } - 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; - - Serial.printf("✓ TX 메시지 추가: ID=0x%X\n", txMessages[i].id); - 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; - Serial.printf("✓ TX 메시지 제거: ID=0x%X\n", id); - break; - } - } - } - else if (strcmp(cmd, "updateTx") == 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].interval = doc["interval"]; - Serial.printf("✓ TX 주기 변경: ID=0x%X, Interval=%u\n", - id, txMessages[i].interval); - break; - } } } else if (strcmp(cmd, "sendOnce") == 0) { - // Transmit-Only 모드: 송신 전 Normal 모드로 전환 if (currentMcpMode == MCP_MODE_TRANSMIT) { mcp2515.setNormalMode(); } @@ -1700,79 +1395,12 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) { totalTxCount++; - Serial.printf("✓ CAN 메시지 전송: ID=0x%X\n", frame.can_id & 0x1FFFFFFF); } - // Transmit-Only 모드: 송신 후 Listen-Only로 복귀 if (currentMcpMode == MCP_MODE_TRANSMIT) { mcp2515.setListenOnlyMode(); } } - else if (strcmp(cmd, "addSequence") == 0) { - if (sequenceCount < MAX_SEQUENCES) { - CANSequence* seq = &sequences[sequenceCount]; - - strncpy(seq->name, doc["name"] | "Unnamed", sizeof(seq->name) - 1); - seq->name[sizeof(seq->name) - 1] = '\0'; - - JsonArray stepsArray = doc["steps"]; - seq->stepCount = min((int)stepsArray.size(), 20); - - for (int i = 0; i < seq->stepCount; i++) { - JsonObject stepObj = stepsArray[i]; - seq->steps[i].canId = strtoul(stepObj["id"], NULL, 16); - seq->steps[i].extended = stepObj["ext"] | false; - seq->steps[i].dlc = stepObj["dlc"] | 8; - - JsonArray dataArray = stepObj["data"]; - for (int j = 0; j < 8; j++) { - seq->steps[i].data[j] = dataArray[j] | 0; - } - - seq->steps[i].delayMs = stepObj["delay"] | 0; - } - - seq->repeatMode = doc["repeatMode"] | 0; - seq->repeatCount = doc["repeatCount"] | 1; - - sequenceCount++; - saveSequences(); - - Serial.printf("✓ 시퀀스 추가: %s (%d steps)\n", seq->name, seq->stepCount); - } - } - else if (strcmp(cmd, "removeSequence") == 0) { - int index = doc["index"]; - - if (index >= 0 && index < sequenceCount) { - for (int i = index; i < sequenceCount - 1; i++) { - sequences[i] = sequences[i + 1]; - } - sequenceCount--; - saveSequences(); - - Serial.printf("✓ 시퀀스 삭제: index=%d\n", index); - } - } - else if (strcmp(cmd, "startSequence") == 0) { - int index = doc["index"]; - - if (index >= 0 && index < sequenceCount && !seqRuntime.running) { - seqRuntime.running = true; - seqRuntime.currentStep = 0; - seqRuntime.currentRepeat = 0; - seqRuntime.lastStepTime = millis(); - seqRuntime.activeSequenceIndex = index; - - Serial.printf("✓ 시퀀스 시작: %s\n", sequences[index].name); - } - } - else if (strcmp(cmd, "stopSequence") == 0) { - if (seqRuntime.running) { - seqRuntime.running = false; - Serial.println("✓ 시퀀스 중지"); - } - } else if (strcmp(cmd, "getSequences") == 0) { DynamicJsonDocument response(3072); response["type"] = "sequences"; @@ -1794,19 +1422,16 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) } // ======================================== -// 웹 업데이트 Task (Serial 데이터 전송 추가) +// Web Update Task // ======================================== - void webUpdateTask(void *parameter) { const TickType_t xDelay = pdMS_TO_TICKS(100); - Serial.println("✓ Web Update Task 시작"); - while (1) { webSocket.loop(); if (webSocket.connectedClients() > 0) { - DynamicJsonDocument doc(4096); // 3072 → 4096으로 증가 (Serial 메시지 포함) + DynamicJsonDocument doc(4096); doc["type"] = "update"; doc["logging"] = loggingEnabled; doc["serialLogging"] = serialLoggingEnabled; @@ -1825,13 +1450,12 @@ void webUpdateTask(void *parameter) { doc["timeSync"] = timeSyncStatus.synchronized; doc["rtcAvail"] = timeSyncStatus.rtcAvailable; doc["rtcSyncCnt"] = timeSyncStatus.rtcSyncCount; - doc["syncCount"] = timeSyncStatus.syncCount; // 전체 동기화 횟수 (NTP + 수동) + 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 { @@ -1864,18 +1488,16 @@ void webUpdateTask(void *parameter) { } } - // Serial 메시지 배열 (Queue에서 읽기) + // Serial 메시지 배열 SerialMessage serialMsg; JsonArray serialMessages = doc.createNestedArray("serialMessages"); int serialCount = 0; - // Queue에서 최대 10개의 Serial 메시지 읽기 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'; @@ -1883,11 +1505,10 @@ void webUpdateTask(void *parameter) { serialCount++; - // Serial 로깅이 활성화되어 있으면 SD 카드에 저장 + // Serial 로깅 if (serialLoggingEnabled && sdCardReady) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(10)) == pdTRUE) { if (serialLogFormatCSV) { - // CSV 형식 로깅 (상대 시간 사용) uint64_t relativeTime = serialMsg.timestamp_us - serialLogStartTime; char csvLine[256]; @@ -1897,14 +1518,12 @@ void webUpdateTask(void *parameter) { serialMsg.isTx ? "TX" : "RX", dataStr); - // CSV 버퍼에 추가 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); @@ -1913,12 +1532,10 @@ void webUpdateTask(void *parameter) { } } } else { - // BIN 형식 로깅 (기존 방식) if (serialLogFile) { serialLogFile.write((uint8_t*)&serialMsg, sizeof(SerialMessage)); currentSerialFileSize += sizeof(SerialMessage); - // 주기적으로 플러시 static int binFlushCounter = 0; if (++binFlushCounter >= 50) { serialLogFile.flush(); @@ -1944,84 +1561,67 @@ void webUpdateTask(void *parameter) { // ======================================== // Setup // ======================================== - void setup() { Serial.begin(115200); delay(1000); Serial.println("\n========================================"); Serial.println(" Byun CAN Logger + Serial Terminal"); - Serial.println(" Version 2.3 - ESP32-S3 Optimized"); - Serial.println(" 8MB PSRAM High Performance Edition"); + Serial.println(" Version 2.3 - PSRAM Optimized"); + Serial.println(" ESP32-S3 Complete Edition"); Serial.println("========================================\n"); - // PSRAM 확인 (ESP32-S3 N16R8) - if (psramFound()) { - Serial.printf("✓ PSRAM 감지: %d MB\n", ESP.getPsramSize() / 1024 / 1024); - Serial.printf("✓ PSRAM 여유: %d KB\n", ESP.getFreePsram() / 1024); - } else { - Serial.println("✗ PSRAM 없음 - Arduino IDE에서 PSRAM: OPI PSRAM 설정 필요!"); - Serial.println("✗ Tools → PSRAM → OPI PSRAM 선택"); + // ★★★ PSRAM 초기화 (가장 먼저!) ★★★ + if (!initPSRAM()) { + Serial.println("\n✗ PSRAM 초기화 실패!"); + Serial.println("✗ Arduino IDE 설정:"); + Serial.println(" Tools → PSRAM → OPI PSRAM"); while (1) { delay(1000); - Serial.println("✗ PSRAM 설정 후 재업로드 필요!"); + Serial.println("✗ 설정 후 재업로드 필요!"); } } loadSettings(); analogSetPinAttenuation(MONITORING_VOLT, ADC_11db); - Serial.println("💡 설정 변경: http://[IP]/settings\n"); - - memset(recentData, 0, sizeof(recentData)); - memset(txMessages, 0, sizeof(txMessages)); - memset(fileComments, 0, sizeof(fileComments)); pinMode(CAN_INT_PIN, INPUT_PULLUP); - - // ADC 설정 analogSetAttenuation(ADC_11db); // SPI 초기화 - Serial.println("SPI 초기화 중..."); + Serial.println("SPI 초기화..."); hspi.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS); hspi.beginTransaction(SPISettings(40000000, MSBFIRST, SPI_MODE0)); - hspi.endTransaction(); + 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 완전 비활성화 - Serial.println("Watchdog 비활성화..."); + // Watchdog 비활성화 esp_task_wdt_deinit(); - Serial.println("✓ Watchdog 비활성화 완료"); // MCP2515 초기화 - Serial.println("MCP2515 초기화 중..."); + Serial.println("MCP2515 초기화..."); mcp2515.reset(); delay(50); - - MCP2515::ERROR result = mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ); - if (result != MCP2515::ERROR_OK) { - Serial.printf("! MCP2515 Bitrate 설정 실패: %d (계속 진행)\n", result); - } - + mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ); mcp2515.setNormalMode(); Serial.println("✓ MCP2515 초기화 완료"); // Serial 통신 초기화 applySerialSettings(); - Serial.println("✓ Serial 통신 초기화 완료 (UART1)"); + Serial.println("✓ Serial 통신 초기화 완료"); // Mutex 생성 sdMutex = xSemaphoreCreateMutex(); rtcMutex = xSemaphoreCreateMutex(); serialMutex = xSemaphoreCreateMutex(); - if (sdMutex == NULL || rtcMutex == NULL || serialMutex == NULL) { + if (!sdMutex || !rtcMutex || !serialMutex) { Serial.println("✗ Mutex 생성 실패!"); while (1) delay(1000); } @@ -2034,23 +1634,20 @@ void setup() { sdCardReady = true; Serial.println("✓ SD 카드 초기화 완료"); loadFileComments(); + loadSequences(); } else { Serial.println("✗ SD 카드 초기화 실패"); } // WiFi 설정 if (enableSTAMode && strlen(staSSID) > 0) { - Serial.println("\n📶 WiFi APSTA 모드 시작..."); + Serial.println("\n📶 WiFi APSTA 모드..."); WiFi.mode(WIFI_AP_STA); WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4); - Serial.print("✓ AP SSID: "); - Serial.println(wifiSSID); - Serial.print("✓ AP IP: "); - Serial.println(WiFi.softAPIP()); + Serial.printf("✓ AP: %s\n", wifiSSID); + Serial.printf("✓ AP IP: %s\n", WiFi.softAPIP().toString().c_str()); - Serial.printf("📡 WiFi 연결 시도: %s\n", staSSID); WiFi.begin(staSSID, staPassword); - int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 20) { delay(500); @@ -2060,37 +1657,15 @@ void setup() { Serial.println(); if (WiFi.status() == WL_CONNECTED) { - Serial.println("✓ WiFi 연결 성공!"); - Serial.print("✓ STA IP: "); - Serial.println(WiFi.localIP()); - - // NTP 시간 동기화 시작 + Serial.printf("✓ STA IP: %s\n", WiFi.localIP().toString().c_str()); initNTP(); - - // NTP 동기화 대기 (최대 5초) - Serial.print("⏱ NTP 시간 동기화 대기 중"); - for (int i = 0; i < 10; i++) { - delay(500); - Serial.print("."); - if (timeSyncStatus.synchronized) { - Serial.println(" 완료!"); - break; - } - } - if (!timeSyncStatus.synchronized) { - Serial.println(" 시간 초과 (계속 진행)"); - } - } else { - Serial.println("✗ WiFi 연결 실패 (AP 모드는 정상 동작)"); } } else { - Serial.println("\n📶 WiFi AP 모드 시작..."); + Serial.println("\n📶 WiFi AP 모드..."); WiFi.mode(WIFI_AP); WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4); - Serial.print("✓ AP SSID: "); - Serial.println(wifiSSID); - Serial.print("✓ AP IP: "); - Serial.println(WiFi.softAPIP()); + Serial.printf("✓ AP: %s\n", wifiSSID); + Serial.printf("✓ AP IP: %s\n", WiFi.softAPIP().toString().c_str()); } WiFi.setSleep(false); @@ -2100,7 +1675,7 @@ void setup() { webSocket.begin(); webSocket.onEvent(webSocketEvent); - // 웹 서버 라우팅 + // ★★★ 웹 서버 라우팅 (중요!) ★★★ server.on("/", HTTP_GET, []() { server.send_P(200, "text/html", index_html); }); @@ -2112,19 +1687,19 @@ void setup() { 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"); @@ -2149,130 +1724,58 @@ void setup() { } }); - server.on("/delete", HTTP_GET, []() { - if (server.hasArg("file")) { - String filename = server.arg("file"); - - if (loggingEnabled && currentFilename[0] != '\0') { - String currentFileStr = String(currentFilename); - if (currentFileStr.startsWith("/")) currentFileStr = currentFileStr.substring(1); - - if (filename == currentFileStr) { - server.send(403, "text/plain", "Cannot delete file currently being logged"); - return; - } - } - - String fullPath = "/" + filename; - - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - if (SD.exists(fullPath)) { - if (SD.remove(fullPath)) { - xSemaphoreGive(sdMutex); - server.send(200, "text/plain", "File deleted successfully"); - Serial.printf("✓ HTTP 파일 삭제: %s\n", filename.c_str()); - } else { - xSemaphoreGive(sdMutex); - server.send(500, "text/plain", "Failed to delete file"); - } - } else { - xSemaphoreGive(sdMutex); - server.send(404, "text/plain", "File not found"); - } - } else { - server.send(503, "text/plain", "SD card busy"); - } - } else { - server.send(400, "text/plain", "Bad request"); - } - }); - server.begin(); + Serial.println("✓ 웹 서버 시작 완료"); - // Queue 생성 (Dynamic - 크기 축소) - Serial.println("Queue 생성 중..."); - - // CAN Queue: 1,000개 × 21 bytes = 21 KB - canQueue = xQueueCreate(CAN_QUEUE_SIZE, sizeof(CANMessage)); - - // Serial Queue: 200개 × 75 bytes = 15 KB - serialQueue = xQueueCreate(SERIAL_QUEUE_SIZE, sizeof(SerialMessage)); - - if (canQueue == NULL || serialQueue == NULL) { + // ★★★ Queue 생성 (PSRAM 사용) ★★★ + if (!createQueues()) { Serial.println("✗ Queue 생성 실패!"); - - - Serial.println("\n✗ 시스템 중지"); - Serial.println(" 메모리 부족 - Queue 크기를 더 줄이거나"); - Serial.println(" 불필요한 변수를 제거하세요"); while (1) delay(1000); } - Serial.printf("✓ CAN Queue: %d개 (%.1f KB)\n", CAN_QUEUE_SIZE, - (float)(CAN_QUEUE_SIZE * sizeof(CANMessage)) / 1024.0); - Serial.printf("✓ Serial Queue: %d개 (%.1f KB)\n", SERIAL_QUEUE_SIZE, - (float)(SERIAL_QUEUE_SIZE * sizeof(SerialMessage)) / 1024.0); - Serial.printf("✓ 총 Queue 메모리: %.1f KB\n\n", - (float)(CAN_QUEUE_SIZE * sizeof(CANMessage) + SERIAL_QUEUE_SIZE * sizeof(SerialMessage)) / 1024.0); - // CAN 인터럽트 활성화 attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING); - // Task 생성 (ESP32-S3 듀얼코어 최적화) - // Core 1 (사용자 전용 - 고성능 작업) - xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 4096, NULL, 6, &canRxTaskHandle, 1); // 최고 우선순위 - xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 24576, NULL, 4, &sdWriteTaskHandle, 1); // 높음 (큰 버퍼) - xTaskCreatePinnedToCore(sequenceTask, "SEQ_TASK", 4096, NULL, 2, NULL, 1); // 보통 + // 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); - // Core 0 (WiFi/시스템 공유) - xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", 6144, NULL, 5, &serialRxTaskHandle, 0); // 높음 - xTaskCreatePinnedToCore(txTask, "TX_TASK", 4096, NULL, 3, NULL, 0); // 보통-높음 - xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 10240, NULL, 2, &webTaskHandle, 0); // 보통 (JSON 버퍼) - xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 0); // 낮음 - - // RTC 동기화 Task if (timeSyncStatus.rtcAvailable) { - xTaskCreatePinnedToCore(rtcSyncTask, "RTC_SYNC", 3072, NULL, 0, &rtcTaskHandle, 0); // 최저 - Serial.println("✓ RTC 자동 동기화 Task 시작"); + xTaskCreatePinnedToCore(rtcSyncTask, "RTC_SYNC", 3072, NULL, 0, &rtcTaskHandle, 0); } - // 시퀀스 로드 - loadSequences(); - - Serial.println("✓ 모든 태스크 시작 완료"); + Serial.println("✓ 모든 Task 시작 완료"); Serial.println("\n========================================"); - Serial.println(" 웹 인터페이스 접속 방법"); + Serial.println(" 접속 방법"); Serial.println("========================================"); - Serial.printf(" 1. WiFi 연결\n"); - Serial.printf(" - SSID : %s\n", wifiSSID); - Serial.printf(" - Password : %s\n", wifiPassword); - Serial.printf(" 2. 웹 브라우저에서 접속\n"); - Serial.print(" - URL : http://"); - Serial.println(WiFi.softAPIP()); - Serial.println(" 3. 페이지 메뉴:"); - Serial.println(" - Monitor : /"); - Serial.println(" - Transmit : /transmit"); - Serial.println(" - Graph : /graph"); - Serial.println(" - Settings : /settings"); - Serial.println(" - Serial : /serial"); + 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 | CAN로깅: %s | Serial로깅: %s | SD: %s | CAN RX: %lu | CAN TX: %lu | Serial RX: %lu | Serial TX: %lu | 모드: %d\n", + Serial.printf("[상태] CAN큐: %d/%d | Serial큐: %d/%d | PSRAM: %d KB\n", uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE, uxQueueMessagesWaiting(serialQueue), SERIAL_QUEUE_SIZE, - loggingEnabled ? "ON " : "OFF", - serialLoggingEnabled ? "ON " : "OFF", - sdCardReady ? "OK" : "NO", - totalMsgCount, totalTxCount, - totalSerialRxCount, totalSerialTxCount, - currentMcpMode); + ESP.getFreePsram() / 1024); lastPrint = millis(); } } +