/*
 * Byun CAN Logger with Web Interface + RTC Time Synchronization + Timezone Settings
 * Version: 1.7
 * Mode: Listen-Only (Default, Safe) / Normal (Transmit enabled)
 * Features: File comment, Auto time sync, Multiple file operations, CAN mode switching
 */

#include <Arduino.h>
#include <SPI.h>
#include <mcp2515.h>
#include <SoftWire.h>
#include <SD.h>
#include <WiFi.h>
#include <WebServer.h>
#include <WebSocketsServer.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/queue.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"

// GPIO 핀 정의
#define CAN_INT_PIN 27

// HSPI 핀 (CAN)
#define HSPI_MISO 12
#define HSPI_MOSI 13
#define HSPI_SCLK 14
#define HSPI_CS 15

// VSPI 핀 (SD Card)
#define VSPI_MISO 19
#define VSPI_MOSI 23
#define VSPI_SCLK 18
#define VSPI_CS 5

// I2C2 핀 (RTC DS3231) - SoftWire 사용
#define RTC_SDA 25
#define RTC_SCL 26
#define DS3231_ADDRESS 0x68

// 버퍼 설정
#define CAN_QUEUE_SIZE 1000
#define FILE_BUFFER_SIZE 8192
#define MAX_FILENAME_LEN 64
#define RECENT_MSG_COUNT 100
#define MAX_TX_MESSAGES 20

// RTC 동기화 설정
#define RTC_SYNC_INTERVAL_MS 60000  // 1분마다 RTC와 동기화

// 전력 모니터링 설정
#define VOLTAGE_CHECK_INTERVAL_MS 5000  // 5초마다 전압 체크
#define LOW_VOLTAGE_THRESHOLD 3.0       // 3.0V 이하이면 경고

// CAN 메시지 구조체 - 마이크로초 단위 타임스탬프
struct CANMessage {
    uint64_t timestamp_us;  // 마이크로초 단위 Unix timestamp
    uint32_t id;
    uint8_t dlc;
    uint8_t data[8];
} __attribute__((packed));

// 실시간 모니터링용 구조체
struct RecentCANData {
    CANMessage msg;
    uint32_t count;
};

// CAN 송신용 구조체
struct TxMessage {
    uint32_t id;
    bool extended;
    uint8_t dlc;
    uint8_t data[8];
    uint32_t interval;
    uint32_t lastSent;
    bool active;
};

// 시간 동기화 상태
struct TimeSyncStatus {
    bool synchronized;
    uint64_t lastSyncTime;
    int32_t offsetUs;
    uint32_t syncCount;
    bool rtcAvailable;
    uint32_t rtcSyncCount;
} timeSyncStatus = {false, 0, 0, 0, false, 0};

// 전력 모니터링 상태
struct PowerStatus {
    float voltage;           // 현재 전압
    float minVoltage;        // 1초 단위 최소 전압
    bool lowVoltage;
    uint32_t lastCheck;
    uint32_t lastMinReset;   // 최소값 리셋 시간
} powerStatus = {0.0, 999.9, false, 0, 0};

// WiFi AP 기본 설정
char wifiSSID[32] = "Byun_CAN_Logger";
char wifiPassword[64] = "12345678";
int timezoneOffset = 9;  // 기본값: 서울 (UTC+9)

// 전역 변수
SPIClass hspi(HSPI);
SPIClass vspi(VSPI);
MCP2515 mcp2515(HSPI_CS, 10000000, &hspi);

WebServer server(80);
WebSocketsServer webSocket = WebSocketsServer(81);
Preferences preferences;

QueueHandle_t canQueue;
SemaphoreHandle_t sdMutex;
SemaphoreHandle_t rtcMutex;
TaskHandle_t canRxTaskHandle = NULL;
TaskHandle_t sdWriteTaskHandle = NULL;
TaskHandle_t webTaskHandle = NULL;
TaskHandle_t rtcTaskHandle = NULL;

volatile bool loggingEnabled = false;
volatile bool sdCardReady = false;
File logFile;
char currentFilename[MAX_FILENAME_LEN];
uint8_t fileBuffer[FILE_BUFFER_SIZE];
uint16_t bufferIndex = 0;

// 로깅 파일 크기 추적 (실시간 모니터링용)
volatile uint32_t currentFileSize = 0;

// 자동 시간 동기화 상태
volatile bool autoTimeSyncRequested = false;
volatile bool autoTimeSyncCompleted = false;

// RTC 관련
SoftWire rtcWire(RTC_SDA, RTC_SCL);
char rtcSyncBuffer[20];

// CAN 속도 설정
CAN_SPEED currentCanSpeed = CAN_1000KBPS;
const char* canSpeedNames[] = {"125K", "250K", "500K", "1M"};
CAN_SPEED canSpeedValues[] = {CAN_125KBPS, CAN_250KBPS, CAN_500KBPS, CAN_1000KBPS};

// CAN 모드 설정
enum CANMode {
    CAN_MODE_LISTEN_ONLY = 0,  // 수신 전용 (기본값, 안전)
    CAN_MODE_NORMAL = 1         // 송수신 가능 (Transmit 기능용)
};
volatile CANMode currentCanMode = CAN_MODE_LISTEN_ONLY;

// 실시간 모니터링용
RecentCANData recentData[RECENT_MSG_COUNT];
uint32_t totalMsgCount = 0;
uint32_t msgPerSecond = 0;
uint32_t lastMsgCountTime = 0;
uint32_t lastMsgCount = 0;

// 그래프 최대 개수
#define MAX_GRAPH_SIGNALS 20

// CAN 송신용
TxMessage txMessages[MAX_TX_MESSAGES];
uint32_t totalTxCount = 0;

// ========================================
// 설정 저장/로드 함수
// ========================================

const char* getTimezoneName(int offset) {
    switch(offset) {
        case -12: return "Baker Island";
        case -11: return "American Samoa";
        case -10: return "Hawaii";
        case -9: return "Alaska";
        case -8: return "Pacific (LA)";
        case -7: return "Mountain (Denver)";
        case -6: return "Central (Chicago)";
        case -5: return "Eastern (NY)";
        case -4: return "Atlantic";
        case -3: return "Buenos Aires";
        case -2: return "Mid-Atlantic";
        case -1: return "Azores";
        case 0: return "London/UTC";
        case 1: return "Paris/Berlin";
        case 2: return "Athens/Cairo";
        case 3: return "Moscow";
        case 4: return "Dubai";
        case 5: return "Karachi";
        case 6: return "Dhaka";
        case 7: return "Bangkok";
        case 8: return "Beijing/Singapore";
        case 9: return "Seoul/Tokyo";
        case 10: return "Sydney";
        case 11: return "Solomon Islands";
        case 12: return "Auckland/Fiji";
        case 13: return "Samoa";
        case 14: return "Line Islands";
        default: return "Custom";
    }
}

