/*
 * Byun CAN Logger with Web Interface + RTC Time Synchronization
 * Version: 2.0
 * Added: Phone time sync to RTC, File comments, MCP2515 mode control
 */

#include <Arduino.h>
#include <SPI.h>
#include <mcp2515.h>
#include <SoftWire.h>
#include <SD.h>
#include <WiFi.h>
#include <esp_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 2000  // 1000 → 2000으로 증가
#define FILE_BUFFER_SIZE 16384  // 8192 → 16384 (16KB)로 증가
#define MAX_FILENAME_LEN 64
#define RECENT_MSG_COUNT 100
#define MAX_TX_MESSAGES 20
#define MAX_COMMENT_LEN 128

// 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;
};

// CAN 시퀀스 스텝 구조체
struct SequenceStep {
    uint32_t canId;
    bool extended;
    uint8_t dlc;
    uint8_t data[8];
    uint32_t delayMs;  // 이 스텝 실행 후 대기 시간 (ms)
};

// CAN 시퀀스 구조체
struct CANSequence {
    char name[32];
    SequenceStep steps[20];  // 최대 20개 스텝
    uint8_t stepCount;
    uint8_t repeatMode;  // 0=한번, 1=특정횟수, 2=무한
    uint32_t repeatCount;  // repeatMode=1일 때 반복 횟수
};

// 시퀀스 실행 상태
struct SequenceRuntime {
    bool running;
    uint8_t currentStep;
    uint32_t currentRepeat;
    uint32_t lastStepTime;
    int8_t activeSequenceIndex;  // 실행 중인 시퀀스 인덱스
};

// 파일 커멘트 구조체
struct FileComment {
    char filename[MAX_FILENAME_LEN];
    char comment[MAX_COMMENT_LEN];
};

// 시간 동기화 상태
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};

// MCP2515 레지스터 주소 정의 (라이브러리에 없는 경우)
#ifndef MCP_CANCTRL
#define MCP_CANCTRL 0x0F
#endif

#ifndef MCP_CANSTAT
#define MCP_CANSTAT 0x0E
#endif

// MCP2515 모드 정의
enum MCP2515Mode {
    MCP_MODE_NORMAL = 0,
    MCP_MODE_LISTEN_ONLY = 1,
    MCP_MODE_LOOPBACK = 2,
    MCP_MODE_TRANSMIT = 3  // 송신만 (ACK 없음)
};

// WiFi AP 기본 설정
char wifiSSID[32] = "Byun_CAN_Logger";
char wifiPassword[64] = "12345678";

// WiFi Station 모드 설정 (추가)
bool enableSTAMode = false;  // APSTA 모드 활성화 여부
char staSSID[32] = "";       // 연결할 WiFi SSID
char staPassword[64] = "";   // 연결할 WiFi 비밀번호

// 전역 변수
SPIClass hspi(HSPI);
SPIClass vspi(VSPI);
MCP2515 mcp2515(HSPI_CS, 20000000, &hspi);  // 20MHz로 증가 (10MHz → 20MHz)

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

// Forward declaration
void IRAM_ATTR canISR();

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;

// 현재 MCP2515 모드
MCP2515Mode currentMcpMode = MCP_MODE_NORMAL;

// 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};

// 실시간 모니터링용
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;

// CAN 시퀀스 (최대 10개 저장 가능)
#define MAX_SEQUENCES 10
CANSequence sequences[MAX_SEQUENCES];
uint8_t sequenceCount = 0;
SequenceRuntime seqRuntime = {false, 0, 0, 0, -1};

// 파일 커멘트 저장 (최대 50개)
#define MAX_FILE_COMMENTS 50
FileComment fileComments[MAX_FILE_COMMENTS];
int commentCount = 0;

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

void loadSettings() {
    preferences.begin("can-logger", false);
    
    // WiFi AP 설정 로드
    preferences.getString("wifi_ssid", wifiSSID, sizeof(wifiSSID));
    preferences.getString("wifi_pass", wifiPassword, sizeof(wifiPassword));
    
    // WiFi STA 모드 설정 로드 (추가)
    enableSTAMode = preferences.getBool("sta_enable", false);
    preferences.getString("sta_ssid", staSSID, sizeof(staSSID));
    preferences.getString("sta_pass", staPassword, sizeof(staPassword));
    
    // 설정이 없으면 기본값 사용
    if (strlen(wifiSSID) == 0) {
        strcpy(wifiSSID, "Byun_CAN_Logger");
    }
    if (strlen(wifiPassword) == 0) {
        strcpy(wifiPassword, "12345678");
    }
    
    // CAN 속도 로드 (기본값: 1Mbps = 3)
    int speedIndex = preferences.getInt("can_speed", 3);
    if (speedIndex >= 0 && speedIndex < 4) {
        currentCanSpeed = canSpeedValues[speedIndex];
        Serial.printf("✓ 저장된 CAN 속도 로드: %s\n", canSpeedNames[speedIndex]);
    }
    
    // MCP2515 모드 로드 (기본값: Normal = 0)
    int mcpMode = preferences.getInt("mcp_mode", 0);
    if (mcpMode >= 0 && mcpMode <= 3) {
        currentMcpMode = (MCP2515Mode)mcpMode;
        Serial.printf("✓ 저장된 MCP 모드 로드: %d\n", mcpMode);
    }
    
    preferences.end();
    
    // STA 모드 설정 출력
    if (enableSTAMode && strlen(staSSID) > 0) {
        Serial.printf("✓ WiFi STA 모드: 활성화 (SSID: %s)\n", staSSID);
    }
}

void saveSettings() {
    preferences.begin("can-logger", false);
    
    // WiFi AP 설정 저장
    preferences.putString("wifi_ssid", wifiSSID);
    preferences.putString("wifi_pass", wifiPassword);
    
    // WiFi STA 모드 설정 저장 (추가)
    preferences.putBool("sta_enable", enableSTAMode);
    preferences.putString("sta_ssid", staSSID);
    preferences.putString("sta_pass", staPassword);
    
    preferences.end();
    
    Serial.println("\n✓ 설정 저장 완료:");
    Serial.println("----------------------------------------");
    Serial.printf("  WiFi AP SSID     : %s\n", wifiSSID);
    Serial.printf("  WiFi AP Password : %s\n", wifiPassword);
    if (enableSTAMode && strlen(staSSID) > 0) {
        Serial.printf("  STA Mode         : 활성화\n");
        Serial.printf("  STA SSID         : %s\n", staSSID);
    } else {
        Serial.printf("  STA Mode         : 비활성화\n");
    }
    Serial.println("----------------------------------------");
    Serial.println("⚠️  재부팅 후 WiFi 설정이 적용됩니다.");
}

