From 8e497c515da30a52d84d69a27d0689f13aefc6fc Mon Sep 17 00:00:00 2001 From: byun Date: Sun, 12 Oct 2025 16:46:31 +0000 Subject: [PATCH] test --- test_i2c_reset/ESP32_CAN_Logger.ino | 842 ++++++++++++++++++++++++++++ test_i2c_reset/graph.h | 762 +++++++++++++++++++++++++ test_i2c_reset/graph_viewer.h | 559 ++++++++++++++++++ test_i2c_reset/index.h | 709 +++++++++++++++++++++++ test_i2c_reset/transmit.h | 772 +++++++++++++++++++++++++ 5 files changed, 3644 insertions(+) create mode 100644 test_i2c_reset/ESP32_CAN_Logger.ino create mode 100644 test_i2c_reset/graph.h create mode 100644 test_i2c_reset/graph_viewer.h create mode 100644 test_i2c_reset/index.h create mode 100644 test_i2c_reset/transmit.h diff --git a/test_i2c_reset/ESP32_CAN_Logger.ino b/test_i2c_reset/ESP32_CAN_Logger.ino new file mode 100644 index 0000000..ee6c226 --- /dev/null +++ b/test_i2c_reset/ESP32_CAN_Logger.ino @@ -0,0 +1,842 @@ +/* + * 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(); + } +} \ No newline at end of file diff --git a/test_i2c_reset/graph.h b/test_i2c_reset/graph.h new file mode 100644 index 0000000..3decd7c --- /dev/null +++ b/test_i2c_reset/graph.h @@ -0,0 +1,762 @@ +#ifndef GRAPH_H +#define GRAPH_H + +const char graph_html[] PROGMEM = R"rawliteral( + + + + + + CAN Signal Graph + + + +
+
+

CAN Signal Graph

+

Real-time Signal Visualization (Offline Mode)

+
+ + + +
+ + +

Upload DBC File

+
+
+ +

Click to upload DBC

+

No file loaded

+
+
+ + +
+
+ + + + +)rawliteral"; + +#endif \ No newline at end of file diff --git a/test_i2c_reset/graph_viewer.h b/test_i2c_reset/graph_viewer.h new file mode 100644 index 0000000..adfaa4b --- /dev/null +++ b/test_i2c_reset/graph_viewer.h @@ -0,0 +1,559 @@ +#ifndef GRAPH_VIEWER_H +#define GRAPH_VIEWER_H + +const char graph_viewer_html[] PROGMEM = R"rawliteral( + + + + + + CAN Signal Graph Viewer + + + +
+

Real-time CAN Signal Graphs (Scatter Mode)

+
+ +
+
+ + + +
+
+ X-Axis Scale: + + +
+
+ X-Axis Range: + + +
+
+ +
Connecting...
+ +
+ + + + +)rawliteral"; + +#endif \ No newline at end of file diff --git a/test_i2c_reset/index.h b/test_i2c_reset/index.h new file mode 100644 index 0000000..33c42e6 --- /dev/null +++ b/test_i2c_reset/index.h @@ -0,0 +1,709 @@ +#ifndef INDEX_H +#define INDEX_H + +const char index_html[] PROGMEM = R"rawliteral( + + + + + + Byun CAN Logger + + + +
+
+

Byun CAN Logger

+

Real-time CAN Bus Monitor & Data Logger with Time Sync

+
+ + + +
+
+
+
+ ⏰ 시간 동기화 상태 + 대기 중... +
+
+ 🕐 현재 시간 + --:--:-- +
+
+ +
+ +
+
+

LOGGING

+
OFF
+
+
+

SD CARD

+
NOT READY
+
+
+

MESSAGES

+
0
+
+
+

SPEED

+
0/s
+
+
+

TIME SYNC

+
0
+
+
+

CURRENT FILE

+
-
+
+
+ +
+

Control Panel

+
+ + + + +
+
+ + + + +
+
+ +

CAN Messages (by ID)

+
+ + + + + + + + + + + +
IDDLCDataCountTime(ms)
+
+ +

Log Files

+
+

Loading...

+
+
+
+ + + + +)rawliteral"; + +#endif \ No newline at end of file diff --git a/test_i2c_reset/transmit.h b/test_i2c_reset/transmit.h new file mode 100644 index 0000000..72c88b5 --- /dev/null +++ b/test_i2c_reset/transmit.h @@ -0,0 +1,772 @@ +#ifndef TRANSMIT_H +#define TRANSMIT_H + +const char transmit_html[] PROGMEM = R"rawliteral( + + + + + + CAN Transmitter + + + +
+
+

CAN Transmitter

+

Send CAN Messages

+
+ + + +
+
+ Disconnected + Sent: 0 +
+ +

Message List Presets

+
+
+ + +
+
+

No saved presets

+
+
+ +

Add CAN Message

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + + Delay between messages when using "Send All Once" button + +
+ +
+ +
+ + + + + + + + +
+
+ +
+ + +
+
+ +

Message List

+
+ + + + +
+ +
+

No messages added yet

+
+
+
+ + + + +)rawliteral"; + +#endif \ No newline at end of file