void loadSettings() {
    preferences.begin("can-logger", false);
    
    preferences.getString("wifi_ssid", wifiSSID, sizeof(wifiSSID));
    preferences.getString("wifi_pass", wifiPassword, sizeof(wifiPassword));
    timezoneOffset = preferences.getInt("timezone", 9);
    
    // 설정이 없으면 기본값 사용
    if (strlen(wifiSSID) == 0) {
        strcpy(wifiSSID, "Byun_CAN_Logger");
    }
    if (strlen(wifiPassword) == 0) {
        strcpy(wifiPassword, "12345678");
    }
    
    preferences.end();
}

void saveSettings() {
    preferences.begin("can-logger", false);
    
    preferences.putString("wifi_ssid", wifiSSID);
    preferences.putString("wifi_pass", wifiPassword);
    preferences.putInt("timezone", timezoneOffset);
    
    preferences.end();
    
    Serial.println("\n✓ 설정 저장 완료:");
    Serial.println("----------------------------------------");
    Serial.printf("  WiFi SSID     : %s\n", wifiSSID);
    Serial.printf("  WiFi Password : %s\n", wifiPassword);
    Serial.printf("  Timezone      : UTC%+d (%s)\n", timezoneOffset, getTimezoneName(timezoneOffset));
    Serial.println("----------------------------------------");
    Serial.println("⚠️  재부팅 후 WiFi 설정이 적용됩니다.");
}

// 파일 코멘트 저장/로드 함수
void saveFileComment(const String& filename, const String& comment) {
    preferences.begin("comments", false);
    
    // 파일명을 키로 사용 (최대 15자 제한이 있으므로 해시 사용)
    String key = "c_" + filename;
    if (key.length() > 15) {
        // 파일명이 길면 CRC32로 해시
        uint32_t hash = 0;
        for (int i = 0; i < filename.length(); i++) {
            hash = ((hash << 5) - hash) + filename[i];
        }
        key = "c_" + String(hash, HEX);
    }
    
    preferences.putString(key.c_str(), comment);
    preferences.end();
    
    Serial.printf("✓ 코멘트 저장: %s -> %s\n", filename.c_str(), comment.c_str());
}

String loadFileComment(const String& filename) {
    preferences.begin("comments", true);  // read-only
    
    String key = "c_" + filename;
    if (key.length() > 15) {
        uint32_t hash = 0;
        for (int i = 0; i < filename.length(); i++) {
            hash = ((hash << 5) - hash) + filename[i];
        }
        key = "c_" + String(hash, HEX);
    }
    
    String comment = preferences.getString(key.c_str(), "");
    preferences.end();
    
    return comment;
}

void deleteFileComment(const String& filename) {
    preferences.begin("comments", false);
    
    String key = "c_" + filename;
    if (key.length() > 15) {
        uint32_t hash = 0;
        for (int i = 0; i < filename.length(); i++) {
            hash = ((hash << 5) - hash) + filename[i];
        }
        key = "c_" + String(hash, HEX);
    }
    
    preferences.remove(key.c_str());
    preferences.end();
}

// ========================================
// 전력 모니터링 함수
// ========================================

float readVoltage() {
    // ESP32 내부 ADC로 전압 측정
    // GPIO34는 ADC1_CH6에 연결되어 있음
    // 실제 배터리 전압을 측정하려면 분압 회로가 필요할 수 있음
    
    // 여러 번 샘플링하여 평균값 계산
    const int samples = 10;
    uint32_t sum = 0;
    
    for (int i = 0; i < samples; i++) {
        sum += analogRead(34);
        delayMicroseconds(100);
    }
    
    float avgReading = sum / (float)samples;
    // ADC 값을 전압으로 변환 (0-4095 -> 0-3.3V)
    float voltage = (avgReading / 4095.0) * 3.3;
    
    return voltage;
}

void updatePowerStatus() {
    uint32_t now = millis();
    
    if (now - powerStatus.lastCheck >= VOLTAGE_CHECK_INTERVAL_MS) {
        powerStatus.voltage = readVoltage();
        
        // 1초 단위 최소값 추적
        if (now - powerStatus.lastMinReset >= 1000) {
            powerStatus.minVoltage = powerStatus.voltage;
            powerStatus.lastMinReset = now;
        } else {
            if (powerStatus.voltage < powerStatus.minVoltage) {
                powerStatus.minVoltage = powerStatus.voltage;
            }
        }
        
        // 저전압 경고
        powerStatus.lowVoltage = (powerStatus.voltage < LOW_VOLTAGE_THRESHOLD);
        
        powerStatus.lastCheck = now;
    }
}

// ========================================
// RTC (DS3231) 함수
// ========================================

void initRTC() {
    rtcWire.begin();
    rtcWire.setClock(100000);
    
    rtcWire.beginTransmission(DS3231_ADDRESS);
    uint8_t error = rtcWire.endTransmission();
    
    if (error == 0) {
        timeSyncStatus.rtcAvailable = true;
        Serial.println("✓ RTC DS3231 감지됨");
        
        // RTC에서 시간 읽어서 시스템 시간 초기화
        struct tm timeinfo;
        if (readRTC(&timeinfo)) {
            // RTC의 로컬 시간을 UTC로 변환
            time_t localTime = mktime(&timeinfo);
            time_t utcTime = localTime - (timezoneOffset * 3600);
            
            struct timeval tv = { .tv_sec = utcTime, .tv_usec = 0 };
            settimeofday(&tv, NULL);
            
            timeSyncStatus.synchronized = true;
            timeSyncStatus.rtcSyncCount++;
            
            char timeStr[64];
            strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
            Serial.printf("✓ RTC에서 시간 로드: %s (로컬 시간, UTC%+d)\n", timeStr, timezoneOffset);
        }
    } else {
        timeSyncStatus.rtcAvailable = false;
        Serial.println("⚠️  RTC DS3231 없음 - 웹에서 시간 동기화 필요");
    }
}

uint8_t bcdToDec(uint8_t val) {
    return ((val / 16 * 10) + (val % 16));
}

uint8_t decToBcd(uint8_t val) {
    return ((val / 10 * 16) + (val % 10));
}

bool readRTC(struct tm* timeinfo) {
    if (!timeSyncStatus.rtcAvailable) return false;
    
    if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(100)) != pdTRUE) {
        return false;
    }
    
    rtcWire.beginTransmission(DS3231_ADDRESS);
    rtcWire.write(0x00);
    uint8_t error = rtcWire.endTransmission();
    
    if (error != 0) {
        xSemaphoreGive(rtcMutex);
        return false;
    }
    
    if (rtcWire.requestFrom(DS3231_ADDRESS, 7) != 7) {
        xSemaphoreGive(rtcMutex);
        return false;
    }
    
    uint8_t second = bcdToDec(rtcWire.read() & 0x7F);
    uint8_t minute = bcdToDec(rtcWire.read());
    uint8_t hour = bcdToDec(rtcWire.read() & 0x3F);
    rtcWire.read();
    uint8_t day = bcdToDec(rtcWire.read());
    uint8_t month = bcdToDec(rtcWire.read());
    uint8_t year = bcdToDec(rtcWire.read());
    
    xSemaphoreGive(rtcMutex);
    
    timeinfo->tm_sec = second;
    timeinfo->tm_min = minute;
    timeinfo->tm_hour = hour;
    timeinfo->tm_mday = day;
    timeinfo->tm_mon = month - 1;
    timeinfo->tm_year = year + 100;
    timeinfo->tm_wday = 0;
    timeinfo->tm_yday = 0;
    timeinfo->tm_isdst = -1;
    
    return true;
}