void saveCANSettings() {
    preferences.begin("can-logger", false);
    
    // CAN 속도 저장 (인덱스로 저장)
    int speedIndex = 3;  // 기본값: 1M
    for (int i = 0; i < 4; i++) {
        if (canSpeedValues[i] == currentCanSpeed) {
            speedIndex = i;
            break;
        }
    }
    preferences.putInt("can_speed", speedIndex);
    
    // MCP2515 모드 저장
    preferences.putInt("mcp_mode", (int)currentMcpMode);
    
    preferences.end();
    
    Serial.println("\n✓ CAN 설정 저장 완료:");
    Serial.println("----------------------------------------");
    Serial.printf("  CAN Speed     : %s\n", canSpeedNames[speedIndex]);
    Serial.printf("  MCP Mode      : %d\n", (int)currentMcpMode);
    Serial.println("----------------------------------------");
}

// ========================================
// 시퀀스 관리 함수
// ========================================

void loadSequences() {
    if (!sdCardReady) return;
    
    if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
        File seqFile = SD.open("/sequences.bin", FILE_READ);
        if (seqFile) {
            sequenceCount = 0;
            while (seqFile.available() && sequenceCount < MAX_SEQUENCES) {
                seqFile.read((uint8_t*)&sequences[sequenceCount], sizeof(CANSequence));
                sequenceCount++;
            }
            seqFile.close();
            Serial.printf("✓ 시퀀스 로드: %d개\n", sequenceCount);
        }
        xSemaphoreGive(sdMutex);
    }
}

void saveSequences() {
    if (!sdCardReady) return;
    
    if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
        // 기존 파일 삭제
        if (SD.exists("/sequences.bin")) {
            SD.remove("/sequences.bin");
        }
        
        File seqFile = SD.open("/sequences.bin", FILE_WRITE);
        if (seqFile) {
            for (int i = 0; i < sequenceCount; i++) {
                seqFile.write((uint8_t*)&sequences[i], sizeof(CANSequence));
            }
            seqFile.close();
            Serial.println("✓ 시퀀스 저장 완료");
        }
        xSemaphoreGive(sdMutex);
    }
}

int addSequence(const CANSequence& seq) {
    if (sequenceCount >= MAX_SEQUENCES) {
        return -1;  // 가득 참
    }
    
    sequences[sequenceCount] = seq;
    sequenceCount++;
    saveSequences();
    return sequenceCount - 1;
}

bool deleteSequence(uint8_t index) {
    if (index >= sequenceCount) return false;
    
    // 배열 왼쪽으로 시프트
    for (int i = index; i < sequenceCount - 1; i++) {
        sequences[i] = sequences[i + 1];
    }
    sequenceCount--;
    saveSequences();
    return true;
}

void stopSequence() {
    seqRuntime.running = false;
    seqRuntime.currentStep = 0;
    seqRuntime.currentRepeat = 0;
    seqRuntime.activeSequenceIndex = -1;
    Serial.println("✓ 시퀀스 실행 중지");
}

void startSequence(uint8_t index) {
    if (index >= sequenceCount) return;
    
    seqRuntime.running = true;
    seqRuntime.currentStep = 0;
    seqRuntime.currentRepeat = 0;
    seqRuntime.lastStepTime = millis();
    seqRuntime.activeSequenceIndex = index;
    
    Serial.printf("✓ 시퀀스 실행 시작: %s\n", sequences[index].name);
}

// ========================================
// 파일 커멘트 관리 함수
// ========================================

void loadFileComments() {
    if (!sdCardReady) return;
    
    if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
        File commentFile = SD.open("/comments.txt", FILE_READ);
        if (commentFile) {
            commentCount = 0;
            while (commentFile.available() && commentCount < MAX_FILE_COMMENTS) {
                String line = commentFile.readStringUntil('\n');
                line.trim();
                
                int separatorPos = line.indexOf('|');
                if (separatorPos > 0) {
                    String filename = line.substring(0, separatorPos);
                    String comment = line.substring(separatorPos + 1);
                    
                    strncpy(fileComments[commentCount].filename, filename.c_str(), MAX_FILENAME_LEN - 1);
                    strncpy(fileComments[commentCount].comment, comment.c_str(), MAX_COMMENT_LEN - 1);
                    commentCount++;
                }
            }
            commentFile.close();
            Serial.printf("✓ 파일 커멘트 로드: %d개\n", commentCount);
        }
        xSemaphoreGive(sdMutex);
    }
}

void saveFileComments() {
    if (!sdCardReady) return;
    
    if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
        // 기존 파일 삭제
        if (SD.exists("/comments.txt")) {
            SD.remove("/comments.txt");
        }
        
        File commentFile = SD.open("/comments.txt", FILE_WRITE);
        if (commentFile) {
            for (int i = 0; i < commentCount; i++) {
                commentFile.print(fileComments[i].filename);
                commentFile.print("|");
                commentFile.println(fileComments[i].comment);
            }
            commentFile.close();
            Serial.println("✓ 파일 커멘트 저장 완료");
        }
        xSemaphoreGive(sdMutex);
    }
}

void addFileComment(const char* filename, const char* comment) {
    // 기존 커멘트가 있는지 확인
    for (int i = 0; i < commentCount; i++) {
        if (strcmp(fileComments[i].filename, filename) == 0) {
            // 업데이트
            strncpy(fileComments[i].comment, comment, MAX_COMMENT_LEN - 1);
            saveFileComments();
            return;
        }
    }
    
    // 새로운 커멘트 추가
    if (commentCount < MAX_FILE_COMMENTS) {
        strncpy(fileComments[commentCount].filename, filename, MAX_FILENAME_LEN - 1);
        strncpy(fileComments[commentCount].comment, comment, MAX_COMMENT_LEN - 1);
        commentCount++;
        saveFileComments();
    }
}

const char* getFileComment(const char* filename) {
    for (int i = 0; i < commentCount; i++) {
        if (strcmp(fileComments[i].filename, filename) == 0) {
            return fileComments[i].comment;
        }
    }
    return "";
}

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

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);
    }
    
    uint32_t avg = sum / samples;
    
    // ESP32 ADC: 12bit (0-4095), 참조전압 3.3V
    // 실제 전압 = (ADC값 / 4095) * 3.3V
    float voltage = (avg / 4095.0) * 3.3;
    
    return voltage;
}

void updatePowerStatus() {
    uint32_t now = millis();
    
    // 5초마다 전압 체크
    if (now - powerStatus.lastCheck >= VOLTAGE_CHECK_INTERVAL_MS) {
        powerStatus.voltage = readVoltage();
        
        // 1초 단위 최소값 업데이트
        if (powerStatus.voltage < powerStatus.minVoltage) {
            powerStatus.minVoltage = powerStatus.voltage;
        }
        
        // 1초마다 최소값 리셋
        if (now - powerStatus.lastMinReset >= 1000) {
            powerStatus.minVoltage = powerStatus.voltage;
            powerStatus.lastMinReset = now;
        }
        
        // 저전압 경고
        if (powerStatus.voltage < LOW_VOLTAGE_THRESHOLD) {
            if (!powerStatus.lowVoltage) {
                Serial.printf("⚠️  저전압 경고: %.2fV\n", powerStatus.voltage);
                powerStatus.lowVoltage = true;
            }
        } else {
            powerStatus.lowVoltage = false;
        }
        
        powerStatus.lastCheck = now;
    }
}

