/* * Byun CANFD Logger with Web Interface * Version: 1.0 * Hardware: ESP32-S3 + MCP2518FD (ACAN2517FD 라이브러리) * * 라이브러리: ACAN2517FD by Pierre Molinaro (라이브러리 매니저에서 설치) * * 핀 구성: * CAN SPI : SCK=12, MOSI=11, MISO=13, CS=10, INT=9(폴링255) * SD SDIO : CLK=39, CMD=38, D0=40, D1=41, D2=42, D3=2 * RTC I2C : SDA=8, SCL=18 (DS3231) * * Arduino IDE 설정: * Board: ESP32S3 Dev Module * PSRAM: OPI PSRAM (필수!) * Flash: 16MB / Partition: 16MB(3MB APP/9.9MB FATFS) */ #include #include #include #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" #include "settings.h" // ───────────────────────────────────────────── // 핀 정의 // ───────────────────────────────────────────── #define PIN_CAN_SCK 12 #define PIN_CAN_MOSI 11 #define PIN_CAN_MISO 13 #define PIN_CAN_CS 10 #define PIN_CAN_INT 255 // 255 = 폴링모드 (INT핀 미사용) #define PIN_SD_CLK 39 #define PIN_SD_CMD 38 #define PIN_SD_D0 40 #define PIN_SD_D1 41 #define PIN_SD_D2 42 #define PIN_SD_D3 2 #define PIN_I2C_SDA 8 #define PIN_I2C_SCL 18 #define DS3231_ADDR 0x68 // ───────────────────────────────────────────── // CAN FD 설정 // ───────────────────────────────────────────── // OSC: 20MHz (알리 보드 확인) // 변경 필요시: OSC_40MHz, OSC_4MHz10xPLL 등 #define CANFD_OSC ACAN2517FDSettings::OSC_20MHz // ── CAN SPI 클럭 설정 ────────────────────────────────── // MCP2518FD 최대 SPI = 0.85 × Fosc // OSC_20MHz → 최대 17 MHz // OSC_40MHz → 최대 34 MHz // 배선이 길거나 불안정하면 낮추세요 (최소 1 MHz) // 5 MHz : 안전 (노이즈에 강함) // 10 MHz : 권장 (안정성/속도 균형) // 15 MHz : 고속 (짧은 배선에서) // 17 MHz : 최대 (OSC_20MHz 기준) #define CAN_SPI_CLOCK 10000000UL // ← 여기서 변경 // 최대 데이터 길이 (CAN FD = 64, Classic CAN = 8) #define CANFD_MAX_DATA 64 // DLC → 실제 바이트 수 변환 테이블 static const uint8_t dlcToLen[16] = { 0,1,2,3,4,5,6,7,8,12,16,20,24,32,48,64 }; // ───────────────────────────────────────────── // 버퍼 크기 (2의 거듭제곱 필수) // ───────────────────────────────────────────── #define CAN_RING_SIZE 4096 // CAN FD 메시지 링버퍼 슬롯 수 #define FILE_BUFFER_SIZE 65536 // 64KB 더블버퍼 (SD 쓰기) #define MAX_FILENAME_LEN 64 #define RECENT_MSG_COUNT 128 #define MAX_TX_MESSAGES 16 #define MAX_COMMENT_LEN 128 #define MAX_FILE_COMMENTS 50 #define RTC_SYNC_INTERVAL 60000 // ───────────────────────────────────────────── // CAN FD 속도 프리셋 // ───────────────────────────────────────────── struct CANFDPreset { const char* name; uint32_t arbBPS; DataBitRateFactor factor; bool isFD; }; static const CANFDPreset speedPresets[] = { {"Classic 125K", 125000UL, DataBitRateFactor::x1, false}, {"Classic 250K", 250000UL, DataBitRateFactor::x1, false}, {"Classic 500K", 500000UL, DataBitRateFactor::x1, false}, {"Classic 1M", 1000000UL, DataBitRateFactor::x1, false}, {"FD 500K/2M", 500000UL, DataBitRateFactor::x4, true}, {"FD 500K/4M", 500000UL, DataBitRateFactor::x8, true}, {"FD 1M/4M", 1000000UL, DataBitRateFactor::x4, true}, {"FD 1M/8M", 1000000UL, DataBitRateFactor::x8, true}, }; #define NUM_SPEED_PRESETS (sizeof(speedPresets)/sizeof(speedPresets[0])) // ───────────────────────────────────────────── // PCAP 구조체 (Wireshark, LINKTYPE_CAN_SOCKETCAN=227) // ───────────────────────────────────────────── struct PcapGlobalHeader { uint32_t magic; // 0xa1b2c3d4 uint16_t ver_major; // 2 uint16_t ver_minor; // 4 int32_t thiszone; uint32_t sigfigs; uint32_t snaplen; uint32_t network; // 227 = LINKTYPE_CAN_SOCKETCAN } __attribute__((packed)); struct PcapPktHdr { uint32_t ts_sec; uint32_t ts_usec; uint32_t incl_len; uint32_t orig_len; } __attribute__((packed)); // Classic CAN SocketCAN frame (16 bytes) struct SCFrame { uint32_t can_id; uint8_t can_dlc; uint8_t pad, res0, res1; uint8_t data[8]; } __attribute__((packed)); // CAN FD SocketCAN frame (72 bytes) struct SCFDFrame { uint32_t can_id; uint8_t len; uint8_t flags; // CANFD_BRS=1, CANFD_ESI=2, CANFD_FDF=4 uint8_t res0, res1; uint8_t data[64]; } __attribute__((packed)); static_assert(sizeof(SCFrame) == 16, "SCFrame 16 bytes"); static_assert(sizeof(SCFDFrame) == 72, "SCFDFrame 72 bytes"); // ───────────────────────────────────────────── // 로깅용 메시지 구조체 (PSRAM 링버퍼 원소) // ───────────────────────────────────────────── struct CANFDLog { uint64_t timestamp_us; uint32_t id; uint8_t dlc; // DLC 코드 (0~15) uint8_t len; // 실제 바이트 수 (0~64) uint8_t flags; // bit7=EFF, bit6=RTR, bit5=FDF, bit4=BRS, bit3=ESI uint8_t data[CANFD_MAX_DATA]; } __attribute__((packed)); // 8+4+1+1+1+64 = 79 bytes // ───────────────────────────────────────────── // SPSC 링버퍼 // ───────────────────────────────────────────── struct CANRingBuffer { volatile uint32_t head; volatile uint32_t tail; uint32_t size; uint32_t mask; CANFDLog* buf; volatile uint32_t dropped; }; CANRingBuffer canRing; static inline bool IRAM_ATTR ring_push(CANRingBuffer* rb, const CANFDLog* msg) { uint32_t h = rb->head; if ((h - rb->tail) >= rb->size) { rb->dropped++; return false; } memcpy(&rb->buf[h & rb->mask], msg, sizeof(CANFDLog)); __sync_synchronize(); rb->head = h + 1; return true; } static inline bool ring_pop(CANRingBuffer* rb, CANFDLog* msg) { uint32_t t = rb->tail; if (rb->head == t) return false; memcpy(msg, &rb->buf[t & rb->mask], sizeof(CANFDLog)); __sync_synchronize(); rb->tail = t + 1; return true; } static inline uint32_t ring_count(const CANRingBuffer* rb) { return rb->head - rb->tail; } static inline void ring_clear(CANRingBuffer* rb) { rb->tail = rb->head; rb->dropped = 0; } // ───────────────────────────────────────────── // 최근 메시지 (웹 모니터링 테이블) // ───────────────────────────────────────────── struct RecentMsg { CANFDLog msg; uint32_t count; }; // ───────────────────────────────────────────── // TX 메시지 // ───────────────────────────────────────────── struct TxMessage { uint32_t id; bool extended; bool isFD; bool brs; uint8_t dlc; uint8_t data[CANFD_MAX_DATA]; uint32_t intervalMs; uint32_t lastSent; bool active; }; struct FileComment { char filename[MAX_FILENAME_LEN]; char comment[MAX_COMMENT_LEN]; }; // ───────────────────────────────────────────── // 시간 동기화 상태 // ───────────────────────────────────────────── struct TimeSyncStatus { bool synced; uint64_t lastSyncUs; uint32_t syncCount; bool rtcAvail; uint32_t rtcSyncCount; } timeSyncSt = {}; // ───────────────────────────────────────────── // 전역 객체 // ───────────────────────────────────────────── SPIClass gSPI(HSPI); ACAN2517FD gCAN(PIN_CAN_CS, gSPI, PIN_CAN_INT); SoftWire rtcWire(PIN_I2C_SDA, PIN_I2C_SCL); WebServer server(80); WebSocketsServer webSocket(81); Preferences prefs; // PSRAM 버퍼 RecentMsg* recentData = nullptr; TxMessage* txMessages = nullptr; FileComment* fileComments = nullptr; uint8_t* fileBuf1 = nullptr; uint8_t* fileBuf2 = nullptr; // ───────────────────────────────────────────── // 상태 변수 // ───────────────────────────────────────────── char wifiSSID[32] = "CANFD_Logger"; char wifiPassword[64] = "12345678"; bool staMode = false; char staSSID[32] = ""; char staPassword[64] = ""; volatile bool loggingEnabled = false; volatile bool sdCardReady = false; volatile uint32_t totalMsgCount = 0; volatile uint32_t msgPerSecond = 0; uint32_t lastMsgCountMs = 0; uint32_t lastMsgCount = 0; uint32_t totalTxCount = 0; uint32_t lastBroadcast = 0; // 현재 CAN 설정 int speedPresetIdx = 2; // 기본: Classic 500K (안전한 기본값) bool listenOnly = false; volatile bool canInitOK = false; // CAN 초기화 성공 여부 // 더블 버퍼 uint8_t* curWriteBuf = nullptr; uint8_t* curFlushBuf = nullptr; uint32_t writeBufIdx = 0; uint32_t flushBufSize = 0; volatile bool flushInProg = false; // 로그 파일 File logFile; char curFilename[MAX_FILENAME_LEN]; volatile uint32_t curFileSize = 0; volatile bool canLogCSV = false; volatile bool canLogPCAP = false; volatile uint64_t logStartUs = 0; // 파일 코멘트 int commentCount = 0; // ───────────────────────────────────────────── // FreeRTOS 핸들 // ───────────────────────────────────────────── SemaphoreHandle_t sdMutex = NULL; SemaphoreHandle_t rtcMutex = NULL; TaskHandle_t canRxTaskH = NULL; TaskHandle_t sdWriteTaskH = NULL; TaskHandle_t sdFlushTaskH = NULL; TaskHandle_t webTaskH = NULL; TaskHandle_t rtcTaskH = NULL; // ───────────────────────────────────────────── // 유틸리티 // ───────────────────────────────────────────── uint8_t bcdToDec(uint8_t v) { return (v >> 4) * 10 + (v & 0x0F); } uint8_t decToBcd(uint8_t v) { return ((v / 10) << 4) | (v % 10); } // CANFDMessage → 내부 플래그 변환 static inline uint8_t makeFlags(const CANFDMessage& m) { uint8_t f = 0; if (m.ext) f |= 0x80; if (m.type == CANFDMessage::CAN_REMOTE) f |= 0x40; if (m.type == CANFDMessage::CANFD_WITH_BIT_RATE_SWITCH || m.type == CANFDMessage::CANFD_NO_BIT_RATE_SWITCH) f |= 0x20; // FDF if (m.type == CANFDMessage::CANFD_WITH_BIT_RATE_SWITCH) f |= 0x10; // BRS return f; } bool writePcapGlobalHeader(File& f) { PcapGlobalHeader h; h.magic = 0xa1b2c3d4; h.ver_major = 2; h.ver_minor = 4; h.thiszone = 0; h.sigfigs = 0; h.snaplen = 65535; h.network = 227; return f.write((uint8_t*)&h, sizeof(h)) == sizeof(h); } // ───────────────────────────────────────────── // PSRAM 초기화 // ───────────────────────────────────────────── bool initPSRAM() { Serial.println("\n=== PSRAM 초기화 (CAN FD Logger) ==="); if (!psramFound()) { Serial.println("✗ PSRAM 없음!"); return false; } Serial.printf("✓ PSRAM: %d MB\n", ESP.getPsramSize() / 1024 / 1024); // CAN 링버퍼 (4096 × 79 bytes ≈ 316 KB) canRing.buf = (CANFDLog*)ps_malloc(CAN_RING_SIZE * sizeof(CANFDLog)); if (!canRing.buf) { Serial.println("✗ CAN RingBuffer 실패"); return false; } canRing.size = CAN_RING_SIZE; canRing.mask = CAN_RING_SIZE - 1; canRing.head = canRing.tail = canRing.dropped = 0; Serial.printf("✓ CAN RingBuffer: %d슬롯 × %d bytes = %.1f KB\n", CAN_RING_SIZE, sizeof(CANFDLog), (float)(CAN_RING_SIZE * sizeof(CANFDLog)) / 1024.0f); // 더블 버퍼 (2 × 64KB = 128 KB) fileBuf1 = (uint8_t*)ps_malloc(FILE_BUFFER_SIZE); fileBuf2 = (uint8_t*)ps_malloc(FILE_BUFFER_SIZE); if (!fileBuf1 || !fileBuf2) { Serial.println("✗ 더블버퍼 실패"); return false; } curWriteBuf = fileBuf1; curFlushBuf = fileBuf2; Serial.printf("✓ Double Buffer: 2 × %d KB\n", FILE_BUFFER_SIZE / 1024); // 기타 데이터 recentData = (RecentMsg*) ps_calloc(RECENT_MSG_COUNT, sizeof(RecentMsg)); txMessages = (TxMessage*) ps_calloc(MAX_TX_MESSAGES, sizeof(TxMessage)); fileComments = (FileComment*) ps_calloc(MAX_FILE_COMMENTS,sizeof(FileComment)); if (!recentData || !txMessages || !fileComments) { Serial.println("✗ 데이터 버퍼 실패"); return false; } Serial.printf("✓ PSRAM 남은 용량: %.1f KB\n\n", (float)ESP.getFreePsram() / 1024.0f); return true; } // ───────────────────────────────────────────── // 설정 저장/로드 // ───────────────────────────────────────────── void loadSettings() { prefs.begin("canfd-logger", true); prefs.getString("ssid", wifiSSID, sizeof(wifiSSID)); prefs.getString("pass", wifiPassword, sizeof(wifiPassword)); staMode = prefs.getBool("sta_en", false); prefs.getString("sta_ssid", staSSID, sizeof(staSSID)); prefs.getString("sta_pass", staPassword, sizeof(staPassword)); if (!strlen(wifiSSID)) strcpy(wifiSSID, "CANFD_Logger"); if (!strlen(wifiPassword)) strcpy(wifiPassword,"12345678"); speedPresetIdx = prefs.getInt("speed_idx", 2); if (speedPresetIdx < 0 || speedPresetIdx >= (int)NUM_SPEED_PRESETS) speedPresetIdx = 2; listenOnly = prefs.getBool("listen_only", false); prefs.end(); } void saveSettings() { prefs.begin("canfd-logger", false); prefs.putString("ssid", wifiSSID); prefs.putString("pass", wifiPassword); prefs.putBool ("sta_en", staMode); prefs.putString("sta_ssid", staSSID); prefs.putString("sta_pass", staPassword); prefs.putInt ("speed_idx",speedPresetIdx); prefs.putBool ("listen_only", listenOnly); prefs.end(); } // ───────────────────────────────────────────── // CAN FD 초기화 (ACAN2517FD) // ───────────────────────────────────────────── bool initCANFD() { const CANFDPreset& p = speedPresets[speedPresetIdx]; Serial.printf("CAN FD 초기화: %s (Listen:%s, SPI:%luMHz)\n", p.name, listenOnly ? "ON" : "OFF", CAN_SPI_CLOCK / 1000000UL); ACAN2517FDSettings settings(CANFD_OSC, p.arbBPS, p.factor); if (listenOnly) { settings.mRequestedMode = ACAN2517FDSettings::ListenOnly; } else if (!p.isFD) { settings.mRequestedMode = ACAN2517FDSettings::Normal20B; } else { settings.mRequestedMode = ACAN2517FDSettings::NormalFD; } // ★ TX FIFO 크기 미설정 (기본값 유지) → 0x1000(kTXQSizeTooLarge) 에러 방지 // ★ RX FIFO 16 = 동작 확인된 값 settings.mControllerReceiveFIFOSize = 16; Serial.printf(" Arb : %lu bps (실제 %lu bps)\n", p.arbBPS, settings.actualArbitrationBitRate()); if (p.isFD) { Serial.printf(" Data : %lu bps (실제 %lu bps)\n", p.arbBPS * (uint32_t)p.factor, settings.actualDataBitRate()); } // ★ CS 핀 명시적으로 HIGH 보장 (이전 작업 잔류 상태 초기화) pinMode(PIN_CAN_CS, OUTPUT); digitalWrite(PIN_CAN_CS, HIGH); delay(10); // ★ 최대 3회 재시도 (0x2=readback오류는 타이밍 문제로 재시도하면 해결됨) uint32_t err = 0xFFFFFFFF; for (int attempt = 0; attempt < 3; attempt++) { if (attempt > 0) { Serial.printf(" 재시도 %d/3 (SPI 재초기화)...\n", attempt + 1); gSPI.end(); delay(50); gSPI.begin(PIN_CAN_SCK, PIN_CAN_MISO, PIN_CAN_MOSI, PIN_CAN_CS); gSPI.setFrequency(CAN_SPI_CLOCK); digitalWrite(PIN_CAN_CS, HIGH); delay(100); } err = gCAN.begin(settings, NULL); // NULL = 폴링모드 if (err == 0) break; Serial.printf(" 시도 %d 실패: 0x%08X\n", attempt + 1, err); delay(200); } if (err == 0) { Serial.printf("✓ CAN FD 초기화 성공: %s\n", p.name); canInitOK = true; return true; } else { canInitOK = false; Serial.printf("✗ CAN FD 초기화 최종 실패: 0x%08X\n", err); if (err & 0x00000001) Serial.println(" 0x1 → Configuration모드 타임아웃 (SPI배선 확인)"); if (err & 0x00000002) Serial.println(" 0x2 → 레지스터 readback 오류 (SPI노이즈/속도)"); if (err & 0x00001000) Serial.println(" 0x1000 → FIFO크기 초과 (TX FIFO 설정 확인)"); if (err & 0x00010000) Serial.println(" 0x10000 → 모드전환 타임아웃 (CS/INT 확인)"); // ★ FD 설정 실패 시 Classic 500K(index=2)로 자동 폴백 (1회) if (speedPresets[speedPresetIdx].isFD && speedPresetIdx != 2) { Serial.println(" → Classic 500K으로 자동 폴백 시도..."); speedPresetIdx = 2; saveSettings(); return initCANFD(); // 1회 재귀 (Classic은 isFD=false → 재귀 없음) } return false; } } // ───────────────────────────────────────────── // RTC (DS3231) // ───────────────────────────────────────────── void initRTC() { rtcWire.begin(); rtcWire.setClock(100000); rtcWire.beginTransmission(DS3231_ADDR); timeSyncSt.rtcAvail = (rtcWire.endTransmission() == 0); Serial.printf("%s RTC(DS3231) %s\n", timeSyncSt.rtcAvail ? "✓" : "!", timeSyncSt.rtcAvail ? "감지됨" : "없음"); } bool readRTC(struct tm* t) { if (!timeSyncSt.rtcAvail) return false; if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(100)) != pdTRUE) return false; rtcWire.beginTransmission(DS3231_ADDR); rtcWire.write(0x00); if (rtcWire.endTransmission() != 0) { xSemaphoreGive(rtcMutex); return false; } if (rtcWire.requestFrom(DS3231_ADDR, 7) != 7) { xSemaphoreGive(rtcMutex); return false; } uint8_t b[7]; for (int i=0;i<7;i++) b[i]=rtcWire.read(); xSemaphoreGive(rtcMutex); t->tm_sec = bcdToDec(b[0]&0x7F); t->tm_min = bcdToDec(b[1]&0x7F); t->tm_hour = bcdToDec(b[2]&0x3F); t->tm_mday = bcdToDec(b[4]&0x3F); t->tm_mon = bcdToDec(b[5]&0x1F)-1; t->tm_year = bcdToDec(b[6])+100; return true; } bool writeRTC(const struct tm* t) { if (!timeSyncSt.rtcAvail) return false; if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(100)) != pdTRUE) return false; rtcWire.beginTransmission(DS3231_ADDR); rtcWire.write(0x00); rtcWire.write(decToBcd(t->tm_sec)); rtcWire.write(decToBcd(t->tm_min)); rtcWire.write(decToBcd(t->tm_hour)); rtcWire.write(decToBcd(t->tm_wday+1)); rtcWire.write(decToBcd(t->tm_mday)); rtcWire.write(decToBcd(t->tm_mon+1)); rtcWire.write(decToBcd(t->tm_year-100)); bool ok = (rtcWire.endTransmission() == 0); xSemaphoreGive(rtcMutex); return ok; } void ntpCallback(struct timeval* tv) { timeSyncSt.synced = true; timeSyncSt.lastSyncUs= (uint64_t)tv->tv_sec*1000000ULL + tv->tv_usec; timeSyncSt.syncCount++; struct tm t; time_t now = tv->tv_sec; localtime_r(&now, &t); if (timeSyncSt.rtcAvail && writeRTC(&t)) timeSyncSt.rtcSyncCount++; } void rtcSyncTask(void* pv) { while (1) { if (timeSyncSt.rtcAvail) { struct tm t; if (readRTC(&t)) { time_t now = mktime(&t); struct timeval tv = {now, 0}; settimeofday(&tv, NULL); timeSyncSt.synced = true; timeSyncSt.lastSyncUs = (uint64_t)now * 1000000ULL; timeSyncSt.rtcSyncCount++; } } vTaskDelay(pdMS_TO_TICKS(RTC_SYNC_INTERVAL)); } } // ───────────────────────────────────────────── // 파일 코멘트 // ───────────────────────────────────────────── void saveFileComments() { if (!sdCardReady) return; if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { File f = SD_MMC.open("/comments.dat", FILE_WRITE); if (f) { f.write((uint8_t*)&commentCount, sizeof(commentCount)); f.write((uint8_t*)fileComments, sizeof(FileComment)*commentCount); f.close(); } xSemaphoreGive(sdMutex); } } void loadFileComments() { if (!sdCardReady) return; if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { if (SD_MMC.exists("/comments.dat")) { File f = SD_MMC.open("/comments.dat", FILE_READ); if (f) { f.read((uint8_t*)&commentCount, sizeof(commentCount)); if (commentCount > MAX_FILE_COMMENTS) commentCount = MAX_FILE_COMMENTS; f.read((uint8_t*)fileComments, sizeof(FileComment)*commentCount); f.close(); } } xSemaphoreGive(sdMutex); } } const char* getComment(const char* fn) { for (int i=0;i CANFD_MAX_DATA) log.len = CANFD_MAX_DATA; memcpy(log.data, rxMsg.data, log.len); if (ring_push(&canRing, &log)) totalMsgCount++; } // ★ 메시지 없을 때 5ms 대기 → Core1 idle task가 WDT 리셋 가능 // ★ 메시지 있을 때 2ms → 연속 수신 처리 유지 vTaskDelay(pdMS_TO_TICKS(gotMsg ? 2 : 5)); } } // ───────────────────────────────────────────── // SD Flush Task (더블 버퍼 비동기 flush) // ───────────────────────────────────────────── void sdFlushTask(void* pv) { Serial.println("✓ SD Flush Task 시작 (Core 0, Pri 9)"); uint32_t flushCnt = 0; while (1) { uint32_t flushSize; if (xTaskNotifyWait(0, 0xFFFFFFFF, &flushSize, portMAX_DELAY) == pdTRUE) { if (flushSize > 0 && flushSize <= FILE_BUFFER_SIZE) { flushInProg = true; if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(5000)) == pdTRUE) { if (logFile && sdCardReady && loggingEnabled) { uint32_t t0 = millis(); logFile.write(curFlushBuf, flushSize); if (++flushCnt % 4 == 0) logFile.flush(); uint32_t el = millis()-t0; if (el > 200) Serial.printf("⚠ SD Flush 지연: %dms (%d bytes)\n",el,(int)flushSize); } xSemaphoreGive(sdMutex); } flushInProg = false; } } } } // ───────────────────────────────────────────── // SD Write Task (링버퍼 Consumer) // ───────────────────────────────────────────── void sdWriteTask(void* pv) { Serial.println("✓ SD Write Task 시작 (Core 0, Pri 8)"); CANFDLog msg; while (1) { bool worked = false; if (ring_pop(&canRing, &msg)) { worked = true; // 모니터링 테이블 업데이트 bool found = false; for (int i=0;i=1000){logFile.flush();cc=0;} } xSemaphoreGive(sdMutex); } } else if (canLogPCAP) { // PCAP 더블 버퍼 bool isFD = (msg.flags & 0x20) != 0; size_t recSize = sizeof(PcapPktHdr) + (isFD ? sizeof(SCFDFrame) : sizeof(SCFrame)); if (writeBufIdx + recSize > FILE_BUFFER_SIZE) { while (flushInProg) vTaskDelay(1); uint8_t* tmp=curWriteBuf; curWriteBuf=curFlushBuf; curFlushBuf=tmp; flushBufSize=writeBufIdx; writeBufIdx=0; if (sdFlushTaskH) xTaskNotify(sdFlushTaskH, flushBufSize, eSetValueWithOverwrite); } PcapPktHdr ph; ph.ts_sec = (uint32_t)(msg.timestamp_us/1000000ULL); ph.ts_usec = (uint32_t)(msg.timestamp_us%1000000ULL); ph.incl_len= ph.orig_len = isFD ? sizeof(SCFDFrame) : sizeof(SCFrame); memcpy(&curWriteBuf[writeBufIdx], &ph, sizeof(ph)); writeBufIdx+=sizeof(ph); if (isFD) { SCFDFrame fr; memset(&fr,0,sizeof(fr)); fr.can_id = msg.id; if (msg.flags & 0x80) fr.can_id |= 0x80000000U; fr.len = msg.len; fr.flags = 0x04; // CANFD_FDF if (msg.flags & 0x10) fr.flags |= 0x01; // BRS memcpy(fr.data, msg.data, msg.len); memcpy(&curWriteBuf[writeBufIdx], &fr, sizeof(fr)); writeBufIdx+=sizeof(fr); } else { SCFrame fr; memset(&fr,0,sizeof(fr)); fr.can_id = msg.id; if (msg.flags & 0x80) fr.can_id |= 0x80000000U; if (msg.flags & 0x40) fr.can_id |= 0x40000000U; fr.can_dlc = msg.dlc; memcpy(fr.data, msg.data, msg.len < 8 ? msg.len : 8); memcpy(&curWriteBuf[writeBufIdx], &fr, sizeof(fr)); writeBufIdx+=sizeof(fr); } curFileSize += recSize; } else { // BIN 더블 버퍼 (가장 빠름, 손실 없음) if (writeBufIdx + sizeof(CANFDLog) > FILE_BUFFER_SIZE) { while (flushInProg) vTaskDelay(1); uint8_t* tmp=curWriteBuf; curWriteBuf=curFlushBuf; curFlushBuf=tmp; flushBufSize=writeBufIdx; writeBufIdx=0; if (sdFlushTaskH) xTaskNotify(sdFlushTaskH, flushBufSize, eSetValueWithOverwrite); } memcpy(&curWriteBuf[writeBufIdx], &msg, sizeof(CANFDLog)); writeBufIdx += sizeof(CANFDLog); curFileSize += sizeof(CANFDLog); } } } if (!worked) vTaskDelay(1); } } // ───────────────────────────────────────────── // TX Task // ───────────────────────────────────────────── void txTask(void* pv) { while (1) { uint32_t now = millis(); bool any = false; for (int i=0;i= tx.intervalMs) { CANFDMessage m; m.id = tx.id; m.ext = tx.extended; m.len = tx.dlc < 16 ? dlcToLen[tx.dlc] : tx.dlc; if (tx.isFD && tx.brs) m.type = CANFDMessage::CANFD_WITH_BIT_RATE_SWITCH; else if (tx.isFD) m.type = CANFDMessage::CANFD_NO_BIT_RATE_SWITCH; else m.type = CANFDMessage::CAN_DATA; memcpy(m.data, tx.data, m.len); if (gCAN.tryToSend(m)) { totalTxCount++; tx.lastSent = now; } } } vTaskDelay(any ? pdMS_TO_TICKS(1) : pdMS_TO_TICKS(10)); } } // ───────────────────────────────────────────── // WebSocket 이벤트 // ───────────────────────────────────────────── void wsEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) { if (type == WStype_CONNECTED) { lastBroadcast = 0; return; } if (type != WStype_TEXT) return; DynamicJsonDocument doc(4096); if (deserializeJson(doc, payload)) return; const char* cmd = doc["cmd"]; if (!cmd) return; // ── 설정 ──────────────────────────────── if (strcmp(cmd, "getSettings") == 0) { DynamicJsonDocument r(1024); r["type"]="settings"; r["ssid"]=wifiSSID; r["password"]=wifiPassword; r["staEnable"]=staMode; r["staSSID"]=staSSID; r["speedIdx"]=speedPresetIdx; r["listenOnly"]=listenOnly; r["staConnected"]=(WiFi.status()==WL_CONNECTED); r["staIP"]=WiFi.localIP().toString(); String j; serializeJson(r,j); webSocket.sendTXT(num,j); } else if (strcmp(cmd, "saveSettings") == 0) { const char* s=doc["ssid"]; if(s&&strlen(s)) strncpy(wifiSSID,s,31); const char* p=doc["password"]; if(p) strncpy(wifiPassword,p,63); staMode=doc["staEnable"]|false; const char* ss=doc["staSSID"]; if(ss) strncpy(staSSID,ss,31); const char* sp=doc["staPassword"]; if(sp) strncpy(staPassword,sp,63); int si=doc["speedIdx"]|-1; if(si>=0&&si<(int)NUM_SPEED_PRESETS) speedPresetIdx=si; listenOnly=doc["listenOnly"]|false; saveSettings(); webSocket.sendTXT(num,"{\"type\":\"settingsSaved\",\"success\":true}"); } // ── CAN FD 재초기화 ───────────────────── else if (strcmp(cmd, "reinitCAN") == 0) { int si = doc["speedIdx"]|-1; if (si>=0&&si<(int)NUM_SPEED_PRESETS) speedPresetIdx=si; listenOnly = doc["listenOnly"]|false; saveSettings(); bool ok = initCANFD(); DynamicJsonDocument r(256); r["type"]="reinitResult"; r["success"]=ok; r["preset"]=speedPresets[speedPresetIdx].name; String j; serializeJson(r,j); webSocket.sendTXT(num,j); } // ── 로깅 ──────────────────────────────── else if (strcmp(cmd, "startLogging") == 0) { if (!loggingEnabled && sdCardReady) { const char* fmt=doc["format"]; canLogCSV = (fmt && strcmp(fmt,"csv") ==0); canLogPCAP = (fmt && strcmp(fmt,"pcap")==0); if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { time_t now2; struct tm ti; time(&now2); localtime_r(&now2,&ti); struct timeval tv; gettimeofday(&tv,NULL); logStartUs = (uint64_t)tv.tv_sec*1000000ULL+tv.tv_usec; const char* ext = canLogPCAP?"pcap":(canLogCSV?"csv":"bin"); snprintf(curFilename,sizeof(curFilename), "/CANFD_%04d%02d%02d_%02d%02d%02d.%s", ti.tm_year+1900,ti.tm_mon+1,ti.tm_mday, ti.tm_hour,ti.tm_min,ti.tm_sec,ext); logFile = SD_MMC.open(curFilename, FILE_WRITE); if (logFile) { if (canLogCSV) { logFile.println("Time_us,CAN_ID,DLC,Type,BRS,DataLen,Data"); logFile.flush(); } else if (canLogPCAP) { writePcapGlobalHeader(logFile); logFile.flush(); } logFile.close(); logFile = SD_MMC.open(curFilename, FILE_APPEND); if (logFile) { loggingEnabled=true; writeBufIdx=0; flushBufSize=0; flushInProg=false; curFileSize=logFile.size(); Serial.printf("✓ 로깅 시작 [%s]: %s\n", canLogPCAP?"PCAP":(canLogCSV?"CSV":"BIN"), curFilename); } } xSemaphoreGive(sdMutex); } } } else if (strcmp(cmd, "stopLogging") == 0) { if (loggingEnabled) { loggingEnabled = false; int wc=0; while(flushInProg&&wc++<500) vTaskDelay(10); if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(2000)) == pdTRUE) { if (writeBufIdx>0&&logFile) { logFile.write(curWriteBuf,writeBufIdx); writeBufIdx=0; } if (logFile) { logFile.flush(); logFile.close(); } curFilename[0]='\0'; flushBufSize=0; xSemaphoreGive(sdMutex); } Serial.println("✓ 로깅 중지"); } } // ── 파일 목록 ──────────────────────────── else if (strcmp(cmd, "getFiles") == 0) { if (sdCardReady && xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { DynamicJsonDocument r(8192); r["type"]="files"; JsonArray fa = r.createNestedArray("files"); File root = SD_MMC.open("/"); if (root) { File f = root.openNextFile(); int cnt=0; while (f && cnt<60) { if (!f.isDirectory()) { const char* fn=f.name(); if(fn[0]=='/') fn++; if(fn[0]!='.'&&strlen(fn)>0) { JsonObject fo=fa.createNestedObject(); fo["name"]=fn; fo["size"]=f.size(); const char* cm=getComment(fn); if(strlen(cm)>0) fo["comment"]=cm; cnt++; } } f.close(); f=root.openNextFile(); } root.close(); } xSemaphoreGive(sdMutex); String j; serializeJson(r,j); webSocket.sendTXT(num,j); } } else if (strcmp(cmd, "deleteFile") == 0) { const char* fn=doc["filename"]; if (fn&&strlen(fn)>0) { String fp="/"+String(fn); if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { bool ok=SD_MMC.exists(fp)&&SD_MMC.remove(fp); xSemaphoreGive(sdMutex); char r[64]; snprintf(r,sizeof(r),"{\"type\":\"deleteResult\",\"success\":%s}",ok?"true":"false"); webSocket.sendTXT(num,r); } } } else if (strcmp(cmd, "addComment") == 0) { const char* fn=doc["filename"], *cm=doc["comment"]; if(fn&&cm) addComment(fn,cm); } // ── TX ─────────────────────────────────── else if (strcmp(cmd, "sendOnce") == 0) { CANFDMessage m; m.id = strtoul(doc["id"]|"0x0", NULL, 16); m.ext = doc["ext"]|false; bool isFD=doc["fd"]|false, brs=doc["brs"]|false; if(isFD&&brs) m.type=CANFDMessage::CANFD_WITH_BIT_RATE_SWITCH; else if(isFD) m.type=CANFDMessage::CANFD_NO_BIT_RATE_SWITCH; else m.type=CANFDMessage::CAN_DATA; uint8_t dlc=doc["dlc"]|8; m.len = dlc<16?dlcToLen[dlc]:dlc; JsonArray da=doc["data"]; for(int i=0;i<(int)m.len&&i<64;i++) m.data[i]=da[i]|0; if(gCAN.tryToSend(m)) totalTxCount++; } else if (strcmp(cmd, "setTxMsg") == 0) { int idx=doc["idx"]|-1; if(idx>=0&&idx= 1000) { uint32_t cur=totalMsgCount; msgPerSecond = cur - lastMsgCount; lastMsgCount = cur; lastMsgCountMs= now2; } if (now2 - lastBroadcast < 500) { vTaskDelay(10); continue; } lastBroadcast = now2; if (webSocket.connectedClients() == 0) { vTaskDelay(10); continue; } DynamicJsonDocument doc(12288); doc["type"] = "update"; doc["logging"] = loggingEnabled; doc["sdReady"] = sdCardReady; doc["totalMsg"] = totalMsgCount; doc["msgPerSec"] = msgPerSecond; doc["totalTx"] = totalTxCount; doc["fileSize"] = curFileSize; doc["ringUsed"] = ring_count(&canRing); doc["ringSize"] = CAN_RING_SIZE; doc["dropped"] = canRing.dropped; doc["timeSync"] = timeSyncSt.synced; doc["rtcAvail"] = timeSyncSt.rtcAvail; doc["rtcSyncCnt"]= timeSyncSt.rtcSyncCount; doc["syncCount"] = timeSyncSt.syncCount; doc["speedIdx"] = speedPresetIdx; doc["apIP"] = WiFi.softAPIP().toString(); doc["staIP"] = WiFi.localIP().toString(); doc["staConnected"]= (WiFi.status()==WL_CONNECTED); doc["speedName"] = speedPresets[speedPresetIdx].name; doc["listenOnly"]= listenOnly; doc["currentFile"]= (loggingEnabled&&curFilename[0]) ? String(curFilename) : ""; doc["logFormat"] = canLogPCAP?"pcap":(canLogCSV?"csv":"bin"); // 버스 부하율 추정 (FD는 가변이므로 근사) const CANFDPreset& pr = speedPresets[speedPresetIdx]; uint32_t maxMPS = pr.arbBPS / 100; // 대략적 추정 float busLoad = maxMPS>0?(msgPerSecond*100.0f)/maxMPS:0; if(busLoad>100.0f) busLoad=100.0f; doc["busLoad"] = (int)busLoad; time_t ts; time(&ts); doc["timestamp"]=(uint64_t)ts; // 최근 메시지 (최대 30개) JsonArray msgs = doc.createNestedArray("messages"); int cnt=0; for(int i=0;i0&&sz<8192) webSocket.broadcastTXT(json); } } // ───────────────────────────────────────────── // Download Handler // ───────────────────────────────────────────── void handleDownload() { if (!server.hasArg("file")) { server.send(400,"text/plain","Bad request"); return; } String fname = "/" + server.arg("file"); bool wasLog = loggingEnabled; if (wasLog) { loggingEnabled=false; int w=0; while(flushInProg&&w++<200) delay(10); delay(50); } if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(3000)) != pdTRUE) { if(wasLog) loggingEnabled=true; server.send(503,"text/plain","SD busy"); return; } if (!SD_MMC.exists(fname)) { xSemaphoreGive(sdMutex); if(wasLog) loggingEnabled=true; server.send(404,"text/plain","Not found"); return; } // 1-bit 모드로 전환 (WiFi 간섭 방지) SD_MMC.end(); delay(50); SD_MMC.setPins(PIN_SD_CLK, PIN_SD_CMD, PIN_SD_D0); if (!SD_MMC.begin("/sdcard", true, false, 10000000)) { xSemaphoreGive(sdMutex); if(wasLog) loggingEnabled=true; server.send(500,"text/plain","SD 1-bit init fail"); return; } File file = SD_MMC.open(fname, FILE_READ); if (!file) { xSemaphoreGive(sdMutex); if(wasLog) loggingEnabled=true; server.send(500,"text/plain","Open fail"); return; } size_t fsz = file.size(); server.setContentLength(fsz); server.sendHeader("Content-Disposition","attachment; filename=\""+server.arg("file")+"\""); server.sendHeader("Content-Type","application/octet-stream"); server.sendHeader("Connection","close"); server.send(200,"application/octet-stream",""); const size_t CHUNK=4096; uint8_t* buf=(uint8_t*)malloc(CHUNK); if(buf) { size_t tot=0; WiFiClient cl=server.client(); cl.setNoDelay(true); while(tot0) w+=wr; else { yield();delay(1); } } tot+=r; yield(); } free(buf); } file.close(); // 4-bit 모드 복원 SD_MMC.end(); delay(50); SD_MMC.setPins(PIN_SD_CLK,PIN_SD_CMD,PIN_SD_D0,PIN_SD_D1,PIN_SD_D2,PIN_SD_D3); if(SD_MMC.begin("/sdcard",false,false,10000000)) sdCardReady=true; xSemaphoreGive(sdMutex); if(wasLog) loggingEnabled=true; } // ───────────────────────────────────────────── // Setup // ───────────────────────────────────────────── void setup() { Serial.begin(115200); delay(1000); Serial.println("\n========================================"); Serial.println(" Byun CANFD Logger v1.0"); Serial.println(" MCP2518FD + ACAN2517FD 라이브러리"); Serial.println("========================================\n"); if (!initPSRAM()) { Serial.println("✗ PSRAM 실패! Tools→PSRAM→OPI PSRAM 설정 확인"); while(1) { delay(1000); Serial.println("재업로드 필요"); } } loadSettings(); // SPI + CAN FD 초기화 // ★ CS는 라이브러리가 직접 관리 → SPI.begin에 CS 미전달 (HW SS 충돌 방지) gSPI.begin(PIN_CAN_SCK, PIN_CAN_MISO, PIN_CAN_MOSI, -1); gSPI.setFrequency(CAN_SPI_CLOCK); // ★ SPI 클럭 설정 pinMode(PIN_CAN_CS, OUTPUT); digitalWrite(PIN_CAN_CS, HIGH); delay(50); // SPI 버스 안정화 대기 Serial.printf("SPI 클럭: %lu MHz\n", CAN_SPI_CLOCK / 1000000UL); if (!initCANFD()) { Serial.println("⚠ CAN FD 초기화 실패 → 설정 페이지에서 속도 변경 후 재초기화 하세요"); } // Mutex sdMutex = xSemaphoreCreateMutex(); rtcMutex = xSemaphoreCreateMutex(); initRTC(); esp_task_wdt_deinit(); // SD 카드 (SDIO 4-bit) Serial.println("SD 카드 초기화..."); if (SD_MMC.setPins(PIN_SD_CLK,PIN_SD_CMD,PIN_SD_D0,PIN_SD_D1,PIN_SD_D2,PIN_SD_D3) && SD_MMC.begin("/sdcard",false,false,10000000)) { sdCardReady=true; Serial.printf("✓ SD: %llu MB\n", SD_MMC.cardSize()/1024/1024); loadFileComments(); } else { Serial.println("✗ SD 초기화 실패"); } // WiFi: AP는 항상 켜짐 (STA 성공 여부 무관) WiFi.setSleep(false); if (staMode && strlen(staSSID)>0) { WiFi.mode(WIFI_AP_STA); } else { WiFi.mode(WIFI_AP); } // ★ AP 먼저 기동 (STA 결과 무관하게 항상 접속 가능) WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4); delay(200); Serial.println(); Serial.println("╔══════════════════════════════════╗"); Serial.printf ("║ AP SSID : %-22s║\n", wifiSSID); Serial.printf ("║ AP IP : %-22s║\n", WiFi.softAPIP().toString().c_str()); Serial.printf ("║ AP PW : %-22s║\n", wifiPassword); Serial.println("╚══════════════════════════════════╝"); Serial.println(" ★ 위 AP로 항상 접속 가능합니다."); if (staMode && strlen(staSSID)>0) { Serial.printf(" STA 연결 시도: %s\n", staSSID); WiFi.begin(staSSID, staPassword); int at=0; while(WiFi.status()!=WL_CONNECTED && at++<20) { delay(500); Serial.print("."); } Serial.println(); if(WiFi.status()==WL_CONNECTED){ Serial.printf(" ✓ STA 연결 성공 IP: %s\n", WiFi.localIP().toString().c_str()); configTime(9*3600,0,"pool.ntp.org"); sntp_set_time_sync_notification_cb(ntpCallback); } else { Serial.println(" ✗ STA 연결 실패 → AP로만 접속하세요"); } } esp_wifi_set_max_tx_power(84); webSocket.begin(); webSocket.onEvent(wsEvent); 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("/settings", HTTP_GET, [](){ server.send_P(200,"text/html",settings_html); }); server.on("/download", HTTP_GET, handleDownload); server.begin(); Serial.println("✓ 웹 서버 시작"); // ── Task 생성 ──────────────────────────── xTaskCreatePinnedToCore(webUpdateTask,"WEB", 12288, NULL, 8, &webTaskH, 0); xTaskCreatePinnedToCore(sdFlushTask, "FLUSH", 4096, NULL, 9, &sdFlushTaskH,0); xTaskCreatePinnedToCore(sdWriteTask, "WRITE",12288, NULL, 8, &sdWriteTaskH,0); xTaskCreatePinnedToCore(txTask, "TX", 4096, NULL, 3, NULL, 1); if(timeSyncSt.rtcAvail) xTaskCreatePinnedToCore(rtcSyncTask,"RTC",3072, NULL, 0, &rtcTaskH, 0); // canRxTask는 마지막에 생성 (Core 1 최고 우선순위) xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 8192, NULL, 24, &canRxTaskH, 1); Serial.println("\n========================================"); Serial.println(" Task 구성 완료"); Serial.println(" Core1: CAN_RX(24) TX(3)"); Serial.println(" Core0: FLUSH(9) WRITE(8) WEB(8)"); Serial.println("========================================\n"); } // ───────────────────────────────────────────── // Loop (Core 1, 저우선순위 - 상태 출력만) // ───────────────────────────────────────────── void loop() { vTaskDelay(pdMS_TO_TICKS(30)); static uint32_t lastPrint = 0; if (millis() - lastPrint > 30000) { lastPrint = millis(); Serial.printf("[상태] CAN:%s Ring:%u/%u drop:%u Msg/s:%u PSRAM:%dKB\n", canInitOK?"OK":"FAIL", ring_count(&canRing), CAN_RING_SIZE, canRing.dropped, msgPerSecond, ESP.getFreePsram()/1024); Serial.printf(" AP접속: SSID=%s IP=%s\n", wifiSSID, WiFi.softAPIP().toString().c_str()); } }