diff --git a/test_i2c_reset/ESP32_CAN_Logger.ino b/test_i2c_reset/ESP32_CAN_Logger.ino index 5ed794c..29b2bdb 100644 --- a/test_i2c_reset/ESP32_CAN_Logger.ino +++ b/test_i2c_reset/ESP32_CAN_Logger.ino @@ -1,7 +1,7 @@ /* - * Byun CAN Logger with Web Interface + RTC Time Synchronization - * Version: 2.0 - * Added: Phone time sync to RTC, File comments, MCP2515 mode control + * Byun CAN Logger with Web Interface + Serial Terminal + * Version: 2.1 + * Added: Serial communication (RS232) with web terminal interface */ #include @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -25,10 +26,15 @@ #include "graph.h" #include "graph_viewer.h" #include "settings.h" +#include "serial_terminal.h" // GPIO 핀 정의 #define CAN_INT_PIN 27 +// Serial 통신 핀 (추가) +#define SERIAL_TX_PIN 17 +#define SERIAL_RX_PIN 16 + // HSPI 핀 (CAN) #define HSPI_MISO 12 #define HSPI_MOSI 13 @@ -47,28 +53,49 @@ #define DS3231_ADDRESS 0x68 // 버퍼 설정 -#define CAN_QUEUE_SIZE 2000 // 1000 → 2000으로 증가 -#define FILE_BUFFER_SIZE 16384 // 8192 → 16384 (16KB)로 증가 +#define CAN_QUEUE_SIZE 1500 // 2000 → 1500으로 축소 +#define FILE_BUFFER_SIZE 16384 #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 // 500 → 200으로 축소 +#define SERIAL_BUFFER_SIZE 2048 // 4096 → 2048로 축소 +#define MAX_SERIAL_LINE_LEN 128 // 256 → 128로 축소 + // RTC 동기화 설정 -#define RTC_SYNC_INTERVAL_MS 60000 // 1분마다 RTC와 동기화 +#define RTC_SYNC_INTERVAL_MS 60000 // 전력 모니터링 설정 -#define VOLTAGE_CHECK_INTERVAL_MS 5000 // 5초마다 전압 체크 -#define LOW_VOLTAGE_THRESHOLD 3.0 // 3.0V 이하이면 경고 +#define VOLTAGE_CHECK_INTERVAL_MS 5000 +#define LOW_VOLTAGE_THRESHOLD 3.0 -// CAN 메시지 구조체 - 마이크로초 단위 타임스탬프 +// CAN 메시지 구조체 struct CANMessage { - uint64_t timestamp_us; // 마이크로초 단위 Unix timestamp + uint64_t timestamp_us; uint32_t id; uint8_t dlc; 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=수신 +} __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 +} serialSettings = {115200, 8, 0, 1}; + // 실시간 모니터링용 구조체 struct RecentCANData { CANMessage msg; @@ -92,16 +119,16 @@ struct SequenceStep { bool extended; uint8_t dlc; uint8_t data[8]; - uint32_t delayMs; // 이 스텝 실행 후 대기 시간 (ms) + uint32_t delayMs; }; // CAN 시퀀스 구조체 struct CANSequence { char name[32]; - SequenceStep steps[20]; // 최대 20개 스텝 + SequenceStep steps[20]; uint8_t stepCount; - uint8_t repeatMode; // 0=한번, 1=특정횟수, 2=무한 - uint32_t repeatCount; // repeatMode=1일 때 반복 횟수 + uint8_t repeatMode; + uint32_t repeatCount; }; // 시퀀스 실행 상태 @@ -110,7 +137,7 @@ struct SequenceRuntime { uint8_t currentStep; uint32_t currentRepeat; uint32_t lastStepTime; - int8_t activeSequenceIndex; // 실행 중인 시퀀스 인덱스 + int8_t activeSequenceIndex; }; // 파일 커멘트 구조체 @@ -131,14 +158,14 @@ struct TimeSyncStatus { // 전력 모니터링 상태 struct PowerStatus { - float voltage; // 현재 전압 - float minVoltage; // 1초 단위 최소 전압 + float voltage; + float minVoltage; bool lowVoltage; uint32_t lastCheck; - uint32_t lastMinReset; // 최소값 리셋 시간 + uint32_t lastMinReset; } powerStatus = {0.0, 999.9, false, 0, 0}; -// MCP2515 레지스터 주소 정의 (라이브러리에 없는 경우) +// MCP2515 레지스터 주소 정의 #ifndef MCP_CANCTRL #define MCP_CANCTRL 0x0F #endif @@ -152,22 +179,25 @@ enum MCP2515Mode { MCP_MODE_NORMAL = 0, MCP_MODE_LISTEN_ONLY = 1, MCP_MODE_LOOPBACK = 2, - MCP_MODE_TRANSMIT = 3 // 송신만 (ACK 없음) + MCP_MODE_TRANSMIT = 3 }; // WiFi AP 기본 설정 char wifiSSID[32] = "Byun_CAN_Logger"; char wifiPassword[64] = "12345678"; -// WiFi Station 모드 설정 (추가) -bool enableSTAMode = false; // APSTA 모드 활성화 여부 -char staSSID[32] = ""; // 연결할 WiFi SSID -char staPassword[64] = ""; // 연결할 WiFi 비밀번호 +// WiFi Station 모드 설정 +bool enableSTAMode = false; +char staSSID[32] = ""; +char staPassword[64] = ""; // 전역 변수 SPIClass hspi(HSPI); SPIClass vspi(VSPI); -MCP2515 mcp2515(HSPI_CS, 20000000, &hspi); // 20MHz로 증가 (10MHz → 20MHz) +MCP2515 mcp2515(HSPI_CS, 20000000, &hspi); + +// Serial 통신용 (추가) +HardwareSerial SerialComm(2); // UART2 사용 WebServer server(80); WebSocketsServer webSocket = WebSocketsServer(81); @@ -177,22 +207,31 @@ Preferences preferences; void IRAM_ATTR canISR(); QueueHandle_t canQueue; +QueueHandle_t serialQueue; // Serial Queue 추가 SemaphoreHandle_t sdMutex; SemaphoreHandle_t rtcMutex; +SemaphoreHandle_t serialMutex; // Serial Mutex 추가 TaskHandle_t canRxTaskHandle = NULL; TaskHandle_t sdWriteTaskHandle = NULL; TaskHandle_t webTaskHandle = NULL; TaskHandle_t rtcTaskHandle = NULL; +TaskHandle_t serialRxTaskHandle = NULL; // Serial Task 추가 volatile bool loggingEnabled = false; +volatile bool serialLoggingEnabled = false; // Serial 로깅 상태 추가 volatile bool sdCardReady = false; File logFile; +File serialLogFile; // Serial 로그 파일 추가 char currentFilename[MAX_FILENAME_LEN]; +char currentSerialFilename[MAX_FILENAME_LEN]; // Serial 로그 파일명 추가 uint8_t fileBuffer[FILE_BUFFER_SIZE]; +uint8_t serialFileBuffer[SERIAL_BUFFER_SIZE]; // Serial 파일 버퍼 추가 uint16_t bufferIndex = 0; +uint16_t serialBufferIndex = 0; // Serial 버퍼 인덱스 추가 -// 로깅 파일 크기 추적 (실시간 모니터링용) +// 로깅 파일 크기 추적 volatile uint32_t currentFileSize = 0; +volatile uint32_t currentSerialFileSize = 0; // Serial 파일 크기 추가 // 현재 MCP2515 모드 MCP2515Mode currentMcpMode = MCP_MODE_NORMAL; @@ -213,6 +252,10 @@ 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 @@ -220,17 +263,77 @@ uint32_t lastMsgCount = 0; TxMessage txMessages[MAX_TX_MESSAGES]; uint32_t totalTxCount = 0; -// CAN 시퀀스 (최대 10개 저장 가능) +// CAN 시퀀스 #define MAX_SEQUENCES 10 CANSequence sequences[MAX_SEQUENCES]; uint8_t sequenceCount = 0; SequenceRuntime seqRuntime = {false, 0, 0, 0, -1}; -// 파일 커멘트 저장 (최대 50개) +// 파일 커멘트 저장 #define MAX_FILE_COMMENTS 50 FileComment fileComments[MAX_FILE_COMMENTS]; int commentCount = 0; +// ======================================== +// 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); + + Serial.printf("✓ Serial 설정 로드: %u-%u-%u-%u\n", + serialSettings.baudRate, serialSettings.dataBits, + serialSettings.parity, serialSettings.stopBits); +} + +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); + + 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; + 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 { // 8 bits + 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 + } + + 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); +} + // ======================================== // 설정 저장/로드 함수 // ======================================== @@ -242,7 +345,7 @@ void loadSettings() { preferences.getString("wifi_ssid", wifiSSID, sizeof(wifiSSID)); preferences.getString("wifi_pass", wifiPassword, sizeof(wifiPassword)); - // WiFi STA 모드 설정 로드 (추가) + // WiFi STA 모드 설정 로드 enableSTAMode = preferences.getBool("sta_enable", false); preferences.getString("sta_ssid", staSSID, sizeof(staSSID)); preferences.getString("sta_pass", staPassword, sizeof(staPassword)); @@ -255,575 +358,307 @@ void loadSettings() { strcpy(wifiPassword, "12345678"); } - // CAN 속도 로드 (기본값: 1Mbps = 3) + // CAN 속도 로드 int speedIndex = preferences.getInt("can_speed", 3); if (speedIndex >= 0 && speedIndex < 4) { currentCanSpeed = canSpeedValues[speedIndex]; Serial.printf("✓ 저장된 CAN 속도 로드: %s\n", canSpeedNames[speedIndex]); } - // MCP2515 모드 로드 (기본값: Normal = 0) - int mcpMode = preferences.getInt("mcp_mode", 0); - if (mcpMode >= 0 && mcpMode <= 3) { - currentMcpMode = (MCP2515Mode)mcpMode; - Serial.printf("✓ 저장된 MCP 모드 로드: %d\n", mcpMode); + // 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(); - - // STA 모드 설정 출력 - if (enableSTAMode && strlen(staSSID) > 0) { - Serial.printf("✓ WiFi STA 모드: 활성화 (SSID: %s)\n", staSSID); - } } void saveSettings() { preferences.begin("can-logger", false); - // WiFi AP 설정 저장 preferences.putString("wifi_ssid", wifiSSID); preferences.putString("wifi_pass", wifiPassword); - - // WiFi STA 모드 설정 저장 (추가) preferences.putBool("sta_enable", enableSTAMode); preferences.putString("sta_ssid", staSSID); preferences.putString("sta_pass", staPassword); - preferences.end(); - - Serial.println("\n✓ 설정 저장 완료:"); - Serial.println("----------------------------------------"); - Serial.printf(" WiFi AP SSID : %s\n", wifiSSID); - Serial.printf(" WiFi AP Password : %s\n", wifiPassword); - if (enableSTAMode && strlen(staSSID) > 0) { - Serial.printf(" STA Mode : 활성화\n"); - Serial.printf(" STA SSID : %s\n", staSSID); - } else { - Serial.printf(" STA Mode : 비활성화\n"); - } - Serial.println("----------------------------------------"); - Serial.println("⚠️ 재부팅 후 WiFi 설정이 적용됩니다."); -} - -void saveCANSettings() { - preferences.begin("can-logger", false); - - // CAN 속도 저장 (인덱스로 저장) - int speedIndex = 3; // 기본값: 1M + // CAN 속도 저장 for (int i = 0; i < 4; i++) { if (canSpeedValues[i] == currentCanSpeed) { - speedIndex = i; + preferences.putInt("can_speed", i); + Serial.printf("✓ CAN 속도 저장: %s (인덱스 %d)\n", canSpeedNames[i], i); break; } } - preferences.putInt("can_speed", speedIndex); // MCP2515 모드 저장 preferences.putInt("mcp_mode", (int)currentMcpMode); + Serial.printf("✓ MCP 모드 저장: %d\n", (int)currentMcpMode); preferences.end(); - Serial.println("\n✓ CAN 설정 저장 완료:"); - Serial.println("----------------------------------------"); - Serial.printf(" CAN Speed : %s\n", canSpeedNames[speedIndex]); - Serial.printf(" MCP Mode : %d\n", (int)currentMcpMode); - Serial.println("----------------------------------------"); -} - -// ======================================== -// 시퀀스 관리 함수 -// ======================================== - -void loadSequences() { - if (!sdCardReady) return; - - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - File seqFile = SD.open("/sequences.bin", FILE_READ); - if (seqFile) { - sequenceCount = 0; - while (seqFile.available() && sequenceCount < MAX_SEQUENCES) { - seqFile.read((uint8_t*)&sequences[sequenceCount], sizeof(CANSequence)); - sequenceCount++; - } - seqFile.close(); - Serial.printf("✓ 시퀀스 로드: %d개\n", sequenceCount); - } - xSemaphoreGive(sdMutex); - } -} - -void saveSequences() { - if (!sdCardReady) return; - - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - // 기존 파일 삭제 - if (SD.exists("/sequences.bin")) { - SD.remove("/sequences.bin"); - } - - File seqFile = SD.open("/sequences.bin", FILE_WRITE); - if (seqFile) { - for (int i = 0; i < sequenceCount; i++) { - seqFile.write((uint8_t*)&sequences[i], sizeof(CANSequence)); - } - seqFile.close(); - Serial.println("✓ 시퀀스 저장 완료"); - } - xSemaphoreGive(sdMutex); - } -} - -int addSequence(const CANSequence& seq) { - if (sequenceCount >= MAX_SEQUENCES) { - return -1; // 가득 참 - } - - sequences[sequenceCount] = seq; - sequenceCount++; - saveSequences(); - return sequenceCount - 1; -} - -bool deleteSequence(uint8_t index) { - if (index >= sequenceCount) return false; - - // 배열 왼쪽으로 시프트 - for (int i = index; i < sequenceCount - 1; i++) { - sequences[i] = sequences[i + 1]; - } - sequenceCount--; - saveSequences(); - return true; -} - -void stopSequence() { - seqRuntime.running = false; - seqRuntime.currentStep = 0; - seqRuntime.currentRepeat = 0; - seqRuntime.activeSequenceIndex = -1; - Serial.println("✓ 시퀀스 실행 중지"); -} - -void startSequence(uint8_t index) { - if (index >= sequenceCount) return; - - seqRuntime.running = true; - seqRuntime.currentStep = 0; - seqRuntime.currentRepeat = 0; - seqRuntime.lastStepTime = millis(); - seqRuntime.activeSequenceIndex = index; - - Serial.printf("✓ 시퀀스 실행 시작: %s\n", sequences[index].name); -} - -// ======================================== -// 파일 커멘트 관리 함수 -// ======================================== - -void loadFileComments() { - if (!sdCardReady) return; - - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - File commentFile = SD.open("/comments.txt", FILE_READ); - if (commentFile) { - commentCount = 0; - while (commentFile.available() && commentCount < MAX_FILE_COMMENTS) { - String line = commentFile.readStringUntil('\n'); - line.trim(); - - int separatorPos = line.indexOf('|'); - if (separatorPos > 0) { - String filename = line.substring(0, separatorPos); - String comment = line.substring(separatorPos + 1); - - strncpy(fileComments[commentCount].filename, filename.c_str(), MAX_FILENAME_LEN - 1); - strncpy(fileComments[commentCount].comment, comment.c_str(), MAX_COMMENT_LEN - 1); - commentCount++; - } - } - commentFile.close(); - Serial.printf("✓ 파일 커멘트 로드: %d개\n", commentCount); - } - xSemaphoreGive(sdMutex); - } -} - -void saveFileComments() { - if (!sdCardReady) return; - - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - // 기존 파일 삭제 - if (SD.exists("/comments.txt")) { - SD.remove("/comments.txt"); - } - - File commentFile = SD.open("/comments.txt", FILE_WRITE); - if (commentFile) { - for (int i = 0; i < commentCount; i++) { - commentFile.print(fileComments[i].filename); - commentFile.print("|"); - commentFile.println(fileComments[i].comment); - } - commentFile.close(); - Serial.println("✓ 파일 커멘트 저장 완료"); - } - xSemaphoreGive(sdMutex); - } -} - -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); - saveFileComments(); - return; - } - } - - // 새로운 커멘트 추가 - if (commentCount < MAX_FILE_COMMENTS) { - strncpy(fileComments[commentCount].filename, filename, MAX_FILENAME_LEN - 1); - strncpy(fileComments[commentCount].comment, comment, MAX_COMMENT_LEN - 1); - commentCount++; - saveFileComments(); - } -} - -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 ""; -} - -// ======================================== -// 전력 모니터링 함수 -// ======================================== - -float readVoltage() { - // ESP32 내부 ADC로 전압 측정 - // GPIO34는 ADC1_CH6에 연결되어 있음 - // 실제 배터리 전압을 측정하려면 분압 회로가 필요할 수 있음 - - // 여러 번 샘플링하여 평균값 계산 - const int samples = 10; - uint32_t sum = 0; - - for (int i = 0; i < samples; i++) { - sum += analogRead(34); - delayMicroseconds(100); - } - - uint32_t avg = sum / samples; - - // ESP32 ADC: 12bit (0-4095), 참조전압 3.3V - // 실제 전압 = (ADC값 / 4095) * 3.3V - float voltage = (avg / 4095.0) * 3.3; - - return voltage; -} - -void updatePowerStatus() { - uint32_t now = millis(); - - // 5초마다 전압 체크 - if (now - powerStatus.lastCheck >= VOLTAGE_CHECK_INTERVAL_MS) { - powerStatus.voltage = readVoltage(); - - // 1초 단위 최소값 업데이트 - if (powerStatus.voltage < powerStatus.minVoltage) { - powerStatus.minVoltage = powerStatus.voltage; - } - - // 1초마다 최소값 리셋 - if (now - powerStatus.lastMinReset >= 1000) { - powerStatus.minVoltage = powerStatus.voltage; - powerStatus.lastMinReset = now; - } - - // 저전압 경고 - if (powerStatus.voltage < LOW_VOLTAGE_THRESHOLD) { - if (!powerStatus.lowVoltage) { - Serial.printf("⚠️ 저전압 경고: %.2fV\n", powerStatus.voltage); - powerStatus.lowVoltage = true; - } - } else { - powerStatus.lowVoltage = false; - } - - powerStatus.lastCheck = now; - } + Serial.println("✓ 설정 저장 완료"); } // ======================================== // 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 / 16 * 10) + (val % 16)); + return (val >> 4) * 10 + (val & 0x0F); } uint8_t decToBcd(uint8_t val) { - return ((val / 10 * 16) + (val % 10)); + return ((val / 10) << 4) | (val % 10); } -bool initRTC() { - rtcWire.begin(); - rtcWire.setTimeout(1000); +bool readRTC(struct tm *timeinfo) { + if (!timeSyncStatus.rtcAvailable) return false; - rtcWire.beginTransmission(DS3231_ADDRESS); - uint8_t error = rtcWire.endTransmission(); - - if (error == 0) { - timeSyncStatus.rtcAvailable = true; - Serial.println("✓ RTC DS3231 초기화 완료"); - return true; - } else { - timeSyncStatus.rtcAvailable = false; - Serial.println("✗ RTC DS3231 없음 (수동 시간 설정 필요)"); + if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(100)) != pdTRUE) { return false; } -} - -bool setRTCTime(int year, int month, int day, int hour, int minute, int second) { - if (!timeSyncStatus.rtcAvailable) return false; - if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - rtcWire.beginTransmission(DS3231_ADDRESS); - rtcWire.write(0x00); // 레지스터 주소 - - rtcWire.write(decToBcd(second)); - rtcWire.write(decToBcd(minute)); - rtcWire.write(decToBcd(hour)); - rtcWire.write(decToBcd(1)); // 요일 (사용안함) - rtcWire.write(decToBcd(day)); - rtcWire.write(decToBcd(month)); - rtcWire.write(decToBcd(year - 2000)); - - uint8_t error = rtcWire.endTransmission(); - xSemaphoreGive(rtcMutex); - - if (error == 0) { - Serial.printf("✓ RTC 시간 설정: %04d-%02d-%02d %02d:%02d:%02d\n", - year, month, day, hour, minute, second); - return true; - } - } - return false; -} - -bool getRTCTime(struct tm* timeinfo) { - if (!timeSyncStatus.rtcAvailable) return false; - - if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - rtcWire.beginTransmission(DS3231_ADDRESS); - rtcWire.write(0x00); // 레지스터 시작 주소 - - if (rtcWire.endTransmission() != 0) { - xSemaphoreGive(rtcMutex); - return false; - } - - rtcWire.requestFrom(DS3231_ADDRESS, 7); - - if (rtcWire.available() >= 7) { - uint8_t second = bcdToDec(rtcWire.read() & 0x7F); - uint8_t minute = bcdToDec(rtcWire.read()); - uint8_t hour = bcdToDec(rtcWire.read() & 0x3F); - rtcWire.read(); // 요일 스킵 - uint8_t day = bcdToDec(rtcWire.read()); - uint8_t month = bcdToDec(rtcWire.read()); - uint8_t year = bcdToDec(rtcWire.read()); - - timeinfo->tm_sec = second; - timeinfo->tm_min = minute; - timeinfo->tm_hour = hour; - timeinfo->tm_mday = day; - timeinfo->tm_mon = month - 1; - timeinfo->tm_year = year + 100; - - xSemaphoreGive(rtcMutex); - return true; - } - + rtcWire.beginTransmission(DS3231_ADDRESS); + rtcWire.write(0x00); + if (rtcWire.endTransmission() != 0) { xSemaphoreGive(rtcMutex); + return false; } - return false; + if (rtcWire.requestFrom(DS3231_ADDRESS, 7) != 7) { + xSemaphoreGive(rtcMutex); + return false; + } + + uint8_t buffer[7]; + for (int i = 0; i < 7; i++) { + buffer[i] = rtcWire.read(); + } + + xSemaphoreGive(rtcMutex); + + timeinfo->tm_sec = bcdToDec(buffer[0] & 0x7F); + timeinfo->tm_min = bcdToDec(buffer[1] & 0x7F); + timeinfo->tm_hour = bcdToDec(buffer[2] & 0x3F); + timeinfo->tm_wday = bcdToDec(buffer[3] & 0x07) - 1; + timeinfo->tm_mday = bcdToDec(buffer[4] & 0x3F); + timeinfo->tm_mon = bcdToDec(buffer[5] & 0x1F) - 1; + timeinfo->tm_year = bcdToDec(buffer[6]) + 100; + + return true; } -void syncSystemTimeFromRTC() { - struct tm timeinfo; +bool writeRTC(const struct tm *timeinfo) { + if (!timeSyncStatus.rtcAvailable) return false; - if (getRTCTime(&timeinfo)) { - struct timeval tv; - tv.tv_sec = mktime(&timeinfo); - tv.tv_usec = 0; - settimeofday(&tv, NULL); + if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(100)) != pdTRUE) { + return false; + } + + rtcWire.beginTransmission(DS3231_ADDRESS); + rtcWire.write(0x00); + rtcWire.write(decToBcd(timeinfo->tm_sec)); + rtcWire.write(decToBcd(timeinfo->tm_min)); + rtcWire.write(decToBcd(timeinfo->tm_hour)); + rtcWire.write(decToBcd(timeinfo->tm_wday + 1)); + rtcWire.write(decToBcd(timeinfo->tm_mday)); + rtcWire.write(decToBcd(timeinfo->tm_mon + 1)); + rtcWire.write(decToBcd(timeinfo->tm_year - 100)); + + bool success = (rtcWire.endTransmission() == 0); + + xSemaphoreGive(rtcMutex); + + return success; +} + +void 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++; + } + } - timeSyncStatus.synchronized = true; - timeSyncStatus.lastSyncTime = esp_timer_get_time(); - timeSyncStatus.rtcSyncCount++; - - Serial.printf("✓ RTC→시스템 동기화 (%d회): %04d-%02d-%02d %02d:%02d:%02d\n", - timeSyncStatus.rtcSyncCount, - timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, - timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); + vTaskDelay(xDelay); } } // ======================================== -// MCP2515 모드 제어 함수 +// MCP2515 모드 설정 // ======================================== bool setMCP2515Mode(MCP2515Mode mode) { + const char* modeName; MCP2515::ERROR result; switch (mode) { case MCP_MODE_NORMAL: result = mcp2515.setNormalMode(); - if (result == MCP2515::ERROR_OK) { - // 인터럽트 재활성화 - attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING); - currentMcpMode = MCP_MODE_NORMAL; - Serial.println("✓ MCP2515 모드: NORMAL"); - Serial.println(" 송수신 모두 가능, ACK 전송"); - return true; - } + modeName = "Normal"; break; - case MCP_MODE_LISTEN_ONLY: result = mcp2515.setListenOnlyMode(); - if (result == MCP2515::ERROR_OK) { - // 인터럽트 재활성화 - attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING); - currentMcpMode = MCP_MODE_LISTEN_ONLY; - Serial.println("✓ MCP2515 모드: LISTEN-ONLY"); - Serial.println(" 수신만 가능, ACK 전송 안 함"); - return true; - } + modeName = "Listen-Only"; break; - case MCP_MODE_LOOPBACK: result = mcp2515.setLoopbackMode(); - if (result == MCP2515::ERROR_OK) { - // 인터럽트 재활성화 - attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING); - currentMcpMode = MCP_MODE_LOOPBACK; - Serial.println("✓ MCP2515 모드: LOOPBACK"); - Serial.println(" 자가 테스트 모드"); - return true; - } + modeName = "Loopback"; break; - case MCP_MODE_TRANSMIT: - // TRANSMIT 모드: Listen-Only 기반 - // 평상시에는 Listen-Only로 동작하여 ACK를 보내지 않음 - // 송신이 필요할 때만 일시적으로 Normal 모드로 전환 - result = mcp2515.setListenOnlyMode(); - if (result == MCP2515::ERROR_OK) { - // 인터럽트는 비활성화 (수신 처리하지 않음) - detachInterrupt(digitalPinToInterrupt(CAN_INT_PIN)); - - // 수신 버퍼를 비움 - struct can_frame frame; - while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) { - // 버퍼만 비움 - } - - currentMcpMode = MCP_MODE_TRANSMIT; - Serial.println("✓ MCP2515 모드: TRANSMIT-ONLY"); - Serial.println(" 기본: Listen-Only (ACK 안 함)"); - Serial.println(" 송신 시: 일시적으로 Normal 전환"); - Serial.println(" 수신 인터럽트: 비활성화"); - return true; - } + result = mcp2515.setNormalMode(); // Transmit는 Normal 모드 사용 + modeName = "Transmit"; break; + default: + return false; } - Serial.println("✗ MCP2515 모드 변경 실패"); - return false; -} - -// ======================================== -// CAN 송신 헬퍼 함수 (TRANSMIT 모드 자동 처리) -// ======================================== - -MCP2515::ERROR sendCANMessage(struct can_frame* frame) { - MCP2515::ERROR result; - - // TRANSMIT 모드일 때만 특별 처리 - if (currentMcpMode == MCP_MODE_TRANSMIT) { - // 1. 일시적으로 Normal 모드로 전환 - result = mcp2515.setNormalMode(); - if (result != MCP2515::ERROR_OK) { - Serial.println("✗ TRANSMIT 모드: Normal 전환 실패"); - return result; - } - - // 2. 메시지 송신 - result = mcp2515.sendMessage(frame); - - // 3. 즉시 Listen-Only 모드로 복귀 - mcp2515.setListenOnlyMode(); - - // 4. 수신 버퍼 비우기 (전환 중 수신된 데이터) - struct can_frame dummy; - while (mcp2515.readMessage(&dummy) == MCP2515::ERROR_OK) { - // 버퍼만 비움 - } - - return result; + if (result == MCP2515::ERROR_OK) { + currentMcpMode = mode; + Serial.printf("✓ MCP2515 모드 변경: %s\n", modeName); + return true; } else { - // 다른 모드에서는 바로 송신 - return mcp2515.sendMessage(frame); + Serial.printf("✗ MCP2515 모드 변경 실패: %s (error=%d)\n", modeName, result); + return false; } } // ======================================== -// CAN 인터럽트 및 수신 함수 +// CAN 인터럽트 핸들러 // ======================================== void IRAM_ATTR canISR() { BaseType_t xHigherPriorityTaskWoken = pdFALSE; - vTaskNotifyGiveFromISR(canRxTaskHandle, &xHigherPriorityTaskWoken); - if (xHigherPriorityTaskWoken) { - portYIELD_FROM_ISR(); + if (canRxTaskHandle != NULL) { + vTaskNotifyGiveFromISR(canRxTaskHandle, &xHigherPriorityTaskWoken); + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } } -void canRxTask(void* parameter) { - struct can_frame frame; - CANMessage canMsg; +// ======================================== +// Serial 수신 Task (추가) +// ======================================== + +void serialRxTask(void *parameter) { + SerialMessage serialMsg; + uint8_t lineBuffer[MAX_SERIAL_LINE_LEN]; + uint16_t lineIndex = 0; - for (;;) { + Serial.println("✓ Serial RX Task 시작"); + + while (1) { + // Serial 데이터 수신 + while (SerialComm.available()) { + uint8_t c = SerialComm.read(); + + // 바이너리 모드로 처리 (라인 단위) + lineBuffer[lineIndex++] = c; + + // 개행 문자 또는 버퍼 가득 참 + 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; // 수신 데이터 + + // Queue에 전송 + if (xQueueSend(serialQueue, &serialMsg, 0) == pdTRUE) { + totalSerialRxCount++; + } + + lineIndex = 0; + } + } + } + + vTaskDelay(pdMS_TO_TICKS(1)); // 1ms 간격 + } +} + +// ======================================== +// CAN 수신 Task +// ======================================== + +void canRxTask(void *parameter) { + struct can_frame frame; + CANMessage msg; + + Serial.println("✓ CAN RX Task 시작"); + + while (1) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); - // 한 번에 여러 메시지를 읽어서 처리 속도 향상 - int readCount = 0; - while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK && readCount < 20) { - // 현재 시간 저장 (마이크로초) + while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) { struct timeval tv; gettimeofday(&tv, NULL); - canMsg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + (uint64_t)tv.tv_usec; + msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; - canMsg.id = frame.can_id; - canMsg.dlc = frame.can_dlc; - memcpy(canMsg.data, frame.data, 8); + msg.id = frame.can_id & 0x1FFFFFFF; + msg.dlc = frame.can_dlc; + memcpy(msg.data, frame.data, 8); - // 로깅 중일 때만 큐에 추가 - if (loggingEnabled) { - xQueueSend(canQueue, &canMsg, 0); + if (xQueueSend(canQueue, &msg, 0) == pdTRUE) { + totalMsgCount++; } + } + } +} + +// ======================================== +// SD 쓰기 Task (CAN + Serial 동시 지원) +// ======================================== + +void sdWriteTask(void *parameter) { + CANMessage canMsg; + SerialMessage serialMsg; + + Serial.println("✓ SD Write Task 시작"); + + while (1) { + bool hasWork = false; + + // CAN 메시지 처리 + if (xQueueReceive(canQueue, &canMsg, 0) == pdTRUE) { + hasWork = true; - // 실시간 데이터 업데이트 (웹 표시용) + // 실시간 모니터링 업데이트 bool found = false; for (int i = 0; i < RECENT_MSG_COUNT; i++) { - if (recentData[i].count > 0 && recentData[i].msg.id == canMsg.id) { + if (recentData[i].msg.id == canMsg.id) { recentData[i].msg = canMsg; recentData[i].count++; found = true; @@ -841,170 +676,227 @@ void canRxTask(void* parameter) { } } - totalMsgCount++; - readCount++; - } - - // 메시지/초 계산 - uint32_t now = millis(); - if (now - lastMsgCountTime >= 1000) { - msgPerSecond = totalMsgCount - lastMsgCount; - lastMsgCount = totalMsgCount; - lastMsgCountTime = now; - } - } -} - -// ======================================== -// SD 카드 쓰기 태스크 -// ======================================== - -void sdWriteTask(void* parameter) { - CANMessage msg; - - for (;;) { - if (loggingEnabled && sdCardReady) { - if (xQueueReceive(canQueue, &msg, pdMS_TO_TICKS(10)) == pdTRUE) { - // 버퍼에 데이터 추가 - memcpy(&fileBuffer[bufferIndex], &msg, sizeof(CANMessage)); - bufferIndex += sizeof(CANMessage); - currentFileSize += sizeof(CANMessage); - - // 버퍼가 가득 차면 SD에 쓰기 - if (bufferIndex >= FILE_BUFFER_SIZE - sizeof(CANMessage)) { - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(100)) == pdTRUE) { + // CAN 로깅 + if (loggingEnabled && sdCardReady) { + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(100)) == pdTRUE) { + if (bufferIndex + sizeof(CANMessage) <= FILE_BUFFER_SIZE) { + memcpy(&fileBuffer[bufferIndex], &canMsg, sizeof(CANMessage)); + bufferIndex += sizeof(CANMessage); + currentFileSize += sizeof(CANMessage); + } + + if (bufferIndex >= FILE_BUFFER_SIZE - sizeof(CANMessage)) { if (logFile) { logFile.write(fileBuffer, bufferIndex); logFile.flush(); + bufferIndex = 0; } - xSemaphoreGive(sdMutex); } - bufferIndex = 0; + + xSemaphoreGive(sdMutex); } } - } else { - vTaskDelay(pdMS_TO_TICKS(100)); } - } -} - -// ======================================== -// 로깅 제어 함수 -// ======================================== - -void startLogging() { - if (!sdCardReady) { - Serial.println("✗ SD 카드가 준비되지 않음"); - return; - } - - if (loggingEnabled) { - Serial.println("⚠️ 이미 로깅 중"); - return; - } - - // 파일명 생성 (현재 시간 사용) - time_t now; - struct tm timeinfo; - time(&now); - localtime_r(&now, &timeinfo); - - snprintf(currentFilename, MAX_FILENAME_LEN, - "/CAN_%04d%02d%02d_%02d%02d%02d.bin", - timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, - timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); - - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - logFile = SD.open(currentFilename, FILE_WRITE); - if (logFile) { - loggingEnabled = true; - bufferIndex = 0; - currentFileSize = 0; + // Serial 메시지 처리 (추가) + if (xQueueReceive(serialQueue, &serialMsg, 0) == pdTRUE) { + hasWork = true; - Serial.print("✓ 로깅 시작: "); - Serial.println(currentFilename); - } else { - Serial.println("✗ 파일 생성 실패"); + // Serial 로깅 + if (serialLoggingEnabled && sdCardReady) { + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(100)) == pdTRUE) { + if (serialBufferIndex + sizeof(SerialMessage) <= SERIAL_BUFFER_SIZE) { + memcpy(&serialFileBuffer[serialBufferIndex], &serialMsg, sizeof(SerialMessage)); + serialBufferIndex += sizeof(SerialMessage); + currentSerialFileSize += sizeof(SerialMessage); + } + + if (serialBufferIndex >= SERIAL_BUFFER_SIZE - sizeof(SerialMessage)) { + if (serialLogFile) { + serialLogFile.write(serialFileBuffer, serialBufferIndex); + serialLogFile.flush(); + serialBufferIndex = 0; + } + } + + xSemaphoreGive(sdMutex); + } + } } - xSemaphoreGive(sdMutex); + if (!hasWork) { + vTaskDelay(pdMS_TO_TICKS(5)); + } } } -void stopLogging() { - if (!loggingEnabled) { - Serial.println("⚠️ 로깅 중이 아님"); - return; +// ======================================== +// SD 모니터링 Task +// ======================================== + +void sdMonitorTask(void *parameter) { + const TickType_t xDelay = pdMS_TO_TICKS(1000); + + while (1) { + if (!sdCardReady) { + if (SD.begin(VSPI_CS, vspi)) { + sdCardReady = true; + Serial.println("✓ SD 카드 재연결 감지"); + } + } + + 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(36) * (3.3 / 4095.0); + powerStatus.voltage = rawVoltage * 2.0; + + // 1초 단위 최소값 업데이트 + if (currentTime - powerStatus.lastMinReset >= 1000) { + powerStatus.minVoltage = powerStatus.voltage; + powerStatus.lastMinReset = currentTime; + } else { + if (powerStatus.voltage < powerStatus.minVoltage) { + powerStatus.minVoltage = powerStatus.voltage; + } + } + + powerStatus.lowVoltage = (powerStatus.voltage < LOW_VOLTAGE_THRESHOLD); + powerStatus.lastCheck = currentTime; + } + + vTaskDelay(xDelay); } +} + +// ======================================== +// 파일 커멘트 관리 +// ======================================== + +void saveFileComments() { + if (!sdCardReady) return; - loggingEnabled = false; - - // 남은 버퍼 데이터 쓰기 if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - if (bufferIndex > 0 && logFile) { - logFile.write(fileBuffer, bufferIndex); + 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("✓ 파일 커멘트 저장 완료"); } - - if (logFile) { - logFile.close(); - } - xSemaphoreGive(sdMutex); } - - bufferIndex = 0; - - // 큐 비우기 (로깅 중이 아닐 때는 큐가 불필요) - CANMessage dummyMsg; - while (xQueueReceive(canQueue, &dummyMsg, 0) == pdTRUE) { - // 큐의 모든 메시지 제거 - } - - Serial.print("✓ 로깅 종료: "); - Serial.println(currentFilename); - Serial.printf(" 파일 크기: %u bytes\n", currentFileSize); - Serial.println("✓ 큐 비움 완료"); - - // 현재 파일명 초기화 - currentFilename[0] = '\0'; } -// ======================================== -// SD 모니터링 태스크 -// ======================================== +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(); + Serial.printf("✓ 파일 커멘트 %d개 로드 완료\n", commentCount); + } + } + xSemaphoreGive(sdMutex); + } +} -void sdMonitorTask(void* parameter) { - for (;;) { - updatePowerStatus(); - vTaskDelay(pdMS_TO_TICKS(1000)); +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(); } } // ======================================== -// RTC 동기화 태스크 +// CAN 시퀀스 관리 // ======================================== -void rtcSyncTask(void* parameter) { - for (;;) { - syncSystemTimeFromRTC(); - vTaskDelay(pdMS_TO_TICKS(RTC_SYNC_INTERVAL_MS)); +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); + } +} + +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(); + Serial.printf("✓ 시퀀스 %d개 로드 완료\n", sequenceCount); + } + } + xSemaphoreGive(sdMutex); } } // ======================================== -// CAN 송신 태스크 +// CAN TX Task // ======================================== -void txTask(void* parameter) { +void txTask(void *parameter) { struct can_frame frame; - for (;;) { + Serial.println("✓ TX Task 시작"); + + 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) { frame.can_id = txMessages[i].id; if (txMessages[i].extended) { @@ -1013,36 +905,41 @@ void txTask(void* parameter) { frame.can_dlc = txMessages[i].dlc; memcpy(frame.data, txMessages[i].data, 8); - if (sendCANMessage(&frame) == MCP2515::ERROR_OK) { - txMessages[i].lastSent = now; + if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) { totalTxCount++; + txMessages[i].lastSent = now; } } } } - vTaskDelay(pdMS_TO_TICKS(1)); + if (anyActive) { + vTaskDelay(pdMS_TO_TICKS(1)); + } else { + vTaskDelay(pdMS_TO_TICKS(10)); + } } } // ======================================== -// 시퀀스 실행 태스크 +// 시퀀스 Task // ======================================== -void sequenceTask(void* parameter) { - struct can_frame frame; +void sequenceTask(void *parameter) { + Serial.println("✓ Sequence Task 시작"); - for (;;) { - if (seqRuntime.running && seqRuntime.activeSequenceIndex >= 0) { + 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 (seqRuntime.currentStep == 0 || (now - seqRuntime.lastStepTime >= step->delayMs)) { - // CAN 메시지 전송 + if (now - seqRuntime.lastStepTime >= step->delayMs) { + struct can_frame frame; frame.can_id = step->canId; if (step->extended) { frame.can_id |= CAN_EFF_FLAG; @@ -1050,596 +947,667 @@ void sequenceTask(void* parameter) { frame.can_dlc = step->dlc; memcpy(frame.data, step->data, 8); - MCP2515::ERROR result = sendCANMessage(&frame); - if (result == MCP2515::ERROR_OK) { + if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) { totalTxCount++; - Serial.printf(" [Seq] Step %d/%d: ID=0x%X, DLC=%d, Delay=%dms - OK\n", - seqRuntime.currentStep + 1, seq->stepCount, - step->canId, step->dlc, step->delayMs); - } else { - Serial.printf(" [Seq] Step %d/%d: ID=0x%X - FAIL (Error %d)\n", - seqRuntime.currentStep + 1, seq->stepCount, step->canId, result); } seqRuntime.currentStep++; seqRuntime.lastStepTime = now; } } else { - // 모든 스텝 완료 - seqRuntime.currentRepeat++; - - // 반복 체크 - bool shouldContinue = false; - if (seq->repeatMode == 0) { - // 한 번만 - shouldContinue = false; + seqRuntime.running = false; } else if (seq->repeatMode == 1) { - // 특정 횟수 - if (seqRuntime.currentRepeat < seq->repeatCount) { - shouldContinue = true; + seqRuntime.currentRepeat++; + if (seqRuntime.currentRepeat >= seq->repeatCount) { + seqRuntime.running = false; + } else { + seqRuntime.currentStep = 0; + seqRuntime.lastStepTime = now; } } else if (seq->repeatMode == 2) { - // 무한 반복 - shouldContinue = true; - } - - if (shouldContinue) { seqRuntime.currentStep = 0; seqRuntime.lastStepTime = now; - Serial.printf(" [Seq] 반복 %d회 시작\n", seqRuntime.currentRepeat + 1); - } else { - Serial.printf("✓ 시퀀스 실행 완료: %s (총 %d회 반복)\n", - seq->name, seqRuntime.currentRepeat); - stopSequence(); } } + + vTaskDelay(pdMS_TO_TICKS(1)); + } else { + vTaskDelay(pdMS_TO_TICKS(10)); } - - vTaskDelay(pdMS_TO_TICKS(10)); } } // ======================================== -// 파일 리스트 전송 함수 +// WebSocket 이벤트 처리 (Serial 명령 추가) // ======================================== -void sendFileList() { - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - File root = SD.open("/"); - String fileList = "{\"type\":\"files\",\"files\":["; - bool first = true; - - File file = root.openNextFile(); - while (file) { - if (!file.isDirectory()) { - String fname = file.name(); - - // comments.txt 파일은 제외 - if (fname != "comments.txt" && fname != "/comments.txt") { - if (fname.startsWith("/")) fname = fname.substring(1); - - if (!first) fileList += ","; - fileList += "{\"name\":\"" + fname + "\","; - fileList += "\"size\":" + String(file.size()); - - // 커멘트 추가 - const char* comment = getFileComment(fname.c_str()); - if (strlen(comment) > 0) { - fileList += ",\"comment\":\"" + String(comment) + "\""; - } - - fileList += "}"; - first = false; - } - } - file.close(); - file = root.openNextFile(); - } - - root.close(); - fileList += "]}"; - - xSemaphoreGive(sdMutex); - webSocket.broadcastTXT(fileList); - } -} - -// ======================================== -// WebSocket 이벤트 처리 -// ======================================== - -void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) { +void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) { if (type == WStype_TEXT) { - String message = String((char*)payload); + DynamicJsonDocument doc(2048); + DeserializationError error = deserializeJson(doc, payload); - // JSON 파싱 (간단한 방식) - if (message.indexOf("\"cmd\":\"startLogging\"") >= 0) { - startLogging(); - // 파일 리스트 자동 갱신 - delay(100); - sendFileList(); - - } else if (message.indexOf("\"cmd\":\"stopLogging\"") >= 0) { - stopLogging(); - // 파일 리스트 자동 갱신 - delay(100); - sendFileList(); - - } else if (message.indexOf("\"cmd\":\"getFiles\"") >= 0) { - // 파일 리스트 전송 - sendFileList(); - - } else if (message.indexOf("\"cmd\":\"deleteFile\"") >= 0) { - // 파일 삭제 - int filenameStart = message.indexOf("\"filename\":\"") + 12; - int filenameEnd = message.indexOf("\"", filenameStart); - String filename = message.substring(filenameStart, filenameEnd); - - // 로깅 중인 파일 체크 - bool canDelete = true; - if (loggingEnabled && currentFilename[0] != '\0') { - String currentFileStr = String(currentFilename); - if (currentFileStr.startsWith("/")) currentFileStr = currentFileStr.substring(1); - - if (filename == currentFileStr) { - canDelete = false; - } - } - - if (!canDelete) { - webSocket.sendTXT(num, "{\"type\":\"deleteResult\",\"success\":false,\"message\":\"Cannot delete file being logged\"}"); - } else { - String fullPath = "/" + filename; - + if (error) { + Serial.print("✗ JSON 파싱 실패: "); + Serial.println(error.c_str()); + return; + } + + const char* cmd = doc["cmd"]; + + if (strcmp(cmd, "startLogging") == 0) { + if (!loggingEnabled && sdCardReady) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - bool success = SD.remove(fullPath); - xSemaphoreGive(sdMutex); + struct tm timeinfo; + time_t now; + time(&now); + localtime_r(&now, &timeinfo); - if (success) { - Serial.printf("✓ 파일 삭제: %s\n", filename.c_str()); - webSocket.sendTXT(num, "{\"type\":\"deleteResult\",\"success\":true}"); - - // 파일 목록 자동 갱신 - delay(100); - sendFileList(); + snprintf(currentFilename, sizeof(currentFilename), + "/CAN_%04d%02d%02d_%02d%02d%02d.bin", + timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, + timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); + + logFile = SD.open(currentFilename, FILE_WRITE); + + if (logFile) { + loggingEnabled = true; + bufferIndex = 0; + currentFileSize = 0; + Serial.printf("✓ CAN 로깅 시작: %s\n", currentFilename); } else { - webSocket.sendTXT(num, "{\"type\":\"deleteResult\",\"success\":false,\"message\":\"Delete failed\"}"); + Serial.println("✗ 파일 생성 실패"); } + + 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 로깅 종료: %s (%u bytes)\n", currentFilename, currentFileSize); + + xSemaphoreGive(sdMutex); + } + } + } + else if (strcmp(cmd, "startSerialLogging") == 0) { // Serial 로깅 시작 + if (!serialLoggingEnabled && sdCardReady) { + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { + struct tm timeinfo; + time_t now; + time(&now); + localtime_r(&now, &timeinfo); + + snprintf(currentSerialFilename, sizeof(currentSerialFilename), + "/SER_%04d%02d%02d_%02d%02d%02d.bin", + timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, + timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); + + serialLogFile = SD.open(currentSerialFilename, FILE_WRITE); + + if (serialLogFile) { + serialLoggingEnabled = true; + serialBufferIndex = 0; + currentSerialFileSize = 0; + Serial.printf("✓ Serial 로깅 시작: %s\n", currentSerialFilename); + } else { + Serial.println("✗ Serial 파일 생성 실패"); + } + + xSemaphoreGive(sdMutex); + } + } + } + else if (strcmp(cmd, "stopSerialLogging") == 0) { // Serial 로깅 종료 + if (serialLoggingEnabled) { + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { + if (serialBufferIndex > 0 && serialLogFile) { + serialLogFile.write(serialFileBuffer, serialBufferIndex); + serialBufferIndex = 0; + } + + if (serialLogFile) { + serialLogFile.close(); + } + + serialLoggingEnabled = false; + Serial.printf("✓ Serial 로깅 종료: %s (%u bytes)\n", + currentSerialFilename, currentSerialFileSize); + + xSemaphoreGive(sdMutex); + } + } + } + else if (strcmp(cmd, "sendSerial") == 0) { // Serial 데이터 전송 + const char* data = doc["data"]; + if (data && strlen(data) > 0) { + SerialComm.println(data); + + // 송신 데이터를 Queue에 추가 (모니터링용) + SerialMessage serialMsg; + struct timeval tv; + gettimeofday(&tv, NULL); + serialMsg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; + serialMsg.length = strlen(data); + if (serialMsg.length > MAX_SERIAL_LINE_LEN - 1) { + serialMsg.length = MAX_SERIAL_LINE_LEN - 1; + } + memcpy(serialMsg.data, data, serialMsg.length); + serialMsg.isTx = true; // 송신 데이터 + + xQueueSend(serialQueue, &serialMsg, 0); + totalSerialTxCount++; + + Serial.printf("→ Serial TX: %s\n", data); + } + } + else if (strcmp(cmd, "setSerialConfig") == 0) { // Serial 설정 변경 + uint32_t baud = doc["baudRate"] | 115200; + uint8_t data = doc["dataBits"] | 8; + uint8_t parity = doc["parity"] | 0; + uint8_t stop = doc["stopBits"] | 1; - } else if (message.indexOf("\"cmd\":\"setSpeed\"") >= 0) { - // CAN 속도 변경 - int speedStart = message.indexOf("\"speed\":") + 8; - int speedEnd = message.indexOf(",", speedStart); - if (speedEnd < 0) speedEnd = message.indexOf("}", speedStart); + serialSettings.baudRate = baud; + serialSettings.dataBits = data; + serialSettings.parity = parity; + serialSettings.stopBits = stop; - int speedIndex = message.substring(speedStart, speedEnd).toInt(); + saveSerialSettings(); + applySerialSettings(); + Serial.printf("✓ Serial 설정 변경: %u-%u-%u-%u\n", baud, data, parity, stop); + } + else if (strcmp(cmd, "getSerialConfig") == 0) { // Serial 설정 조회 + 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); // 현재 모드 유지 + setMCP2515Mode(currentMcpMode); - // 비휘발성 메모리에 저장 - saveCANSettings(); + saveSettings(); - Serial.printf("✓ CAN 속도 변경 및 저장: %s\n", canSpeedNames[speedIndex]); + Serial.printf("✓ CAN 속도 변경: %s\n", canSpeedNames[speedIndex]); } - - } else if (message.indexOf("\"cmd\":\"setMcpMode\"") >= 0) { - // MCP2515 모드 변경 - int modeStart = message.indexOf("\"mode\":") + 7; - int modeEnd = message.indexOf(",", modeStart); - if (modeEnd < 0) modeEnd = message.indexOf("}", modeStart); - - int mode = message.substring(modeStart, modeEnd).toInt(); - - if (mode >= 0 && mode <= 3) { // 0~3으로 확장 (TRANSMIT 모드 포함) - if (setMCP2515Mode((MCP2515Mode)mode)) { - // 모드 변경 성공 시 비휘발성 메모리에 저장 - saveCANSettings(); + } + 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); + Serial.println("✓ 시간 동기화 완료 (Phone → ESP32 → RTC)"); + } else { + Serial.println("✓ 시간 동기화 완료 (Phone → ESP32)"); } } - - } else if (message.indexOf("\"cmd\":\"syncTimeFromPhone\"") >= 0) { - // 핸드폰 시간을 RTC와 시스템에 동기화 - int yearStart = message.indexOf("\"year\":") + 7; - int monthStart = message.indexOf("\"month\":") + 8; - int dayStart = message.indexOf("\"day\":") + 6; - int hourStart = message.indexOf("\"hour\":") + 7; - int minuteStart = message.indexOf("\"minute\":") + 9; - int secondStart = message.indexOf("\"second\":") + 9; - - int year = message.substring(yearStart, message.indexOf(",", yearStart)).toInt(); - int month = message.substring(monthStart, message.indexOf(",", monthStart)).toInt(); - int day = message.substring(dayStart, message.indexOf(",", dayStart)).toInt(); - int hour = message.substring(hourStart, message.indexOf(",", hourStart)).toInt(); - int minute = message.substring(minuteStart, message.indexOf(",", minuteStart)).toInt(); - int second = message.substring(secondStart, message.indexOf(",", secondStart)).toInt(); - - Serial.printf("📱 핸드폰 시간 수신: %04d-%02d-%02d %02d:%02d:%02d\n", - year, month, day, hour, minute, second); - - // 1. RTC에 시간 설정 (가능한 경우) - if (timeSyncStatus.rtcAvailable) { - setRTCTime(year, month, day, hour, minute, second); - } - - // 2. 시스템 시간 설정 - struct tm timeinfo; - timeinfo.tm_year = year - 1900; - timeinfo.tm_mon = month - 1; - timeinfo.tm_mday = day; - timeinfo.tm_hour = hour; - timeinfo.tm_min = minute; - timeinfo.tm_sec = second; - - struct timeval tv; - tv.tv_sec = mktime(&timeinfo); - tv.tv_usec = 0; - settimeofday(&tv, NULL); - - timeSyncStatus.synchronized = true; - timeSyncStatus.lastSyncTime = esp_timer_get_time(); - timeSyncStatus.syncCount++; - - Serial.println("✓ 시스템 시간 동기화 완료"); - - webSocket.sendTXT(num, "{\"type\":\"timeSyncResult\",\"success\":true}"); - - } else if (message.indexOf("\"cmd\":\"addComment\"") >= 0) { - // 파일 커멘트 추가 - int filenameStart = message.indexOf("\"filename\":\"") + 12; - int filenameEnd = message.indexOf("\"", filenameStart); - String filename = message.substring(filenameStart, filenameEnd); - - int commentStart = message.indexOf("\"comment\":\"") + 11; - int commentEnd = message.indexOf("\"", commentStart); - String comment = message.substring(commentStart, commentEnd); - - addFileComment(filename.c_str(), comment.c_str()); - Serial.printf("✓ 커멘트 추가: %s -> %s\n", filename.c_str(), comment.c_str()); - - webSocket.sendTXT(num, "{\"type\":\"commentResult\",\"success\":true}"); - - // 파일 목록 자동 갱신 - delay(100); - sendFileList(); - - } else if (message.indexOf("\"cmd\":\"getSettings\"") >= 0) { - // 설정 전송 - String settings = "{\"type\":\"settings\","; - settings += "\"ssid\":\"" + String(wifiSSID) + "\","; - settings += "\"password\":\"" + String(wifiPassword) + "\","; - settings += "\"staEnable\":" + String(enableSTAMode ? "true" : "false") + ","; - settings += "\"staSSID\":\"" + String(staSSID) + "\","; - settings += "\"staPassword\":\"" + String(staPassword) + "\","; - settings += "\"staConnected\":" + String(WiFi.status() == WL_CONNECTED ? "true" : "false") + ","; - settings += "\"staIP\":\"" + WiFi.localIP().toString() + "\"}"; - - webSocket.sendTXT(num, settings); - - } else if (message.indexOf("\"cmd\":\"saveSettings\"") >= 0) { - // 설정 저장 - int ssidStart = message.indexOf("\"ssid\":\"") + 8; - int ssidEnd = message.indexOf("\"", ssidStart); - String ssid = message.substring(ssidStart, ssidEnd); - - int passStart = message.indexOf("\"password\":\"") + 12; - int passEnd = message.indexOf("\"", passStart); - String password = message.substring(passStart, passEnd); - - // STA 모드 설정 파싱 - int staEnableIdx = message.indexOf("\"staEnable\":"); - if (staEnableIdx >= 0) { - String staEnableStr = message.substring(staEnableIdx + 12, staEnableIdx + 16); - enableSTAMode = (staEnableStr.indexOf("true") >= 0); - - if (enableSTAMode) { - int staSSIDStart = message.indexOf("\"staSSID\":\"") + 11; - int staSSIDEnd = message.indexOf("\"", staSSIDStart); - String staSsid = message.substring(staSSIDStart, staSSIDEnd); + } + else if (strcmp(cmd, "getFiles") == 0) { + if (sdCardReady) { + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { + DynamicJsonDocument response(8192); + response["type"] = "files"; + JsonArray files = response.createNestedArray("list"); - int staPassStart = message.indexOf("\"staPassword\":\"") + 15; - int staPassEnd = message.indexOf("\"", staPassStart); - String staPass = message.substring(staPassStart, staPassEnd); + File root = SD.open("/"); + File file = root.openNextFile(); - strncpy(staSSID, staSsid.c_str(), sizeof(staSSID) - 1); - strncpy(staPassword, staPass.c_str(), sizeof(staPassword) - 1); + 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"]; - strncpy(wifiSSID, ssid.c_str(), sizeof(wifiSSID) - 1); - strncpy(wifiPassword, password.c_str(), sizeof(wifiPassword) - 1); - - saveSettings(); - - webSocket.sendTXT(num, "{\"type\":\"settingsSaved\",\"success\":true}"); - - } else if (message.indexOf("\"cmd\":\"getSequences\"") >= 0) { - // 시퀀스 리스트 전송 - String seqList = "{\"type\":\"sequences\",\"sequences\":["; - for (int i = 0; i < sequenceCount; i++) { - if (i > 0) seqList += ","; - seqList += "{\"index\":" + String(i); - seqList += ",\"name\":\"" + String(sequences[i].name) + "\""; - seqList += ",\"steps\":" + String(sequences[i].stepCount); - seqList += ",\"mode\":" + String(sequences[i].repeatMode); - seqList += ",\"count\":" + String(sequences[i].repeatCount) + "}"; + 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); + + 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(8192); + 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); + } + } } - seqList += "]}"; - webSocket.sendTXT(num, seqList); + } + else if (strcmp(cmd, "addComment") == 0) { + const char* filename = doc["filename"]; + const char* comment = doc["comment"]; - } else if (message.indexOf("\"cmd\":\"getSequence\"") >= 0) { - // 특정 시퀀스 상세 정보 전송 - int indexStart = message.indexOf("\"index\":") + 8; - int indexEnd = message.indexOf(",", indexStart); - if (indexEnd < 0) indexEnd = message.indexOf("}", indexStart); - int index = message.substring(indexStart, indexEnd).toInt(); + 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(8192); + 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) { + 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++; + Serial.printf("✓ CAN 메시지 전송: ID=0x%X\n", frame.can_id & 0x1FFFFFFF); + } + } + 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) { - String seqData = "{\"type\":\"sequenceDetail\",\"sequence\":{"; - seqData += "\"name\":\"" + String(sequences[index].name) + "\","; - seqData += "\"mode\":" + String(sequences[index].repeatMode) + ","; - seqData += "\"count\":" + String(sequences[index].repeatCount) + ","; - seqData += "\"steps\":["; - - for (int i = 0; i < sequences[index].stepCount; i++) { - if (i > 0) seqData += ","; - SequenceStep* step = &sequences[index].steps[i]; - seqData += "{\"id\":" + String(step->canId); - seqData += ",\"ext\":" + String(step->extended ? "true" : "false"); - seqData += ",\"dlc\":" + String(step->dlc); - seqData += ",\"data\":\""; - for (int j = 0; j < 8; j++) { - if (step->data[j] < 0x10) seqData += "0"; - seqData += String(step->data[j], HEX); - if (j < 7) seqData += " "; - } - seqData += "\",\"delay\":" + String(step->delayMs) + "}"; + for (int i = index; i < sequenceCount - 1; i++) { + sequences[i] = sequences[i + 1]; } - seqData += "]}}"; - webSocket.sendTXT(num, seqData); - } - - } else if (message.indexOf("\"cmd\":\"saveSequence\"") >= 0) { - // 새 시퀀스 저장 (JSON 파싱) - CANSequence newSeq; - memset(&newSeq, 0, sizeof(CANSequence)); - - // 이름 추출 - int nameStart = message.indexOf("\"name\":\"") + 8; - int nameEnd = message.indexOf("\"", nameStart); - String name = message.substring(nameStart, nameEnd); - strncpy(newSeq.name, name.c_str(), sizeof(newSeq.name) - 1); - - // 모드 추출 - int modeStart = message.indexOf("\"mode\":") + 7; - int modeEnd = message.indexOf(",", modeStart); - newSeq.repeatMode = message.substring(modeStart, modeEnd).toInt(); - - // 반복 횟수 추출 - int countStart = message.indexOf("\"repeatCount\":") + 14; - int countEnd = message.indexOf(",", countStart); - if (countEnd < 0) countEnd = message.indexOf("}", countStart); - newSeq.repeatCount = message.substring(countStart, countEnd).toInt(); - - // 스텝 배열 파싱 - int stepsStart = message.indexOf("\"steps\":["); - if (stepsStart >= 0) { - stepsStart += 9; // "steps":[ 길이 - int stepsEnd = message.indexOf("]}", stepsStart); - String stepsJson = message.substring(stepsStart, stepsEnd); + sequenceCount--; + saveSequences(); - // 각 스텝 파싱 - newSeq.stepCount = 0; - int pos = 0; + 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; - while (pos < stepsJson.length() && newSeq.stepCount < 20) { - int stepStart = stepsJson.indexOf("{", pos); - if (stepStart < 0) break; - - int stepEnd = stepsJson.indexOf("}", stepStart); - if (stepEnd < 0) break; - - String stepJson = stepsJson.substring(stepStart, stepEnd + 1); - - // canId 추출 - int idStart = stepJson.indexOf("\"canId\":") + 8; - int idEnd = stepJson.indexOf(",", idStart); - if (idEnd < 0) idEnd = stepJson.indexOf("}", idStart); - newSeq.steps[newSeq.stepCount].canId = stepJson.substring(idStart, idEnd).toInt(); - - // extended 추출 - int extStart = stepJson.indexOf("\"extended\":") + 11; - String extStr = stepJson.substring(extStart, extStart + 5); - newSeq.steps[newSeq.stepCount].extended = (extStr.indexOf("true") >= 0); - - // dlc 추출 - int dlcStart = stepJson.indexOf("\"dlc\":") + 6; - int dlcEnd = stepJson.indexOf(",", dlcStart); - newSeq.steps[newSeq.stepCount].dlc = stepJson.substring(dlcStart, dlcEnd).toInt(); - - // data 배열 추출 - int dataStart = stepJson.indexOf("\"data\":[") + 8; - int dataEnd = stepJson.indexOf("]", dataStart); - String dataStr = stepJson.substring(dataStart, dataEnd); - - // data 바이트 파싱 - int bytePos = 0; - int byteIdx = 0; - while (bytePos < dataStr.length() && byteIdx < 8) { - int commaPos = dataStr.indexOf(",", bytePos); - if (commaPos < 0) commaPos = dataStr.length(); - - String byteStr = dataStr.substring(bytePos, commaPos); - byteStr.trim(); - newSeq.steps[newSeq.stepCount].data[byteIdx] = byteStr.toInt(); - - byteIdx++; - bytePos = commaPos + 1; - } - - // delay 추출 - int delayStart = stepJson.indexOf("\"delayMs\":") + 10; - int delayEnd = stepJson.indexOf(",", delayStart); - if (delayEnd < 0) delayEnd = stepJson.indexOf("}", delayStart); - newSeq.steps[newSeq.stepCount].delayMs = stepJson.substring(delayStart, delayEnd).toInt(); - - newSeq.stepCount++; - pos = stepEnd + 1; - } + 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(4096); + response["type"] = "sequences"; + JsonArray seqArray = response.createNestedArray("list"); + + for (int i = 0; i < sequenceCount; i++) { + JsonObject seqObj = seqArray.createNestedObject(); + seqObj["name"] = sequences[i].name; + seqObj["steps"] = sequences[i].stepCount; + seqObj["repeatMode"] = sequences[i].repeatMode; + seqObj["repeatCount"] = sequences[i].repeatCount; } - Serial.printf("📝 시퀀스 저장: %s (%d 스텝)\n", newSeq.name, newSeq.stepCount); - for (int i = 0; i < newSeq.stepCount; i++) { - Serial.printf(" Step %d: ID=0x%X, DLC=%d, Delay=%dms\n", - i, newSeq.steps[i].canId, newSeq.steps[i].dlc, newSeq.steps[i].delayMs); - } - - int result = addSequence(newSeq); - if (result >= 0) { - webSocket.sendTXT(num, "{\"type\":\"sequenceSaved\",\"success\":true,\"index\":" + String(result) + "}"); - } else { - webSocket.sendTXT(num, "{\"type\":\"sequenceSaved\",\"success\":false}"); - } - - } else if (message.indexOf("\"cmd\":\"deleteSequence\"") >= 0) { - // 시퀀스 삭제 - int indexStart = message.indexOf("\"index\":") + 8; - int indexEnd = message.indexOf(",", indexStart); - if (indexEnd < 0) indexEnd = message.indexOf("}", indexStart); - int index = message.substring(indexStart, indexEnd).toInt(); - - bool success = deleteSequence(index); - webSocket.sendTXT(num, "{\"type\":\"sequenceDeleted\",\"success\":" + String(success ? "true" : "false") + "}"); - - } else if (message.indexOf("\"cmd\":\"startSequence\"") >= 0) { - // 시퀀스 실행 - int indexStart = message.indexOf("\"index\":") + 8; - int indexEnd = message.indexOf(",", indexStart); - if (indexEnd < 0) indexEnd = message.indexOf("}", indexStart); - int index = message.substring(indexStart, indexEnd).toInt(); - - startSequence(index); - webSocket.sendTXT(num, "{\"type\":\"sequenceStarted\",\"success\":true}"); - - } else if (message.indexOf("\"cmd\":\"stopSequence\"") >= 0) { - // 시퀀스 중지 - stopSequence(); - webSocket.sendTXT(num, "{\"type\":\"sequenceStopped\",\"success\":true}"); + String json; + serializeJson(response, json); + webSocket.sendTXT(num, json); } } } // ======================================== -// 웹 업데이트 태스크 +// 웹 업데이트 Task (Serial 데이터 전송 추가) // ======================================== -void webUpdateTask(void* parameter) { - uint32_t lastStatusUpdate = 0; - uint32_t lastCanUpdate = 0; +void webUpdateTask(void *parameter) { + const TickType_t xDelay = pdMS_TO_TICKS(100); - for (;;) { + Serial.println("✓ Web Update Task 시작"); + + while (1) { webSocket.loop(); - uint32_t now = millis(); - - // 상태 업데이트 (500ms) - if (now - lastStatusUpdate >= 500) { - String status = "{\"type\":\"status\","; - status += "\"logging\":" + String(loggingEnabled ? "true" : "false") + ","; - status += "\"sdReady\":" + String(sdCardReady ? "true" : "false") + ","; - status += "\"queueSize\":" + String(uxQueueMessagesWaiting(canQueue)) + ","; - status += "\"queueMax\":" + String(CAN_QUEUE_SIZE) + ","; - status += "\"totalMsg\":" + String(totalMsgCount) + ","; - status += "\"msgPerSec\":" + String(msgPerSecond) + ","; - status += "\"fileSize\":" + String(currentFileSize) + ","; - status += "\"currentFile\":\""; + // CAN 데이터 전송 + if (webSocket.connectedClients() > 0) { + DynamicJsonDocument doc(4096); + doc["type"] = "update"; + doc["logging"] = loggingEnabled; + doc["serialLogging"] = serialLoggingEnabled; // Serial 로깅 상태 추가 + doc["sdReady"] = sdCardReady; + doc["totalMsg"] = totalMsgCount; + doc["msgPerSec"] = msgPerSecond; + doc["totalTx"] = totalTxCount; + doc["totalSerialRx"] = totalSerialRxCount; // Serial RX 카운터 추가 + doc["totalSerialTx"] = totalSerialTxCount; // Serial TX 카운터 추가 + doc["fileSize"] = currentFileSize; + doc["serialFileSize"] = currentSerialFileSize; // Serial 파일 크기 추가 + doc["queueUsed"] = uxQueueMessagesWaiting(canQueue); + doc["queueSize"] = CAN_QUEUE_SIZE; + doc["serialQueueUsed"] = uxQueueMessagesWaiting(serialQueue); // Serial Queue 사용량 추가 + doc["serialQueueSize"] = SERIAL_QUEUE_SIZE; + doc["timeSync"] = timeSyncStatus.synchronized; + doc["rtcAvail"] = timeSyncStatus.rtcAvailable; + doc["rtcSyncCnt"] = timeSyncStatus.rtcSyncCount; + doc["voltage"] = powerStatus.voltage; + doc["minVoltage"] = powerStatus.minVoltage; + doc["lowVoltage"] = powerStatus.lowVoltage; + doc["mcpMode"] = (int)currentMcpMode; - if (currentFilename[0] != '\0') { - String fname = String(currentFilename); - if (fname.startsWith("/")) fname = fname.substring(1); - status += fname; - } - - status += "\",\"timeSynced\":" + String(timeSyncStatus.synchronized ? "true" : "false") + ","; - status += "\"rtcAvailable\":" + String(timeSyncStatus.rtcAvailable ? "true" : "false") + ","; - status += "\"rtcSyncCount\":" + String(timeSyncStatus.rtcSyncCount) + ","; - status += "\"voltage\":" + String(powerStatus.voltage, 2) + ","; - status += "\"minVoltage\":" + String(powerStatus.minVoltage, 2) + ","; - status += "\"lowVoltage\":" + String(powerStatus.lowVoltage ? "true" : "false") + ","; - status += "\"mcpMode\":" + String(currentMcpMode); - status += "}"; - - webSocket.broadcastTXT(status); - lastStatusUpdate = now; - } - - // CAN 데이터 배치 전송 (100ms) - if (now - lastCanUpdate >= 100) { - String canBatch = "{\"type\":\"canBatch\",\"messages\":["; - int messageCount = 0; + 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) { - CANMessage* msg = &recentData[i].msg; + JsonObject msgObj = messages.createNestedObject(); + msgObj["id"] = recentData[i].msg.id; + msgObj["dlc"] = recentData[i].msg.dlc; + msgObj["count"] = recentData[i].count; - if (messageCount > 0) canBatch += ","; - - canBatch += "{\"id\":\"0x"; - canBatch += String(msg->id, HEX); - canBatch += "\",\"dlc\":" + String(msg->dlc); - canBatch += ",\"data\":\""; - - for (int j = 0; j < msg->dlc; j++) { - if (msg->data[j] < 0x10) canBatch += "0"; - canBatch += String(msg->data[j], HEX); - if (j < msg->dlc - 1) canBatch += " "; + JsonArray dataArray = msgObj.createNestedArray("data"); + for (int j = 0; j < recentData[i].msg.dlc; j++) { + dataArray.add(recentData[i].msg.data[j]); } - - uint64_t timestamp_ms = msg->timestamp_us / 1000; - canBatch += "\",\"timestamp\":" + String((uint32_t)timestamp_ms); - canBatch += ",\"count\":" + String(recentData[i].count) + "}"; - - messageCount++; } } - canBatch += "]}"; + // Serial 메시지 전송 (추가) + SerialMessage serialMsg; + JsonArray serialMessages = doc.createNestedArray("serialMessages"); + int serialCount = 0; - if (messageCount > 0) { - webSocket.broadcastTXT(canBatch); + while (serialCount < 5 && xQueueReceive(serialQueue, &serialMsg, 0) == pdTRUE) { // 10 → 5개로 축소 + JsonObject serMsgObj = serialMessages.createNestedObject(); + serMsgObj["timestamp"] = serialMsg.timestamp_us; + serMsgObj["isTx"] = serialMsg.isTx; + + // 데이터를 문자열로 변환 (printable characters) + char dataStr[MAX_SERIAL_LINE_LEN + 1]; + memcpy(dataStr, serialMsg.data, serialMsg.length); + dataStr[serialMsg.length] = '\0'; + serMsgObj["data"] = dataStr; + + serialCount++; } - lastCanUpdate = now; + String json; + serializeJson(doc, json); + webSocket.broadcastTXT(json); } - vTaskDelay(pdMS_TO_TICKS(50)); + vTaskDelay(xDelay); } } +// ======================================== +// Setup +// ======================================== + void setup() { Serial.begin(115200); delay(1000); - Serial.println("\n========================================"); - Serial.println(" ESP32 CAN Logger v2.0 "); - Serial.println(" + Phone Time Sync & File Comments "); - Serial.println(" + MCP2515 Mode Control "); - Serial.println("========================================"); - // 설정 로드 + Serial.println("\n========================================"); + Serial.println(" Byun CAN Logger + Serial Terminal"); + Serial.println(" Version 2.1"); + Serial.println("========================================\n"); + loadSettings(); - // 설정값 표시 - Serial.println("\n📋 현재 설정값:"); - Serial.println("----------------------------------------"); - Serial.printf(" WiFi SSID : %s\n", wifiSSID); - Serial.printf(" WiFi Password : %s\n", wifiPassword); - Serial.println("----------------------------------------"); Serial.println("💡 설정 변경: http://[IP]/settings\n"); memset(recentData, 0, sizeof(recentData)); @@ -1648,66 +1616,59 @@ void setup() { pinMode(CAN_INT_PIN, INPUT_PULLUP); - // ADC 설정 (전압 모니터링용) - analogSetAttenuation(ADC_11db); // 0-3.3V 범위 + // ADC 설정 + analogSetAttenuation(ADC_11db); // SPI 초기화 hspi.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS); vspi.begin(VSPI_SCLK, VSPI_MISO, VSPI_MOSI, VSPI_CS); - - // VSPI 클럭 속도를 40MHz로 설정 (SD 카드용) - vspi.setFrequency(40000000); // 40MHz + vspi.setFrequency(40000000); // MCP2515 초기화 mcp2515.reset(); - // 저장된 CAN 속도 적용 (loadSettings에서 로드됨) mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ); - // 저장된 MCP 모드 적용 (loadSettings에서 로드됨) setMCP2515Mode(currentMcpMode); - Serial.println("✓ MCP2515 초기화 완료 (저장된 설정 적용)"); + Serial.println("✓ MCP2515 초기화 완료"); - // Mutex 생성 (다른 초기화보다 먼저!) + // Serial 통신 초기화 (추가) + applySerialSettings(); + Serial.println("✓ Serial 통신 초기화 완료"); + + // Mutex 생성 sdMutex = xSemaphoreCreateMutex(); rtcMutex = xSemaphoreCreateMutex(); + serialMutex = xSemaphoreCreateMutex(); - if (sdMutex == NULL || rtcMutex == NULL) { + if (sdMutex == NULL || rtcMutex == NULL || serialMutex == NULL) { Serial.println("✗ Mutex 생성 실패!"); while (1) delay(1000); } - // RTC 초기화 (SoftWire 사용) - Mutex 생성 후 + // RTC 초기화 initRTC(); // SD 카드 초기화 if (SD.begin(VSPI_CS, vspi)) { sdCardReady = true; Serial.println("✓ SD 카드 초기화 완료"); - - // 파일 커멘트 로드 loadFileComments(); } else { Serial.println("✗ SD 카드 초기화 실패"); } - // WiFi 설정 - APSTA 모드 지원 + // WiFi 설정 if (enableSTAMode && strlen(staSSID) > 0) { - // APSTA 모드 (AP + Station 동시 동작) Serial.println("\n📶 WiFi APSTA 모드 시작..."); - WiFi.mode(WIFI_AP_STA); - - // AP 모드 시작 WiFi.softAP(wifiSSID, wifiPassword); Serial.print("✓ AP SSID: "); Serial.println(wifiSSID); Serial.print("✓ AP IP: "); Serial.println(WiFi.softAPIP()); - // Station 모드로 WiFi 연결 시도 Serial.printf("📡 WiFi 연결 시도: %s\n", staSSID); WiFi.begin(staSSID, staPassword); - // 연결 대기 (최대 10초) int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 20) { delay(500); @@ -1720,28 +1681,21 @@ void setup() { Serial.println("✓ WiFi 연결 성공!"); Serial.print("✓ STA IP: "); Serial.println(WiFi.localIP()); - Serial.print("✓ Gateway: "); - Serial.println(WiFi.gatewayIP()); - Serial.print("✓ DNS: "); - Serial.println(WiFi.dnsIP()); } else { Serial.println("✗ WiFi 연결 실패 (AP 모드는 정상 동작)"); } } else { - // AP 모드만 사용 Serial.println("\n📶 WiFi AP 모드 시작..."); WiFi.mode(WIFI_AP); WiFi.softAP(wifiSSID, wifiPassword); - Serial.print("✓ AP SSID: "); Serial.println(wifiSSID); Serial.print("✓ AP IP: "); Serial.println(WiFi.softAPIP()); } - // WiFi 성능 최적화 - WiFi.setSleep(false); // WiFi 절전 모드 비활성화 (신호 강도 개선) - esp_wifi_set_max_tx_power(84); // TX 출력 최대화 (20.5dBm = 84/4) + WiFi.setSleep(false); + esp_wifi_set_max_tx_power(84); // WebSocket 시작 webSocket.begin(); @@ -1768,6 +1722,10 @@ void setup() { server.send_P(200, "text/html", settings_html); }); + server.on("/serial", HTTP_GET, []() { // Serial 페이지 추가 + server.send_P(200, "text/html", serial_terminal_html); + }); + server.on("/download", HTTP_GET, []() { if (server.hasArg("file")) { String filename = "/" + server.arg("file"); @@ -1776,11 +1734,9 @@ void setup() { File file = SD.open(filename, FILE_READ); if (file) { String displayName = server.arg("file"); - server.sendHeader("Content-Disposition", "attachment; filename=\"" + displayName + "\""); server.sendHeader("Content-Type", "application/octet-stream"); - server.streamFile(file, "application/octet-stream"); file.close(); } else { @@ -1794,12 +1750,10 @@ void setup() { } }); - // 파일 삭제 HTTP 엔드포인트 추가 (백업용 - 주로 WebSocket 사용) 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); @@ -1838,8 +1792,9 @@ void setup() { // Queue 생성 canQueue = xQueueCreate(CAN_QUEUE_SIZE, sizeof(CANMessage)); + serialQueue = xQueueCreate(SERIAL_QUEUE_SIZE, sizeof(SerialMessage)); // Serial Queue 생성 - if (canQueue == NULL) { + if (canQueue == NULL || serialQueue == NULL) { Serial.println("✗ Queue 생성 실패!"); while (1) delay(1000); } @@ -1848,12 +1803,13 @@ void setup() { attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING); // Task 생성 - xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 4096, NULL, 5, &canRxTaskHandle, 1); // 우선순위 4→5 - xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 12288, NULL, 3, &sdWriteTaskHandle, 1); - xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 0); + xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 4096, NULL, 5, &canRxTaskHandle, 1); + xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", 3072, NULL, 4, &serialRxTaskHandle, 1); // 4096 → 3072 + xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 10240, NULL, 3, &sdWriteTaskHandle, 1); // 12288 → 10240 + xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 3072, NULL, 1, NULL, 0); // 4096 → 3072 xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 8192, NULL, 2, &webTaskHandle, 0); - xTaskCreatePinnedToCore(txTask, "TX_TASK", 4096, NULL, 2, NULL, 1); - xTaskCreatePinnedToCore(sequenceTask, "SEQ_TASK", 4096, NULL, 2, NULL, 1); // 시퀀스 Task 추가 + xTaskCreatePinnedToCore(txTask, "TX_TASK", 3072, NULL, 2, NULL, 1); // 4096 → 3072 + xTaskCreatePinnedToCore(sequenceTask, "SEQ_TASK", 3072, NULL, 2, NULL, 1); // 4096 → 3072 // RTC 동기화 Task if (timeSyncStatus.rtcAvailable) { @@ -1879,6 +1835,7 @@ void setup() { Serial.println(" - Transmit : /transmit"); Serial.println(" - Graph : /graph"); Serial.println(" - Settings : /settings"); + Serial.println(" - Serial : /serial ← NEW!"); Serial.println("========================================\n"); } @@ -1888,17 +1845,14 @@ void loop() { static uint32_t lastPrint = 0; if (millis() - lastPrint > 10000) { - Serial.printf("[상태] 큐: %d/%d | 로깅: %s | SD: %s | RX: %lu | TX: %lu | 파일크기: %u | 시간: %s | RTC: %s(%u) | 전압: %.2fV%s | 모드: %d\n", + 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", uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE, + uxQueueMessagesWaiting(serialQueue), SERIAL_QUEUE_SIZE, loggingEnabled ? "ON " : "OFF", + serialLoggingEnabled ? "ON " : "OFF", sdCardReady ? "OK" : "NO", totalMsgCount, totalTxCount, - currentFileSize, - timeSyncStatus.synchronized ? "OK" : "NO", - timeSyncStatus.rtcAvailable ? "OK" : "NO", - timeSyncStatus.rtcSyncCount, - powerStatus.voltage, - powerStatus.lowVoltage ? " ⚠️" : "", + totalSerialRxCount, totalSerialTxCount, currentMcpMode); lastPrint = millis(); } diff --git a/test_i2c_reset/index.h b/test_i2c_reset/index.h index 0909ef1..98bbf2c 100644 --- a/test_i2c_reset/index.h +++ b/test_i2c_reset/index.h @@ -605,6 +605,7 @@ const char index_html[] PROGMEM = R"rawliteral( 📈 Graph 📊 Graph View ⚙️ Settings + 📡 Serial