// ========================================
// RTC 함수
// ========================================

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 initRTC() {
    rtcWire.begin();
    rtcWire.setTimeout(1000);
    
    rtcWire.beginTransmission(DS3231_ADDRESS);
    uint8_t error = rtcWire.endTransmission();
    
    if (error == 0) {
        timeSyncStatus.rtcAvailable = true;
        Serial.println("✓ RTC DS3231 초기화 완료");
        return true;
    } else {
        timeSyncStatus.rtcAvailable = false;
        Serial.println("✗ RTC DS3231 없음 (수동 시간 설정 필요)");
        return false;
    }
}

bool setRTCTime(int year, int month, int day, int hour, int minute, int second) {
    if (!timeSyncStatus.rtcAvailable) return false;
    
    if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
        rtcWire.beginTransmission(DS3231_ADDRESS);
        rtcWire.write(0x00);  // 레지스터 주소
        
        rtcWire.write(decToBcd(second));
        rtcWire.write(decToBcd(minute));
        rtcWire.write(decToBcd(hour));
        rtcWire.write(decToBcd(1));  // 요일 (사용안함)
        rtcWire.write(decToBcd(day));
        rtcWire.write(decToBcd(month));
        rtcWire.write(decToBcd(year - 2000));
        
        uint8_t error = rtcWire.endTransmission();
        xSemaphoreGive(rtcMutex);
        
        if (error == 0) {
            Serial.printf("✓ RTC 시간 설정: %04d-%02d-%02d %02d:%02d:%02d\n",
                year, month, day, hour, minute, second);
            return true;
        }
    }
    return false;
}

bool getRTCTime(struct tm* timeinfo) {
    if (!timeSyncStatus.rtcAvailable) return false;
    
    if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
        rtcWire.beginTransmission(DS3231_ADDRESS);
        rtcWire.write(0x00);  // 레지스터 시작 주소
        
        if (rtcWire.endTransmission() != 0) {
            xSemaphoreGive(rtcMutex);
            return false;
        }
        
        rtcWire.requestFrom(DS3231_ADDRESS, 7);
        
        if (rtcWire.available() >= 7) {
            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());
            
            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;
            
            xSemaphoreGive(rtcMutex);
            return true;
        }
        
        xSemaphoreGive(rtcMutex);
    }
    
    return false;
}

void syncSystemTimeFromRTC() {
    struct tm timeinfo;
    
    if (getRTCTime(&timeinfo)) {
        struct timeval tv;
        tv.tv_sec = mktime(&timeinfo);
        tv.tv_usec = 0;
        settimeofday(&tv, NULL);
        
        timeSyncStatus.synchronized = true;
        timeSyncStatus.lastSyncTime = esp_timer_get_time();
        timeSyncStatus.rtcSyncCount++;
        
        Serial.printf("✓ RTC→시스템 동기화 (%d회): %04d-%02d-%02d %02d:%02d:%02d\n",
            timeSyncStatus.rtcSyncCount,
            timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
            timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
    }
}

// ========================================
// MCP2515 모드 제어 함수
// ========================================

bool setMCP2515Mode(MCP2515Mode mode) {
    MCP2515::ERROR result;
    
    switch (mode) {
        case MCP_MODE_NORMAL:
            result = mcp2515.setNormalMode();
            if (result == MCP2515::ERROR_OK) {
                // 인터럽트 재활성화
                attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING);
                currentMcpMode = MCP_MODE_NORMAL;
                Serial.println("✓ MCP2515 모드: NORMAL");
                Serial.println("  송수신 모두 가능, ACK 전송");
                return true;
            }
            break;
            
        case MCP_MODE_LISTEN_ONLY:
            result = mcp2515.setListenOnlyMode();
            if (result == MCP2515::ERROR_OK) {
                // 인터럽트 재활성화
                attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING);
                currentMcpMode = MCP_MODE_LISTEN_ONLY;
                Serial.println("✓ MCP2515 모드: LISTEN-ONLY");
                Serial.println("  수신만 가능, ACK 전송 안 함");
                return true;
            }
            break;
            
        case MCP_MODE_LOOPBACK:
            result = mcp2515.setLoopbackMode();
            if (result == MCP2515::ERROR_OK) {
                // 인터럽트 재활성화
                attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING);
                currentMcpMode = MCP_MODE_LOOPBACK;
                Serial.println("✓ MCP2515 모드: LOOPBACK");
                Serial.println("  자가 테스트 모드");
                return true;
            }
            break;
            
        case MCP_MODE_TRANSMIT:
            // TRANSMIT 모드: Listen-Only 기반
            // 평상시에는 Listen-Only로 동작하여 ACK를 보내지 않음
            // 송신이 필요할 때만 일시적으로 Normal 모드로 전환
            result = mcp2515.setListenOnlyMode();
            if (result == MCP2515::ERROR_OK) {
                // 인터럽트는 비활성화 (수신 처리하지 않음)
                detachInterrupt(digitalPinToInterrupt(CAN_INT_PIN));
                
                // 수신 버퍼를 비움
                struct can_frame frame;
                while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {
                    // 버퍼만 비움
                }
                
                currentMcpMode = MCP_MODE_TRANSMIT;
                Serial.println("✓ MCP2515 모드: TRANSMIT-ONLY");
                Serial.println("  기본: Listen-Only (ACK 안 함)");
                Serial.println("  송신 시: 일시적으로 Normal 전환");
                Serial.println("  수신 인터럽트: 비활성화");
                return true;
            }
            break;
    }
    
    Serial.println("✗ MCP2515 모드 변경 실패");
    return false;
}

// ========================================
// CAN 송신 헬퍼 함수 (TRANSMIT 모드 자동 처리)
// ========================================

MCP2515::ERROR sendCANMessage(struct can_frame* frame) {
    MCP2515::ERROR result;
    
    // TRANSMIT 모드일 때만 특별 처리
    if (currentMcpMode == MCP_MODE_TRANSMIT) {
        // 1. 일시적으로 Normal 모드로 전환
        result = mcp2515.setNormalMode();
        if (result != MCP2515::ERROR_OK) {
            Serial.println("✗ TRANSMIT 모드: Normal 전환 실패");
            return result;
        }
        
        // 2. 메시지 송신
        result = mcp2515.sendMessage(frame);
        
        // 3. 즉시 Listen-Only 모드로 복귀
        mcp2515.setListenOnlyMode();
        
        // 4. 수신 버퍼 비우기 (전환 중 수신된 데이터)
        struct can_frame dummy;
        while (mcp2515.readMessage(&dummy) == MCP2515::ERROR_OK) {
            // 버퍼만 비움
        }
        
        return result;
    } else {
        // 다른 모드에서는 바로 송신
        return mcp2515.sendMessage(frame);
    }
}