bool writeRTC(struct tm* timeinfo) {
    if (!timeSyncStatus.rtcAvailable) return false;
    
    if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(100)) != pdTRUE) {
        return false;
    }
    
    rtcWire.beginTransmission(DS3231_ADDRESS);
    rtcWire.write(0x00);
    rtcWire.write(decToBcd(timeinfo->tm_sec));
    rtcWire.write(decToBcd(timeinfo->tm_min));
    rtcWire.write(decToBcd(timeinfo->tm_hour));
    rtcWire.write(decToBcd(0));
    rtcWire.write(decToBcd(timeinfo->tm_mday));
    rtcWire.write(decToBcd(timeinfo->tm_mon + 1));
    rtcWire.write(decToBcd(timeinfo->tm_year - 100));
    
    uint8_t error = rtcWire.endTransmission();
    xSemaphoreGive(rtcMutex);
    
    return (error == 0);
}

// ========================================
// 시간 관련 함수
// ========================================

uint64_t getMicrosecondTimestamp() {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return (uint64_t)tv.tv_sec * 1000000ULL + (uint64_t)tv.tv_usec;
}

// ========================================
// CAN 모드 관리 함수
// ========================================

void setCANMode(CANMode mode) {
    currentCanMode = mode;
    
    mcp2515.reset();
    mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ);
    
    if (mode == CAN_MODE_LISTEN_ONLY) {
        mcp2515.setListenOnlyMode();
        Serial.println("✓ CAN 모드: Listen-Only (수신 전용, 버스 영향 없음)");
    } else {
        mcp2515.setNormalMode();
        Serial.println("⚠️  CAN 모드: Normal (송수신 가능, 버스 영향 있음)");
    }
}

// ========================================
// CAN 관련 함수
// ========================================

void IRAM_ATTR canISR() {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    if (canRxTaskHandle != NULL) {
        vTaskNotifyGiveFromISR(canRxTaskHandle, &xHigherPriorityTaskWoken);
        if (xHigherPriorityTaskWoken) {
            portYIELD_FROM_ISR();
        }
    }
}

void canRxTask(void *parameter) {
    CANMessage msg;
    struct can_frame frame;
    
    while (1) {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        
        while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {
            msg.timestamp_us = getMicrosecondTimestamp();
            msg.id = frame.can_id;
            msg.dlc = frame.can_dlc;
            memcpy(msg.data, frame.data, 8);
            
            xQueueSend(canQueue, &msg, 0);
            totalMsgCount++;
            
            // 실시간 모니터링 업데이트
            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].msg.timestamp_us == 0) {
                        recentData[i].msg = msg;
                        recentData[i].count = 1;
                        break;
                    }
                }
            }
        }
    }
}

// ========================================
// SD 카드 관련 함수
// ========================================

void flushBuffer() {
    if (bufferIndex > 0 && loggingEnabled && logFile) {
        if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
            size_t written = logFile.write(fileBuffer, bufferIndex);
            if (written == bufferIndex) {
                currentFileSize += bufferIndex;  // 파일 크기 업데이트
                bufferIndex = 0;
            }
            xSemaphoreGive(sdMutex);
        }
    }
}

void sdWriteTask(void *parameter) {
    CANMessage msg;
    
    while (1) {
        if (xQueueReceive(canQueue, &msg, pdMS_TO_TICKS(10)) == pdTRUE) {
            if (loggingEnabled) {
                if (bufferIndex + sizeof(CANMessage) > FILE_BUFFER_SIZE) {
                    flushBuffer();
                }
                
                memcpy(&fileBuffer[bufferIndex], &msg, sizeof(CANMessage));
                bufferIndex += sizeof(CANMessage);
            }
        } else {
            if (loggingEnabled && bufferIndex > 0) {
                flushBuffer();
            }
            vTaskDelay(pdMS_TO_TICKS(10));
        }
    }
}

