/*
 * 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 <Arduino.h>
#include <SPI.h>
#include <ACAN2517FD.h>
#include <SoftWire.h>
#include <SD_MMC.h>
#include <WiFi.h>
#include <esp_wifi.h>
#include <esp_task_wdt.h>
#include <esp_sntp.h>
#include <WebServer.h>
#include <WebSocketsServer.h>
#include <ArduinoJson.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/semphr.h>
#include <sys/time.h>
#include <time.h>
#include <Preferences.h>

#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<commentCount;i++)
        if (strcmp(fileComments[i].filename, fn)==0) return fileComments[i].comment;
    return "";
}

void addComment(const char* fn, const char* cm) {
    for (int i=0;i<commentCount;i++) {
        if (strcmp(fileComments[i].filename,fn)==0) {
            strncpy(fileComments[i].comment,cm,MAX_COMMENT_LEN-1); saveFileComments(); return;
        }
    }
    if (commentCount < MAX_FILE_COMMENTS) {
        strncpy(fileComments[commentCount].filename,fn,MAX_FILENAME_LEN-1);
        strncpy(fileComments[commentCount].comment,cm,MAX_COMMENT_LEN-1);
        commentCount++; saveFileComments();
    }
}

// ─────────────────────────────────────────────
//  CAN RX Task (ACAN2517FD 폴링)
// ─────────────────────────────────────────────
void canRxTask(void* pv) {
    Serial.println("✓ CAN RX Task 시작 (Core 1, Pri 24)");
    vTaskDelay(pdMS_TO_TICKS(500));

    while (1) {
        // ★ CAN 미초기화 시 sleep (CPU 독점 방지)
        if (!canInitOK) {
            vTaskDelay(pdMS_TO_TICKS(50));
            continue;
        }

        // ★ 폴링 모드: isr() 호출 후 receive
        gCAN.isr();

        bool gotMsg = false;
        CANFDMessage rxMsg;
        while (gCAN.receive(rxMsg)) {
            gotMsg = true;
            CANFDLog log;
            struct timeval tv; gettimeofday(&tv, NULL);
            log.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
            log.id    = rxMsg.id;
            log.flags = makeFlags(rxMsg);
            log.dlc   = rxMsg.len;  // ACAN2517FD: len 필드에 DLC가 들어옴
            log.len   = rxMsg.len <= 15 ? dlcToLen[rxMsg.len] : rxMsg.len;
            if (log.len > 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<RECENT_MSG_COUNT;i++) {
                if (recentData[i].msg.id == msg.id) {
                    recentData[i].msg = msg; recentData[i].count++; found = true; break;
                }
            }
            if (!found) {
                for (int i=0;i<RECENT_MSG_COUNT;i++) {
                    if (recentData[i].count == 0) {
                        recentData[i].msg = msg; recentData[i].count = 1; break;
                    }
                }
            }

            // SD 로깅
            if (loggingEnabled && sdCardReady) {
                if (canLogCSV) {
                    // CSV 직접 쓰기
                    if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1)) == pdTRUE) {
                        char line[256];
                        uint64_t rel = msg.timestamp_us - logStartUs;
                        char dat[CANFD_MAX_DATA*3+1]; int di=0;
                        for (int i=0;i<msg.len;i++) {
                            di += sprintf(&dat[di], "%02X", msg.data[i]);
                            if (i<msg.len-1) dat[di++]=' ';
                        }
                        dat[di]='\0';
                        bool isFD  = (msg.flags & 0x20) != 0;
                        bool isBRS = (msg.flags & 0x10) != 0;
                        int ll = snprintf(line, sizeof(line),
                            "%llu,0x%08X,%d,%s,%s,%d,%s\n",
                            rel, msg.id, msg.dlc,
                            isFD?"FD":"CL", isBRS?"BRS":"-",
                            msg.len, dat);
                        if (logFile) { logFile.write((uint8_t*)line, ll); curFileSize += ll;
                            static int cc=0; if(++cc>=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<MAX_TX_MESSAGES;i++) {
            TxMessage& tx = txMessages[i];
            if (!tx.active || tx.intervalMs == 0) continue;
            any = true;
            if (now - tx.lastSent >= 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<MAX_TX_MESSAGES) {
            TxMessage& tx=txMessages[idx];
            const char* ids=doc["id"]; tx.id=ids?strtoul(ids,NULL,16):0;
            tx.extended=doc["ext"]|false;
            tx.isFD=doc["fd"]|false; tx.brs=doc["brs"]|false;
            uint8_t dlc=doc["dlc"]|8; tx.dlc=dlc;
            JsonArray da=doc["data"];
            for(int i=0;i<64;i++) tx.data[i]=(i<(int)da.size())?((uint8_t)da[i]):0;
            tx.intervalMs=doc["interval"]|0;
            tx.active=doc["active"]|false;
            tx.lastSent=0;
        }
    }
    // ── 시간 동기화 ──────────────────────────
    else if (strcmp(cmd, "syncTime") == 0) {
        struct tm ti;
        ti.tm_year=((int)doc["year"])-1900; ti.tm_mon=((int)doc["month"])-1;
        ti.tm_mday=doc["day"]|1; ti.tm_hour=doc["hour"]|0;
        ti.tm_min=doc["minute"]|0; ti.tm_sec=doc["second"]|0;
        time_t t=mktime(&ti);
        struct timeval tv={t,0}; settimeofday(&tv,NULL);
        timeSyncSt.synced=true; timeSyncSt.lastSyncUs=(uint64_t)t*1000000ULL;
        timeSyncSt.syncCount++;
        if(timeSyncSt.rtcAvail) writeRTC(&ti);
    }
    else if (strcmp(cmd, "hwReset") == 0) {
        webSocket.sendTXT(num,"{\"type\":\"hwReset\"}");
        delay(200); ESP.restart();
    }
    else if (strcmp(cmd, "clearStats") == 0) {
        ring_clear(&canRing);
        totalMsgCount=0; totalTxCount=0; msgPerSecond=0; lastMsgCount=0;
        for(int i=0;i<RECENT_MSG_COUNT;i++) recentData[i].count=0;
    }
}

// ─────────────────────────────────────────────
//  Web Update Task
// ─────────────────────────────────────────────
void webUpdateTask(void* pv) {
    vTaskDelay(pdMS_TO_TICKS(1000));
    while (1) {
        server.handleClient();
        webSocket.loop();

        // 초당 메시지 수 계산
        uint32_t now2 = millis();
        if (now2 - lastMsgCountMs >= 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;i<RECENT_MSG_COUNT&&cnt<30;i++) {
            if(recentData[i].count==0) continue;
            const CANFDLog& m=recentData[i].msg;
            JsonObject mo=msgs.createNestedObject();
            mo["id"]=m.id; mo["dlc"]=m.dlc; mo["len"]=m.len;
            mo["count"]=recentData[i].count;
            bool isFD=(m.flags&0x20)!=0, isBRS=(m.flags&0x10)!=0, isExt=(m.flags&0x80)!=0;
            mo["fd"]=isFD; mo["brs"]=isBRS; mo["ext"]=isExt;
            JsonArray da=mo.createNestedArray("data");
            for(int j=0;j<m.len&&j<16;j++) da.add(m.data[j]);
            cnt++;
        }

        String json; size_t sz=serializeJson(doc,json);
        if(sz>0&&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(tot<fsz&&cl.connected()) {
            size_t r=file.read(buf,min((size_t)(fsz-tot),CHUNK));
            if(r==0) break;
            size_t w=0;
            while(w<r) { size_t wr=cl.write(buf+w,r-w); if(wr>0) 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());
    }
}