// ========================================
// CAN 인터럽트 및 수신 함수
// ========================================

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

void canRxTask(void* parameter) {
    struct can_frame frame;
    CANMessage canMsg;
    
    for (;;) {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        
        // 한 번에 여러 메시지를 읽어서 처리 속도 향상
        int readCount = 0;
        while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK && readCount < 20) {
            // 현재 시간 저장 (마이크로초)
            struct timeval tv;
            gettimeofday(&tv, NULL);
            canMsg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + (uint64_t)tv.tv_usec;
            
            canMsg.id = frame.can_id;
            canMsg.dlc = frame.can_dlc;
            memcpy(canMsg.data, frame.data, 8);
            
            // 로깅 중일 때만 큐에 추가
            if (loggingEnabled) {
                xQueueSend(canQueue, &canMsg, 0);
            }
            
            // 실시간 데이터 업데이트 (웹 표시용)
            bool found = false;
            for (int i = 0; i < RECENT_MSG_COUNT; i++) {
                if (recentData[i].count > 0 && recentData[i].msg.id == canMsg.id) {
                    recentData[i].msg = canMsg;
                    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 = canMsg;
                        recentData[i].count = 1;
                        break;
                    }
                }
            }
            
            totalMsgCount++;
            readCount++;
        }
        
        // 메시지/초 계산
        uint32_t now = millis();
        if (now - lastMsgCountTime >= 1000) {
            msgPerSecond = totalMsgCount - lastMsgCount;
            lastMsgCount = totalMsgCount;
            lastMsgCountTime = now;
        }
    }
}

// ========================================
// SD 카드 쓰기 태스크
// ========================================

void sdWriteTask(void* parameter) {
    CANMessage msg;
    
    for (;;) {
        if (loggingEnabled && sdCardReady) {
            if (xQueueReceive(canQueue, &msg, pdMS_TO_TICKS(10)) == pdTRUE) {
                // 버퍼에 데이터 추가
                memcpy(&fileBuffer[bufferIndex], &msg, sizeof(CANMessage));
                bufferIndex += sizeof(CANMessage);
                currentFileSize += sizeof(CANMessage);
                
                // 버퍼가 가득 차면 SD에 쓰기
                if (bufferIndex >= FILE_BUFFER_SIZE - sizeof(CANMessage)) {
                    if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
                        if (logFile) {
                            logFile.write(fileBuffer, bufferIndex);
                            logFile.flush();
                        }
                        xSemaphoreGive(sdMutex);
                    }
                    bufferIndex = 0;
                }
            }
        } else {
            vTaskDelay(pdMS_TO_TICKS(100));
        }
    }
}

// ========================================
// 로깅 제어 함수
// ========================================

void startLogging() {
    if (!sdCardReady) {
        Serial.println("✗ SD 카드가 준비되지 않음");
        return;
    }
    
    if (loggingEnabled) {
        Serial.println("⚠️  이미 로깅 중");
        return;
    }
    
    // 파일명 생성 (현재 시간 사용)
    time_t now;
    struct tm timeinfo;
    time(&now);
    localtime_r(&now, &timeinfo);
    
    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.print("✓ 로깅 시작: ");
            Serial.println(currentFilename);
        } else {
            Serial.println("✗ 파일 생성 실패");
        }
        
        xSemaphoreGive(sdMutex);
    }
}

void stopLogging() {
    if (!loggingEnabled) {
        Serial.println("⚠️  로깅 중이 아님");
        return;
    }
    
    loggingEnabled = false;
    
    // 남은 버퍼 데이터 쓰기
    if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
        if (bufferIndex > 0 && logFile) {
            logFile.write(fileBuffer, bufferIndex);
        }
        
        if (logFile) {
            logFile.close();
        }
        
        xSemaphoreGive(sdMutex);
    }
    
    bufferIndex = 0;
    
    // 큐 비우기 (로깅 중이 아닐 때는 큐가 불필요)
    CANMessage dummyMsg;
    while (xQueueReceive(canQueue, &dummyMsg, 0) == pdTRUE) {
        // 큐의 모든 메시지 제거
    }
    
    Serial.print("✓ 로깅 종료: ");
    Serial.println(currentFilename);
    Serial.printf("  파일 크기: %u bytes\n", currentFileSize);
    Serial.println("✓ 큐 비움 완료");
    
    // 현재 파일명 초기화
    currentFilename[0] = '\0';
}

// ========================================
// SD 모니터링 태스크
// ========================================

