1449 lines
44 KiB
C++
1449 lines
44 KiB
C++
/*
|
|
* ============================================================================
|
|
* Byun CAN Logger v2.5 PSRAM Edition - Final Release
|
|
* ============================================================================
|
|
*
|
|
* ESP32-S3 기반 고성능 CAN Bus & Serial 데이터 로거
|
|
*
|
|
* 주요 기능:
|
|
* - 1Mbps CAN Bus 통신 (MCP2515)
|
|
* - RS232/UART Serial 통신
|
|
* - PSRAM 활용 대용량 큐 (CAN: 10,000개, Serial: 2,000개)
|
|
* - 실시간 웹 모니터링 (WebSocket)
|
|
* - SD 카드 로깅 (CSV/BIN 형식)
|
|
* - CAN 시퀀스 전송
|
|
* - DBC 기반 실시간 그래프
|
|
* - WiFi AP/APSTA 모드
|
|
* - NTP 시간 동기화
|
|
*
|
|
* 하드웨어:
|
|
* - ESP32-S3 (PSRAM 8MB)
|
|
* - MCP2515 CAN Controller
|
|
* - SD Card Module
|
|
* - RS232 Transceiver (Optional)
|
|
*
|
|
* 작성자: Byun
|
|
* 버전: 2.5 Final
|
|
* 날짜: 2024
|
|
* ============================================================================
|
|
*/
|
|
|
|
#include <WiFi.h>
|
|
#include <ESPAsyncWebServer.h>
|
|
#include <WebSocketsServer.h>
|
|
#include <SPI.h>
|
|
#include <SD.h>
|
|
#include <mcp2515.h>
|
|
#include <ArduinoJson.h>
|
|
#include <time.h>
|
|
#include <Preferences.h>
|
|
|
|
// ============================================================================
|
|
// HTML 페이지 포함
|
|
// ============================================================================
|
|
#include "index.h"
|
|
#include "graph.h"
|
|
#include "graph_viewer.h"
|
|
#include "transmit.h"
|
|
#include "serial_terminal.h"
|
|
#include "settings.h"
|
|
|
|
// ============================================================================
|
|
// 핀 정의
|
|
// ============================================================================
|
|
#define CAN_CS_PIN 5 // MCP2515 CS
|
|
#define SD_CS_PIN 15 // SD Card CS
|
|
#define VOLTAGE_PIN 4 // 전압 측정 (ADC)
|
|
|
|
// Serial2 핀 정의 (ESP32-S3)
|
|
#define SERIAL2_RX 16
|
|
#define SERIAL2_TX 17
|
|
|
|
// ============================================================================
|
|
// 상수 정의
|
|
// ============================================================================
|
|
#define RECENT_MSG_COUNT 100 // 최근 메시지 추적 개수
|
|
#define MAX_SERIAL_LINE_LEN 256 // Serial 라인 최대 길이
|
|
#define SEQUENCE_MAX_STEPS 50 // 시퀀스 최대 스텝
|
|
#define MAX_SEQUENCES 10 // 최대 시퀀스 개수
|
|
|
|
// PSRAM 기반 대용량 큐
|
|
#define CAN_QUEUE_SIZE 10000 // CAN 메시지 큐 (PSRAM)
|
|
#define SERIAL_QUEUE_SIZE 2000 // Serial 메시지 큐 (PSRAM)
|
|
|
|
// 전압 관련
|
|
#define LOW_VOLTAGE_THRESHOLD 11.0 // 저전압 경고 (12V 기준)
|
|
#define VOLTAGE_SAMPLES 10 // 전압 측정 샘플 수
|
|
|
|
// WebSocket 업데이트 주기
|
|
#define WEB_UPDATE_INTERVAL 500 // 500ms
|
|
|
|
// ============================================================================
|
|
// 구조체 정의
|
|
// ============================================================================
|
|
|
|
// CAN 메시지 구조체
|
|
struct CANMessage {
|
|
uint32_t id;
|
|
uint8_t dlc;
|
|
uint8_t data[8];
|
|
uint32_t timestamp_ms;
|
|
bool extended;
|
|
};
|
|
|
|
// CAN 데이터 추적 구조체
|
|
struct CANData {
|
|
struct can_frame msg;
|
|
uint32_t count;
|
|
uint32_t lastUpdate;
|
|
};
|
|
|
|
// Serial 메시지 구조체
|
|
struct SerialMessage {
|
|
uint64_t timestamp_us;
|
|
char data[MAX_SERIAL_LINE_LEN];
|
|
uint16_t length;
|
|
bool isTx;
|
|
};
|
|
|
|
// CAN 시퀀스 스텝
|
|
struct SequenceStep {
|
|
uint32_t canId;
|
|
bool extended;
|
|
uint8_t dlc;
|
|
uint8_t data[8];
|
|
uint32_t delayMs;
|
|
};
|
|
|
|
// CAN 시퀀스
|
|
struct CANSequence {
|
|
char name[32];
|
|
uint8_t repeatMode; // 0=Once, 1=Count, 2=Infinite
|
|
uint16_t repeatCount;
|
|
uint8_t stepCount;
|
|
SequenceStep steps[SEQUENCE_MAX_STEPS];
|
|
};
|
|
|
|
// 시퀀스 실행 상태
|
|
struct SequenceRuntime {
|
|
bool running;
|
|
int8_t activeSequenceIndex;
|
|
uint8_t currentStep;
|
|
uint16_t currentRepeat;
|
|
uint32_t lastStepTime;
|
|
};
|
|
|
|
// WiFi 설정
|
|
struct WiFiSettings {
|
|
char ssid[32];
|
|
char password[64];
|
|
bool staEnable;
|
|
char staSSID[32];
|
|
char staPassword[64];
|
|
};
|
|
|
|
// Serial 설정
|
|
struct SerialConfig {
|
|
uint32_t baudRate;
|
|
uint8_t dataBits;
|
|
uint8_t parity; // 0=None, 1=Even, 2=Odd
|
|
uint8_t stopBits;
|
|
};
|
|
|
|
// ============================================================================
|
|
// 전역 변수
|
|
// ============================================================================
|
|
|
|
// 하드웨어 객체
|
|
MCP2515 mcp2515(CAN_CS_PIN);
|
|
AsyncWebServer server(80);
|
|
WebSocketsServer webSocket(81);
|
|
Preferences preferences;
|
|
|
|
// FreeRTOS 큐 (PSRAM 할당)
|
|
QueueHandle_t canQueue = nullptr;
|
|
QueueHandle_t serialQueue = nullptr;
|
|
|
|
// FreeRTOS 태스크 핸들
|
|
TaskHandle_t canRxTaskHandle = nullptr;
|
|
TaskHandle_t canTxTaskHandle = nullptr;
|
|
TaskHandle_t serialRxTaskHandle = nullptr;
|
|
TaskHandle_t loggingTaskHandle = nullptr;
|
|
TaskHandle_t webUpdateTaskHandle = nullptr;
|
|
|
|
// CAN 데이터
|
|
CANData recentData[RECENT_MSG_COUNT];
|
|
|
|
// CAN 통계
|
|
uint32_t totalCanRxCount = 0;
|
|
uint32_t totalCanTxCount = 0;
|
|
uint32_t canMessagesPerSecond = 0;
|
|
uint32_t lastCanCountTime = 0;
|
|
uint32_t lastCanCount = 0;
|
|
|
|
// Serial 통계
|
|
uint32_t totalSerialRxCount = 0;
|
|
uint32_t totalSerialTxCount = 0;
|
|
|
|
// 로깅 상태
|
|
volatile bool canLoggingEnabled = false;
|
|
volatile bool serialLoggingEnabled = false;
|
|
File canLogFile;
|
|
File serialLogFile;
|
|
char currentCanFileName64 = {0};
|
|
char currentSerialFileName[64] = {0};
|
|
uint32_t currentCanFileSize = 0;
|
|
uint32_t currentSerialFileSize = 0;
|
|
String canLogFormat = "bin"; // "bin" or "csv"
|
|
String serialLogFormat = "csv"; // "csv" or "bin"
|
|
|
|
// SD 카드 상태
|
|
bool sdCardReady = false;
|
|
|
|
// 시간 동기화
|
|
bool timeSynced = false;
|
|
uint32_t rtcSyncCount = 0;
|
|
struct tm timeinfo;
|
|
|
|
// 전압 모니터링
|
|
float currentVoltage = 0.0;
|
|
float minVoltage1s = 99.0;
|
|
bool lowVoltageDetected = false;
|
|
uint32_t lastVoltageResetTime = 0;
|
|
|
|
// CAN 시퀀스
|
|
CANSequence sequences[MAX_SEQUENCES];
|
|
uint8_t sequenceCount = 0;
|
|
SequenceRuntime seqRuntime = {false, -1, 0, 0, 0};
|
|
|
|
// WiFi 설정
|
|
WiFiSettings wifiSettings;
|
|
|
|
// Serial 설정
|
|
SerialConfig serialConfig = {115200, 8, 0, 1};
|
|
|
|
// MCP2515 모드
|
|
uint8_t currentMcpMode = 0; // 0=Normal, 1=Listen-Only, 2=Loopback, 3=TX-Only
|
|
|
|
// WebSocket 클라이언트 수
|
|
uint8_t wsClientCount = 0;
|
|
|
|
// ============================================================================
|
|
// 함수 선언
|
|
// ============================================================================
|
|
|
|
// 초기화
|
|
void initSPIBus();
|
|
void initSDCard();
|
|
void initMCP2515();
|
|
void initSerial2();
|
|
void loadSettings();
|
|
void saveSettings();
|
|
|
|
// FreeRTOS 태스크
|
|
void canRxTask(void* parameter);
|
|
void canTxTask(void* parameter);
|
|
void serialRxTask(void* parameter);
|
|
void loggingTask(void* parameter);
|
|
void webUpdateTask(void* parameter);
|
|
|
|
// CAN 기능
|
|
void setCANSpeed(uint8_t speed);
|
|
void setMCPMode(uint8_t mode);
|
|
bool sendCANMessage(uint32_t id, bool ext, uint8_t dlc, const uint8_t* data);
|
|
|
|
// 로깅
|
|
void startCANLogging(const String& format);
|
|
void stopCANLogging();
|
|
void startSerialLogging(const String& format);
|
|
void stopSerialLogging();
|
|
void writeCANMessageToFile(const CANMessage& msg);
|
|
void writeSerialMessageToFile(const SerialMessage& msg);
|
|
|
|
// 시퀀스
|
|
void loadSequences();
|
|
void saveSequences();
|
|
void startSequence(uint8_t index);
|
|
void stopSequence();
|
|
void processSequence();
|
|
|
|
// 시간
|
|
void syncTimeFromPhone(int year, int month, int day, int hour, int minute, int second);
|
|
String getTimestamp();
|
|
|
|
// 전압
|
|
void updateVoltage();
|
|
|
|
// WebSocket
|
|
void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length);
|
|
void sendWebUpdate();
|
|
void handleWebSocketCommand(uint8_t num, const char* payload);
|
|
|
|
// 파일 관리
|
|
void sendFileList(uint8_t clientNum);
|
|
void deleteFile(const String& filename, uint8_t clientNum);
|
|
void addFileComment(const String& filename, const String& comment);
|
|
String getFileComment(const String& filename);
|
|
|
|
// 유틸리티
|
|
String formatBytes(uint32_t bytes);
|
|
void printSystemInfo();
|
|
|
|
// ============================================================================
|
|
// setup()
|
|
// ============================================================================
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
delay(1000);
|
|
|
|
Serial.println("\n\n");
|
|
Serial.println("============================================================================");
|
|
Serial.println(" Byun CAN Logger v2.5 PSRAM Edition - Final Release");
|
|
Serial.println("============================================================================");
|
|
|
|
// PSRAM 확인
|
|
if (psramFound()) {
|
|
Serial.printf("✓ PSRAM detected: %d KB\n", ESP.getPsramSize() / 1024);
|
|
Serial.printf(" Free PSRAM: %d KB\n", ESP.getFreePsram() / 1024);
|
|
} else {
|
|
Serial.println("✗ WARNING: PSRAM not found!");
|
|
}
|
|
|
|
// 설정 로드
|
|
preferences.begin("can-logger", false);
|
|
loadSettings();
|
|
|
|
// SPI 초기화
|
|
initSPIBus();
|
|
|
|
// SD 카드 초기화
|
|
initSDCard();
|
|
|
|
// MCP2515 초기화
|
|
initMCP2515();
|
|
|
|
// Serial2 초기화
|
|
initSerial2();
|
|
|
|
// 시퀀스 로드
|
|
loadSequences();
|
|
|
|
// PSRAM 기반 큐 생성
|
|
Serial.println("\n[Queue Creation]");
|
|
canQueue = xQueueCreate(CAN_QUEUE_SIZE, sizeof(CANMessage));
|
|
if (canQueue == nullptr) {
|
|
Serial.println("✗ Failed to create CAN queue!");
|
|
} else {
|
|
Serial.printf("✓ CAN Queue created: %d slots\n", CAN_QUEUE_SIZE);
|
|
}
|
|
|
|
serialQueue = xQueueCreate(SERIAL_QUEUE_SIZE, sizeof(SerialMessage));
|
|
if (serialQueue == nullptr) {
|
|
Serial.println("✗ Failed to create Serial queue!");
|
|
} else {
|
|
Serial.printf("✓ Serial Queue created: %d slots\n", SERIAL_QUEUE_SIZE);
|
|
}
|
|
|
|
// FreeRTOS 태스크 생성
|
|
Serial.println("\n[Task Creation]");
|
|
|
|
xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 4096, NULL, 5, &canRxTaskHandle, 0);
|
|
Serial.println("✓ CAN RX Task created (Core 0)");
|
|
|
|
xTaskCreatePinnedToCore(canTxTask, "CAN_TX", 4096, NULL, 4, &canTxTaskHandle, 0);
|
|
Serial.println("✓ CAN TX Task created (Core 0)");
|
|
|
|
xTaskCreatePinnedToCore(serialRxTask, "Serial_RX", 4096, NULL, 3, &serialRxTaskHandle, 1);
|
|
Serial.println("✓ Serial RX Task created (Core 1)");
|
|
|
|
xTaskCreatePinnedToCore(loggingTask, "Logging", 8192, NULL, 2, &loggingTaskHandle, 1);
|
|
Serial.println("✓ Logging Task created (Core 1)");
|
|
|
|
xTaskCreatePinnedToCore(webUpdateTask, "Web_Update", 8192, NULL, 1, &webUpdateTaskHandle, 1);
|
|
Serial.println("✓ Web Update Task created (Core 1)");
|
|
|
|
// WiFi 시작
|
|
Serial.println("\n[WiFi Configuration]");
|
|
|
|
// AP 모드 시작
|
|
WiFi.mode(WIFI_AP_STA); // 항상 APSTA 모드로 시작
|
|
WiFi.softAP(wifiSettings.ssid, wifiSettings.password);
|
|
Serial.printf("✓ AP Mode started\n");
|
|
Serial.printf(" SSID: %s\n", wifiSettings.ssid);
|
|
Serial.printf(" IP: %s\n", WiFi.softAPIP().toString().c_str());
|
|
|
|
// Station 모드 (옵션)
|
|
if (wifiSettings.staEnable && strlen(wifiSettings.staSSID) > 0) {
|
|
Serial.printf(" Connecting to WiFi: %s\n", wifiSettings.staSSID);
|
|
WiFi.begin(wifiSettings.staSSID, wifiSettings.staPassword);
|
|
|
|
int attempts = 0;
|
|
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
|
|
delay(500);
|
|
Serial.print(".");
|
|
attempts++;
|
|
}
|
|
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
Serial.printf("\n✓ Station connected\n");
|
|
Serial.printf(" IP: %s\n", WiFi.localIP().toString().c_str());
|
|
|
|
// NTP 시간 동기화
|
|
configTime(9 * 3600, 0, "pool.ntp.org", "time.nist.gov");
|
|
if (getLocalTime(&timeinfo)) {
|
|
timeSynced = true;
|
|
Serial.println("✓ NTP time synchronized");
|
|
}
|
|
} else {
|
|
Serial.println("\n✗ Station connection failed (AP mode still active)");
|
|
}
|
|
}
|
|
|
|
// 웹 서버 라우트 설정
|
|
Serial.println("\n[Web Server Setup]");
|
|
|
|
server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
|
|
request->send_P(200, "text/html", index_html);
|
|
});
|
|
|
|
server.on("/graph", HTTP_GET, [](AsyncWebServerRequest* request) {
|
|
request->send_P(200, "text/html", graph_html);
|
|
});
|
|
|
|
server.on("/graph-view", HTTP_GET, [](AsyncWebServerRequest* request) {
|
|
request->send_P(200, "text/html", graph_viewer_html);
|
|
});
|
|
|
|
server.on("/transmit", HTTP_GET, [](AsyncWebServerRequest* request) {
|
|
request->send_P(200, "text/html", transmit_html);
|
|
});
|
|
|
|
server.on("/serial", HTTP_GET, [](AsyncWebServerRequest* request) {
|
|
request->send_P(200, "text/html", serial_terminal_html);
|
|
});
|
|
|
|
server.on("/settings", HTTP_GET, [](AsyncWebServerRequest* request) {
|
|
request->send_P(200, "text/html", settings_html);
|
|
});
|
|
|
|
// 파일 다운로드
|
|
server.on("/download", HTTP_GET, [](AsyncWebServerRequest* request) {
|
|
if (request->hasParam("file")) {
|
|
String filename = "/" + request->getParam("file")->value();
|
|
if (SD.exists(filename)) {
|
|
request->send(SD, filename, "application/octet-stream");
|
|
} else {
|
|
request->send(404, "text/plain", "File not found");
|
|
}
|
|
} else {
|
|
request->send(400, "text/plain", "Missing file parameter");
|
|
}
|
|
});
|
|
|
|
server.begin();
|
|
Serial.println("✓ HTTP Server started on port 80");
|
|
|
|
// WebSocket 시작
|
|
webSocket.begin();
|
|
webSocket.onEvent(webSocketEvent);
|
|
Serial.println("✓ WebSocket Server started on port 81");
|
|
|
|
// 시스템 정보 출력
|
|
printSystemInfo();
|
|
|
|
Serial.println("\n============================================================================");
|
|
Serial.println(" System Ready!");
|
|
Serial.println(" Connect to WiFi and access: http://192.168.4.1");
|
|
Serial.println("============================================================================\n");
|
|
}
|
|
|
|
// ============================================================================
|
|
// loop()
|
|
// ============================================================================
|
|
void loop() {
|
|
webSocket.loop();
|
|
|
|
// 전압 업데이트 (1초마다)
|
|
static uint32_t lastVoltageUpdate = 0;
|
|
if (millis() - lastVoltageUpdate > 1000) {
|
|
updateVoltage();
|
|
lastVoltageUpdate = millis();
|
|
}
|
|
|
|
// 시퀀스 처리
|
|
if (seqRuntime.running) {
|
|
processSequence();
|
|
}
|
|
|
|
// CAN 메시지 속도 계산 (1초마다)
|
|
if (millis() - lastCanCountTime >= 1000) {
|
|
canMessagesPerSecond = totalCanRxCount - lastCanCount;
|
|
lastCanCount = totalCanRxCount;
|
|
lastCanCountTime = millis();
|
|
|
|
// 1초마다 최소 전압 리셋
|
|
if (millis() - lastVoltageResetTime >= 1000) {
|
|
minVoltage1s = currentVoltage;
|
|
lastVoltageResetTime = millis();
|
|
}
|
|
}
|
|
|
|
delay(10);
|
|
}
|
|
|
|
// ============================================================================
|
|
// 초기화 함수들
|
|
// ============================================================================
|
|
|
|
void initSPIBus() {
|
|
Serial.println("\n[SPI Bus Initialization]");
|
|
SPI.begin();
|
|
Serial.println("✓ SPI Bus initialized");
|
|
}
|
|
|
|
void initSDCard() {
|
|
Serial.println("\n[SD Card Initialization]");
|
|
|
|
if (!SD.begin(SD_CS_PIN)) {
|
|
Serial.println("✗ SD Card initialization failed!");
|
|
sdCardReady = false;
|
|
return;
|
|
}
|
|
|
|
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
|
|
Serial.printf("✓ SD Card detected: %llu MB\n", cardSize);
|
|
|
|
uint64_t usedBytes = SD.usedBytes() / (1024 * 1024);
|
|
uint64_t totalBytes = SD.totalBytes() / (1024 * 1024);
|
|
Serial.printf(" Used: %llu MB / %llu MB\n", usedBytes, totalBytes);
|
|
|
|
sdCardReady = true;
|
|
}
|
|
|
|
void initMCP2515() {
|
|
Serial.println("\n[MCP2515 Initialization]");
|
|
|
|
mcp2515.reset();
|
|
mcp2515.setBitrate(CAN_1000KBPS, MCP_8MHZ);
|
|
mcp2515.setNormalMode();
|
|
|
|
Serial.println("✓ MCP2515 initialized");
|
|
Serial.println(" Speed: 1 Mbps");
|
|
Serial.println(" Mode: Normal");
|
|
|
|
currentMcpMode = 0;
|
|
}
|
|
|
|
void initSerial2() {
|
|
Serial.println("\n[Serial2 Initialization]");
|
|
|
|
Serial2.begin(serialConfig.baudRate, SERIAL_8N1, SERIAL2_RX, SERIAL2_TX);
|
|
|
|
Serial.printf("✓ Serial2 initialized\n");
|
|
Serial.printf(" Baud: %d\n", serialConfig.baudRate);
|
|
Serial.printf(" Config: 8N1\n");
|
|
Serial.printf(" RX: GPIO%d, TX: GPIO%d\n", SERIAL2_RX, SERIAL2_TX);
|
|
}
|
|
|
|
void loadSettings() {
|
|
Serial.println("\n[Loading Settings]");
|
|
|
|
// WiFi 설정
|
|
preferences.getString("ssid", wifiSettings.ssid, sizeof(wifiSettings.ssid));
|
|
preferences.getString("password", wifiSettings.password, sizeof(wifiSettings.password));
|
|
wifiSettings.staEnable = preferences.getBool("staEnable", false);
|
|
preferences.getString("staSSID", wifiSettings.staSSID, sizeof(wifiSettings.staSSID));
|
|
preferences.getString("staPassword", wifiSettings.staPassword, sizeof(wifiSettings.staPassword));
|
|
|
|
// 기본값 설정
|
|
if (strlen(wifiSettings.ssid) == 0) {
|
|
strcpy(wifiSettings.ssid, "Byun_CAN_Logger");
|
|
strcpy(wifiSettings.password, "12345678");
|
|
}
|
|
|
|
Serial.printf("✓ WiFi AP: %s\n", wifiSettings.ssid);
|
|
if (wifiSettings.staEnable) {
|
|
Serial.printf("✓ WiFi STA enabled: %s\n", wifiSettings.staSSID);
|
|
}
|
|
|
|
// Serial 설정
|
|
serialConfig.baudRate = preferences.getUInt("serialBaud", 115200);
|
|
serialConfig.dataBits = preferences.getUChar("serialData", 8);
|
|
serialConfig.parity = preferences.getUChar("serialParity", 0);
|
|
serialConfig.stopBits = preferences.getUChar("serialStop", 1);
|
|
|
|
Serial.printf("✓ Serial: %d 8N1\n", serialConfig.baudRate);
|
|
}
|
|
|
|
void saveSettings() {
|
|
Serial.println("\n[Saving Settings]");
|
|
|
|
preferences.putString("ssid", wifiSettings.ssid);
|
|
preferences.putString("password", wifiSettings.password);
|
|
preferences.putBool("staEnable", wifiSettings.staEnable);
|
|
preferences.putString("staSSID", wifiSettings.staSSID);
|
|
preferences.putString("staPassword", wifiSettings.staPassword);
|
|
|
|
preferences.putUInt("serialBaud", serialConfig.baudRate);
|
|
preferences.putUChar("serialData", serialConfig.dataBits);
|
|
preferences.putUChar("serialParity", serialConfig.parity);
|
|
preferences.putUChar("serialStop", serialConfig.stopBits);
|
|
|
|
Serial.println("✓ Settings saved to NVS");
|
|
}
|
|
|
|
// ============================================================================
|
|
// FreeRTOS 태스크들
|
|
// ============================================================================
|
|
|
|
void canRxTask(void* parameter) {
|
|
struct can_frame frame;
|
|
CANMessage msg;
|
|
|
|
Serial.println("[CAN RX Task] Started");
|
|
|
|
while (true) {
|
|
if (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {
|
|
// CAN 메시지를 큐에 추가
|
|
msg.id = frame.can_id & 0x1FFFFFFF; // Extended ID 비트 제거
|
|
msg.extended = (frame.can_id & CAN_EFF_FLAG) ? true : false;
|
|
msg.dlc = frame.can_dlc;
|
|
memcpy(msg.data, frame.data, 8);
|
|
msg.timestamp_ms = millis();
|
|
|
|
if (xQueueSend(canQueue, &msg, 0) != pdTRUE) {
|
|
// 큐가 가득 찬 경우 (드롭)
|
|
}
|
|
|
|
// 최근 메시지 업데이트
|
|
bool found = false;
|
|
for (int i = 0; i < RECENT_MSG_COUNT; i++) {
|
|
if (recentData[i].msg.can_id == frame.can_id) {
|
|
memcpy(&recentData[i].msg, &frame, sizeof(struct can_frame));
|
|
recentData[i].count++;
|
|
recentData[i].lastUpdate = millis();
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
// 빈 슬롯 찾기
|
|
for (int i = 0; i < RECENT_MSG_COUNT; i++) {
|
|
if (recentData[i].count == 0) {
|
|
memcpy(&recentData[i].msg, &frame, sizeof(struct can_frame));
|
|
recentData[i].count = 1;
|
|
recentData[i].lastUpdate = millis();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
totalCanRxCount++;
|
|
}
|
|
|
|
vTaskDelay(1 / portTICK_PERIOD_MS); // 1ms 대기
|
|
}
|
|
}
|
|
|
|
void canTxTask(void* parameter) {
|
|
Serial.println("[CAN TX Task] Started");
|
|
|
|
while (true) {
|
|
// 현재는 시퀀스에서만 TX 사용
|
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
}
|
|
}
|
|
|
|
void serialRxTask(void* parameter) {
|
|
static char lineBuffer[MAX_SERIAL_LINE_LEN];
|
|
static uint16_t bufferIndex = 0;
|
|
SerialMessage msg;
|
|
|
|
Serial.println("[Serial RX Task] Started");
|
|
|
|
while (true) {
|
|
while (Serial2.available()) {
|
|
char c = Serial2.read();
|
|
|
|
if (c == '\n' || c == '\r') {
|
|
if (bufferIndex > 0) {
|
|
// 라인 완성
|
|
msg.timestamp_us = esp_timer_get_time();
|
|
msg.isTx = false;
|
|
msg.length = bufferIndex;
|
|
memcpy(msg.data, lineBuffer, bufferIndex);
|
|
msg.data[bufferIndex] = '\0';
|
|
|
|
xQueueSend(serialQueue, &msg, 0);
|
|
|
|
totalSerialRxCount++;
|
|
bufferIndex = 0;
|
|
}
|
|
} else {
|
|
if (bufferIndex < MAX_SERIAL_LINE_LEN - 1) {
|
|
lineBuffer[bufferIndex++] = c;
|
|
}
|
|
}
|
|
}
|
|
|
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
}
|
|
}
|
|
|
|
void loggingTask(void* parameter) {
|
|
CANMessage canMsg;
|
|
SerialMessage serialMsg;
|
|
|
|
Serial.println("[Logging Task] Started");
|
|
|
|
while (true) {
|
|
// CAN 로깅
|
|
if (canLoggingEnabled && canLogFile) {
|
|
while (xQueueReceive(canQueue, &canMsg, 0) == pdTRUE) {
|
|
writeCANMessageToFile(canMsg);
|
|
}
|
|
}
|
|
|
|
// Serial 로깅
|
|
if (serialLoggingEnabled && serialLogFile) {
|
|
while (xQueueReceive(serialQueue, &serialMsg, 0) == pdTRUE) {
|
|
writeSerialMessageToFile(serialMsg);
|
|
}
|
|
}
|
|
|
|
vTaskDelay(100 / portTICK_PERIOD_MS);
|
|
}
|
|
}
|
|
|
|
void webUpdateTask(void* parameter) {
|
|
Serial.println("[Web Update Task] Started");
|
|
|
|
while (true) {
|
|
if (wsClientCount > 0) {
|
|
sendWebUpdate();
|
|
}
|
|
|
|
vTaskDelay(WEB_UPDATE_INTERVAL / portTICK_PERIOD_MS);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// CAN 기능
|
|
// ============================================================================
|
|
|
|
void setCANSpeed(uint8_t speed) {
|
|
mcp2515.reset();
|
|
|
|
switch (speed) {
|
|
case 0: mcp2515.setBitrate(CAN_125KBPS, MCP_8MHZ); break;
|
|
case 1: mcp2515.setBitrate(CAN_250KBPS, MCP_8MHZ); break;
|
|
case 2: mcp2515.setBitrate(CAN_500KBPS, MCP_8MHZ); break;
|
|
case 3: mcp2515.setBitrate(CAN_1000KBPS, MCP_8MHZ); break;
|
|
default: mcp2515.setBitrate(CAN_1000KBPS, MCP_8MHZ); break;
|
|
}
|
|
|
|
// 모드 복원
|
|
setMCPMode(currentMcpMode);
|
|
|
|
Serial.printf("CAN speed changed to: %d\n", speed);
|
|
}
|
|
|
|
void setMCPMode(uint8_t mode) {
|
|
currentMcpMode = mode;
|
|
|
|
switch (mode) {
|
|
case 0: mcp2515.setNormalMode(); break;
|
|
case 1: mcp2515.setListenOnlyMode(); break;
|
|
case 2: mcp2515.setLoopbackMode(); break;
|
|
case 3: mcp2515.setNormalMode(); break; // TX-Only는 Normal 사용
|
|
default: mcp2515.setNormalMode(); break;
|
|
}
|
|
|
|
Serial.printf("MCP2515 mode changed to: %d\n", mode);
|
|
}
|
|
|
|
bool sendCANMessage(uint32_t id, bool ext, uint8_t dlc, const uint8_t* data) {
|
|
struct can_frame frame;
|
|
|
|
frame.can_id = id;
|
|
if (ext) {
|
|
frame.can_id |= CAN_EFF_FLAG;
|
|
}
|
|
frame.can_dlc = dlc;
|
|
memcpy(frame.data, data, dlc);
|
|
|
|
if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) {
|
|
totalCanTxCount++;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// ============================================================================
|
|
// 로깅 기능
|
|
// ============================================================================
|
|
|
|
void startCANLogging(const String& format) {
|
|
if (canLoggingEnabled) {
|
|
stopCANLogging();
|
|
}
|
|
|
|
if (!sdCardReady) {
|
|
Serial.println("Cannot start CAN logging: SD card not ready");
|
|
return;
|
|
}
|
|
|
|
canLogFormat = format;
|
|
|
|
// 파일명 생성
|
|
String timestamp = getTimestamp();
|
|
String extension = (format == "csv") ? ".csv" : ".bin";
|
|
snprintf(currentCanFileName, sizeof(currentCanFileName), "/CAN_%s%s",
|
|
timestamp.c_str(), extension.c_str());
|
|
|
|
canLogFile = SD.open(currentCanFileName, FILE_WRITE);
|
|
if (!canLogFile) {
|
|
Serial.println("Failed to create CAN log file");
|
|
return;
|
|
}
|
|
|
|
// CSV 헤더
|
|
if (format == "csv") {
|
|
canLogFile.println("Timestamp_ms,ID,Extended,DLC,Data");
|
|
}
|
|
|
|
canLoggingEnabled = true;
|
|
currentCanFileSize = 0;
|
|
|
|
Serial.printf("CAN logging started: %s (format: %s)\n", currentCanFileName, format.c_str());
|
|
}
|
|
|
|
void stopCANLogging() {
|
|
if (!canLoggingEnabled) return;
|
|
|
|
canLoggingEnabled = false;
|
|
|
|
if (canLogFile) {
|
|
canLogFile.flush();
|
|
canLogFile.close();
|
|
}
|
|
|
|
Serial.printf("CAN logging stopped: %s (%d bytes)\n", currentCanFileName, currentCanFileSize);
|
|
|
|
currentCanFileName[0] = '\0';
|
|
currentCanFileSize = 0;
|
|
}
|
|
|
|
void startSerialLogging(const String& format) {
|
|
if (serialLoggingEnabled) {
|
|
stopSerialLogging();
|
|
}
|
|
|
|
if (!sdCardReady) {
|
|
Serial.println("Cannot start Serial logging: SD card not ready");
|
|
return;
|
|
}
|
|
|
|
serialLogFormat = format;
|
|
|
|
String timestamp = getTimestamp();
|
|
String extension = (format == "csv") ? ".csv" : ".bin";
|
|
snprintf(currentSerialFileName, sizeof(currentSerialFileName), "/SER_%s%s",
|
|
timestamp.c_str(), extension.c_str());
|
|
|
|
serialLogFile = SD.open(currentSerialFileName, FILE_WRITE);
|
|
if (!serialLogFile) {
|
|
Serial.println("Failed to create Serial log file");
|
|
return;
|
|
}
|
|
|
|
if (format == "csv") {
|
|
serialLogFile.println("Timestamp_us,Direction,Data");
|
|
}
|
|
|
|
serialLoggingEnabled = true;
|
|
currentSerialFileSize = 0;
|
|
|
|
Serial.printf("Serial logging started: %s (format: %s)\n", currentSerialFileName, format.c_str());
|
|
}
|
|
|
|
void stopSerialLogging() {
|
|
if (!serialLoggingEnabled) return;
|
|
|
|
serialLoggingEnabled = false;
|
|
|
|
if (serialLogFile) {
|
|
serialLogFile.flush();
|
|
serialLogFile.close();
|
|
}
|
|
|
|
Serial.printf("Serial logging stopped: %s (%d bytes)\n", currentSerialFileName, currentSerialFileSize);
|
|
|
|
currentSerialFileName[0] = '\0';
|
|
currentSerialFileSize = 0;
|
|
}
|
|
|
|
void writeCANMessageToFile(const CANMessage& msg) {
|
|
if (!canLogFile) return;
|
|
|
|
if (canLogFormat == "csv") {
|
|
// CSV 형식
|
|
char line[128];
|
|
char dataStr[32];
|
|
|
|
for (int i = 0; i < msg.dlc; i++) {
|
|
sprintf(dataStr + i * 3, "%02X ", msg.data[i]);
|
|
}
|
|
|
|
sprintf(line, "%u,0x%X,%d,%d,%s\n",
|
|
msg.timestamp_ms, msg.id, msg.extended ? 1 : 0, msg.dlc, dataStr);
|
|
|
|
canLogFile.print(line);
|
|
currentCanFileSize += strlen(line);
|
|
} else {
|
|
// Binary 형식
|
|
canLogFile.write((uint8_t*)&msg, sizeof(CANMessage));
|
|
currentCanFileSize += sizeof(CANMessage);
|
|
}
|
|
|
|
// 주기적으로 flush (100개마다)
|
|
static uint32_t writeCount = 0;
|
|
if (++writeCount % 100 == 0) {
|
|
canLogFile.flush();
|
|
}
|
|
}
|
|
|
|
void writeSerialMessageToFile(const SerialMessage& msg) {
|
|
if (!serialLogFile) return;
|
|
|
|
if (serialLogFormat == "csv") {
|
|
char line[MAX_SERIAL_LINE_LEN + 64];
|
|
sprintf(line, "%llu,%s,%s\n",
|
|
msg.timestamp_us, msg.isTx ? "TX" : "RX", msg.data);
|
|
|
|
serialLogFile.print(line);
|
|
currentSerialFileSize += strlen(line);
|
|
} else {
|
|
serialLogFile.write((uint8_t*)&msg, sizeof(SerialMessage));
|
|
currentSerialFileSize += sizeof(SerialMessage);
|
|
}
|
|
|
|
static uint32_t writeCount = 0;
|
|
if (++writeCount % 50 == 0) {
|
|
serialLogFile.flush();
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 시퀀스 기능
|
|
// ============================================================================
|
|
|
|
void loadSequences() {
|
|
// Preferences에서 시퀀스 로드 (간단한 구현)
|
|
sequenceCount = preferences.getUChar("seqCount", 0);
|
|
Serial.printf("Loaded %d sequences from NVS\n", sequenceCount);
|
|
}
|
|
|
|
void saveSequences() {
|
|
preferences.putUChar("seqCount", sequenceCount);
|
|
Serial.printf("Saved %d sequences to NVS\n", sequenceCount);
|
|
}
|
|
|
|
void startSequence(uint8_t index) {
|
|
if (index >= sequenceCount) return;
|
|
|
|
seqRuntime.running = true;
|
|
seqRuntime.activeSequenceIndex = index;
|
|
seqRuntime.currentStep = 0;
|
|
seqRuntime.currentRepeat = 0;
|
|
seqRuntime.lastStepTime = millis();
|
|
|
|
Serial.printf("Sequence started: %s\n", sequences[index].name);
|
|
}
|
|
|
|
void stopSequence() {
|
|
if (!seqRuntime.running) return;
|
|
|
|
Serial.printf("Sequence stopped: %s\n", sequences[seqRuntime.activeSequenceIndex].name);
|
|
|
|
seqRuntime.running = false;
|
|
seqRuntime.activeSequenceIndex = -1;
|
|
}
|
|
|
|
void processSequence() {
|
|
if (!seqRuntime.running || seqRuntime.activeSequenceIndex < 0) return;
|
|
|
|
CANSequence& seq = sequences[seqRuntime.activeSequenceIndex];
|
|
|
|
if (seqRuntime.currentStep >= seq.stepCount) {
|
|
// 반복 처리
|
|
seqRuntime.currentRepeat++;
|
|
|
|
if (seq.repeatMode == 0) {
|
|
// Once
|
|
stopSequence();
|
|
return;
|
|
} else if (seq.repeatMode == 1) {
|
|
// Count
|
|
if (seqRuntime.currentRepeat >= seq.repeatCount) {
|
|
stopSequence();
|
|
return;
|
|
}
|
|
}
|
|
// Infinite: 계속 실행
|
|
|
|
seqRuntime.currentStep = 0;
|
|
}
|
|
|
|
SequenceStep& step = seq.steps[seqRuntime.currentStep];
|
|
|
|
if (millis() - seqRuntime.lastStepTime >= step.delayMs) {
|
|
// CAN 메시지 전송
|
|
sendCANMessage(step.canId, step.extended, step.dlc, step.data);
|
|
|
|
seqRuntime.currentStep++;
|
|
seqRuntime.lastStepTime = millis();
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 시간 함수
|
|
// ============================================================================
|
|
|
|
void syncTimeFromPhone(int year, int month, int day, int hour, int minute, int second) {
|
|
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;
|
|
|
|
time_t t = mktime(&timeinfo);
|
|
struct timeval now = { .tv_sec = t };
|
|
settimeofday(&now, NULL);
|
|
|
|
timeSynced = true;
|
|
rtcSyncCount++;
|
|
|
|
Serial.printf("Time synced from phone: %04d-%02d-%02d %02d:%02d:%02d\n",
|
|
year, month, day, hour, minute, second);
|
|
}
|
|
|
|
String getTimestamp() {
|
|
if (timeSynced) {
|
|
time_t now;
|
|
time(&now);
|
|
struct tm* tm_now = localtime(&now);
|
|
|
|
char buf[32];
|
|
sprintf(buf, "%04d%02d%02d_%02d%02d%02d",
|
|
tm_now->tm_year + 1900, tm_now->tm_mon + 1, tm_now->tm_mday,
|
|
tm_now->tm_hour, tm_now->tm_min, tm_now->tm_sec);
|
|
return String(buf);
|
|
} else {
|
|
char buf[32];
|
|
sprintf(buf, "%010lu", millis());
|
|
return String(buf);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 전압 모니터링
|
|
// ============================================================================
|
|
|
|
void updateVoltage() {
|
|
float sum = 0;
|
|
for (int i = 0; i < VOLTAGE_SAMPLES; i++) {
|
|
int rawValue = analogRead(VOLTAGE_PIN);
|
|
float voltage = (rawValue / 4095.0) * 3.3 * (12.0 / 3.3); // 분압비 가정
|
|
sum += voltage;
|
|
delay(1);
|
|
}
|
|
|
|
currentVoltage = sum / VOLTAGE_SAMPLES;
|
|
|
|
if (currentVoltage < minVoltage1s) {
|
|
minVoltage1s = currentVoltage;
|
|
}
|
|
|
|
lowVoltageDetected = (currentVoltage < LOW_VOLTAGE_THRESHOLD);
|
|
}
|
|
|
|
// ============================================================================
|
|
// WebSocket 함수
|
|
// ============================================================================
|
|
|
|
void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) {
|
|
switch (type) {
|
|
case WStype_DISCONNECTED:
|
|
Serial.printf("[WS] Client #%u disconnected\n", num);
|
|
if (wsClientCount > 0) wsClientCount--;
|
|
break;
|
|
|
|
case WStype_CONNECTED:
|
|
{
|
|
IPAddress ip = webSocket.remoteIP(num);
|
|
Serial.printf("[WS] Client #%u connected from %s\n", num, ip.toString().c_str());
|
|
wsClientCount++;
|
|
}
|
|
break;
|
|
|
|
case WStype_TEXT:
|
|
Serial.printf("[WS] Client #%u: %s\n", num, payload);
|
|
handleWebSocketCommand(num, (char*)payload);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void handleWebSocketCommand(uint8_t num, const char* payload) {
|
|
DynamicJsonDocument doc(2048);
|
|
DeserializationError error = deserializeJson(doc, payload);
|
|
|
|
if (error) {
|
|
Serial.printf("JSON parse error: %s\n", error.c_str());
|
|
return;
|
|
}
|
|
|
|
const char* cmd = doc["cmd"];
|
|
|
|
if (strcmp(cmd, "setSpeed") == 0) {
|
|
uint8_t speed = doc["speed"];
|
|
setCANSpeed(speed);
|
|
}
|
|
else if (strcmp(cmd, "setMcpMode") == 0) {
|
|
uint8_t mode = doc["mode"];
|
|
setMCPMode(mode);
|
|
}
|
|
else if (strcmp(cmd, "startLogging") == 0) {
|
|
String format = doc["format"] | "bin";
|
|
startCANLogging(format);
|
|
}
|
|
else if (strcmp(cmd, "stopLogging") == 0) {
|
|
stopCANLogging();
|
|
}
|
|
else if (strcmp(cmd, "startSerialLogging") == 0) {
|
|
String format = doc["format"] | "csv";
|
|
startSerialLogging(format);
|
|
}
|
|
else if (strcmp(cmd, "stopSerialLogging") == 0) {
|
|
stopSerialLogging();
|
|
}
|
|
else if (strcmp(cmd, "getFiles") == 0) {
|
|
sendFileList(num);
|
|
}
|
|
else if (strcmp(cmd, "deleteFile") == 0) {
|
|
String filename = "/" + String((const char*)doc["filename"]);
|
|
deleteFile(filename, num);
|
|
}
|
|
else if (strcmp(cmd, "syncTimeFromPhone") == 0) {
|
|
int year = doc["year"];
|
|
int month = doc["month"];
|
|
int day = doc["day"];
|
|
int hour = doc["hour"];
|
|
int minute = doc["minute"];
|
|
int second = doc["second"];
|
|
syncTimeFromPhone(year, month, day, hour, minute, second);
|
|
}
|
|
else if (strcmp(cmd, "getSettings") == 0) {
|
|
DynamicJsonDocument response(1024);
|
|
response["type"] = "settings";
|
|
response["ssid"] = wifiSettings.ssid;
|
|
response["password"] = wifiSettings.password;
|
|
response["staEnable"] = wifiSettings.staEnable;
|
|
response["staSSID"] = wifiSettings.staSSID;
|
|
response["staPassword"] = wifiSettings.staPassword;
|
|
response["staConnected"] = (WiFi.status() == WL_CONNECTED);
|
|
response["staIP"] = WiFi.localIP().toString();
|
|
|
|
String json;
|
|
serializeJson(response, json);
|
|
webSocket.sendTXT(num, json);
|
|
}
|
|
else if (strcmp(cmd, "saveSettings") == 0) {
|
|
strcpy(wifiSettings.ssid, doc["ssid"]);
|
|
strcpy(wifiSettings.password, doc["password"]);
|
|
wifiSettings.staEnable = doc["staEnable"];
|
|
strcpy(wifiSettings.staSSID, doc["staSSID"]);
|
|
strcpy(wifiSettings.staPassword, doc["staPassword"]);
|
|
|
|
saveSettings();
|
|
|
|
DynamicJsonDocument response(256);
|
|
response["type"] = "settingsSaved";
|
|
response["success"] = true;
|
|
|
|
String json;
|
|
serializeJson(response, json);
|
|
webSocket.sendTXT(num, json);
|
|
}
|
|
else if (strcmp(cmd, "reboot") == 0) {
|
|
Serial.println("Rebooting by user request...");
|
|
delay(500);
|
|
ESP.restart();
|
|
}
|
|
else if (strcmp(cmd, "getSequences") == 0) {
|
|
DynamicJsonDocument response(4096);
|
|
response["type"] = "sequences";
|
|
|
|
JsonArray list = response.createNestedArray("list");
|
|
for (int i = 0; i < sequenceCount; i++) {
|
|
JsonObject seq = list.createNestedObject();
|
|
seq["name"] = sequences[i].name;
|
|
seq["index"] = i;
|
|
seq["mode"] = sequences[i].repeatMode;
|
|
seq["count"] = sequences[i].repeatCount;
|
|
seq["stepCount"] = sequences[i].stepCount;
|
|
seq["running"] = (seqRuntime.running && seqRuntime.activeSequenceIndex == i);
|
|
}
|
|
|
|
String json;
|
|
serializeJson(response, json);
|
|
webSocket.sendTXT(num, json);
|
|
}
|
|
else if (strcmp(cmd, "startSequence") == 0) {
|
|
uint8_t index = doc["index"];
|
|
startSequence(index);
|
|
}
|
|
else if (strcmp(cmd, "stopSequence") == 0) {
|
|
stopSequence();
|
|
}
|
|
else if (strcmp(cmd, "addSequence") == 0) {
|
|
if (sequenceCount < MAX_SEQUENCES) {
|
|
CANSequence& seq = sequences[sequenceCount];
|
|
|
|
strcpy(seq.name, doc["name"]);
|
|
seq.repeatMode = doc["repeatMode"];
|
|
seq.repeatCount = doc["repeatCount"];
|
|
|
|
JsonArray steps = doc["steps"];
|
|
seq.stepCount = 0;
|
|
|
|
for (JsonObject step : steps) {
|
|
if (seq.stepCount >= SEQUENCE_MAX_STEPS) break;
|
|
|
|
SequenceStep& s = seq.steps[seq.stepCount];
|
|
|
|
String idStr = step["id"];
|
|
s.canId = strtoul(idStr.c_str(), NULL, 16);
|
|
s.extended = step["ext"];
|
|
s.dlc = step["dlc"];
|
|
|
|
JsonArray data = step["data"];
|
|
for (int i = 0; i < 8; i++) {
|
|
s.data[i] = data[i];
|
|
}
|
|
|
|
s.delayMs = step["delay"];
|
|
|
|
seq.stepCount++;
|
|
}
|
|
|
|
sequenceCount++;
|
|
saveSequences();
|
|
|
|
DynamicJsonDocument response(256);
|
|
response["type"] = "sequenceSaved";
|
|
response["success"] = true;
|
|
|
|
String json;
|
|
serializeJson(response, json);
|
|
webSocket.sendTXT(num, json);
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "removeSequence") == 0) {
|
|
uint8_t index = doc["index"];
|
|
|
|
if (index < sequenceCount) {
|
|
// 시퀀스 삭제 (배열 이동)
|
|
for (int i = index; i < sequenceCount - 1; i++) {
|
|
sequences[i] = sequences[i + 1];
|
|
}
|
|
sequenceCount--;
|
|
saveSequences();
|
|
|
|
DynamicJsonDocument response(256);
|
|
response["type"] = "sequenceDeleted";
|
|
response["success"] = true;
|
|
|
|
String json;
|
|
serializeJson(response, json);
|
|
webSocket.sendTXT(num, json);
|
|
}
|
|
}
|
|
}
|
|
|
|
void sendWebUpdate() {
|
|
DynamicJsonDocument doc(8192);
|
|
|
|
doc["type"] = "update";
|
|
doc["logging"] = canLoggingEnabled;
|
|
doc["serialLogging"] = serialLoggingEnabled;
|
|
doc["sdReady"] = sdCardReady;
|
|
doc["totalMsg"] = totalCanRxCount;
|
|
doc["msgPerSec"] = canMessagesPerSecond;
|
|
doc["totalTx"] = totalCanTxCount;
|
|
doc["timeSync"] = timeSynced;
|
|
doc["syncCount"] = rtcSyncCount;
|
|
doc["mcpMode"] = currentMcpMode;
|
|
doc["currentFile"] = String(currentCanFileName);
|
|
doc["fileSize"] = currentCanFileSize;
|
|
doc["voltage"] = currentVoltage;
|
|
doc["minVoltage"] = minVoltage1s;
|
|
doc["lowVoltage"] = lowVoltageDetected;
|
|
|
|
// 큐 상태
|
|
doc["queueUsed"] = uxQueueMessagesWaiting(canQueue);
|
|
doc["queueSize"] = CAN_QUEUE_SIZE;
|
|
doc["serialQueueUsed"] = uxQueueMessagesWaiting(serialQueue);
|
|
doc["serialQueueSize"] = SERIAL_QUEUE_SIZE;
|
|
|
|
// PSRAM 상태
|
|
doc["psramFree"] = ESP.getFreePsram();
|
|
|
|
// Serial 통계
|
|
doc["totalSerialRx"] = totalSerialRxCount;
|
|
doc["totalSerialTx"] = totalSerialTxCount;
|
|
doc["serialFileSize"] = currentSerialFileSize;
|
|
doc["currentSerialFile"] = String(currentSerialFileName);
|
|
|
|
// CAN 메시지 배열
|
|
JsonArray messages = doc.createNestedArray("messages");
|
|
for (int i = 0; i < RECENT_MSG_COUNT; i++) {
|
|
if (recentData[i].count > 0) {
|
|
JsonObject msgObj = messages.createNestedObject();
|
|
msgObj["id"] = recentData[i].msg.can_id & 0x1FFFFFFF;
|
|
msgObj["dlc"] = recentData[i].msg.can_dlc;
|
|
msgObj["count"] = recentData[i].count;
|
|
|
|
JsonArray dataArray = msgObj.createNestedArray("data");
|
|
for (int j = 0; j < recentData[i].msg.can_dlc; j++) {
|
|
dataArray.add(recentData[i].msg.data[j]);
|
|
}
|
|
}
|
|
}
|
|
|
|
String json;
|
|
serializeJson(doc, json);
|
|
webSocket.broadcastTXT(json);
|
|
}
|
|
|
|
// ============================================================================
|
|
// 파일 관리
|
|
// ============================================================================
|
|
|
|
void sendFileList(uint8_t clientNum) {
|
|
if (!sdCardReady) return;
|
|
|
|
DynamicJsonDocument doc(8192);
|
|
doc["type"] = "files";
|
|
|
|
JsonArray list = doc.createNestedArray("list");
|
|
|
|
File root = SD.open("/");
|
|
File file = root.openNextFile();
|
|
|
|
while (file) {
|
|
if (!file.isDirectory()) {
|
|
String filename = file.name();
|
|
|
|
// '/' 제거
|
|
if (filename.startsWith("/")) {
|
|
filename = filename.substring(1);
|
|
}
|
|
|
|
JsonObject fileObj = list.createNestedObject();
|
|
fileObj["name"] = filename;
|
|
fileObj["size"] = file.size();
|
|
|
|
String comment = getFileComment(filename);
|
|
if (comment.length() > 0) {
|
|
fileObj["comment"] = comment;
|
|
}
|
|
}
|
|
file = root.openNextFile();
|
|
}
|
|
|
|
String json;
|
|
serializeJson(doc, json);
|
|
webSocket.sendTXT(clientNum, json);
|
|
}
|
|
|
|
void deleteFile(const String& filename, uint8_t clientNum) {
|
|
bool success = false;
|
|
String message = "Unknown error";
|
|
|
|
if (sdCardReady) {
|
|
if (SD.exists(filename)) {
|
|
if (SD.remove(filename)) {
|
|
success = true;
|
|
message = "File deleted";
|
|
Serial.printf("File deleted: %s\n", filename.c_str());
|
|
} else {
|
|
message = "Failed to delete file";
|
|
}
|
|
} else {
|
|
message = "File not found";
|
|
}
|
|
} else {
|
|
message = "SD card not ready";
|
|
}
|
|
|
|
DynamicJsonDocument response(256);
|
|
response["type"] = "deleteResult";
|
|
response["success"] = success;
|
|
response["message"] = message;
|
|
|
|
String json;
|
|
serializeJson(response, json);
|
|
webSocket.sendTXT(clientNum, json);
|
|
}
|
|
|
|
void addFileComment(const String& filename, const String& comment) {
|
|
String key = "cmt_" + filename;
|
|
key.replace("/", "_");
|
|
key.replace(".", "_");
|
|
|
|
preferences.putString(key.c_str(), comment);
|
|
Serial.printf("Comment added: %s = %s\n", filename.c_str(), comment.c_str());
|
|
}
|
|
|
|
String getFileComment(const String& filename) {
|
|
String key = "cmt_" + filename;
|
|
key.replace("/", "_");
|
|
key.replace(".", "_");
|
|
|
|
return preferences.getString(key.c_str(), "");
|
|
}
|
|
|
|
// ============================================================================
|
|
// 유틸리티
|
|
// ============================================================================
|
|
|
|
String formatBytes(uint32_t bytes) {
|
|
if (bytes < 1024) return String(bytes) + " B";
|
|
if (bytes < 1024 * 1024) return String(bytes / 1024.0, 2) + " KB";
|
|
return String(bytes / 1024.0 / 1024.0, 2) + " MB";
|
|
}
|
|
|
|
void printSystemInfo() {
|
|
Serial.println("\n[System Information]");
|
|
Serial.printf("Chip Model: %s\n", ESP.getChipModel());
|
|
Serial.printf("Chip Revision: %d\n", ESP.getChipRevision());
|
|
Serial.printf("CPU Cores: %d\n", ESP.getChipCores());
|
|
Serial.printf("CPU Frequency: %d MHz\n", ESP.getCpuFreqMHz());
|
|
Serial.printf("Flash Size: %d MB\n", ESP.getFlashChipSize() / (1024 * 1024));
|
|
Serial.printf("Free Heap: %d KB\n", ESP.getFreeHeap() / 1024);
|
|
|
|
if (psramFound()) {
|
|
Serial.printf("PSRAM Size: %d KB\n", ESP.getPsramSize() / 1024);
|
|
Serial.printf("Free PSRAM: %d KB\n", ESP.getFreePsram() / 1024);
|
|
}
|
|
|
|
Serial.printf("SDK Version: %s\n", ESP.getSdkVersion());
|
|
}
|