commit 3e8982c7564fe381974d8d1f86b7e516178c8e0a Author: byun Date: Sat Oct 4 16:48:47 2025 +0000 Upload files to "/" diff --git a/ESP32_CAN_Logger.ino b/ESP32_CAN_Logger.ino new file mode 100644 index 0000000..2013f47 --- /dev/null +++ b/ESP32_CAN_Logger.ino @@ -0,0 +1,663 @@ +/* + * ESP32 CAN Logger with Web Interface + * + * Features: + * - CAN Bus logging to SD card (binary format) + * - WiFi AP with web interface + * - Real-time CAN message monitoring (by ID, BUSMASTER style) + * - CAN speed adjustment (125K - 1M) + * - Log file download + * - Batch update every 0.5s for real-time performance + * + * Hardware: + * - ESP32 WROVER Module (ESP32-WROOM-32D) + * - MCP2515 CAN Controller (HSPI, INT: GPIO27) + * - SD Card (VSPI) + * - GPIO17: Logging control (HIGH=start, LOW=stop) + * - GPIO16: Logging status LED + * - GPIO26: SD card ready LED + * + * Required Libraries: + * - WebSockets by Markus Sattler + * - mcp2515 by autowp + * + * Author: ESP32 CAN Logger Project + * Version: 1.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "index.h" // HTML 페이지 + +// ==================== GPIO 핀 정의 ==================== +#define CAN_INT_PIN 27 +#define LOGGING_CONTROL_PIN 17 +#define LOGGING_STATUS_LED 16 +#define SD_READY_LED 26 + +// 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 32 +#define WEB_UPDATE_INTERVAL 100 + +// ==================== CAN 메시지 구조체 ==================== +struct CANMessage { + uint32_t timestamp; + uint32_t id; + uint8_t dlc; + uint8_t data[8]; +} __attribute__((packed)); + +// ==================== WiFi AP 설정 ==================== +const char* ssid = "ESP32_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 controlTaskHandle = 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; +uint32_t fileCounter = 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}; + +// 실시간 모니터링용 +#define RECENT_MSG_COUNT 50 +CANMessage recentMessages[RECENT_MSG_COUNT]; +uint8_t recentMsgIndex = 0; +uint32_t totalMsgCount = 0; +uint32_t msgPerSecond = 0; +uint32_t lastMsgCountTime = 0; +uint32_t lastMsgCount = 0; + +// ==================== 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]); +} + +// ==================== 기존 파일 번호 스캔 ==================== +void scanExistingFiles() { + if (!sdCardReady) { + fileCounter = 0; + Serial.println("SD 카드 없음 - 파일 카운터 0으로 시작"); + return; + } + + uint32_t maxFileNumber = 0; + bool foundFiles = false; + + File root = SD.open("/"); + if (!root) { + Serial.println("루트 디렉토리 열기 실패"); + fileCounter = 0; + return; + } + + File file = root.openNextFile(); + while (file) { + if (!file.isDirectory()) { + String name = file.name(); + if (name.startsWith("/")) name = name.substring(1); + + // canlog_XXXXX.bin 형식 확인 + if (name.startsWith("canlog_") && (name.endsWith(".bin") || name.endsWith(".BIN"))) { + // 숫자 부분 추출 (canlog_00005.bin -> 5) + int startIdx = 7; // "canlog_" 길이 + int endIdx = name.lastIndexOf('.'); + + if (endIdx > startIdx) { + String numStr = name.substring(startIdx, endIdx); + uint32_t fileNum = numStr.toInt(); + + if (fileNum > maxFileNumber) { + maxFileNumber = fileNum; + } + foundFiles = true; + } + } + } + file.close(); + file = root.openNextFile(); + } + root.close(); + + if (foundFiles) { + fileCounter = maxFileNumber + 1; + Serial.printf("기존 파일 발견 - 다음 파일 번호: %lu (canlog_%05lu.bin)\n", + fileCounter, fileCounter); + } else { + fileCounter = 0; + Serial.println("기존 파일 없음 - 파일 카운터 0으로 시작"); + } +} +bool createNewLogFile() { + if (logFile) { + logFile.flush(); + logFile.close(); + vTaskDelay(pdMS_TO_TICKS(10)); + } + + char filename[MAX_FILENAME_LEN]; + snprintf(filename, MAX_FILENAME_LEN, "/canlog_%05lu.bin", fileCounter++); + + 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); + 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; +} + +// ==================== 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 = millis(); + 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(); + } + } + + recentMessages[recentMsgIndex] = msg; + recentMsgIndex = (recentMsgIndex + 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()) { + digitalWrite(LOGGING_STATUS_LED, LOW); + continue; + } + } + + memcpy(&fileBuffer[bufferIndex], &msg, sizeof(CANMessage)); + bufferIndex += sizeof(CANMessage); + + digitalWrite(LOGGING_STATUS_LED, HIGH); + } + } else { + if (loggingEnabled && bufferIndex > 0) { + flushBuffer(); + } + } + } +} + +// ==================== 제어 태스크 ==================== +void controlTask(void *pvParameters) { + bool lastLoggingState = false; + + Serial.println("제어 태스크 시작"); + + while (1) { + bool currentState = digitalRead(LOGGING_CONTROL_PIN); + + if (currentState != lastLoggingState) { + if (currentState == HIGH && sdCardReady) { + Serial.println("로깅 시작"); + + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { + if (createNewLogFile()) { + loggingEnabled = true; + bufferIndex = 0; + } + xSemaphoreGive(sdMutex); + } + } else if (currentState == LOW && loggingEnabled) { + Serial.println("로깅 정지"); + loggingEnabled = false; + + flushBuffer(); + + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { + if (logFile) { + logFile.close(); + } + xSemaphoreGive(sdMutex); + } + + digitalWrite(LOGGING_STATUS_LED, LOW); + } + + lastLoggingState = currentState; + } + + vTaskDelay(pdMS_TO_TICKS(50)); + } +} + +// ==================== SD 모니터 태스크 ==================== +void sdMonitorTask(void *pvParameters) { + Serial.println("SD 모니터 태스크 시작"); + + while (1) { + bool cardPresent = SD.begin(VSPI_CS, vspi); + + if (cardPresent != sdCardReady) { + sdCardReady = cardPresent; + digitalWrite(SD_READY_LED, sdCardReady ? HIGH : LOW); + + if (sdCardReady) { + Serial.println("SD 카드 준비됨"); + // SD 카드가 새로 삽입되면 파일 번호 다시 스캔 + scanExistingFiles(); + } else { + Serial.println("SD 카드 없음"); + if (loggingEnabled) { + loggingEnabled = false; + digitalWrite(LOGGING_STATUS_LED, LOW); + } + } + } + + vTaskDelay(pdMS_TO_TICKS(1000)); + } +} + +// ==================== 웹소켓 이벤트 핸들러 ==================== +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); + } + 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); + } + } + break; + } +} + +// ==================== 파일 목록 전송 함수 ==================== +void sendFileList(uint8_t clientNum) { + String fileList = "{\"type\":\"files\",\"files\":["; + + if (!sdCardReady) { + fileList += "],\"error\":\"SD card not ready\"}"; + webSocket.sendTXT(clientNum, fileList); + Serial.println("파일 목록 요청: SD 카드 없음"); + return; + } + + File root = SD.open("/"); + if (!root) { + fileList += "],\"error\":\"Cannot open root directory\"}"; + webSocket.sendTXT(clientNum, fileList); + Serial.println("파일 목록 요청: 루트 디렉토리 열기 실패"); + 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); +} + +// ==================== 웹 업데이트 태스크 (0.5초 일괄 전송) ==================== +void webUpdateTask(void *pvParameters) { + uint32_t lastStatusUpdate = 0; + uint32_t lastCanUpdate = 0; + const uint32_t CAN_UPDATE_INTERVAL = 500; // 0.5초마다 CAN 데이터 전송 + + Serial.println("웹 업데이트 태스크 시작"); + + while (1) { + uint32_t now = millis(); + + webSocket.loop(); + + // 상태 업데이트 (1초마다) + 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) + "}"; + + webSocket.broadcastTXT(status); + lastStatusUpdate = now; + } + + // CAN 메시지 일괄 업데이트 (0.5초마다) + if (now - lastCanUpdate >= CAN_UPDATE_INTERVAL) { + String canBatch = "{\"type\":\"canBatch\",\"messages\":["; + bool first = true; + + // 모든 최근 메시지를 JSON 배열로 구성 + for (int i = 0; i < RECENT_MSG_COUNT; i++) { + if (recentMessages[i].timestamp > 0) { + CANMessage* msg = &recentMessages[i]; + + 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 += " "; + } + + canBatch += "\",\"timestamp\":" + String(msg->timestamp) + "}"; + } + } + + canBatch += "]}"; + + if (!first) { // 메시지가 있을 때만 전송 + webSocket.broadcastTXT(canBatch); + } + + lastCanUpdate = now; + } + + vTaskDelay(pdMS_TO_TICKS(50)); + } +} + +// ==================== SETUP ==================== +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println("\n\n"); + Serial.println("========================================"); + Serial.println(" ESP32 CAN Logger with Web Interface "); + Serial.println("========================================"); + + // GPIO 초기화 + pinMode(LOGGING_CONTROL_PIN, INPUT); + pinMode(LOGGING_STATUS_LED, OUTPUT); + pinMode(SD_READY_LED, OUTPUT); + pinMode(CAN_INT_PIN, INPUT_PULLUP); + + digitalWrite(LOGGING_STATUS_LED, LOW); + digitalWrite(SD_READY_LED, LOW); + + // SPI 초기화 + hspi.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS); + vspi.begin(VSPI_SCLK, VSPI_MISO, VSPI_MOSI, VSPI_CS); + + // MCP2515 초기화 + mcp2515.reset(); + mcp2515.setBitrate(CAN_1000KBPS, MCP_8MHZ); + mcp2515.setNormalMode(); + Serial.println("✓ MCP2515 초기화 완료 (1Mbps)"); + + // SD 카드 초기화 + if (SD.begin(VSPI_CS, vspi)) { + sdCardReady = true; + digitalWrite(SD_READY_LED, HIGH); + Serial.println("✓ SD 카드 초기화 완료"); + + // 기존 파일 번호 스캔 + scanExistingFiles(); + } else { + Serial.println("✗ SD 카드 초기화 실패"); + fileCounter = 0; + } + + // WiFi AP 시작 + Serial.println("\nWiFi AP 시작..."); + WiFi.softAP(ssid, password); + Serial.print("✓ AP IP 주소: "); + Serial.println(WiFi.softAPIP()); + + // 웹소켓 설정 + webSocket.begin(); + webSocket.onEvent(webSocketEvent); + Serial.println("✓ WebSocket 서버 시작 (포트 81)"); + + // HTTP 핸들러 + server.on("/", HTTP_GET, []() { + server.send_P(200, "text/html", index_html); + }); + + // 파일 다운로드 핸들러 + server.on("/download", HTTP_GET, []() { + if (server.hasArg("file")) { + String filename = "/" + server.arg("file"); + + if (SD.exists(filename)) { + File file = SD.open(filename, FILE_READ); + if (file) { + server.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(); + Serial.println("✓ 웹 서버 시작 (포트 80)"); + + // RTOS 객체 생성 + canQueue = xQueueCreate(CAN_QUEUE_SIZE, sizeof(CANMessage)); + sdMutex = xSemaphoreCreateMutex(); + + if (canQueue == NULL || sdMutex == NULL) { + Serial.println("✗ RTOS 객체 생성 실패!"); + while (1) delay(1000); + } + Serial.println("✓ RTOS 객체 생성 완료"); + + // 인터럽트 설정 + 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(controlTask, "CONTROL", 8192, NULL, 2, &controlTaskHandle, 0); + xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 0); + xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 8192, NULL, 2, &webTaskHandle, 0); + + Serial.println("✓ 모든 RTOS 태스크 시작 완료"); + + Serial.println("\n========================================"); + Serial.println(" GPIO 핀 설정"); + Serial.println("========================================"); + Serial.println(" GPIO17: 로깅 제어 (HIGH=시작, LOW=정지)"); + Serial.println(" GPIO16: 로깅 상태 LED"); + Serial.println(" GPIO26: SD 카드 준비 LED"); + Serial.println(" GPIO27: CAN 인터럽트"); + + Serial.println("\n========================================"); + Serial.println(" 웹 인터페이스 접속 방법"); + Serial.println("========================================"); + Serial.println(" 1. WiFi 연결: ESP32_CAN_Logger"); + Serial.println(" 2. 비밀번호: 12345678"); + Serial.print(" 3. 브라우저: http://"); + Serial.println(WiFi.softAPIP()); + Serial.println("========================================\n"); +} + +// ==================== LOOP ==================== +void loop() { + server.handleClient(); + vTaskDelay(pdMS_TO_TICKS(10)); + + // 디버그 정보 (10초마다) + static uint32_t lastPrint = 0; + if (millis() - lastPrint > 10000) { + Serial.printf("[상태] 큐: %d/%d | 로깅: %s | SD: %s | 총: %lu msg | 속도: %lu msg/s\n", + uxQueueMessagesWaiting(canQueue), + CAN_QUEUE_SIZE, + loggingEnabled ? "ON " : "OFF", + sdCardReady ? "Ready " : "Not Ready", + totalMsgCount, + msgPerSecond + ); + lastPrint = millis(); + } +} \ No newline at end of file