void sdMonitorTask(void* parameter) {
    for (;;) {
        updatePowerStatus();
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

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

void rtcSyncTask(void* parameter) {
    for (;;) {
        syncSystemTimeFromRTC();
        vTaskDelay(pdMS_TO_TICKS(RTC_SYNC_INTERVAL_MS));
    }
}

// ========================================
// CAN 송신 태스크
// ========================================

void txTask(void* parameter) {
    struct can_frame frame;
    
    for (;;) {
        uint32_t now = millis();
        
        // 주기적 송신 (기존)
        for (int i = 0; i < MAX_TX_MESSAGES; i++) {
            if (txMessages[i].active && txMessages[i].interval > 0) {
                if (now - txMessages[i].lastSent >= txMessages[i].interval) {
                    frame.can_id = txMessages[i].id;
                    if (txMessages[i].extended) {
                        frame.can_id |= CAN_EFF_FLAG;
                    }
                    frame.can_dlc = txMessages[i].dlc;
                    memcpy(frame.data, txMessages[i].data, 8);
                    
                    if (sendCANMessage(&frame) == MCP2515::ERROR_OK) {
                        txMessages[i].lastSent = now;
                        totalTxCount++;
                    }
                }
            }
        }
        
        vTaskDelay(pdMS_TO_TICKS(1));
    }
}

// ========================================
// 시퀀스 실행 태스크
// ========================================

void sequenceTask(void* parameter) {
    struct can_frame frame;
    
    for (;;) {
        if (seqRuntime.running && seqRuntime.activeSequenceIndex >= 0) {
            CANSequence* seq = &sequences[seqRuntime.activeSequenceIndex];
            uint32_t now = millis();
            
            if (seqRuntime.currentStep < seq->stepCount) {
                SequenceStep* step = &seq->steps[seqRuntime.currentStep];
                
                // 첫 번째 스텝이거나 딜레이 시간이 지났으면 실행
                if (seqRuntime.currentStep == 0 || (now - seqRuntime.lastStepTime >= step->delayMs)) {
                    // CAN 메시지 전송
                    frame.can_id = step->canId;
                    if (step->extended) {
                        frame.can_id |= CAN_EFF_FLAG;
                    }
                    frame.can_dlc = step->dlc;
                    memcpy(frame.data, step->data, 8);
                    
                    MCP2515::ERROR result = sendCANMessage(&frame);
                    if (result == MCP2515::ERROR_OK) {
                        totalTxCount++;
                        Serial.printf("  [Seq] Step %d/%d: ID=0x%X, DLC=%d, Delay=%dms - OK\n", 
                            seqRuntime.currentStep + 1, seq->stepCount, 
                            step->canId, step->dlc, step->delayMs);
                    } else {
                        Serial.printf("  [Seq] Step %d/%d: ID=0x%X - FAIL (Error %d)\n", 
                            seqRuntime.currentStep + 1, seq->stepCount, step->canId, result);
                    }
                    
                    seqRuntime.currentStep++;
                    seqRuntime.lastStepTime = now;
                }
            } else {
                // 모든 스텝 완료
                seqRuntime.currentRepeat++;
                
                // 반복 체크
                bool shouldContinue = false;
                
                if (seq->repeatMode == 0) {
                    // 한 번만
                    shouldContinue = false;
                } else if (seq->repeatMode == 1) {
                    // 특정 횟수
                    if (seqRuntime.currentRepeat < seq->repeatCount) {
                        shouldContinue = true;
                    }
                } else if (seq->repeatMode == 2) {
                    // 무한 반복
                    shouldContinue = true;
                }
                
                if (shouldContinue) {
                    seqRuntime.currentStep = 0;
                    seqRuntime.lastStepTime = now;
                    Serial.printf("  [Seq] 반복 %d회 시작\n", seqRuntime.currentRepeat + 1);
                } else {
                    Serial.printf("✓ 시퀀스 실행 완료: %s (총 %d회 반복)\n", 
                        seq->name, seqRuntime.currentRepeat);
                    stopSequence();
                }
            }
        }
        
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

// ========================================
// 파일 리스트 전송 함수
// ========================================

void sendFileList() {
    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()) {
                String fname = file.name();
                
                // comments.txt 파일은 제외
                if (fname != "comments.txt" && fname != "/comments.txt") {
                    if (fname.startsWith("/")) fname = fname.substring(1);
                    
                    if (!first) fileList += ",";
                    fileList += "{\"name\":\"" + fname + "\",";
                    fileList += "\"size\":" + String(file.size());
                    
                    // 커멘트 추가
                    const char* comment = getFileComment(fname.c_str());
                    if (strlen(comment) > 0) {
                        fileList += ",\"comment\":\"" + String(comment) + "\"";
                    }
                    
                    fileList += "}";
                    first = false;
                }
            }
            file.close();
            file = root.openNextFile();
        }
        
        root.close();
        fileList += "]}";
        
        xSemaphoreGive(sdMutex);
        webSocket.broadcastTXT(fileList);
    }
}

// ========================================
// WebSocket 이벤트 처리
// ========================================

void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) {
    if (type == WStype_TEXT) {
        String message = String((char*)payload);
        
        // JSON 파싱 (간단한 방식)
        if (message.indexOf("\"cmd\":\"startLogging\"") >= 0) {
            startLogging();
            // 파일 리스트 자동 갱신
            delay(100);
            sendFileList();
            
        } else if (message.indexOf("\"cmd\":\"stopLogging\"") >= 0) {
            stopLogging();
            // 파일 리스트 자동 갱신
            delay(100);
            sendFileList();
            
        } else if (message.indexOf("\"cmd\":\"getFiles\"") >= 0) {
            // 파일 리스트 전송
            sendFileList();
            
        } else if (message.indexOf("\"cmd\":\"deleteFile\"") >= 0) {
            // 파일 삭제
            int filenameStart = message.indexOf("\"filename\":\"") + 12;
            int filenameEnd = message.indexOf("\"", filenameStart);
            String filename = message.substring(filenameStart, filenameEnd);
            
            // 로깅 중인 파일 체크
            bool canDelete = true;
            if (loggingEnabled && currentFilename[0] != '\0') {
                String currentFileStr = String(currentFilename);
                if (currentFileStr.startsWith("/")) currentFileStr = currentFileStr.substring(1);
                
                if (filename == currentFileStr) {
                    canDelete = false;
                }
            }
            
            if (!canDelete) {
                webSocket.sendTXT(num, "{\"type\":\"deleteResult\",\"success\":false,\"message\":\"Cannot delete file being logged\"}");
            } else {
                String fullPath = "/" + filename;
                
                if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
                    bool success = SD.remove(fullPath);
                    xSemaphoreGive(sdMutex);
                    
                    if (success) {
                        Serial.printf("✓ 파일 삭제: %s\n", filename.c_str());
                        webSocket.sendTXT(num, "{\"type\":\"deleteResult\",\"success\":true}");
                        
                        // 파일 목록 자동 갱신
                        delay(100);
                        sendFileList();
                    } else {
                        webSocket.sendTXT(num, "{\"type\":\"deleteResult\",\"success\":false,\"message\":\"Delete failed\"}");
                    }
                }
            }
            
        } else if (message.indexOf("\"cmd\":\"setSpeed\"") >= 0) {
            // CAN 속도 변경
            int speedStart = message.indexOf("\"speed\":") + 8;
            int speedEnd = message.indexOf(",", speedStart);
            if (speedEnd < 0) speedEnd = message.indexOf("}", speedStart);
            
            int speedIndex = message.substring(speedStart, speedEnd).toInt();
            
            if (speedIndex >= 0 && speedIndex < 4) {
                currentCanSpeed = canSpeedValues[speedIndex];
                mcp2515.reset();
                mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ);
                setMCP2515Mode(currentMcpMode);  // 현재 모드 유지
                
                // 비휘발성 메모리에 저장
                saveCANSettings();
                
                Serial.printf("✓ CAN 속도 변경 및 저장: %s\n", canSpeedNames[speedIndex]);
            }
            
        } else if (message.indexOf("\"cmd\":\"setMcpMode\"") >= 0) {
            // MCP2515 모드 변경
            int modeStart = message.indexOf("\"mode\":") + 7;
            int modeEnd = message.indexOf(",", modeStart);
            if (modeEnd < 0) modeEnd = message.indexOf("}", modeStart);
            
            int mode = message.substring(modeStart, modeEnd).toInt();
            
            if (mode >= 0 && mode <= 3) {  // 0~3으로 확장 (TRANSMIT 모드 포함)
                if (setMCP2515Mode((MCP2515Mode)mode)) {
                    // 모드 변경 성공 시 비휘발성 메모리에 저장
                    saveCANSettings();
                }
            }
            
        } else if (message.indexOf("\"cmd\":\"syncTimeFromPhone\"") >= 0) {
            // 핸드폰 시간을 RTC와 시스템에 동기화
            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 minuteStart = message.indexOf("\"minute\":") + 9;
            int secondStart = message.indexOf("\"second\":") + 9;
            
            int year = message.substring(yearStart, message.indexOf(",", yearStart)).toInt();
            int month = message.substring(monthStart, message.indexOf(",", monthStart)).toInt();
            int day = message.substring(dayStart, message.indexOf(",", dayStart)).toInt();
            int hour = message.substring(hourStart, message.indexOf(",", hourStart)).toInt();
            int minute = message.substring(minuteStart, message.indexOf(",", minuteStart)).toInt();
            int second = message.substring(secondStart, message.indexOf(",", secondStart)).toInt();
            
            Serial.printf("📱 핸드폰 시간 수신: %04d-%02d-%02d %02d:%02d:%02d\n",
                year, month, day, hour, minute, second);
            
            // 1. RTC에 시간 설정 (가능한 경우)
            if (timeSyncStatus.rtcAvailable) {
                setRTCTime(year, month, day, hour, minute, second);
            }
            
            // 2. 시스템 시간 설정
            struct tm timeinfo;
            timeinfo.tm_year = year - 1900;
            timeinfo.tm_mon = month - 1;
            timeinfo.tm_mday = day;
            timeinfo.tm_hour = hour;
            timeinfo.tm_min = minute;
            timeinfo.tm_sec = second;
            
            struct timeval tv;
            tv.tv_sec = mktime(&timeinfo);
            tv.tv_usec = 0;
            settimeofday(&tv, NULL);
            
            timeSyncStatus.synchronized = true;
            timeSyncStatus.lastSyncTime = esp_timer_get_time();
            timeSyncStatus.syncCount++;
            
            Serial.println("✓ 시스템 시간 동기화 완료");
            
            webSocket.sendTXT(num, "{\"type\":\"timeSyncResult\",\"success\":true}");
            
        } else if (message.indexOf("\"cmd\":\"addComment\"") >= 0) {
            // 파일 커멘트 추가
            int filenameStart = message.indexOf("\"filename\":\"") + 12;
            int filenameEnd = message.indexOf("\"", filenameStart);
            String filename = message.substring(filenameStart, filenameEnd);
            
            int commentStart = message.indexOf("\"comment\":\"") + 11;
            int commentEnd = message.indexOf("\"", commentStart);
            String comment = message.substring(commentStart, commentEnd);
            
            addFileComment(filename.c_str(), comment.c_str());
            Serial.printf("✓ 커멘트 추가: %s -> %s\n", filename.c_str(), comment.c_str());
            
            webSocket.sendTXT(num, "{\"type\":\"commentResult\",\"success\":true}");
            
            // 파일 목록 자동 갱신
            delay(100);
            sendFileList();
            
        } else if (message.indexOf("\"cmd\":\"getSettings\"") >= 0) {
            // 설정 전송
            String settings = "{\"type\":\"settings\",";
            settings += "\"ssid\":\"" + String(wifiSSID) + "\",";
            settings += "\"password\":\"" + String(wifiPassword) + "\",";
            settings += "\"staEnable\":" + String(enableSTAMode ? "true" : "false") + ",";
            settings += "\"staSSID\":\"" + String(staSSID) + "\",";
            settings += "\"staPassword\":\"" + String(staPassword) + "\",";
            settings += "\"staConnected\":" + String(WiFi.status() == WL_CONNECTED ? "true" : "false") + ",";
            settings += "\"staIP\":\"" + WiFi.localIP().toString() + "\"}";
            
            webSocket.sendTXT(num, settings);
            
        } else if (message.indexOf("\"cmd\":\"saveSettings\"") >= 0) {
            // 설정 저장
            int ssidStart = message.indexOf("\"ssid\":\"") + 8;
            int ssidEnd = message.indexOf("\"", ssidStart);
            String ssid = message.substring(ssidStart, ssidEnd);
            
            int passStart = message.indexOf("\"password\":\"") + 12;
            int passEnd = message.indexOf("\"", passStart);
            String password = message.substring(passStart, passEnd);
            
            // STA 모드 설정 파싱
            int staEnableIdx = message.indexOf("\"staEnable\":");
            if (staEnableIdx >= 0) {
                String staEnableStr = message.substring(staEnableIdx + 12, staEnableIdx + 16);
                enableSTAMode = (staEnableStr.indexOf("true") >= 0);
                
                if (enableSTAMode) {
                    int staSSIDStart = message.indexOf("\"staSSID\":\"") + 11;
                    int staSSIDEnd = message.indexOf("\"", staSSIDStart);
                    String staSsid = message.substring(staSSIDStart, staSSIDEnd);
                    
                    int staPassStart = message.indexOf("\"staPassword\":\"") + 15;
                    int staPassEnd = message.indexOf("\"", staPassStart);
                    String staPass = message.substring(staPassStart, staPassEnd);
                    
                    strncpy(staSSID, staSsid.c_str(), sizeof(staSSID) - 1);
                    strncpy(staPassword, staPass.c_str(), sizeof(staPassword) - 1);
                }
            }
            
            strncpy(wifiSSID, ssid.c_str(), sizeof(wifiSSID) - 1);
            strncpy(wifiPassword, password.c_str(), sizeof(wifiPassword) - 1);
            
            saveSettings();
            
            webSocket.sendTXT(num, "{\"type\":\"settingsSaved\",\"success\":true}");
            
        } else if (message.indexOf("\"cmd\":\"getSequences\"") >= 0) {
            // 시퀀스 리스트 전송
            String seqList = "{\"type\":\"sequences\",\"sequences\":[";
            for (int i = 0; i < sequenceCount; i++) {
                if (i > 0) seqList += ",";
                seqList += "{\"index\":" + String(i);
                seqList += ",\"name\":\"" + String(sequences[i].name) + "\"";
                seqList += ",\"steps\":" + String(sequences[i].stepCount);
                seqList += ",\"mode\":" + String(sequences[i].repeatMode);
                seqList += ",\"count\":" + String(sequences[i].repeatCount) + "}";
            }
            seqList += "]}";
            webSocket.sendTXT(num, seqList);
            
        } else if (message.indexOf("\"cmd\":\"getSequence\"") >= 0) {
            // 특정 시퀀스 상세 정보 전송
            int indexStart = message.indexOf("\"index\":") + 8;
            int indexEnd = message.indexOf(",", indexStart);
            if (indexEnd < 0) indexEnd = message.indexOf("}", indexStart);
            int index = message.substring(indexStart, indexEnd).toInt();
            
            if (index >= 0 && index < sequenceCount) {
                String seqData = "{\"type\":\"sequenceDetail\",\"sequence\":{";
                seqData += "\"name\":\"" + String(sequences[index].name) + "\",";
                seqData += "\"mode\":" + String(sequences[index].repeatMode) + ",";
                seqData += "\"count\":" + String(sequences[index].repeatCount) + ",";
                seqData += "\"steps\":[";
                
                for (int i = 0; i < sequences[index].stepCount; i++) {
                    if (i > 0) seqData += ",";
                    SequenceStep* step = &sequences[index].steps[i];
                    seqData += "{\"id\":" + String(step->canId);
                    seqData += ",\"ext\":" + String(step->extended ? "true" : "false");
                    seqData += ",\"dlc\":" + String(step->dlc);
                    seqData += ",\"data\":\"";
                    for (int j = 0; j < 8; j++) {
                        if (step->data[j] < 0x10) seqData += "0";
                        seqData += String(step->data[j], HEX);
                        if (j < 7) seqData += " ";
                    }
                    seqData += "\",\"delay\":" + String(step->delayMs) + "}";
                }
                seqData += "]}}";
                webSocket.sendTXT(num, seqData);
            }
            
        } else if (message.indexOf("\"cmd\":\"saveSequence\"") >= 0) {
            // 새 시퀀스 저장 (JSON 파싱)
            CANSequence newSeq;
            memset(&newSeq, 0, sizeof(CANSequence));
            
            // 이름 추출
            int nameStart = message.indexOf("\"name\":\"") + 8;
            int nameEnd = message.indexOf("\"", nameStart);
            String name = message.substring(nameStart, nameEnd);
            strncpy(newSeq.name, name.c_str(), sizeof(newSeq.name) - 1);
            
            // 모드 추출
            int modeStart = message.indexOf("\"mode\":") + 7;
            int modeEnd = message.indexOf(",", modeStart);
            newSeq.repeatMode = message.substring(modeStart, modeEnd).toInt();
            
            // 반복 횟수 추출
            int countStart = message.indexOf("\"repeatCount\":") + 14;
            int countEnd = message.indexOf(",", countStart);
            if (countEnd < 0) countEnd = message.indexOf("}", countStart);
            newSeq.repeatCount = message.substring(countStart, countEnd).toInt();
            
            // 스텝 배열 파싱
            int stepsStart = message.indexOf("\"steps\":[");
            if (stepsStart >= 0) {
                stepsStart += 9;  // "steps":[ 길이
                int stepsEnd = message.indexOf("]}", stepsStart);
                String stepsJson = message.substring(stepsStart, stepsEnd);
                
                // 각 스텝 파싱
                newSeq.stepCount = 0;
                int pos = 0;
                
                while (pos < stepsJson.length() && newSeq.stepCount < 20) {
                    int stepStart = stepsJson.indexOf("{", pos);
                    if (stepStart < 0) break;
                    
                    int stepEnd = stepsJson.indexOf("}", stepStart);
                    if (stepEnd < 0) break;
                    
                    String stepJson = stepsJson.substring(stepStart, stepEnd + 1);
                    
                    // canId 추출
                    int idStart = stepJson.indexOf("\"canId\":") + 8;
                    int idEnd = stepJson.indexOf(",", idStart);
                    if (idEnd < 0) idEnd = stepJson.indexOf("}", idStart);
                    newSeq.steps[newSeq.stepCount].canId = stepJson.substring(idStart, idEnd).toInt();
                    
                    // extended 추출
                    int extStart = stepJson.indexOf("\"extended\":") + 11;
                    String extStr = stepJson.substring(extStart, extStart + 5);
                    newSeq.steps[newSeq.stepCount].extended = (extStr.indexOf("true") >= 0);
                    
                    // dlc 추출
                    int dlcStart = stepJson.indexOf("\"dlc\":") + 6;
                    int dlcEnd = stepJson.indexOf(",", dlcStart);
                    newSeq.steps[newSeq.stepCount].dlc = stepJson.substring(dlcStart, dlcEnd).toInt();
                    
                    // data 배열 추출
                    int dataStart = stepJson.indexOf("\"data\":[") + 8;
                    int dataEnd = stepJson.indexOf("]", dataStart);
                    String dataStr = stepJson.substring(dataStart, dataEnd);
                    
                    // data 바이트 파싱
                    int bytePos = 0;
                    int byteIdx = 0;
                    while (bytePos < dataStr.length() && byteIdx < 8) {
                        int commaPos = dataStr.indexOf(",", bytePos);
                        if (commaPos < 0) commaPos = dataStr.length();
                        
                        String byteStr = dataStr.substring(bytePos, commaPos);
                        byteStr.trim();
                        newSeq.steps[newSeq.stepCount].data[byteIdx] = byteStr.toInt();
                        
                        byteIdx++;
                        bytePos = commaPos + 1;
                    }
                    
                    // delay 추출
                    int delayStart = stepJson.indexOf("\"delayMs\":") + 10;
                    int delayEnd = stepJson.indexOf(",", delayStart);
                    if (delayEnd < 0) delayEnd = stepJson.indexOf("}", delayStart);
                    newSeq.steps[newSeq.stepCount].delayMs = stepJson.substring(delayStart, delayEnd).toInt();
                    
                    newSeq.stepCount++;
                    pos = stepEnd + 1;
                }
            }
            
            Serial.printf("📝 시퀀스 저장: %s (%d 스텝)\n", newSeq.name, newSeq.stepCount);
            for (int i = 0; i < newSeq.stepCount; i++) {
                Serial.printf("  Step %d: ID=0x%X, DLC=%d, Delay=%dms\n", 
                    i, newSeq.steps[i].canId, newSeq.steps[i].dlc, newSeq.steps[i].delayMs);
            }
            
            int result = addSequence(newSeq);
            if (result >= 0) {
                webSocket.sendTXT(num, "{\"type\":\"sequenceSaved\",\"success\":true,\"index\":" + String(result) + "}");
            } else {
                webSocket.sendTXT(num, "{\"type\":\"sequenceSaved\",\"success\":false}");
            }
            
        } else if (message.indexOf("\"cmd\":\"deleteSequence\"") >= 0) {
            // 시퀀스 삭제
            int indexStart = message.indexOf("\"index\":") + 8;
            int indexEnd = message.indexOf(",", indexStart);
            if (indexEnd < 0) indexEnd = message.indexOf("}", indexStart);
            int index = message.substring(indexStart, indexEnd).toInt();
            
            bool success = deleteSequence(index);
            webSocket.sendTXT(num, "{\"type\":\"sequenceDeleted\",\"success\":" + String(success ? "true" : "false") + "}");
            
        } else if (message.indexOf("\"cmd\":\"startSequence\"") >= 0) {
            // 시퀀스 실행
            int indexStart = message.indexOf("\"index\":") + 8;
            int indexEnd = message.indexOf(",", indexStart);
            if (indexEnd < 0) indexEnd = message.indexOf("}", indexStart);
            int index = message.substring(indexStart, indexEnd).toInt();
            
            startSequence(index);
            webSocket.sendTXT(num, "{\"type\":\"sequenceStarted\",\"success\":true}");
            
        } else if (message.indexOf("\"cmd\":\"stopSequence\"") >= 0) {
            // 시퀀스 중지
            stopSequence();
            webSocket.sendTXT(num, "{\"type\":\"sequenceStopped\",\"success\":true}");
        }
    }
}

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

