From 4dc7c5f0f6c63fd25f5359e8e54f7de13012c133 Mon Sep 17 00:00:00 2001 From: byun Date: Thu, 30 Apr 2026 17:40:07 +0000 Subject: [PATCH] =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EB=8F=99=EC=9E=91=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CANFD_Logger.ino | 1248 ++++++++++++++++++++++++++++++++++++++++++++++ graph.h | 157 ++++++ graph_viewer.h | 222 +++++++++ index.h | 271 ++++++++++ settings.h | 231 +++++++++ transmit.h | 260 ++++++++++ 6 files changed, 2389 insertions(+) create mode 100644 CANFD_Logger.ino create mode 100644 graph.h create mode 100644 graph_viewer.h create mode 100644 index.h create mode 100644 settings.h create mode 100644 transmit.h diff --git a/CANFD_Logger.ino b/CANFD_Logger.ino new file mode 100644 index 0000000..cc1ef7f --- /dev/null +++ b/CANFD_Logger.ino @@ -0,0 +1,1248 @@ +/* + * 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 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)\n", + p.name, listenOnly ? "ON" : "OFF"); + + 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); + 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); + pinMode(PIN_CAN_CS, OUTPUT); + digitalWrite(PIN_CAN_CS, HIGH); + delay(50); // SPI 버스 안정화 대기 + + 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()); + } +} \ No newline at end of file diff --git a/graph.h b/graph.h new file mode 100644 index 0000000..15426c5 --- /dev/null +++ b/graph.h @@ -0,0 +1,157 @@ +#ifndef GRAPH_H +#define GRAPH_H + +const char graph_html[] PROGMEM = R"rawliteral( + + +그래프 - CANFD Logger + + + +
+
+

실시간 신호 그래프 (CAN FD)

+
+
+
+
+
+
+
+
+
+ +
+ + 최대 8개 신호, 500 포인트 표시 +
+
+
+ + +)rawliteral"; +#endif \ No newline at end of file diff --git a/graph_viewer.h b/graph_viewer.h new file mode 100644 index 0000000..0456a16 --- /dev/null +++ b/graph_viewer.h @@ -0,0 +1,222 @@ +#ifndef GRAPH_VIEWER_H +#define GRAPH_VIEWER_H + + +const char graph_viewer_html[] PROGMEM = R"rawliteral( + + +그래프 뷰어 - CANFD Logger + + + +
+
+

BIN 파일 분석 뷰어 (CAN FD)

+
+
+ + +
+
+ +
+
+ +
+
+ + +
+ + + +)rawliteral"; + +#endif diff --git a/index.h b/index.h new file mode 100644 index 0000000..df37224 --- /dev/null +++ b/index.h @@ -0,0 +1,271 @@ +#ifndef INDEX_H +#define INDEX_H_H +const char index_html[] PROGMEM = R"rawliteral( + + +CANFD Logger + + + + +
+ +
+
+

CAN FD 상태

+
속도 설정-
+
수신 모드-
+
총 수신0
+
Msg/s0
+
버스 부하0%
+
총 송신0
+
+
+

링버퍼 / SD

+
SD 카드
+
로깅 상태
+
파일 크기-
+
현재 파일-
+
+
+ 링버퍼0/4096 +
+
+
+
Drop0
+
+
+

시간 / 시스템

+
시각 동기화
+
RTC
+
NTP 동기 수0
+
RTC 동기 수0
+
현재 시각-
+
AP IP (항상 접속가능)-
+
STA IP-
+
+ + +
+
+
+ + +
+

로깅 제어

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

CAN FD 메시지 모니터 (최근 30개 ID)

+
+ + + + + +
CAN ID타입DLC길이수신 수데이터 (hex)
+
+
+ + + +
+ + + +)rawliteral"; + +#endif \ No newline at end of file diff --git a/settings.h b/settings.h new file mode 100644 index 0000000..8bdb4b3 --- /dev/null +++ b/settings.h @@ -0,0 +1,231 @@ +#ifndef SETTINGS_H +#define SETTINGS_H + +const char settings_html[] PROGMEM = R"rawliteral( + + +설정 - CANFD Logger + + + +
+ + +
+

CAN FD 속도 / 모드

+
+ + + + +
#이름중재 비트레이트데이터 비트레이트타입
+
+
+ +
+
속도 프리셋을 선택하세요.
+
⚠ 속도 변경 후 "CAN 재초기화"를 눌러야 즉시 적용됩니다. SD 로깅 중에는 중지 후 변경하세요.
+
+ + +
+
+ + +
+

WiFi 설정 (AP 모드)

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

시스템

+
+ 핀 배선 (고정)
+ CAN SPI: SCK=12, MOSI=11, MISO=13, CS=10 | INT: 폴링모드(255)
+ SD SDIO: CLK=39, CMD=38, D0=40, D1=41, D2=42, D3=2
+ RTC I2C: SDA=8, SCL=18 (DS3231)

+ 오실레이터: OSC_20MHz (알리 MCP2518FD 보드)
+ 라이브러리: ACAN2517FD by Pierre Molinaro
+
+
+ +
+
+
+
+ + + +)rawliteral"; +#endif \ No newline at end of file diff --git a/transmit.h b/transmit.h new file mode 100644 index 0000000..86db021 --- /dev/null +++ b/transmit.h @@ -0,0 +1,260 @@ +#ifndef TRANSMIT_H +#define TRANSMIT_H + +const char transmit_html[] PROGMEM = R"rawliteral( + + +송신 - CANFD Logger + + + +
+ + +
+

CAN / CAN FD 단발 송신

+
+
+ + +
+
+ + +
+
+ + + +
+
+ +
+
DLC 8 = 8 bytes
+
+ + +
+
+ + +
+

주기 송신 (최대 16개)

+
+
+
+
+
+
+
+ + + + +
+ +
+
+ +
+
+ + + +
#ID타입DLC간격상태
+
+
+ + +
+

송신 로그

+
+ +
+
+ + + +)rawliteral"; + +#endif