void sdMonitorTask(void *parameter) {
    const uint32_t FLUSH_INTERVAL = 1000;
    uint32_t lastFlush = 0;
    
    while (1) {
        uint32_t now = millis();
        
        if (loggingEnabled && (now - lastFlush >= FLUSH_INTERVAL)) {
            if (bufferIndex > 0) {
                flushBuffer();
            }
            
            // 주기적으로 sync 호출하여 SD 카드에 완전히 기록
            if (logFile && xSemaphoreTake(sdMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
                logFile.flush();
                xSemaphoreGive(sdMutex);
            }
            
            lastFlush = now;
        }
        
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// ========================================
// RTC 동기화 태스크
// ========================================

void rtcSyncTask(void *parameter) {
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(RTC_SYNC_INTERVAL_MS));
        
        if (timeSyncStatus.rtcAvailable) {
            struct tm timeinfo;
            if (readRTC(&timeinfo)) {
                // RTC의 로컬 시간을 UTC로 변환
                time_t localTime = mktime(&timeinfo);
                time_t utcTime = localTime - (timezoneOffset * 3600);
                
                struct timeval tv = { .tv_sec = utcTime, .tv_usec = 0 };
                settimeofday(&tv, NULL);
                
                timeSyncStatus.rtcSyncCount++;
                
                char timeStr[64];
                strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
                Serial.printf("🕐 RTC 자동 동기화: %s (로컬 시간)\n", timeStr);
            }
        }
    }
}

// ========================================
// WebSocket 이벤트 핸들러
// ========================================

void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) {
    if (type == WStype_DISCONNECTED) {
        Serial.printf("[%u] WebSocket 연결 끊김\n", num);
    }
    else if (type == WStype_CONNECTED) {
        IPAddress ip = webSocket.remoteIP(num);
        Serial.printf("[%u] WebSocket 연결됨: %s\n", num, ip.toString().c_str());
    }
    else if (type == WStype_TEXT) {
        String message = String((char*)payload);
        
        // JSON 파싱
        int cmdStart = message.indexOf("\"cmd\":\"") + 7;
        int cmdEnd = message.indexOf("\"", cmdStart);
        String cmd = message.substring(cmdStart, cmdEnd);
        
        if (cmd == "startLogging") {
            if (sdCardReady && !loggingEnabled) {
                // 시스템 UTC 시간에 타임존 적용하여 로컬 시간으로 변환
                time_t now;
                time(&now);
                now += (timezoneOffset * 3600);  // UTC에 타임존 오프셋 적용
                struct tm* timeinfo = gmtime(&now);  // UTC 기준으로 분해
                
                snprintf(currentFilename, MAX_FILENAME_LEN, 
                        "/CAN_%04d%02d%02d_%02d%02d%02d.bin",
                        timeinfo->tm_year + 1900, timeinfo->tm_mon + 1, timeinfo->tm_mday,
                        timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec);
                
                if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
                    logFile = SD.open(currentFilename, FILE_WRITE);
                    if (logFile) {
                        loggingEnabled = true;
                        bufferIndex = 0;
                        currentFileSize = 0;  // 파일 크기 초기화
                        Serial.printf("✓ 로깅 시작: %s (UTC%+d)\n", currentFilename, timezoneOffset);
                    }
                    xSemaphoreGive(sdMutex);
                }
                
                // 파일 목록 자동 갱신 (로깅 상태 즉시 반영)
                vTaskDelay(pdMS_TO_TICKS(50));
                webSocketEvent(num, WStype_TEXT, (uint8_t*)"{\"cmd\":\"getFiles\"}", 17);
            }
        }
        else if (cmd == "stopLogging") {
            if (loggingEnabled) {
                loggingEnabled = false;
                flushBuffer();
                if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
                    if (logFile) {
                        logFile.close();
                        Serial.printf("✓ 로깅 중지 (최종 크기: %u bytes)\n", currentFileSize);
                        currentFileSize = 0;
                    }
                    xSemaphoreGive(sdMutex);
                }
                
                // 파일 목록 자동 갱신 (로깅 상태 즉시 반영)
                vTaskDelay(pdMS_TO_TICKS(50));
                webSocketEvent(num, WStype_TEXT, (uint8_t*)"{\"cmd\":\"getFiles\"}", 17);
            }
        }
        else if (cmd == "syncTime") {
            int yearStart = message.indexOf("\"year\":") + 7;
            int monthStart = message.indexOf("\"month\":") + 8;
            int dayStart = message.indexOf("\"day\":") + 6;
            int hourStart = message.indexOf("\"hour\":") + 7;
            int minStart = message.indexOf("\"minute\":") + 9;
            int secStart = message.indexOf("\"second\":") + 9;
            
            struct tm timeinfo;
            timeinfo.tm_year = message.substring(yearStart, message.indexOf(",", yearStart)).toInt() - 1900;
            timeinfo.tm_mon = message.substring(monthStart, message.indexOf(",", monthStart)).toInt() - 1;
            timeinfo.tm_mday = message.substring(dayStart, message.indexOf(",", dayStart)).toInt();
            timeinfo.tm_hour = message.substring(hourStart, message.indexOf(",", hourStart)).toInt();
            timeinfo.tm_min = message.substring(minStart, message.indexOf(",", minStart)).toInt();
            timeinfo.tm_sec = message.substring(secStart, message.indexOf("}", secStart)).toInt();
            
            // 브라우저에서 받은 시간은 로컬 시간이므로 UTC로 변환
            time_t localTime = mktime(&timeinfo);
            time_t utcTime = localTime - (timezoneOffset * 3600);
            
            struct timeval tv = { .tv_sec = utcTime, .tv_usec = 0 };
            settimeofday(&tv, NULL);
            
            if (timeSyncStatus.rtcAvailable) {
                // RTC에는 로컬 시간을 저장
                if (writeRTC(&timeinfo)) {
                    Serial.println("✓ RTC 시간 설정 완료 (로컬 시간)");
                }
            }
            
            timeSyncStatus.synchronized = true;
            timeSyncStatus.lastSyncTime = getMicrosecondTimestamp();
            timeSyncStatus.syncCount++;
            
            char timeStr[64];
            strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
            Serial.printf("✓ 시간 동기화 완료: 로컬=%s (UTC%+d)\n", timeStr, timezoneOffset);
        }
        else if (cmd == "getFiles") {
            if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
                File root = SD.open("/");
                String fileList = "{\"type\":\"files\",\"files\":[";
                bool first = true;
                
                File file = root.openNextFile();
                while (file) {
                    if (!file.isDirectory()) {
                        if (!first) fileList += ",";
                        first = false;
                        
                        String fileName = String(file.name());
                        if (fileName.startsWith("/")) fileName = fileName.substring(1);
                        
                        // 파일 코멘트 로드
                        String comment = loadFileComment(fileName);
                        comment.replace("\"", "\\\"");  // JSON 이스케이프
                        comment.replace("\n", "\\n");
                        
                        fileList += "{\"name\":\"" + fileName + "\",\"size\":" + String(file.size()) + 
                                   ",\"comment\":\"" + comment + "\"}";
                    }
                    file.close();
                    file = root.openNextFile();
                }
                
                fileList += "]}";
                root.close();
                xSemaphoreGive(sdMutex);
                
                webSocket.sendTXT(num, fileList);
            }
        }
        else if (cmd == "deleteFile") {
            // 파일 삭제 명령 처리
            int fileStart = message.indexOf("\"filename\":\"") + 12;
            int fileEnd = message.indexOf("\"", fileStart);
            String filename = message.substring(fileStart, fileEnd);
            
            // 로깅 중인 파일은 삭제 불가
            if (loggingEnabled && currentFilename[0] != '\0') {
                String currentFileStr = String(currentFilename);
                if (currentFileStr.startsWith("/")) currentFileStr = currentFileStr.substring(1);
                
                if (filename == currentFileStr) {
                    String response = "{\"type\":\"deleteResult\",\"success\":false,\"message\":\"Cannot delete file currently being logged\"}";
                    webSocket.sendTXT(num, response);
                    Serial.printf("⚠️  파일 삭제 실패: 로깅 중인 파일입니다 (%s)\n", filename.c_str());
                    return;
                }
            }
            
            // 파일 삭제 수행
            String fullPath = "/" + filename;
            bool deleteSuccess = false;
            
            if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
                if (SD.exists(fullPath)) {
                    deleteSuccess = SD.remove(fullPath);
                    if (deleteSuccess) {
                        deleteFileComment(filename);  // 코멘트도 삭제
                        Serial.printf("✓ 파일 삭제 완료: %s\n", filename.c_str());
                    } else {
                        Serial.printf("✗ 파일 삭제 실패: %s\n", filename.c_str());
                    }
                } else {
                    Serial.printf("✗ 파일이 존재하지 않음: %s\n", filename.c_str());
                }
                xSemaphoreGive(sdMutex);
            }
            
            String response = "{\"type\":\"deleteResult\",\"success\":" + String(deleteSuccess ? "true" : "false") + 
                            ",\"message\":\"" + (deleteSuccess ? "File deleted successfully" : "Failed to delete file") + "\"}";
            webSocket.sendTXT(num, response);
            
            // 파일 목록 자동 갱신
            vTaskDelay(pdMS_TO_TICKS(100));
            webSocketEvent(num, WStype_TEXT, (uint8_t*)"{\"cmd\":\"getFiles\"}", 17);
        }
        else if (cmd == "deleteFiles") {
            // 복수 파일 삭제 명령 처리
            int filesStart = message.indexOf("\"filenames\":[") + 13;
            int filesEnd = message.indexOf("]", filesStart);
            String filesStr = message.substring(filesStart, filesEnd);
            
            int deletedCount = 0;
            int failedCount = 0;
            String failedFiles = "";
            
            // JSON 배열 파싱 (간단한 방식)
            int pos = 0;
            while (pos < filesStr.length()) {
                int quoteStart = filesStr.indexOf("\"", pos);
                if (quoteStart < 0) break;
                int quoteEnd = filesStr.indexOf("\"", quoteStart + 1);
                if (quoteEnd < 0) break;
                
                String filename = filesStr.substring(quoteStart + 1, quoteEnd);
                
                // 로깅 중인 파일은 건너뛰기
                bool isLogging = false;
                if (loggingEnabled && currentFilename[0] != '\0') {
                    String currentFileStr = String(currentFilename);
                    if (currentFileStr.startsWith("/")) currentFileStr = currentFileStr.substring(1);
                    if (filename == currentFileStr) {
                        isLogging = true;
                        failedCount++;
                        if (failedFiles.length() > 0) failedFiles += ", ";
                        failedFiles += filename + " (logging)";
                    }
                }
                
                if (!isLogging) {
                    String fullPath = "/" + filename;
                    
                    if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
                        if (SD.exists(fullPath)) {
                            if (SD.remove(fullPath)) {
                                deleteFileComment(filename);  // 코멘트도 삭제
                                deletedCount++;
                                Serial.printf("✓ 파일 삭제: %s\n", filename.c_str());
                            } else {
                                failedCount++;
                                if (failedFiles.length() > 0) failedFiles += ", ";
                                failedFiles += filename;
                                Serial.printf("✗ 파일 삭제 실패: %s\n", filename.c_str());
                            }
                        } else {
                            failedCount++;
                            if (failedFiles.length() > 0) failedFiles += ", ";
                            failedFiles += filename + " (not found)";
                        }
                        xSemaphoreGive(sdMutex);
                    }
                }
                
                pos = quoteEnd + 1;
            }
            
            String response = "{\"type\":\"deleteResult\",\"success\":true,";
            response += "\"deletedCount\":" + String(deletedCount) + ",";
            response += "\"failedCount\":" + String(failedCount) + ",";
            response += "\"message\":\"Deleted " + String(deletedCount) + " files";
            if (failedCount > 0) {
                response += ", Failed: " + String(failedCount);
            }
            response += "\"}";
            
            webSocket.sendTXT(num, response);
            Serial.printf("✓ 복수 삭제 완료: 성공=%d, 실패=%d\n", deletedCount, failedCount);
            
            // 파일 목록 자동 갱신
            vTaskDelay(pdMS_TO_TICKS(100));
            webSocketEvent(num, WStype_TEXT, (uint8_t*)"{\"cmd\":\"getFiles\"}", 17);
        }
        else if (cmd == "setSpeed") {
            int speedStart = message.indexOf("\"speed\":") + 8;
            int speedValue = message.substring(speedStart, message.indexOf("}", speedStart)).toInt();
            
            if (speedValue >= 0 && speedValue < 4) {
                currentCanSpeed = canSpeedValues[speedValue];
                
                // 현재 모드 유지하면서 속도만 변경
                mcp2515.reset();
                mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ);
                
                if (currentCanMode == CAN_MODE_LISTEN_ONLY) {
                    mcp2515.setListenOnlyMode();
                } else {
                    mcp2515.setNormalMode();
                }
                
                Serial.printf("✓ CAN 속도 변경: %s\n", canSpeedNames[speedValue]);
            }
        }
        else if (cmd == "setCanMode") {
            // CAN 모드 변경 (Listen-Only ↔ Normal)
            int modeStart = message.indexOf("\"mode\":") + 7;
            int modeValue = message.substring(modeStart, message.indexOf("}", modeStart)).toInt();
            
            if (modeValue == 0) {
                setCANMode(CAN_MODE_LISTEN_ONLY);
            } else if (modeValue == 1) {
                setCANMode(CAN_MODE_NORMAL);
            }
            
            String response = "{\"type\":\"canModeResult\",\"mode\":" + String(currentCanMode) + "}";
            webSocket.sendTXT(num, response);
        }
        else if (cmd == "getCanMode") {
            // 현재 CAN 모드 조회
            String response = "{\"type\":\"canModeStatus\",\"mode\":" + String(currentCanMode) + "}";
            webSocket.sendTXT(num, response);
        }
        else if (cmd == "getSettings") {
            String settings = "{\"type\":\"settings\",";
            settings += "\"ssid\":\"" + String(wifiSSID) + "\",";
            settings += "\"password\":\"" + String(wifiPassword) + "\",";
            settings += "\"timezone\":" + String(timezoneOffset) + "}";
            
            webSocket.sendTXT(num, settings);
        }
        else if (cmd == "saveSettings") {
            int ssidStart = message.indexOf("\"ssid\":\"") + 8;
            int ssidEnd = message.indexOf("\"", ssidStart);
            int passStart = message.indexOf("\"password\":\"") + 12;
            int passEnd = message.indexOf("\"", passStart);
            int tzStart = message.indexOf("\"timezone\":") + 11;
            
            String newSSID = message.substring(ssidStart, ssidEnd);
            String newPassword = message.substring(passStart, passEnd);
            int newTimezone = message.substring(tzStart, message.indexOf("}", tzStart)).toInt();
            
            newSSID.toCharArray(wifiSSID, sizeof(wifiSSID));
            newPassword.toCharArray(wifiPassword, sizeof(wifiPassword));
            timezoneOffset = newTimezone;
            
            saveSettings();
            
            String response = "{\"type\":\"settingsResult\",\"success\":true}";
            webSocket.sendTXT(num, response);
        }
        else if (cmd == "sendCAN") {
            // Normal Mode에서만 송신 가능
            if (currentCanMode != CAN_MODE_NORMAL) {
                String response = "{\"type\":\"error\",\"message\":\"Switch to Normal Mode to send CAN messages\"}";
                webSocket.sendTXT(num, response);
                Serial.println("⚠️  CAN 송신 차단: Listen-Only Mode");
                return;
            }
            
            int idStart = message.indexOf("\"id\":\"") + 6;
            int idEnd = message.indexOf("\"", idStart);
            String idStr = message.substring(idStart, idEnd);
            
            int dataStart = message.indexOf("\"data\":\"") + 8;
            int dataEnd = message.indexOf("\"", dataStart);
            String dataStr = message.substring(dataStart, dataEnd);
            
            uint32_t canId = strtoul(idStr.c_str(), NULL, 16);
            
            struct can_frame frame;
            frame.can_id = canId;
            frame.can_dlc = 0;
            
            dataStr.replace(" ", "");
            for (int i = 0; i < dataStr.length() && i < 16; i += 2) {
                String byteStr = dataStr.substring(i, i + 2);
                frame.data[frame.can_dlc++] = strtoul(byteStr.c_str(), NULL, 16);
            }
            
            MCP2515::ERROR result = mcp2515.sendMessage(&frame);
            if (result == MCP2515::ERROR_OK) {
                totalTxCount++;
                Serial.printf("✓ CAN 송신: ID=0x%X, DLC=%d\n", canId, frame.can_dlc);
            } else {
                Serial.printf("✗ CAN 송신 실패: ID=0x%X, Error=%d\n", canId, result);
            }
        }
        else if (cmd == "addTxMessage") {
            // Normal Mode에서만 주기 송신 가능
            if (currentCanMode != CAN_MODE_NORMAL) {
                String response = "{\"type\":\"error\",\"message\":\"Switch to Normal Mode for periodic transmission\"}";
                webSocket.sendTXT(num, response);
                Serial.println("⚠️  주기 송신 차단: Listen-Only Mode");
                return;
            }
            
            int slot = -1;
            for (int i = 0; i < MAX_TX_MESSAGES; i++) {
                if (!txMessages[i].active) {
                    slot = i;
                    break;
                }
            }
            
            if (slot >= 0) {
                int idStart = message.indexOf("\"id\":\"") + 6;
                int idEnd = message.indexOf("\"", idStart);
                String idStr = message.substring(idStart, idEnd);
                
                int dataStart = message.indexOf("\"data\":\"") + 8;
                int dataEnd = message.indexOf("\"", dataStart);
                String dataStr = message.substring(dataStart, dataEnd);
                
                int intervalStart = message.indexOf("\"interval\":") + 11;
                int intervalEnd = message.indexOf(",", intervalStart);
                if (intervalEnd < 0) intervalEnd = message.indexOf("}", intervalStart);
                int interval = message.substring(intervalStart, intervalEnd).toInt();
                
                txMessages[slot].id = strtoul(idStr.c_str(), NULL, 16);
                txMessages[slot].extended = false;
                txMessages[slot].interval = interval;
                txMessages[slot].lastSent = 0;
                txMessages[slot].active = true;
                
                dataStr.replace(" ", "");
                txMessages[slot].dlc = 0;
                for (int i = 0; i < dataStr.length() && i < 16; i += 2) {
                    String byteStr = dataStr.substring(i, i + 2);
                    txMessages[slot].data[txMessages[slot].dlc++] = strtoul(byteStr.c_str(), NULL, 16);
                }
                
                Serial.printf("✓ 주기 송신 추가: Slot=%d, ID=0x%X, Interval=%dms\n", 
                            slot, txMessages[slot].id, interval);
            }
        }
        else if (cmd == "removeTxMessage") {
            int slotStart = message.indexOf("\"slot\":") + 7;
            int slot = message.substring(slotStart, message.indexOf("}", slotStart)).toInt();
            
            if (slot >= 0 && slot < MAX_TX_MESSAGES) {
                txMessages[slot].active = false;
                Serial.printf("✓ 주기 송신 제거: Slot=%d\n", slot);
            }
        }
        else if (cmd == "getTxMessages") {
            String txList = "{\"type\":\"txMessages\",\"messages\":[";
            bool first = true;
            
            for (int i = 0; i < MAX_TX_MESSAGES; i++) {
                if (txMessages[i].active) {
                    if (!first) txList += ",";
                    first = false;
                    
                    char idStr[16], dataStr[32];
                    sprintf(idStr, "%08X", txMessages[i].id);
                    
                    dataStr[0] = '\0';
                    for (int j = 0; j < txMessages[i].dlc; j++) {
                        char byteStr[4];
                        sprintf(byteStr, "%02X", txMessages[i].data[j]);
                        strcat(dataStr, byteStr);
                        if (j < txMessages[i].dlc - 1) strcat(dataStr, " ");
                    }
                    
                    txList += "{\"slot\":" + String(i) + 
                            ",\"id\":\"" + String(idStr) + 
                            "\",\"data\":\"" + String(dataStr) + 
                            "\",\"interval\":" + String(txMessages[i].interval) + "}";
                }
            }
            
            txList += "]}";
            webSocket.sendTXT(num, txList);
        }
        else if (cmd == "saveComment") {
            // 파일 코멘트 저장
            int fileStart = message.indexOf("\"filename\":\"") + 12;
            int fileEnd = message.indexOf("\"", fileStart);
            String filename = message.substring(fileStart, fileEnd);
            
            int commentStart = message.indexOf("\"comment\":\"") + 11;
            int commentEnd = message.lastIndexOf("\"");
            String comment = message.substring(commentStart, commentEnd);
            
            // JSON 이스케이프 복원
            comment.replace("\\n", "\n");
            comment.replace("\\\"", "\"");
            
            saveFileComment(filename, comment);
            
            String response = "{\"type\":\"commentResult\",\"success\":true,\"message\":\"Comment saved\"}";
            webSocket.sendTXT(num, response);
        }
        else if (cmd == "requestAutoTimeSync") {
            // 클라이언트가 자동 시간 동기화 완료를 알림
            autoTimeSyncCompleted = true;
            Serial.println("✓ 자동 시간 동기화 완료 (클라이언트)");
        }
    }
}