void webUpdateTask(void* parameter) {
    uint32_t lastStatusUpdate = 0;
    uint32_t lastCanUpdate = 0;
    
    for (;;) {
        webSocket.loop();
        
        uint32_t now = millis();
        
        // 상태 업데이트 (500ms)
        if (now - lastStatusUpdate >= 500) {
            String status = "{\"type\":\"status\",";
            status += "\"logging\":" + String(loggingEnabled ? "true" : "false") + ",";
            status += "\"sdReady\":" + String(sdCardReady ? "true" : "false") + ",";
            status += "\"queueSize\":" + String(uxQueueMessagesWaiting(canQueue)) + ",";
            status += "\"queueMax\":" + String(CAN_QUEUE_SIZE) + ",";
            status += "\"totalMsg\":" + String(totalMsgCount) + ",";
            status += "\"msgPerSec\":" + String(msgPerSecond) + ",";
            status += "\"fileSize\":" + String(currentFileSize) + ",";
            status += "\"currentFile\":\"";
            
            if (currentFilename[0] != '\0') {
                String fname = String(currentFilename);
                if (fname.startsWith("/")) fname = fname.substring(1);
                status += fname;
            }
            
            status += "\",\"timeSynced\":" + String(timeSyncStatus.synchronized ? "true" : "false") + ",";
            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 += "\"mcpMode\":" + String(currentMcpMode);
            status += "}";
            
            webSocket.broadcastTXT(status);
            lastStatusUpdate = now;
        }
        
        // CAN 데이터 배치 전송 (100ms)
        if (now - lastCanUpdate >= 100) {
            String canBatch = "{\"type\":\"canBatch\",\"messages\":[";
            int messageCount = 0;
            
            for (int i = 0; i < RECENT_MSG_COUNT; i++) {
                if (recentData[i].count > 0) {
                    CANMessage* msg = &recentData[i].msg;
                    
                    if (messageCount > 0) canBatch += ",";
                    
                    canBatch += "{\"id\":\"0x";
                    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 v2.0                ");
    Serial.println("  + Phone Time Sync & File Comments   ");
    Serial.println("  + MCP2515 Mode Control               ");
    Serial.println("========================================");
    
    // 설정 로드
    loadSettings();
    
    // 설정값 표시
    Serial.println("\n📋 현재 설정값:");
    Serial.println("----------------------------------------");
    Serial.printf("  WiFi SSID     : %s\n", wifiSSID);
    Serial.printf("  WiFi Password : %s\n", wifiPassword);
    Serial.println("----------------------------------------");
    Serial.println("💡 설정 변경: http://[IP]/settings\n");
    
    memset(recentData, 0, sizeof(recentData));
    memset(txMessages, 0, sizeof(txMessages));
    memset(fileComments, 0, sizeof(fileComments));
    
    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);
    
    // VSPI 클럭 속도를 40MHz로 설정 (SD 카드용)
    vspi.setFrequency(40000000);  // 40MHz
    
    // MCP2515 초기화
    mcp2515.reset();
    // 저장된 CAN 속도 적용 (loadSettings에서 로드됨)
    mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ);
    // 저장된 MCP 모드 적용 (loadSettings에서 로드됨)
    setMCP2515Mode(currentMcpMode);
    Serial.println("✓ MCP2515 초기화 완료 (저장된 설정 적용)");
    
    // Mutex 생성 (다른 초기화보다 먼저!)
    sdMutex = xSemaphoreCreateMutex();
    rtcMutex = xSemaphoreCreateMutex();
    
    if (sdMutex == NULL || rtcMutex == NULL) {
        Serial.println("✗ Mutex 생성 실패!");
        while (1) delay(1000);
    }
    
    // RTC 초기화 (SoftWire 사용) - Mutex 생성 후
    initRTC();
    
    // SD 카드 초기화
    if (SD.begin(VSPI_CS, vspi)) {
        sdCardReady = true;
        Serial.println("✓ SD 카드 초기화 완료");
        
        // 파일 커멘트 로드
        loadFileComments();
    } else {
        Serial.println("✗ SD 카드 초기화 실패");
    }
    
    // WiFi 설정 - APSTA 모드 지원
    if (enableSTAMode && strlen(staSSID) > 0) {
        // APSTA 모드 (AP + Station 동시 동작)
        Serial.println("\n📶 WiFi APSTA 모드 시작...");
        
        WiFi.mode(WIFI_AP_STA);
        
        // AP 모드 시작
        WiFi.softAP(wifiSSID, wifiPassword);
        Serial.print("✓ AP SSID: ");
        Serial.println(wifiSSID);
        Serial.print("✓ AP IP: ");
        Serial.println(WiFi.softAPIP());
        
        // Station 모드로 WiFi 연결 시도
        Serial.printf("📡 WiFi 연결 시도: %s\n", staSSID);
        WiFi.begin(staSSID, staPassword);
        
        // 연결 대기 (최대 10초)
        int attempts = 0;
        while (WiFi.status() != WL_CONNECTED && attempts < 20) {
            delay(500);
            Serial.print(".");
            attempts++;
        }
        Serial.println();
        
        if (WiFi.status() == WL_CONNECTED) {
            Serial.println("✓ WiFi 연결 성공!");
            Serial.print("✓ STA IP: ");
            Serial.println(WiFi.localIP());
            Serial.print("✓ Gateway: ");
            Serial.println(WiFi.gatewayIP());
            Serial.print("✓ DNS: ");
            Serial.println(WiFi.dnsIP());
        } else {
            Serial.println("✗ WiFi 연결 실패 (AP 모드는 정상 동작)");
        }
    } else {
        // AP 모드만 사용
        Serial.println("\n📶 WiFi AP 모드 시작...");
        WiFi.mode(WIFI_AP);
        WiFi.softAP(wifiSSID, wifiPassword);
        
        Serial.print("✓ AP SSID: ");
        Serial.println(wifiSSID);
        Serial.print("✓ AP IP: ");
        Serial.println(WiFi.softAPIP());
    }
    
    // WiFi 성능 최적화
    WiFi.setSleep(false);  // WiFi 절전 모드 비활성화 (신호 강도 개선)
    esp_wifi_set_max_tx_power(84);  // TX 출력 최대화 (20.5dBm = 84/4)
    
    // 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) {
        Serial.println("✗ Queue 생성 실패!");
        while (1) delay(1000);
    }
    
    // CAN 인터럽트 활성화
    attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING);
    
    // Task 생성
    xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 4096, NULL, 5, &canRxTaskHandle, 1);  // 우선순위 4→5
    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);
    xTaskCreatePinnedToCore(sequenceTask, "SEQ_TASK", 4096, NULL, 2, NULL, 1);  // 시퀀스 Task 추가
    
    // RTC 동기화 Task
    if (timeSyncStatus.rtcAvailable) {
        xTaskCreatePinnedToCore(rtcSyncTask, "RTC_SYNC", 4096, NULL, 0, &rtcTaskHandle, 0);
        Serial.println("✓ RTC 자동 동기화 Task 시작");
    }
    
    // 시퀀스 로드
    loadSequences();
    
    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("========================================\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 | 모드: %d\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 ? " ⚠️" : "",
            currentMcpMode);
        lastPrint = millis();
    }
}
