diff --git a/test_i2c_reset/ESP32_CAN_Logger.ino b/test_i2c_reset/ESP32_CAN_Logger.ino index ee6c226..f3e55d0 100644 --- a/test_i2c_reset/ESP32_CAN_Logger.ino +++ b/test_i2c_reset/ESP32_CAN_Logger.ino @@ -1,842 +1,975 @@ -/* - * Byun CAN Logger with Web Interface + Time Synchronization - * Version: 1.2 - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "index.h" -#include "transmit.h" -#include "graph.h" -#include "graph_viewer.h" - - -// GPIO 핀 정의 -#define CAN_INT_PIN 27 - -// HSPI 핀 (CAN) -#define HSPI_MISO 12 -#define HSPI_MOSI 13 -#define HSPI_SCLK 14 -#define HSPI_CS 15 - -// VSPI 핀 (SD Card) -#define VSPI_MISO 19 -#define VSPI_MOSI 23 -#define VSPI_SCLK 18 -#define VSPI_CS 5 - -// 버퍼 설정 -#define CAN_QUEUE_SIZE 1000 -#define FILE_BUFFER_SIZE 8192 -#define MAX_FILENAME_LEN 64 -#define RECENT_MSG_COUNT 100 -#define MAX_TX_MESSAGES 20 - -// CAN 메시지 구조체 - 마이크로초 단위 타임스탬프 -struct CANMessage { - uint64_t timestamp_us; // 마이크로초 단위 Unix timestamp - uint32_t id; - uint8_t dlc; - uint8_t data[8]; -} __attribute__((packed)); - -// 실시간 모니터링용 구조체 -struct RecentCANData { - CANMessage msg; - uint32_t count; -}; - -// CAN 송신용 구조체 -struct TxMessage { - uint32_t id; - bool extended; - uint8_t dlc; - uint8_t data[8]; - uint32_t interval; - uint32_t lastSent; - bool active; -}; - -// 시간 동기화 상태 -struct TimeSyncStatus { - bool synchronized; - uint64_t lastSyncTime; - int32_t offsetUs; - uint32_t syncCount; -} timeSyncStatus = {false, 0, 0, 0}; - -// WiFi AP 설정 -const char* ssid = "Byun_CAN_Logger"; -const char* password = "12345678"; - -// 전역 변수 -SPIClass hspi(HSPI); -SPIClass vspi(VSPI); -MCP2515 mcp2515(HSPI_CS, 10000000, &hspi); - -WebServer server(80); -WebSocketsServer webSocket = WebSocketsServer(81); - -QueueHandle_t canQueue; -SemaphoreHandle_t sdMutex; -TaskHandle_t canRxTaskHandle = NULL; -TaskHandle_t sdWriteTaskHandle = NULL; -TaskHandle_t webTaskHandle = NULL; - -volatile bool loggingEnabled = false; -volatile bool sdCardReady = false; -File logFile; -char currentFilename[MAX_FILENAME_LEN]; -uint8_t fileBuffer[FILE_BUFFER_SIZE]; -uint16_t bufferIndex = 0; - -// 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; - -// CAN 송신용 -TxMessage txMessages[MAX_TX_MESSAGES]; -uint32_t totalTxCount = 0; - -// 정밀한 현재 시간 가져오기 (마이크로초) -uint64_t getMicrosecondTimestamp() { - struct timeval tv; - gettimeofday(&tv, NULL); - return (uint64_t)tv.tv_sec * 1000000ULL + (uint64_t)tv.tv_usec; -} - -// 시간 동기화 설정 -void setSystemTime(uint64_t timestampMs) { - struct timeval tv; - tv.tv_sec = timestampMs / 1000; - tv.tv_usec = (timestampMs % 1000) * 1000; - settimeofday(&tv, NULL); - - timeSyncStatus.synchronized = true; - timeSyncStatus.lastSyncTime = getMicrosecondTimestamp(); - timeSyncStatus.syncCount++; - - // 현재 시간 출력 - time_t now = tv.tv_sec; - struct tm timeinfo; - localtime_r(&now, &timeinfo); - char timeStr[64]; - strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo); - - Serial.printf("⏰ 시간 동기화 완료: %s.%03d (동기화 횟수: %u)\n", - timeStr, (int)(tv.tv_usec / 1000), timeSyncStatus.syncCount); -} - -// 함수 선언 -void changeCanSpeed(CAN_SPEED newSpeed); -bool createNewLogFile(); -bool flushBuffer(); -void startLogging(); -void stopLogging(); -void canRxTask(void *pvParameters); -void sdWriteTask(void *pvParameters); -void sdMonitorTask(void *pvParameters); -void sendFileList(uint8_t clientNum); -void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length); -void handleCanTransmit(String msg); -void handleStartMessage(String msg); -void handleStopMessage(String msg); -void handleTimeSync(String msg); -void webUpdateTask(void *pvParameters); - -// CAN 인터럽트 핸들러 -void IRAM_ATTR canISR() { - BaseType_t xHigherPriorityTaskWoken = pdFALSE; - if (canRxTaskHandle != NULL) { - vTaskNotifyGiveFromISR(canRxTaskHandle, &xHigherPriorityTaskWoken); - portYIELD_FROM_ISR(xHigherPriorityTaskWoken); - } -} - -// CAN 속도 변경 -void changeCanSpeed(CAN_SPEED newSpeed) { - detachInterrupt(digitalPinToInterrupt(CAN_INT_PIN)); - mcp2515.reset(); - mcp2515.setBitrate(newSpeed, MCP_8MHZ); - mcp2515.setNormalMode(); - currentCanSpeed = newSpeed; - attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING); - Serial.printf("CAN 속도 변경: %s\n", canSpeedNames[newSpeed]); -} - -// 새 로그 파일 생성 - 시간 기반 파일명 -bool createNewLogFile() { - if (logFile) { - logFile.flush(); - logFile.close(); - vTaskDelay(pdMS_TO_TICKS(10)); - } - - // 현재 시간으로 파일명 생성 - time_t now; - struct tm timeinfo; - time(&now); - localtime_r(&now, &timeinfo); - - char filename[MAX_FILENAME_LEN]; - snprintf(filename, MAX_FILENAME_LEN, "/canlog_%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(filename, FILE_WRITE); - - if (!logFile) { - Serial.printf("파일 생성 실패: %s\n", filename); - return false; - } - - strncpy(currentFilename, filename, MAX_FILENAME_LEN); - Serial.printf("새 로그 파일 생성: %s\n", currentFilename); - - // 시간 동기화 경고 - if (!timeSyncStatus.synchronized) { - Serial.println("⚠️ 경고: 시간이 동기화되지 않았습니다! 웹페이지에서 시간 동기화를 실행하세요."); - } - - return true; -} - -// 버퍼 플러시 -bool flushBuffer() { - if (bufferIndex == 0) return true; - - if (xSemaphoreTake(sdMutex, portMAX_DELAY) == pdTRUE) { - if (logFile) { - size_t written = logFile.write(fileBuffer, bufferIndex); - logFile.flush(); - xSemaphoreGive(sdMutex); - - if (written != bufferIndex) { - Serial.println("SD 쓰기 오류!"); - return false; - } - bufferIndex = 0; - return true; - } - xSemaphoreGive(sdMutex); - } - return false; -} - -// 로깅 시작 -void startLogging() { - if (loggingEnabled) { - Serial.println("이미 로깅 중"); - return; - } - - if (!sdCardReady) { - Serial.println("SD 카드가 준비되지 않음"); - return; - } - - Serial.println("로깅 시작"); - - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - if (createNewLogFile()) { - loggingEnabled = true; - bufferIndex = 0; - } - xSemaphoreGive(sdMutex); - } -} - -// 로깅 중지 -void stopLogging() { - if (!loggingEnabled) { - Serial.println("로깅이 실행 중이 아님"); - return; - } - - Serial.println("로깅 정지"); - loggingEnabled = false; - - flushBuffer(); - - if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { - if (logFile) { - logFile.close(); - } - xSemaphoreGive(sdMutex); - } -} - -// CAN 수신 태스크 -void canRxTask(void *pvParameters) { - struct can_frame frame; - CANMessage msg; - - Serial.println("CAN 수신 태스크 시작"); - - while (1) { - ulTaskNotifyTake(pdTRUE, portMAX_DELAY); - - while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) { - // 마이크로초 단위 타임스탬프 - msg.timestamp_us = getMicrosecondTimestamp(); - msg.id = frame.can_id; - msg.dlc = frame.can_dlc; - memcpy(msg.data, frame.data, 8); - - if (xQueueSend(canQueue, &msg, 0) != pdTRUE) { - static uint32_t lastWarning = 0; - if (millis() - lastWarning > 1000) { - Serial.println("경고: CAN 큐 오버플로우!"); - lastWarning = millis(); - } - } - - // 최근 메시지 저장 및 카운트 증가 - bool found = false; - for (int i = 0; i < RECENT_MSG_COUNT; i++) { - if (recentData[i].msg.id == msg.id && recentData[i].msg.timestamp_us > 0) { - recentData[i].msg = msg; - recentData[i].count++; - found = true; - break; - } - } - - if (!found) { - for (int i = 0; i < RECENT_MSG_COUNT; i++) { - if (recentData[i].msg.timestamp_us == 0) { - recentData[i].msg = msg; - recentData[i].count = 1; - found = true; - break; - } - } - - if (!found) { - static int replaceIndex = 0; - recentData[replaceIndex].msg = msg; - recentData[replaceIndex].count = 1; - replaceIndex = (replaceIndex + 1) % RECENT_MSG_COUNT; - } - } - - totalMsgCount++; - } - } -} - -// SD 쓰기 태스크 -void sdWriteTask(void *pvParameters) { - CANMessage msg; - - Serial.println("SD 쓰기 태스크 시작"); - - while (1) { - if (xQueueReceive(canQueue, &msg, pdMS_TO_TICKS(100)) == pdTRUE) { - - if (loggingEnabled && sdCardReady) { - if (bufferIndex + sizeof(CANMessage) > FILE_BUFFER_SIZE) { - if (!flushBuffer()) { - continue; - } - } - - memcpy(&fileBuffer[bufferIndex], &msg, sizeof(CANMessage)); - bufferIndex += sizeof(CANMessage); - } - } else { - if (loggingEnabled && bufferIndex > 0) { - flushBuffer(); - } - } - } -} - -// SD 모니터 태스크 -void sdMonitorTask(void *pvParameters) { - Serial.println("SD 모니터 태스크 시작"); - - while (1) { - bool cardPresent = SD.begin(VSPI_CS, vspi); - - if (cardPresent != sdCardReady) { - sdCardReady = cardPresent; - - if (sdCardReady) { - Serial.println("SD 카드 준비됨"); - } else { - Serial.println("SD 카드 없음"); - if (loggingEnabled) { - stopLogging(); - } - } - } - - vTaskDelay(pdMS_TO_TICKS(1000)); - } -} - -// 파일 목록 전송 -void sendFileList(uint8_t clientNum) { - String fileList = "{\"type\":\"files\",\"files\":["; - - if (!sdCardReady) { - fileList += "],\"error\":\"SD card not ready\"}"; - webSocket.sendTXT(clientNum, fileList); - return; - } - - File root = SD.open("/"); - if (!root) { - fileList += "],\"error\":\"Cannot open root directory\"}"; - webSocket.sendTXT(clientNum, fileList); - return; - } - - File file = root.openNextFile(); - bool first = true; - int fileCount = 0; - - while (file) { - if (!file.isDirectory()) { - String name = file.name(); - if (name.startsWith("/")) name = name.substring(1); - - if (name.endsWith(".bin") || name.endsWith(".BIN")) { - if (!first) fileList += ","; - fileList += "{\"name\":\"" + name + "\",\"size\":" + String(file.size()) + "}"; - first = false; - fileCount++; - } - } - file.close(); - file = root.openNextFile(); - } - root.close(); - - fileList += "]}"; - webSocket.sendTXT(clientNum, fileList); - - Serial.printf("파일 목록 전송: %d개\n", fileCount); -} - -// CAN 메시지 전송 처리 -void handleCanTransmit(String msg) { - int idIdx = msg.indexOf("\"id\":\"") + 6; - int idEnd = msg.indexOf("\"", idIdx); - String idStr = msg.substring(idIdx, idEnd); - - int typeIdx = msg.indexOf("\"type\":\"") + 8; - String typeStr = msg.substring(typeIdx, typeIdx + 3); - bool extended = (typeStr == "ext"); - - int dlcIdx = msg.indexOf("\"dlc\":") + 6; - int dlc = msg.substring(dlcIdx, dlcIdx + 1).toInt(); - - int dataIdx = msg.indexOf("\"data\":\"") + 8; - String dataStr = msg.substring(dataIdx, dataIdx + 16); - - uint32_t canId = strtoul(idStr.c_str(), NULL, 16); - - uint8_t data[8] = {0}; - for (int i = 0; i < dlc && i < 8; i++) { - String byteStr = dataStr.substring(i * 2, i * 2 + 2); - data[i] = strtoul(byteStr.c_str(), NULL, 16); - } - - struct can_frame frame; - frame.can_id = canId; - if (extended) frame.can_id |= CAN_EFF_FLAG; - frame.can_dlc = dlc; - memcpy(frame.data, data, 8); - - if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) { - totalTxCount++; - Serial.printf("CAN TX: 0x%X [%d]\n", canId, dlc); - } -} - -// 주기 전송 시작 -void handleStartMessage(String msg) { - int indexIdx = msg.indexOf("\"index\":") + 8; - int index = msg.substring(indexIdx, indexIdx + 2).toInt(); - - if (index >= 0 && index < MAX_TX_MESSAGES) { - int idIdx = msg.indexOf("\"id\":\"") + 6; - int idEnd = msg.indexOf("\"", idIdx); - String idStr = msg.substring(idIdx, idEnd); - - int typeIdx = msg.indexOf("\"type\":\"") + 8; - String typeStr = msg.substring(typeIdx, typeIdx + 3); - - int dlcIdx = msg.indexOf("\"dlc\":") + 6; - int dlc = msg.substring(dlcIdx, dlcIdx + 1).toInt(); - - int dataIdx = msg.indexOf("\"data\":\"") + 8; - String dataStr = msg.substring(dataIdx, dataIdx + 16); - - int intervalIdx = msg.indexOf("\"interval\":") + 11; - int interval = msg.substring(intervalIdx, intervalIdx + 5).toInt(); - - txMessages[index].id = strtoul(idStr.c_str(), NULL, 16); - txMessages[index].extended = (typeStr == "ext"); - txMessages[index].dlc = dlc; - - for (int i = 0; i < 8; i++) { - String byteStr = dataStr.substring(i * 2, i * 2 + 2); - txMessages[index].data[i] = strtoul(byteStr.c_str(), NULL, 16); - } - - txMessages[index].interval = interval; - txMessages[index].lastSent = 0; - txMessages[index].active = true; - - Serial.printf("주기 전송 시작 [%d]: 0x%X\n", index, txMessages[index].id); - } -} - -// 주기 전송 중지 -void handleStopMessage(String msg) { - int indexIdx = msg.indexOf("\"index\":") + 8; - int index = msg.substring(indexIdx, indexIdx + 2).toInt(); - - if (index >= 0 && index < MAX_TX_MESSAGES) { - txMessages[index].active = false; - Serial.printf("주기 전송 중지 [%d]\n", index); - } -} - -// 시간 동기화 처리 -void handleTimeSync(String msg) { - int timestampIdx = msg.indexOf("\"timestamp\":") + 12; - String timestampStr = msg.substring(timestampIdx); - timestampStr = timestampStr.substring(0, timestampStr.indexOf("}")); - - uint64_t clientTimestamp = strtoull(timestampStr.c_str(), NULL, 10); - - if (clientTimestamp > 0) { - setSystemTime(clientTimestamp); - } -} - -// 웹소켓 이벤트 핸들러 -void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { - switch(type) { - case WStype_DISCONNECTED: - Serial.printf("WebSocket #%u 연결 해제\n", num); - break; - - case WStype_CONNECTED: - { - IPAddress ip = webSocket.remoteIP(num); - Serial.printf("WebSocket #%u 연결: %d.%d.%d.%d\n", - num, ip[0], ip[1], ip[2], ip[3]); - sendFileList(num); - - // 시간 동기화 상태 전송 - String syncStatus = "{\"type\":\"timeSyncStatus\",\"synchronized\":"; - syncStatus += timeSyncStatus.synchronized ? "true" : "false"; - syncStatus += ",\"syncCount\":" + String(timeSyncStatus.syncCount) + "}"; - webSocket.sendTXT(num, syncStatus); - } - break; - - case WStype_TEXT: - { - String msg = String((char*)payload); - - if (msg.indexOf("\"cmd\":\"setSpeed\"") >= 0) { - int speedIdx = msg.indexOf("\"speed\":") + 8; - int speed = msg.substring(speedIdx, speedIdx + 1).toInt(); - - if (speed >= 0 && speed < 4) { - changeCanSpeed(canSpeedValues[speed]); - } - } - else if (msg.indexOf("\"cmd\":\"getFiles\"") >= 0) { - sendFileList(num); - } - else if (msg.indexOf("\"cmd\":\"startLogging\"") >= 0) { - startLogging(); - } - else if (msg.indexOf("\"cmd\":\"stopLogging\"") >= 0) { - stopLogging(); - } - else if (msg.indexOf("\"cmd\":\"sendCan\"") >= 0) { - handleCanTransmit(msg); - } - else if (msg.indexOf("\"cmd\":\"startMsg\"") >= 0) { - handleStartMessage(msg); - } - else if (msg.indexOf("\"cmd\":\"stopMsg\"") >= 0) { - handleStopMessage(msg); - } - else if (msg.indexOf("\"cmd\":\"stopAll\"") >= 0) { - for (int i = 0; i < MAX_TX_MESSAGES; i++) { - txMessages[i].active = false; - } - } - else if (msg.indexOf("\"cmd\":\"syncTime\"") >= 0) { - handleTimeSync(msg); - } - } - break; - } -} - -// 웹 업데이트 태스크 -void webUpdateTask(void *pvParameters) { - uint32_t lastStatusUpdate = 0; - uint32_t lastCanUpdate = 0; - uint32_t lastTxStatusUpdate = 0; - const uint32_t CAN_UPDATE_INTERVAL = 500; - - Serial.println("웹 업데이트 태스크 시작"); - - while (1) { - uint32_t now = millis(); - - webSocket.loop(); - - // 주기 전송 처리 - for (int i = 0; i < MAX_TX_MESSAGES; i++) { - if (txMessages[i].active && (now - txMessages[i].lastSent >= txMessages[i].interval)) { - struct can_frame frame; - frame.can_id = txMessages[i].id; - if (txMessages[i].extended) frame.can_id |= CAN_EFF_FLAG; - frame.can_dlc = txMessages[i].dlc; - memcpy(frame.data, txMessages[i].data, 8); - - if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) { - totalTxCount++; - txMessages[i].lastSent = now; - } - } - } - - // TX 상태 업데이트 - if (now - lastTxStatusUpdate >= 1000) { - String txStatus = "{\"type\":\"txStatus\",\"count\":" + String(totalTxCount) + "}"; - webSocket.broadcastTXT(txStatus); - lastTxStatusUpdate = now; - } - - // 상태 업데이트 - if (now - lastStatusUpdate >= 1000) { - if (now - lastMsgCountTime >= 1000) { - msgPerSecond = totalMsgCount - lastMsgCount; - lastMsgCount = totalMsgCount; - lastMsgCountTime = now; - } - - String status = "{\"type\":\"status\","; - status += "\"logging\":" + String(loggingEnabled ? "true" : "false") + ","; - status += "\"sdReady\":" + String(sdCardReady ? "true" : "false") + ","; - status += "\"msgCount\":" + String(totalMsgCount) + ","; - status += "\"msgSpeed\":" + String(msgPerSecond) + ","; - status += "\"timeSync\":" + String(timeSyncStatus.synchronized ? "true" : "false") + ","; - status += "\"syncCount\":" + String(timeSyncStatus.syncCount) + ","; - - if (loggingEnabled && logFile) { - status += "\"currentFile\":\"" + String(currentFilename) + "\""; - } else { - status += "\"currentFile\":\"\""; - } - status += "}"; - - webSocket.broadcastTXT(status); - lastStatusUpdate = now; - } - - // CAN 메시지 일괄 업데이트 - if (now - lastCanUpdate >= CAN_UPDATE_INTERVAL) { - String canBatch = "{\"type\":\"canBatch\",\"messages\":["; - bool first = true; - - for (int i = 0; i < RECENT_MSG_COUNT; i++) { - if (recentData[i].msg.timestamp_us > 0) { - CANMessage* msg = &recentData[i].msg; - - if (!first) canBatch += ","; - first = false; - - canBatch += "{\"id\":\""; - if (msg->id < 0x10) canBatch += "0"; - if (msg->id < 0x100) canBatch += "0"; - if (msg->id < 0x1000) canBatch += "0"; - 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 += " "; - } - - // 마이크로초 타임스탬프를 밀리초로 변환하여 전송 - uint64_t timestamp_ms = msg->timestamp_us / 1000; - canBatch += "\",\"timestamp\":" + String((uint32_t)timestamp_ms); - canBatch += ",\"count\":" + String(recentData[i].count) + "}"; - } - } - - canBatch += "]}"; - - if (!first) { - webSocket.broadcastTXT(canBatch); - } - - lastCanUpdate = now; - } - - vTaskDelay(pdMS_TO_TICKS(50)); - } -} - -void setup() { - Serial.begin(115200); - delay(1000); - Serial.println("\n========================================"); - Serial.println(" ESP32 CAN Logger with Time Sync "); - Serial.println("========================================"); - - memset(recentData, 0, sizeof(recentData)); - memset(txMessages, 0, sizeof(txMessages)); - - pinMode(CAN_INT_PIN, INPUT_PULLUP); - - hspi.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS); - vspi.begin(VSPI_SCLK, VSPI_MISO, VSPI_MOSI, VSPI_CS); - - mcp2515.reset(); - mcp2515.setBitrate(CAN_1000KBPS, MCP_8MHZ); - mcp2515.setNormalMode(); - Serial.println("✓ MCP2515 초기화 완료"); - - if (SD.begin(VSPI_CS, vspi)) { - sdCardReady = true; - Serial.println("✓ SD 카드 초기화 완료"); - } else { - Serial.println("✗ SD 카드 초기화 실패"); - } - - WiFi.softAP(ssid, password); - Serial.print("✓ AP IP: "); - Serial.println(WiFi.softAPIP()); - - webSocket.begin(); - webSocket.onEvent(webSocketEvent); - - server.on("/", HTTP_GET, []() { - server.send_P(200, "text/html", index_html); - }); - - server.on("/transmit", HTTP_GET, []() { - server.send_P(200, "text/html", transmit_html); - }); - - server.on("/graph", HTTP_GET, []() { - server.send_P(200, "text/html", graph_html); - }); - - server.on("/graph-view", HTTP_GET, []() { - server.send_P(200, "text/html", graph_viewer_html); - }); - - server.on("/download", HTTP_GET, []() { - if (server.hasArg("file")) { - String filename = "/" + server.arg("file"); - - if (SD.exists(filename)) { - File file = SD.open(filename, FILE_READ); - if (file) { - String displayName = server.arg("file"); - - server.sendHeader("Content-Disposition", - "attachment; filename=\"" + displayName + "\""); - server.sendHeader("Content-Type", "application/octet-stream"); - - server.streamFile(file, "application/octet-stream"); - file.close(); - } else { - server.send(500, "text/plain", "Failed to open file"); - } - } else { - server.send(404, "text/plain", "File not found"); - } - } else { - server.send(400, "text/plain", "Bad request"); - } - }); - server.begin(); - - canQueue = xQueueCreate(CAN_QUEUE_SIZE, sizeof(CANMessage)); - sdMutex = xSemaphoreCreateMutex(); - - if (canQueue == NULL || sdMutex == NULL) { - Serial.println("✗ RTOS 객체 생성 실패!"); - while (1) delay(1000); - } - - attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING); - - xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 4096, NULL, 4, &canRxTaskHandle, 1); - xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 12288, NULL, 3, &sdWriteTaskHandle, 1); - xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 0); - xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 8192, NULL, 2, &webTaskHandle, 0); - - Serial.println("✓ 모든 태스크 시작 완료"); - Serial.println("\n========================================"); - Serial.println(" 웹 인터페이스 접속"); - Serial.println("========================================"); - Serial.println(" 1. WiFi: Byun_CAN_Logger (12345678)"); - Serial.print(" 2. http://"); - Serial.println(WiFi.softAPIP()); - Serial.println(" 3. Pages:"); - Serial.println(" - Monitor: /"); - Serial.println(" - Transmit: /transmit"); - Serial.println(" - Graph: /graph"); - Serial.println("========================================\n"); - Serial.println("⚠️ 시간 동기화를 위해 웹페이지에서 '⏰ 시간 동기화' 버튼을 클릭하세요"); -} - -void loop() { - server.handleClient(); - vTaskDelay(pdMS_TO_TICKS(10)); - - static uint32_t lastPrint = 0; - if (millis() - lastPrint > 10000) { - Serial.printf("[상태] 큐: %d/%d | 로깅: %s | SD: %s | RX: %lu | TX: %lu | 시간동기: %s\n", - uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE, - loggingEnabled ? "ON " : "OFF", - sdCardReady ? "OK" : "NO", - totalMsgCount, totalTxCount, - timeSyncStatus.synchronized ? "OK" : "NO"); - lastPrint = millis(); - } +/* + * Byun CAN Logger with Web Interface + RTC DS3231 + * Version: 1.3 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "index.h" +#include "transmit.h" +#include "graph.h" +#include "graph_viewer.h" + + +// GPIO 핀 정의 +#define CAN_INT_PIN 27 + +// HSPI 핀 (CAN) +#define HSPI_MISO 12 +#define HSPI_MOSI 13 +#define HSPI_SCLK 14 +#define HSPI_CS 15 + +// VSPI 핀 (SD Card) +#define VSPI_MISO 19 +#define VSPI_MOSI 23 +#define VSPI_SCLK 18 +#define VSPI_CS 5 + +// I2C 핀 (RTC DS3231) +#define I2C_SDA 25 +#define I2C_SCL 26 + +// 버퍼 설정 +#define CAN_QUEUE_SIZE 1000 +#define FILE_BUFFER_SIZE 8192 +#define MAX_FILENAME_LEN 64 +#define RECENT_MSG_COUNT 100 +#define MAX_TX_MESSAGES 20 + +// CAN 메시지 구조체 - 마이크로초 단위 타임스탬프 +struct CANMessage { + uint64_t timestamp_us; // 마이크로초 단위 Unix timestamp + uint32_t id; + uint8_t dlc; + uint8_t data[8]; +} __attribute__((packed)); + +// 실시간 모니터링용 구조체 +struct RecentCANData { + CANMessage msg; + uint32_t count; +}; + +// CAN 송신용 구조체 +struct TxMessage { + uint32_t id; + bool extended; + uint8_t dlc; + uint8_t data[8]; + uint32_t interval; + uint32_t lastSent; + bool active; +}; + +// 시간 동기화 상태 +struct TimeSyncStatus { + bool synchronized; + uint64_t lastSyncTime; + int32_t offsetUs; + uint32_t syncCount; + bool rtcAvailable; +} timeSyncStatus = {false, 0, 0, 0, false}; + +// WiFi AP 설정 +const char* ssid = "Byun_CAN_Logger"; +const char* password = "12345678"; + +// 전역 변수 +SPIClass hspi(HSPI); +SPIClass vspi(VSPI); +MCP2515 mcp2515(HSPI_CS, 10000000, &hspi); +RTC_DS3231 rtc; + +WebServer server(80); +WebSocketsServer webSocket = WebSocketsServer(81); + +QueueHandle_t canQueue; +SemaphoreHandle_t sdMutex; +TaskHandle_t canRxTaskHandle = NULL; +TaskHandle_t sdWriteTaskHandle = NULL; +TaskHandle_t webTaskHandle = NULL; + +volatile bool loggingEnabled = false; +volatile bool sdCardReady = false; +File logFile; +char currentFilename[MAX_FILENAME_LEN]; +uint8_t fileBuffer[FILE_BUFFER_SIZE]; +uint16_t bufferIndex = 0; + +// 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; + +// CAN 송신용 +TxMessage txMessages[MAX_TX_MESSAGES]; +uint32_t totalTxCount = 0; + +// 정밀한 현재 시간 가져오기 (마이크로초) +uint64_t getMicrosecondTimestamp() { + struct timeval tv; + gettimeofday(&tv, NULL); + return (uint64_t)tv.tv_sec * 1000000ULL + (uint64_t)tv.tv_usec; +} + +// RTC에서 시간 읽기 +String getRTCTime() { + if (!timeSyncStatus.rtcAvailable) { + return "RTC not available"; + } + + DateTime now = rtc.now(); + char timeStr[32]; + snprintf(timeStr, sizeof(timeStr), "%04d-%02d-%02d %02d:%02d:%02d", + now.year(), now.month(), now.day(), + now.hour(), now.minute(), now.second()); + return String(timeStr); +} + +// RTC에 시간 설정 +bool setRTCTime(uint64_t timestampMs) { + if (!timeSyncStatus.rtcAvailable) { + Serial.println("❌ RTC not available"); + return false; + } + + time_t timestamp = timestampMs / 1000; + DateTime dt(timestamp); + rtc.adjust(dt); + + Serial.printf("⏰ RTC 시간 설정: %s\n", getRTCTime().c_str()); + return true; +} + +// RTC에서 시스템 시간 동기화 +bool syncTimeFromRTC() { + if (!timeSyncStatus.rtcAvailable) { + Serial.println("❌ RTC not available"); + return false; + } + + DateTime now = rtc.now(); + struct timeval tv; + tv.tv_sec = now.unixtime(); + tv.tv_usec = 0; + settimeofday(&tv, NULL); + + timeSyncStatus.synchronized = true; + timeSyncStatus.lastSyncTime = getMicrosecondTimestamp(); + timeSyncStatus.syncCount++; + + char timeStr[64]; + strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", localtime(&tv.tv_sec)); + + Serial.printf("⏰ RTC로부터 시간 동기화: %s (동기화 횟수: %u)\n", + timeStr, timeSyncStatus.syncCount); + return true; +} + +// 시간 동기화 설정 (웹에서) +void setSystemTime(uint64_t timestampMs) { + struct timeval tv; + tv.tv_sec = timestampMs / 1000; + tv.tv_usec = (timestampMs % 1000) * 1000; + settimeofday(&tv, NULL); + + timeSyncStatus.synchronized = true; + timeSyncStatus.lastSyncTime = getMicrosecondTimestamp(); + timeSyncStatus.syncCount++; + + // 현재 시간 출력 + time_t now = tv.tv_sec; + struct tm timeinfo; + localtime_r(&now, &timeinfo); + char timeStr[64]; + strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo); + + Serial.printf("⏰ 웹에서 시간 동기화: %s.%03d (동기화 횟수: %u)\n", + timeStr, (int)(tv.tv_usec / 1000), timeSyncStatus.syncCount); +} + +// 함수 선언 +void changeCanSpeed(CAN_SPEED newSpeed); +bool createNewLogFile(); +bool flushBuffer(); +void startLogging(); +void stopLogging(); +void canRxTask(void *pvParameters); +void sdWriteTask(void *pvParameters); +void sdMonitorTask(void *pvParameters); +void sendFileList(uint8_t clientNum); +void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length); +void handleCanTransmit(String msg); +void handleStartMessage(String msg); +void handleStopMessage(String msg); +void handleTimeSync(String msg); +void handleRTCGet(uint8_t clientNum); +void handleRTCSet(String msg); +void webUpdateTask(void *pvParameters); + +// CAN 인터럽트 핸들러 +void IRAM_ATTR canISR() { + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + if (canRxTaskHandle != NULL) { + vTaskNotifyGiveFromISR(canRxTaskHandle, &xHigherPriorityTaskWoken); + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); + } +} + +// CAN 속도 변경 +void changeCanSpeed(CAN_SPEED newSpeed) { + detachInterrupt(digitalPinToInterrupt(CAN_INT_PIN)); + mcp2515.reset(); + mcp2515.setBitrate(newSpeed, MCP_8MHZ); + mcp2515.setNormalMode(); + currentCanSpeed = newSpeed; + attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING); + Serial.printf("CAN 속도 변경: %s\n", canSpeedNames[newSpeed]); +} + +// 새 로그 파일 생성 - 시간 기반 파일명 +bool createNewLogFile() { + if (logFile) { + logFile.flush(); + logFile.close(); + vTaskDelay(pdMS_TO_TICKS(10)); + } + + // 현재 시간으로 파일명 생성 + time_t now; + struct tm timeinfo; + time(&now); + localtime_r(&now, &timeinfo); + + char filename[MAX_FILENAME_LEN]; + snprintf(filename, MAX_FILENAME_LEN, "/canlog_%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(filename, FILE_WRITE); + + if (!logFile) { + Serial.printf("파일 생성 실패: %s\n", filename); + return false; + } + + strncpy(currentFilename, filename, MAX_FILENAME_LEN); + Serial.printf("새 로그 파일 생성: %s\n", currentFilename); + + // 시간 동기화 경고 + if (!timeSyncStatus.synchronized) { + Serial.println("⚠️ 경고: 시간이 동기화되지 않았습니다! 웹페이지에서 시간 동기화를 실행하세요."); + } + + return true; +} + +// 버퍼 플러시 +bool flushBuffer() { + if (bufferIndex == 0) return true; + + if (xSemaphoreTake(sdMutex, portMAX_DELAY) == pdTRUE) { + if (logFile) { + size_t written = logFile.write(fileBuffer, bufferIndex); + logFile.flush(); + xSemaphoreGive(sdMutex); + + if (written != bufferIndex) { + Serial.println("SD 쓰기 오류!"); + return false; + } + bufferIndex = 0; + return true; + } + xSemaphoreGive(sdMutex); + } + return false; +} + +// 로깅 시작 +void startLogging() { + if (loggingEnabled) { + Serial.println("이미 로깅 중"); + return; + } + + if (!sdCardReady) { + Serial.println("SD 카드가 준비되지 않음"); + return; + } + + Serial.println("로깅 시작"); + + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { + if (createNewLogFile()) { + loggingEnabled = true; + bufferIndex = 0; + } + xSemaphoreGive(sdMutex); + } +} + +// 로깅 중지 +void stopLogging() { + if (!loggingEnabled) { + Serial.println("로깅이 실행 중이 아님"); + return; + } + + Serial.println("로깅 정지"); + loggingEnabled = false; + + flushBuffer(); + + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { + if (logFile) { + logFile.close(); + } + xSemaphoreGive(sdMutex); + } +} + +// CAN 수신 태스크 +void canRxTask(void *pvParameters) { + struct can_frame frame; + CANMessage msg; + + Serial.println("CAN 수신 태스크 시작"); + + while (1) { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) { + // 마이크로초 단위 타임스탬프 + msg.timestamp_us = getMicrosecondTimestamp(); + msg.id = frame.can_id; + msg.dlc = frame.can_dlc; + memcpy(msg.data, frame.data, 8); + + if (xQueueSend(canQueue, &msg, 0) != pdTRUE) { + static uint32_t lastWarning = 0; + if (millis() - lastWarning > 1000) { + Serial.println("경고: CAN 큐 오버플로우!"); + lastWarning = millis(); + } + } + + // 최근 메시지 저장 및 카운트 증가 + bool found = false; + for (int i = 0; i < RECENT_MSG_COUNT; i++) { + if (recentData[i].msg.id == msg.id && recentData[i].msg.timestamp_us > 0) { + recentData[i].msg = msg; + recentData[i].count++; + found = true; + break; + } + } + + if (!found) { + for (int i = 0; i < RECENT_MSG_COUNT; i++) { + if (recentData[i].msg.timestamp_us == 0) { + recentData[i].msg = msg; + recentData[i].count = 1; + found = true; + break; + } + } + + if (!found) { + static int replaceIndex = 0; + recentData[replaceIndex].msg = msg; + recentData[replaceIndex].count = 1; + replaceIndex = (replaceIndex + 1) % RECENT_MSG_COUNT; + } + } + + totalMsgCount++; + } + } +} + +// SD 쓰기 태스크 +void sdWriteTask(void *pvParameters) { + CANMessage msg; + + Serial.println("SD 쓰기 태스크 시작"); + + while (1) { + if (xQueueReceive(canQueue, &msg, pdMS_TO_TICKS(100)) == pdTRUE) { + + if (loggingEnabled && sdCardReady) { + if (bufferIndex + sizeof(CANMessage) > FILE_BUFFER_SIZE) { + if (!flushBuffer()) { + continue; + } + } + + memcpy(&fileBuffer[bufferIndex], &msg, sizeof(CANMessage)); + bufferIndex += sizeof(CANMessage); + } + } else { + if (loggingEnabled && bufferIndex > 0) { + flushBuffer(); + } + } + } +} + +// SD 모니터 태스크 +void sdMonitorTask(void *pvParameters) { + Serial.println("SD 모니터 태스크 시작"); + + while (1) { + bool cardPresent = SD.begin(VSPI_CS, vspi); + + if (cardPresent != sdCardReady) { + sdCardReady = cardPresent; + + if (sdCardReady) { + Serial.println("SD 카드 준비됨"); + } else { + Serial.println("SD 카드 없음"); + if (loggingEnabled) { + stopLogging(); + } + } + } + + vTaskDelay(pdMS_TO_TICKS(1000)); + } +} + +// 파일 목록 전송 +void sendFileList(uint8_t clientNum) { + String fileList = "{\"type\":\"files\",\"files\":["; + + if (!sdCardReady) { + fileList += "],\"error\":\"SD card not ready\"}"; + webSocket.sendTXT(clientNum, fileList); + return; + } + + File root = SD.open("/"); + if (!root) { + fileList += "],\"error\":\"Cannot open root directory\"}"; + webSocket.sendTXT(clientNum, fileList); + return; + } + + File file = root.openNextFile(); + bool first = true; + int fileCount = 0; + + while (file) { + if (!file.isDirectory()) { + String name = file.name(); + if (name.startsWith("/")) name = name.substring(1); + + if (name.endsWith(".bin") || name.endsWith(".BIN")) { + if (!first) fileList += ","; + fileList += "{\"name\":\"" + name + "\",\"size\":" + String(file.size()) + "}"; + first = false; + fileCount++; + } + } + file.close(); + file = root.openNextFile(); + } + root.close(); + + fileList += "]}"; + webSocket.sendTXT(clientNum, fileList); + + Serial.printf("파일 목록 전송: %d개\n", fileCount); +} + +// CAN 메시지 전송 처리 +void handleCanTransmit(String msg) { + int idIdx = msg.indexOf("\"id\":\"") + 6; + int idEnd = msg.indexOf("\"", idIdx); + String idStr = msg.substring(idIdx, idEnd); + + int typeIdx = msg.indexOf("\"type\":\"") + 8; + String typeStr = msg.substring(typeIdx, typeIdx + 3); + bool extended = (typeStr == "ext"); + + int dlcIdx = msg.indexOf("\"dlc\":") + 6; + int dlc = msg.substring(dlcIdx, dlcIdx + 1).toInt(); + + int dataIdx = msg.indexOf("\"data\":\"") + 8; + String dataStr = msg.substring(dataIdx, dataIdx + 16); + + uint32_t canId = strtoul(idStr.c_str(), NULL, 16); + + uint8_t data[8] = {0}; + for (int i = 0; i < dlc && i < 8; i++) { + String byteStr = dataStr.substring(i * 2, i * 2 + 2); + data[i] = strtoul(byteStr.c_str(), NULL, 16); + } + + struct can_frame frame; + frame.can_id = canId; + if (extended) frame.can_id |= CAN_EFF_FLAG; + frame.can_dlc = dlc; + memcpy(frame.data, data, 8); + + if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) { + totalTxCount++; + Serial.printf("CAN TX: 0x%X [%d]\n", canId, dlc); + } +} + +// 주기 전송 시작 +void handleStartMessage(String msg) { + int indexIdx = msg.indexOf("\"index\":") + 8; + int index = msg.substring(indexIdx, indexIdx + 2).toInt(); + + if (index >= 0 && index < MAX_TX_MESSAGES) { + int idIdx = msg.indexOf("\"id\":\"") + 6; + int idEnd = msg.indexOf("\"", idIdx); + String idStr = msg.substring(idIdx, idEnd); + + int typeIdx = msg.indexOf("\"type\":\"") + 8; + String typeStr = msg.substring(typeIdx, typeIdx + 3); + + int dlcIdx = msg.indexOf("\"dlc\":") + 6; + int dlc = msg.substring(dlcIdx, dlcIdx + 1).toInt(); + + int dataIdx = msg.indexOf("\"data\":\"") + 8; + String dataStr = msg.substring(dataIdx, dataIdx + 16); + + int intervalIdx = msg.indexOf("\"interval\":") + 11; + int interval = msg.substring(intervalIdx, intervalIdx + 5).toInt(); + + txMessages[index].id = strtoul(idStr.c_str(), NULL, 16); + txMessages[index].extended = (typeStr == "ext"); + txMessages[index].dlc = dlc; + + for (int i = 0; i < 8; i++) { + String byteStr = dataStr.substring(i * 2, i * 2 + 2); + txMessages[index].data[i] = strtoul(byteStr.c_str(), NULL, 16); + } + + txMessages[index].interval = interval; + txMessages[index].lastSent = 0; + txMessages[index].active = true; + + Serial.printf("주기 전송 시작 [%d]: 0x%X\n", index, txMessages[index].id); + } +} + +// 주기 전송 중지 +void handleStopMessage(String msg) { + int indexIdx = msg.indexOf("\"index\":") + 8; + int index = msg.substring(indexIdx, indexIdx + 2).toInt(); + + if (index >= 0 && index < MAX_TX_MESSAGES) { + txMessages[index].active = false; + Serial.printf("주기 전송 중지 [%d]\n", index); + } +} + +// 시간 동기화 처리 (웹에서) +void handleTimeSync(String msg) { + int timestampIdx = msg.indexOf("\"timestamp\":") + 12; + String timestampStr = msg.substring(timestampIdx); + timestampStr = timestampStr.substring(0, timestampStr.indexOf("}")); + + uint64_t clientTimestamp = strtoull(timestampStr.c_str(), NULL, 10); + + if (clientTimestamp > 0) { + setSystemTime(clientTimestamp); + } +} + +// RTC 시간 읽기 +void handleRTCGet(uint8_t clientNum) { + String response = "{\"type\":\"rtcTime\",\"time\":\""; + response += getRTCTime(); + response += "\",\"available\":"; + response += timeSyncStatus.rtcAvailable ? "true" : "false"; + response += "}"; + + webSocket.sendTXT(clientNum, response); +} + +// RTC 시간 설정 +void handleRTCSet(String msg) { + int timestampIdx = msg.indexOf("\"timestamp\":") + 12; + String timestampStr = msg.substring(timestampIdx); + timestampStr = timestampStr.substring(0, timestampStr.indexOf("}")); + + uint64_t timestamp = strtoull(timestampStr.c_str(), NULL, 10); + + if (timestamp > 0) { + setRTCTime(timestamp); + // RTC 설정 후 시스템 시간도 동기화 + syncTimeFromRTC(); + } +} + +// 웹소켓 이벤트 핸들러 +void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { + switch(type) { + case WStype_DISCONNECTED: + Serial.printf("WebSocket #%u 연결 해제\n", num); + break; + + case WStype_CONNECTED: + { + IPAddress ip = webSocket.remoteIP(num); + Serial.printf("WebSocket #%u 연결: %d.%d.%d.%d\n", + num, ip[0], ip[1], ip[2], ip[3]); + sendFileList(num); + + // 시간 동기화 상태 전송 + String syncStatus = "{\"type\":\"timeSyncStatus\",\"synchronized\":"; + syncStatus += timeSyncStatus.synchronized ? "true" : "false"; + syncStatus += ",\"syncCount\":" + String(timeSyncStatus.syncCount); + syncStatus += ",\"rtcAvailable\":"; + syncStatus += timeSyncStatus.rtcAvailable ? "true" : "false"; + syncStatus += "}"; + webSocket.sendTXT(num, syncStatus); + + // RTC 시간 전송 + handleRTCGet(num); + } + break; + + case WStype_TEXT: + { + String msg = String((char*)payload); + + if (msg.indexOf("\"cmd\":\"setSpeed\"") >= 0) { + int speedIdx = msg.indexOf("\"speed\":") + 8; + int speed = msg.substring(speedIdx, speedIdx + 1).toInt(); + + if (speed >= 0 && speed < 4) { + changeCanSpeed(canSpeedValues[speed]); + } + } + else if (msg.indexOf("\"cmd\":\"getFiles\"") >= 0) { + sendFileList(num); + } + else if (msg.indexOf("\"cmd\":\"startLogging\"") >= 0) { + startLogging(); + } + else if (msg.indexOf("\"cmd\":\"stopLogging\"") >= 0) { + stopLogging(); + } + else if (msg.indexOf("\"cmd\":\"sendCan\"") >= 0) { + handleCanTransmit(msg); + } + else if (msg.indexOf("\"cmd\":\"startMsg\"") >= 0) { + handleStartMessage(msg); + } + else if (msg.indexOf("\"cmd\":\"stopMsg\"") >= 0) { + handleStopMessage(msg); + } + else if (msg.indexOf("\"cmd\":\"stopAll\"") >= 0) { + for (int i = 0; i < MAX_TX_MESSAGES; i++) { + txMessages[i].active = false; + } + } + else if (msg.indexOf("\"cmd\":\"syncTime\"") >= 0) { + handleTimeSync(msg); + } + else if (msg.indexOf("\"cmd\":\"getRTC\"") >= 0) { + handleRTCGet(num); + } + else if (msg.indexOf("\"cmd\":\"setRTC\"") >= 0) { + handleRTCSet(msg); + } + else if (msg.indexOf("\"cmd\":\"syncFromRTC\"") >= 0) { + syncTimeFromRTC(); + } + } + break; + } +} + +// 웹 업데이트 태스크 +void webUpdateTask(void *pvParameters) { + uint32_t lastStatusUpdate = 0; + uint32_t lastCanUpdate = 0; + uint32_t lastTxStatusUpdate = 0; + const uint32_t CAN_UPDATE_INTERVAL = 500; + + Serial.println("웹 업데이트 태스크 시작"); + + while (1) { + uint32_t now = millis(); + + webSocket.loop(); + + // 주기 전송 처리 + for (int i = 0; i < MAX_TX_MESSAGES; i++) { + if (txMessages[i].active && (now - txMessages[i].lastSent >= txMessages[i].interval)) { + struct can_frame frame; + frame.can_id = txMessages[i].id; + if (txMessages[i].extended) frame.can_id |= CAN_EFF_FLAG; + frame.can_dlc = txMessages[i].dlc; + memcpy(frame.data, txMessages[i].data, 8); + + if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) { + totalTxCount++; + txMessages[i].lastSent = now; + } + } + } + + // TX 상태 업데이트 + if (now - lastTxStatusUpdate >= 1000) { + String txStatus = "{\"type\":\"txStatus\",\"count\":" + String(totalTxCount) + "}"; + webSocket.broadcastTXT(txStatus); + lastTxStatusUpdate = now; + } + + // 상태 업데이트 + if (now - lastStatusUpdate >= 1000) { + if (now - lastMsgCountTime >= 1000) { + msgPerSecond = totalMsgCount - lastMsgCount; + lastMsgCount = totalMsgCount; + lastMsgCountTime = now; + } + + String status = "{\"type\":\"status\","; + status += "\"logging\":" + String(loggingEnabled ? "true" : "false") + ","; + status += "\"sdReady\":" + String(sdCardReady ? "true" : "false") + ","; + status += "\"msgCount\":" + String(totalMsgCount) + ","; + status += "\"msgSpeed\":" + String(msgPerSecond) + ","; + status += "\"timeSync\":" + String(timeSyncStatus.synchronized ? "true" : "false") + ","; + status += "\"syncCount\":" + String(timeSyncStatus.syncCount) + ","; + status += "\"rtcAvailable\":" + String(timeSyncStatus.rtcAvailable ? "true" : "false") + ","; + + if (loggingEnabled && logFile) { + status += "\"currentFile\":\"" + String(currentFilename) + "\""; + } else { + status += "\"currentFile\":\"\""; + } + status += "}"; + + webSocket.broadcastTXT(status); + lastStatusUpdate = now; + } + + // CAN 메시지 일괄 업데이트 + if (now - lastCanUpdate >= CAN_UPDATE_INTERVAL) { + String canBatch = "{\"type\":\"canBatch\",\"messages\":["; + bool first = true; + + for (int i = 0; i < RECENT_MSG_COUNT; i++) { + if (recentData[i].msg.timestamp_us > 0) { + CANMessage* msg = &recentData[i].msg; + + if (!first) canBatch += ","; + first = false; + + canBatch += "{\"id\":\""; + if (msg->id < 0x10) canBatch += "0"; + if (msg->id < 0x100) canBatch += "0"; + if (msg->id < 0x1000) canBatch += "0"; + 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 += " "; + } + + // 마이크로초 타임스탬프를 밀리초로 변환하여 전송 + uint64_t timestamp_ms = msg->timestamp_us / 1000; + canBatch += "\",\"timestamp\":" + String((uint32_t)timestamp_ms); + canBatch += ",\"count\":" + String(recentData[i].count) + "}"; + } + } + + canBatch += "]}"; + + if (!first) { + webSocket.broadcastTXT(canBatch); + } + + lastCanUpdate = now; + } + + vTaskDelay(pdMS_TO_TICKS(50)); + } +} + +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println("\n========================================"); + Serial.println(" ESP32 CAN Logger with RTC DS3231 "); + Serial.println("========================================"); + + memset(recentData, 0, sizeof(recentData)); + memset(txMessages, 0, sizeof(txMessages)); + + // I2C 초기화 (RTC) + Wire.begin(I2C_SDA, I2C_SCL); + + // RTC 초기화 + if (rtc.begin(&Wire)) { + Serial.println("✓ RTC DS3231 초기화 완료"); + timeSyncStatus.rtcAvailable = true; + + // RTC 손실 확인 + if (rtc.lostPower()) { + Serial.println("⚠️ RTC 전원 손실 감지 - 시간 설정 필요"); + } else { + Serial.printf("✓ RTC 시간: %s\n", getRTCTime().c_str()); + // RTC에서 시스템 시간 동기화 + syncTimeFromRTC(); + } + } else { + Serial.println("✗ RTC DS3231 초기화 실패"); + timeSyncStatus.rtcAvailable = false; + } + + pinMode(CAN_INT_PIN, INPUT_PULLUP); + + hspi.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS); + vspi.begin(VSPI_SCLK, VSPI_MISO, VSPI_MOSI, VSPI_CS); + + mcp2515.reset(); + mcp2515.setBitrate(CAN_1000KBPS, MCP_8MHZ); + mcp2515.setNormalMode(); + Serial.println("✓ MCP2515 초기화 완료"); + + if (SD.begin(VSPI_CS, vspi)) { + sdCardReady = true; + Serial.println("✓ SD 카드 초기화 완료"); + } else { + Serial.println("✗ SD 카드 초기화 실패"); + } + + WiFi.softAP(ssid, password); + Serial.print("✓ AP IP: "); + Serial.println(WiFi.softAPIP()); + + webSocket.begin(); + webSocket.onEvent(webSocketEvent); + + server.on("/", HTTP_GET, []() { + server.send_P(200, "text/html", index_html); + }); + + server.on("/transmit", HTTP_GET, []() { + server.send_P(200, "text/html", transmit_html); + }); + + server.on("/graph", HTTP_GET, []() { + server.send_P(200, "text/html", graph_html); + }); + + server.on("/graph-view", HTTP_GET, []() { + server.send_P(200, "text/html", graph_viewer_html); + }); + + server.on("/download", HTTP_GET, []() { + if (server.hasArg("file")) { + String filename = "/" + server.arg("file"); + + if (SD.exists(filename)) { + File file = SD.open(filename, FILE_READ); + if (file) { + String displayName = server.arg("file"); + + server.sendHeader("Content-Disposition", + "attachment; filename=\"" + displayName + "\""); + server.sendHeader("Content-Type", "application/octet-stream"); + + server.streamFile(file, "application/octet-stream"); + file.close(); + } else { + server.send(500, "text/plain", "Failed to open file"); + } + } else { + server.send(404, "text/plain", "File not found"); + } + } else { + server.send(400, "text/plain", "Bad request"); + } + }); + server.begin(); + + canQueue = xQueueCreate(CAN_QUEUE_SIZE, sizeof(CANMessage)); + sdMutex = xSemaphoreCreateMutex(); + + if (canQueue == NULL || sdMutex == NULL) { + Serial.println("✗ RTOS 객체 생성 실패!"); + while (1) delay(1000); + } + + attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING); + + xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 4096, NULL, 4, &canRxTaskHandle, 1); + xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 12288, NULL, 3, &sdWriteTaskHandle, 1); + xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 0); + xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 8192, NULL, 2, &webTaskHandle, 0); + + Serial.println("✓ 모든 태스크 시작 완료"); + Serial.println("\n========================================"); + Serial.println(" 웹 인터페이스 접속"); + Serial.println("========================================"); + Serial.println(" 1. WiFi: Byun_CAN_Logger (12345678)"); + Serial.print(" 2. http://"); + Serial.println(WiFi.softAPIP()); + Serial.println(" 3. Pages:"); + Serial.println(" - Monitor: /"); + Serial.println(" - Transmit: /transmit"); + Serial.println(" - Graph: /graph"); + Serial.println("========================================\n"); + + if (timeSyncStatus.rtcAvailable) { + Serial.println("✓ RTC 사용 가능 - 웹페이지에서 RTC 시간 관리 가능"); + } else { + Serial.println("⚠️ RTC 사용 불가 - 웹페이지에서 시간 동기화 필요"); + } +} + +void loop() { + server.handleClient(); + vTaskDelay(pdMS_TO_TICKS(10)); + + static uint32_t lastPrint = 0; + if (millis() - lastPrint > 10000) { + Serial.printf("[상태] 큐: %d/%d | 로깅: %s | SD: %s | RX: %lu | TX: %lu | 시간동기: %s | RTC: %s\n", + uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE, + loggingEnabled ? "ON " : "OFF", + sdCardReady ? "OK" : "NO", + totalMsgCount, totalTxCount, + timeSyncStatus.synchronized ? "OK" : "NO", + timeSyncStatus.rtcAvailable ? "OK" : "NO"); + lastPrint = millis(); + } } \ No newline at end of file