// ========================================
// 주기 송신 태스크 (Normal Mode에서만 동작)
// ========================================

void txTask(void *parameter) {
    while (1) {
        // Normal Mode에서만 주기 송신 동작
        if (currentCanMode == CAN_MODE_NORMAL) {
            uint32_t now = millis();
            
            for (int i = 0; i < MAX_TX_MESSAGES; i++) {
                if (txMessages[i].active) {
                    if (now - txMessages[i].lastSent >= txMessages[i].interval) {
                        struct can_frame frame;
                        frame.can_id = txMessages[i].id;
                        frame.can_dlc = txMessages[i].dlc;
                        memcpy(frame.data, txMessages[i].data, 8);
                        
                        if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) {
                            totalTxCount++;
                            txMessages[i].lastSent = now;
                        }
                    }
                }
            }
            
            vTaskDelay(pdMS_TO_TICKS(1));
        } else {
            // Listen-Only Mode에서는 대기
            vTaskDelay(pdMS_TO_TICKS(100));
        }
    }
}

// ========================================
// 웹 업데이트 태스크
// ========================================

void webUpdateTask(void *parameter) {
    const uint32_t STATUS_UPDATE_INTERVAL = 500;
    const uint32_t CAN_UPDATE_INTERVAL = 200;
    
    uint32_t lastStatusUpdate = 0;
    uint32_t lastCanUpdate = 0;
    uint32_t lastMsgSpeedCalc = 0;
    
    while (1) {
        webSocket.loop();
        updatePowerStatus();
        
        uint32_t now = millis();
        
        // 자동 시간 동기화 요청 (최초 1회, 클라이언트 연결 후)
        if (autoTimeSyncRequested && !autoTimeSyncCompleted) {
            if (webSocket.connectedClients() > 0) {
                String syncRequest = "{\"type\":\"autoTimeSyncRequest\"}";
                webSocket.broadcastTXT(syncRequest);
                Serial.println("⏰ 자동 시간 동기화 요청 전송");
                vTaskDelay(pdMS_TO_TICKS(1000));  // 1초 대기
            }
        }
        
        // 메시지 속도 계산
        if (now - lastMsgSpeedCalc >= 1000) {
            msgPerSecond = totalMsgCount - lastMsgCount;
            lastMsgCount = totalMsgCount;
            lastMsgSpeedCalc = now;
        }
        
        // 상태 업데이트
        if (now - lastStatusUpdate >= STATUS_UPDATE_INTERVAL) {
            String status = "{\"type\":\"status\",";
            status += "\"logging\":" + String(loggingEnabled ? "true" : "false") + ",";
            status += "\"sdReady\":" + String(sdCardReady ? "true" : "false") + ",";
            status += "\"msgCount\":" + String(totalMsgCount) + ",";
            status += "\"msgSpeed\":" + String(msgPerSecond) + ",";
            status += "\"timeSync\":" + String(timeSyncStatus.synchronized ? "true" : "false") + ",";
            status += "\"syncCount\":" + String(timeSyncStatus.syncCount) + ",";
            status += "\"rtcAvailable\":" + String(timeSyncStatus.rtcAvailable ? "true" : "false") + ",";
            status += "\"rtcSyncCount\":" + String(timeSyncStatus.rtcSyncCount) + ",";
            status += "\"voltage\":" + String(powerStatus.voltage, 2) + ",";
            status += "\"minVoltage\":" + String(powerStatus.minVoltage, 2) + ",";
            status += "\"lowVoltage\":" + String(powerStatus.lowVoltage ? "true" : "false") + ",";
            status += "\"queueUsed\":" + String(uxQueueMessagesWaiting(canQueue)) + ",";
            status += "\"queueSize\":" + String(CAN_QUEUE_SIZE) + ",";
            status += "\"canMode\":" + String(currentCanMode) + ",";
            
            // SD 카드 용량 정보 추가
            if (sdCardReady) {
                uint64_t cardSize = SD.cardSize() / (1024 * 1024);  // MB
                uint64_t totalBytes = SD.totalBytes() / (1024 * 1024);  // MB
                uint64_t usedBytes = SD.usedBytes() / (1024 * 1024);  // MB
                uint64_t freeBytes = totalBytes - usedBytes;  // MB
                
                status += "\"sdTotalMB\":" + String((uint32_t)totalBytes) + ",";
                status += "\"sdUsedMB\":" + String((uint32_t)usedBytes) + ",";
                status += "\"sdFreeMB\":" + String((uint32_t)freeBytes) + ",";
            } else {
                status += "\"sdTotalMB\":0,";
                status += "\"sdUsedMB\":0,";
                status += "\"sdFreeMB\":0,";
            }
            
            if (loggingEnabled && logFile) {
                status += "\"currentFile\":\"" + String(currentFilename) + "\",";
                status += "\"currentFileSize\":" + String(currentFileSize);
            } else {
                status += "\"currentFile\":\"\",";
                status += "\"currentFileSize\":0";
            }
            status += "}";
            
            webSocket.broadcastTXT(status);
            lastStatusUpdate = now;
        }
        
        // CAN 메시지 일괄 업데이트
        if (now - lastCanUpdate >= CAN_UPDATE_INTERVAL) {
            String canBatch = "{\"type\":\"canBatch\",\"messages\":[";
            bool first = true;
            int messageCount = 0;
            
            for (int i = 0; i < RECENT_MSG_COUNT; i++) {
                if (recentData[i].msg.timestamp_us > 0) {
                    CANMessage* msg = &recentData[i].msg;
                    
                    if (!first) canBatch += ",";
                    first = false;
                    
                    canBatch += "{\"id\":\"";
                    if (msg->id < 0x10) canBatch += "0";
                    if (msg->id < 0x100) canBatch += "0";
                    if (msg->id < 0x1000) canBatch += "0";
                    canBatch += String(msg->id, HEX);
                    canBatch += "\",\"dlc\":" + String(msg->dlc);
                    canBatch += ",\"data\":\"";
                    
                    for (int j = 0; j < msg->dlc; j++) {
                        if (msg->data[j] < 0x10) canBatch += "0";
                        canBatch += String(msg->data[j], HEX);
                        if (j < msg->dlc - 1) canBatch += " ";
                    }
                    
                    uint64_t timestamp_ms = msg->timestamp_us / 1000;
                    canBatch += "\",\"timestamp\":" + String((uint32_t)timestamp_ms);
                    canBatch += ",\"count\":" + String(recentData[i].count) + "}";
                    
                    messageCount++;
                }
            }
            
            canBatch += "]}";
            
            if (messageCount > 0) {
                webSocket.broadcastTXT(canBatch);
            }
            
            lastCanUpdate = now;
        }
        
        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

void setup() {
    Serial.begin(115200);
    delay(1000);
    Serial.println("\n========================================");
    Serial.println("  ESP32 CAN Logger v1.7                ");
    Serial.println("  Listen-Only Mode (Default, Safe)    ");
    Serial.println("========================================");
    
    // 설정 로드
    loadSettings();
    
    // 설정값 표시
    Serial.println("\n📋 현재 설정값:");
    Serial.println("----------------------------------------");
    Serial.printf("  WiFi SSID     : %s\n", wifiSSID);
    Serial.printf("  WiFi Password : %s\n", wifiPassword);
    Serial.printf("  Timezone      : UTC%+d (%s)\n", timezoneOffset, getTimezoneName(timezoneOffset));
    Serial.println("----------------------------------------");
    Serial.println("💡 설정 변경: http://[IP]/settings\n");
    
    memset(recentData, 0, sizeof(recentData));
    memset(txMessages, 0, sizeof(txMessages));
    
    pinMode(CAN_INT_PIN, INPUT_PULLUP);
    
    // ADC 설정 (전압 모니터링용)
    analogSetAttenuation(ADC_11db);  // 0-3.3V 범위
    
    // SPI 초기화
    hspi.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS);
    vspi.begin(VSPI_SCLK, VSPI_MISO, VSPI_MOSI, VSPI_CS);
    
    // MCP2515 초기화 - Listen-Only Mode (버스에 영향 없음)
    mcp2515.reset();
    mcp2515.setBitrate(CAN_1000KBPS, MCP_8MHZ);
    mcp2515.setListenOnlyMode();  // ⭐ Listen-Only Mode: ACK 전송 안 함, 수신만 가능
    Serial.println("✓ MCP2515 초기화 완료 (Listen-Only Mode)");
    
    // SD 카드 초기화
    if (SD.begin(VSPI_CS, vspi)) {
        sdCardReady = true;
        Serial.println("✓ SD 카드 초기화 완료");
    } else {
        Serial.println("✗ SD 카드 초기화 실패");
    }
    
    // Mutex 생성
    sdMutex = xSemaphoreCreateMutex();
    rtcMutex = xSemaphoreCreateMutex();
    
    // RTC 초기화 (SoftWire 사용)
    initRTC();
    
    // WiFi AP 시작
    WiFi.softAP(wifiSSID, wifiPassword);
    Serial.print("✓ AP SSID: ");
    Serial.println(wifiSSID);
    Serial.print("✓ AP IP: ");
    Serial.println(WiFi.softAPIP());
    
    // WebSocket 시작
    webSocket.begin();
    webSocket.onEvent(webSocketEvent);
    
    // 웹 서버 라우팅
    server.on("/", HTTP_GET, []() {
        server.send_P(200, "text/html", index_html);
    });
    
    server.on("/transmit", HTTP_GET, []() {
        server.send_P(200, "text/html", transmit_html);
    });
    
    server.on("/graph", HTTP_GET, []() {
        server.send_P(200, "text/html", graph_html);
    });

    server.on("/graph-view", HTTP_GET, []() {
        server.send_P(200, "text/html", graph_viewer_html);
    });

    server.on("/settings", HTTP_GET, []() {
        server.send_P(200, "text/html", settings_html);
    });

    server.on("/download", HTTP_GET, []() {
        if (server.hasArg("file")) {
            String filename = "/" + server.arg("file");
            
            if (SD.exists(filename)) {
                File file = SD.open(filename, FILE_READ);
                if (file) {
                    String displayName = server.arg("file");
                    
                    server.sendHeader("Content-Disposition", 
                        "attachment; filename=\"" + displayName + "\"");
                    server.sendHeader("Content-Type", "application/octet-stream");
                    
                    server.streamFile(file, "application/octet-stream");
                    file.close();
                } else {
                    server.send(500, "text/plain", "Failed to open file");
                }
            } else {
                server.send(404, "text/plain", "File not found");
            }
        } else {
            server.send(400, "text/plain", "Bad request");
        }
    });
    
    // 파일 삭제 HTTP 엔드포인트 추가 (백업용 - 주로 WebSocket 사용)
    server.on("/delete", HTTP_GET, []() {
        if (server.hasArg("file")) {
            String filename = server.arg("file");
            
            // 로깅 중인 파일은 삭제 불가
            if (loggingEnabled && currentFilename[0] != '\0') {
                String currentFileStr = String(currentFilename);
                if (currentFileStr.startsWith("/")) currentFileStr = currentFileStr.substring(1);
                
                if (filename == currentFileStr) {
                    server.send(403, "text/plain", "Cannot delete file currently being logged");
                    return;
                }
            }
            
            String fullPath = "/" + filename;
            
            if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
                if (SD.exists(fullPath)) {
                    if (SD.remove(fullPath)) {
                        xSemaphoreGive(sdMutex);
                        server.send(200, "text/plain", "File deleted successfully");
                        Serial.printf("✓ HTTP 파일 삭제: %s\n", filename.c_str());
                    } else {
                        xSemaphoreGive(sdMutex);
                        server.send(500, "text/plain", "Failed to delete file");
                    }
                } else {
                    xSemaphoreGive(sdMutex);
                    server.send(404, "text/plain", "File not found");
                }
            } else {
                server.send(503, "text/plain", "SD card busy");
            }
        } else {
            server.send(400, "text/plain", "Bad request");
        }
    });
    
    server.begin();
    
    // Queue 생성
    canQueue = xQueueCreate(CAN_QUEUE_SIZE, sizeof(CANMessage));
    
    if (canQueue == NULL || sdMutex == NULL || rtcMutex == NULL) {
        Serial.println("✗ RTOS 객체 생성 실패!");
        while (1) delay(1000);
    }
    
    // CAN 인터럽트 활성화
    attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING);
    
    // Task 생성
    xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 4096, NULL, 4, &canRxTaskHandle, 1);
    xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 12288, NULL, 3, &sdWriteTaskHandle, 1);
    xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 0);
    xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 8192, NULL, 2, &webTaskHandle, 0);
    xTaskCreatePinnedToCore(txTask, "TX_TASK", 4096, NULL, 2, NULL, 1);
    
    // RTC 동기화 Task
    if (timeSyncStatus.rtcAvailable) {
        xTaskCreatePinnedToCore(rtcSyncTask, "RTC_SYNC", 4096, NULL, 0, &rtcTaskHandle, 0);
        Serial.println("✓ RTC 자동 동기화 Task 시작");
    } else {
        // RTC가 없으면 웹에서 자동 시간 동기화 요청
        autoTimeSyncRequested = true;
        Serial.println("⏰ 웹 브라우저 연결 시 자동 시간 동기화 예정");
    }
    
    Serial.println("✓ 모든 태스크 시작 완료");
    Serial.println("\n========================================");
    Serial.println("  웹 인터페이스 접속 방법");
    Serial.println("========================================");
    Serial.printf("  1. WiFi 연결\n");
    Serial.printf("     - SSID     : %s\n", wifiSSID);
    Serial.printf("     - Password : %s\n", wifiPassword);
    Serial.printf("  2. 웹 브라우저에서 접속\n");
    Serial.print("     - URL      : http://");
    Serial.println(WiFi.softAPIP());
    Serial.println("  3. 페이지 메뉴:");
    Serial.println("     - Monitor  : /");
    Serial.println("     - Transmit : /transmit");
    Serial.println("     - Graph    : /graph");
    Serial.println("     - Settings : /settings");
    Serial.println("========================================");
    Serial.printf("  Timezone: UTC%+d (%s)\n", timezoneOffset, getTimezoneName(timezoneOffset));
    Serial.println("========================================\n");
}

void loop() {
    server.handleClient();
    vTaskDelay(pdMS_TO_TICKS(10));
    
    static uint32_t lastPrint = 0;
    if (millis() - lastPrint > 10000) {
        Serial.printf("[상태] 큐: %d/%d | 로깅: %s | SD: %s | RX: %lu | TX: %lu | 파일크기: %u | 시간: %s | RTC: %s(%u) | 전압: %.2fV%s\n",
            uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE,
            loggingEnabled ? "ON " : "OFF",
            sdCardReady ? "OK" : "NO",
            totalMsgCount, totalTxCount,
            currentFileSize,
            timeSyncStatus.synchronized ? "OK" : "NO",
            timeSyncStatus.rtcAvailable ? "OK" : "NO",
            timeSyncStatus.rtcSyncCount,
            powerStatus.voltage,
            powerStatus.lowVoltage ? " ⚠️" : "");
        lastPrint = millis();
    }
}
