1972 lines
68 KiB
C++
1972 lines
68 KiB
C++
/*
|
||
* Byun CAN Logger with Web Interface + Serial Terminal
|
||
* Version: 2.5 - ESP32-S3 PSRAM Full Optimized Edition
|
||
* Complete PSRAM allocation with memory verification
|
||
*/
|
||
|
||
#include <Arduino.h>
|
||
#include <SPI.h>
|
||
#include <mcp2515.h>
|
||
#include <SoftWire.h>
|
||
#include <SD.h>
|
||
#include <WiFi.h>
|
||
#include <esp_wifi.h>
|
||
#include <esp_task_wdt.h>
|
||
#include <esp_sntp.h>
|
||
#include <WebServer.h>
|
||
#include <WebSocketsServer.h>
|
||
#include <ArduinoJson.h>
|
||
#include <freertos/FreeRTOS.h>
|
||
#include <freertos/task.h>
|
||
#include <freertos/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"
|
||
#include "serial_terminal.h"
|
||
|
||
// ========================================
|
||
// ESP32-S3 PSRAM 최적화 설정
|
||
// ========================================
|
||
|
||
// PSRAM 활용 - 대용량 버퍼 (8MB PSRAM 기준)
|
||
#define CAN_QUEUE_SIZE 10000 // 10,000개 (210KB PSRAM)
|
||
#define SERIAL_QUEUE_SIZE 2000 // 2,000개 (150KB PSRAM)
|
||
#define FILE_BUFFER_SIZE 131072 // 128KB (PSRAM)
|
||
#define SERIAL_CSV_BUFFER_SIZE 65536 // 64KB (PSRAM)
|
||
|
||
// webUpdateTask에서 전송
|
||
doc["psramFree"] = ESP.getFreePsram(); // ← 추가
|
||
doc["serialQueueUsed"] = uxQueueMessagesWaiting(serialQueue); // ← 추가
|
||
doc["serialQueueSize"] = SERIAL_QUEUE_SIZE; // ← 추가
|
||
|
||
// 기타 상수
|
||
#define MAX_FILENAME_LEN 64
|
||
#define RECENT_MSG_COUNT 100
|
||
#define MAX_TX_MESSAGES 20
|
||
#define MAX_COMMENT_LEN 128
|
||
#define MAX_SERIAL_LINE_LEN 64
|
||
|
||
// GPIO 핀 정의
|
||
#define CAN_INT_PIN 4
|
||
#define SERIAL_TX_PIN 17
|
||
#define SERIAL_RX_PIN 18
|
||
#define HSPI_MISO 13
|
||
#define HSPI_MOSI 11
|
||
#define HSPI_SCLK 12
|
||
#define HSPI_CS 10
|
||
#define VSPI_MISO 41
|
||
#define VSPI_MOSI 40
|
||
#define VSPI_SCLK 39
|
||
#define VSPI_CS 42
|
||
#define RTC_SDA 8
|
||
#define RTC_SCL 9
|
||
#define DS3231_ADDRESS 0x68
|
||
|
||
// 동기화 및 모니터링 설정
|
||
#define RTC_SYNC_INTERVAL_MS 60000
|
||
#define VOLTAGE_CHECK_INTERVAL_MS 5000
|
||
#define LOW_VOLTAGE_THRESHOLD 3.0
|
||
#define MONITORING_VOLT 5
|
||
|
||
// ========================================
|
||
// RTOS 우선순위 정의
|
||
// ========================================
|
||
#define PRIORITY_CAN_RX 24 // 최고 우선순위
|
||
#define PRIORITY_SD_WRITE 20 // 매우 높음
|
||
#define PRIORITY_SERIAL_RX 18 // 높음
|
||
#define PRIORITY_TX_TASK 15 // 중간-높음
|
||
#define PRIORITY_SEQUENCE 12 // 중간
|
||
#define PRIORITY_WEB_UPDATE 8 // 중간-낮음
|
||
#define PRIORITY_SD_MONITOR 5 // 낮음
|
||
#define PRIORITY_RTC_SYNC 2 // 최저
|
||
|
||
// ========================================
|
||
// Stack 크기 정의
|
||
// ========================================
|
||
#define STACK_CAN_RX 6144
|
||
#define STACK_SD_WRITE 32768
|
||
#define STACK_SERIAL_RX 8192
|
||
#define STACK_TX_TASK 6144
|
||
#define STACK_SEQUENCE 6144
|
||
#define STACK_WEB_UPDATE 16384
|
||
#define STACK_SD_MONITOR 4096
|
||
#define STACK_RTC_SYNC 3072
|
||
|
||
// ========================================
|
||
// 구조체 정의
|
||
// ========================================
|
||
|
||
struct CANMessage {
|
||
uint64_t timestamp_us;
|
||
uint32_t id;
|
||
uint8_t dlc;
|
||
uint8_t data[8];
|
||
} __attribute__((packed));
|
||
|
||
struct SerialMessage {
|
||
uint64_t timestamp_us;
|
||
uint16_t length;
|
||
uint8_t data[MAX_SERIAL_LINE_LEN];
|
||
bool isTx;
|
||
} __attribute__((packed));
|
||
|
||
struct SerialSettings {
|
||
uint32_t baudRate;
|
||
uint8_t dataBits;
|
||
uint8_t parity;
|
||
uint8_t stopBits;
|
||
} serialSettings = {115200, 8, 0, 1};
|
||
|
||
struct RecentCANData {
|
||
CANMessage msg;
|
||
uint32_t count;
|
||
};
|
||
|
||
struct TxMessage {
|
||
uint32_t id;
|
||
bool extended;
|
||
uint8_t dlc;
|
||
uint8_t data[8];
|
||
uint32_t interval;
|
||
uint32_t lastSent;
|
||
bool active;
|
||
};
|
||
|
||
struct SequenceStep {
|
||
uint32_t canId;
|
||
bool extended;
|
||
uint8_t dlc;
|
||
uint8_t data[8];
|
||
uint32_t delayMs;
|
||
};
|
||
|
||
struct CANSequence {
|
||
char name[32];
|
||
SequenceStep steps[20];
|
||
uint8_t stepCount;
|
||
uint8_t repeatMode;
|
||
uint32_t repeatCount;
|
||
};
|
||
|
||
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;
|
||
bool lowVoltage;
|
||
uint32_t lastCheck;
|
||
uint32_t lastMinReset;
|
||
} powerStatus = {0.0, 999.9, false, 0, 0};
|
||
|
||
enum MCP2515Mode {
|
||
MCP_MODE_NORMAL = 0,
|
||
MCP_MODE_LISTEN_ONLY = 1,
|
||
MCP_MODE_LOOPBACK = 2,
|
||
MCP_MODE_TRANSMIT = 3
|
||
};
|
||
|
||
// ========================================
|
||
// PSRAM 버퍼 포인터
|
||
// ========================================
|
||
uint8_t *fileBuffer = nullptr; // 128KB PSRAM
|
||
char *serialCsvBuffer = nullptr; // 64KB PSRAM
|
||
uint8_t *canQueueStorage = nullptr; // 210KB PSRAM
|
||
uint8_t *serialQueueStorage = nullptr; // 150KB PSRAM
|
||
|
||
// FreeRTOS 정적 Queue 구조체 (DRAM - 작은 크기)
|
||
StaticQueue_t canQueueBuffer;
|
||
StaticQueue_t serialQueueBuffer;
|
||
|
||
// ========================================
|
||
// 전역 변수
|
||
// ========================================
|
||
|
||
// 하드웨어 객체
|
||
SPIClass hspi(HSPI);
|
||
SPIClass vspi(FSPI);
|
||
MCP2515 mcp2515(HSPI_CS, 20000000, &hspi);
|
||
HardwareSerial SerialComm(1);
|
||
WebServer server(80);
|
||
WebSocketsServer webSocket = WebSocketsServer(81);
|
||
Preferences preferences;
|
||
|
||
// FreeRTOS 핸들
|
||
QueueHandle_t canQueue;
|
||
QueueHandle_t serialQueue;
|
||
SemaphoreHandle_t sdMutex;
|
||
SemaphoreHandle_t rtcMutex;
|
||
SemaphoreHandle_t serialMutex;
|
||
TaskHandle_t canRxTaskHandle = NULL;
|
||
TaskHandle_t sdWriteTaskHandle = NULL;
|
||
TaskHandle_t webTaskHandle = NULL;
|
||
TaskHandle_t rtcTaskHandle = NULL;
|
||
TaskHandle_t serialRxTaskHandle = NULL;
|
||
|
||
// 상태 변수
|
||
volatile bool loggingEnabled = false;
|
||
volatile bool serialLoggingEnabled = false;
|
||
volatile bool sdCardReady = false;
|
||
File logFile;
|
||
File serialLogFile;
|
||
char currentFilename[MAX_FILENAME_LEN];
|
||
char currentSerialFilename[MAX_FILENAME_LEN];
|
||
uint16_t bufferIndex = 0;
|
||
uint16_t serialCsvIndex = 0;
|
||
volatile uint32_t currentFileSize = 0;
|
||
volatile uint32_t currentSerialFileSize = 0;
|
||
volatile bool canLogFormatCSV = false;
|
||
volatile bool serialLogFormatCSV = true;
|
||
volatile uint64_t canLogStartTime = 0;
|
||
volatile uint64_t serialLogStartTime = 0;
|
||
|
||
// CAN 설정
|
||
MCP2515Mode currentMcpMode = MCP_MODE_NORMAL;
|
||
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;
|
||
volatile uint32_t totalSerialRxCount = 0;
|
||
volatile uint32_t totalSerialTxCount = 0;
|
||
uint32_t totalTxCount = 0;
|
||
|
||
// CAN 송신 및 시퀀스
|
||
TxMessage txMessages[MAX_TX_MESSAGES];
|
||
#define MAX_SEQUENCES 10
|
||
CANSequence sequences[MAX_SEQUENCES];
|
||
uint8_t sequenceCount = 0;
|
||
SequenceRuntime seqRuntime = {false, 0, 0, 0, -1};
|
||
|
||
// 파일 커멘트
|
||
#define MAX_FILE_COMMENTS 50
|
||
FileComment fileComments[MAX_FILE_COMMENTS];
|
||
int commentCount = 0;
|
||
|
||
// WiFi 설정
|
||
char wifiSSID[32] = "Byun_CAN_Logger";
|
||
char wifiPassword[64] = "12345678";
|
||
bool enableSTAMode = false;
|
||
char staSSID[32] = "";
|
||
char staPassword[64] = "";
|
||
|
||
// RTC
|
||
SoftWire rtcWire(RTC_SDA, RTC_SCL);
|
||
char rtcSyncBuffer[20];
|
||
|
||
// Forward declarations
|
||
void IRAM_ATTR canISR();
|
||
|
||
// ========================================
|
||
// 메모리 정보 출력 함수
|
||
// ========================================
|
||
|
||
void printMemoryInfo(const char* name, void* ptr) {
|
||
if (ptr == nullptr) {
|
||
Serial.printf(" %-25s: NULL\n", name);
|
||
return;
|
||
}
|
||
|
||
bool isPSRAM = esp_ptr_external_ram(ptr);
|
||
|
||
Serial.printf(" %-25s: %s @ 0x%08X\n",
|
||
name,
|
||
isPSRAM ? "PSRAM" : "DRAM ",
|
||
(uint32_t)ptr);
|
||
}
|
||
|
||
// ========================================
|
||
// PSRAM 버퍼 할당
|
||
// ========================================
|
||
|
||
bool allocatePSRAMBuffers() {
|
||
Serial.println("\n========================================");
|
||
Serial.println("PSRAM 버퍼 할당 중...");
|
||
Serial.println("========================================");
|
||
|
||
// 1. 파일 버퍼 (128KB)
|
||
fileBuffer = (uint8_t*)heap_caps_malloc(FILE_BUFFER_SIZE,
|
||
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||
if (fileBuffer == nullptr) {
|
||
Serial.printf("✗ 파일 버퍼 할당 실패 (%d KB)\n", FILE_BUFFER_SIZE / 1024);
|
||
return false;
|
||
}
|
||
memset(fileBuffer, 0, FILE_BUFFER_SIZE);
|
||
|
||
// 2. Serial CSV 버퍼 (64KB)
|
||
serialCsvBuffer = (char*)heap_caps_malloc(SERIAL_CSV_BUFFER_SIZE,
|
||
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||
if (serialCsvBuffer == nullptr) {
|
||
Serial.printf("✗ Serial CSV 버퍼 할당 실패 (%d KB)\n", SERIAL_CSV_BUFFER_SIZE / 1024);
|
||
heap_caps_free(fileBuffer);
|
||
return false;
|
||
}
|
||
memset(serialCsvBuffer, 0, SERIAL_CSV_BUFFER_SIZE);
|
||
|
||
// 3. CAN Queue 버퍼 (210KB)
|
||
size_t canQueueSize = CAN_QUEUE_SIZE * sizeof(CANMessage);
|
||
canQueueStorage = (uint8_t*)heap_caps_malloc(canQueueSize,
|
||
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||
if (canQueueStorage == nullptr) {
|
||
Serial.printf("✗ CAN Queue 버퍼 할당 실패 (%.1f KB)\n", canQueueSize / 1024.0);
|
||
heap_caps_free(fileBuffer);
|
||
heap_caps_free(serialCsvBuffer);
|
||
return false;
|
||
}
|
||
memset(canQueueStorage, 0, canQueueSize);
|
||
|
||
// 4. Serial Queue 버퍼 (150KB)
|
||
size_t serialQueueSize = SERIAL_QUEUE_SIZE * sizeof(SerialMessage);
|
||
serialQueueStorage = (uint8_t*)heap_caps_malloc(serialQueueSize,
|
||
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||
if (serialQueueStorage == nullptr) {
|
||
Serial.printf("✗ Serial Queue 버퍼 할당 실패 (%.1f KB)\n", serialQueueSize / 1024.0);
|
||
heap_caps_free(fileBuffer);
|
||
heap_caps_free(serialCsvBuffer);
|
||
heap_caps_free(canQueueStorage);
|
||
return false;
|
||
}
|
||
memset(serialQueueStorage, 0, serialQueueSize);
|
||
|
||
// 메모리 정보 출력
|
||
Serial.println("\n✓ PSRAM 버퍼 할당 완료:");
|
||
printMemoryInfo("파일 버퍼 (128KB)", fileBuffer);
|
||
printMemoryInfo("Serial CSV 버퍼 (64KB)", serialCsvBuffer);
|
||
printMemoryInfo("CAN Queue 버퍼 (210KB)", canQueueStorage);
|
||
printMemoryInfo("Serial Queue 버퍼 (150KB)", serialQueueStorage);
|
||
|
||
float totalPSRAM = (FILE_BUFFER_SIZE + SERIAL_CSV_BUFFER_SIZE + canQueueSize + serialQueueSize) / 1024.0;
|
||
Serial.printf("\n총 PSRAM 사용: %.1f KB\n", totalPSRAM);
|
||
Serial.printf("PSRAM 여유: %u KB\n\n", ESP.getFreePsram() / 1024);
|
||
|
||
return true;
|
||
}
|
||
|
||
// ========================================
|
||
// Queue 생성 (PSRAM 버퍼 사용)
|
||
// ========================================
|
||
|
||
bool createQueues() {
|
||
Serial.println("========================================");
|
||
Serial.println("Queue 생성 중 (PSRAM 버퍼 사용)...");
|
||
Serial.println("========================================");
|
||
|
||
// CAN Queue (PSRAM 버퍼 사용)
|
||
canQueue = xQueueCreateStatic(
|
||
CAN_QUEUE_SIZE,
|
||
sizeof(CANMessage),
|
||
canQueueStorage,
|
||
&canQueueBuffer
|
||
);
|
||
|
||
if (canQueue == NULL) {
|
||
Serial.println("✗ CAN Queue 생성 실패!");
|
||
return false;
|
||
}
|
||
|
||
// Serial Queue (PSRAM 버퍼 사용)
|
||
serialQueue = xQueueCreateStatic(
|
||
SERIAL_QUEUE_SIZE,
|
||
sizeof(SerialMessage),
|
||
serialQueueStorage,
|
||
&serialQueueBuffer
|
||
);
|
||
|
||
if (serialQueue == NULL) {
|
||
Serial.println("✗ Serial Queue 생성 실패!");
|
||
return false;
|
||
}
|
||
|
||
Serial.println("\n✓ Queue 생성 완료:");
|
||
Serial.printf(" - CAN Queue : %d개 × %d bytes = %.1f KB\n",
|
||
CAN_QUEUE_SIZE, sizeof(CANMessage),
|
||
(CAN_QUEUE_SIZE * sizeof(CANMessage)) / 1024.0);
|
||
Serial.printf(" - Serial Queue : %d개 × %d bytes = %.1f KB\n",
|
||
SERIAL_QUEUE_SIZE, sizeof(SerialMessage),
|
||
(SERIAL_QUEUE_SIZE * sizeof(SerialMessage)) / 1024.0);
|
||
|
||
Serial.println("\n메모리 위치 확인:");
|
||
printMemoryInfo("CAN Queue 버퍼", canQueueStorage);
|
||
printMemoryInfo("CAN Queue 핸들", canQueue);
|
||
printMemoryInfo("Serial Queue 버퍼", serialQueueStorage);
|
||
printMemoryInfo("Serial Queue 핸들", serialQueue);
|
||
Serial.println();
|
||
|
||
return true;
|
||
}
|
||
|
||
// ========================================
|
||
// Serial 설정 함수
|
||
// ========================================
|
||
|
||
void loadSerialSettings() {
|
||
serialSettings.baudRate = preferences.getUInt("ser_baud", 115200);
|
||
serialSettings.dataBits = preferences.getUChar("ser_data", 8);
|
||
serialSettings.parity = preferences.getUChar("ser_parity", 0);
|
||
serialSettings.stopBits = preferences.getUChar("ser_stop", 1);
|
||
}
|
||
|
||
void saveSerialSettings() {
|
||
preferences.putUInt("ser_baud", serialSettings.baudRate);
|
||
preferences.putUChar("ser_data", serialSettings.dataBits);
|
||
preferences.putUChar("ser_parity", serialSettings.parity);
|
||
preferences.putUChar("ser_stop", serialSettings.stopBits);
|
||
}
|
||
|
||
void applySerialSettings() {
|
||
uint32_t config = SERIAL_8N1;
|
||
|
||
if (serialSettings.dataBits == 5) {
|
||
if (serialSettings.parity == 0) config = SERIAL_5N1;
|
||
else if (serialSettings.parity == 1) config = SERIAL_5E1;
|
||
else if (serialSettings.parity == 2) config = SERIAL_5O1;
|
||
} else if (serialSettings.dataBits == 6) {
|
||
if (serialSettings.parity == 0) config = SERIAL_6N1;
|
||
else if (serialSettings.parity == 1) config = SERIAL_6E1;
|
||
else if (serialSettings.parity == 2) config = SERIAL_6O1;
|
||
} else if (serialSettings.dataBits == 7) {
|
||
if (serialSettings.parity == 0) config = SERIAL_7N1;
|
||
else if (serialSettings.parity == 1) config = SERIAL_7E1;
|
||
else if (serialSettings.parity == 2) config = SERIAL_7O1;
|
||
} else {
|
||
if (serialSettings.parity == 0) config = SERIAL_8N1;
|
||
else if (serialSettings.parity == 1) config = SERIAL_8E1;
|
||
else if (serialSettings.parity == 2) config = SERIAL_8O1;
|
||
}
|
||
|
||
if (serialSettings.stopBits == 2) {
|
||
config |= 0x3000;
|
||
}
|
||
|
||
SerialComm.begin(serialSettings.baudRate, config, SERIAL_RX_PIN, SERIAL_TX_PIN);
|
||
SerialComm.setRxBufferSize(4096);
|
||
|
||
Serial.printf("✓ Serial 설정: %u-%u-%u-%u\n",
|
||
serialSettings.baudRate, serialSettings.dataBits,
|
||
serialSettings.parity, serialSettings.stopBits);
|
||
}
|
||
|
||
// ========================================
|
||
// 설정 저장/로드
|
||
// ========================================
|
||
|
||
void loadSettings() {
|
||
preferences.begin("can-logger", false);
|
||
|
||
preferences.getString("wifi_ssid", wifiSSID, sizeof(wifiSSID));
|
||
preferences.getString("wifi_pass", wifiPassword, sizeof(wifiPassword));
|
||
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");
|
||
|
||
int speedIndex = preferences.getInt("can_speed", 3);
|
||
if (speedIndex >= 0 && speedIndex < 4) {
|
||
currentCanSpeed = canSpeedValues[speedIndex];
|
||
}
|
||
|
||
int savedMode = preferences.getInt("mcp_mode", 0);
|
||
if (savedMode >= 0 && savedMode <= 3) {
|
||
currentMcpMode = (MCP2515Mode)savedMode;
|
||
}
|
||
|
||
loadSerialSettings();
|
||
preferences.end();
|
||
}
|
||
|
||
void saveSettings() {
|
||
preferences.begin("can-logger", false);
|
||
|
||
preferences.putString("wifi_ssid", wifiSSID);
|
||
preferences.putString("wifi_pass", wifiPassword);
|
||
preferences.putBool("sta_enable", enableSTAMode);
|
||
preferences.putString("sta_ssid", staSSID);
|
||
preferences.putString("sta_pass", staPassword);
|
||
|
||
for (int i = 0; i < 4; i++) {
|
||
if (canSpeedValues[i] == currentCanSpeed) {
|
||
preferences.putInt("can_speed", i);
|
||
break;
|
||
}
|
||
}
|
||
|
||
preferences.putInt("mcp_mode", (int)currentMcpMode);
|
||
saveSerialSettings();
|
||
preferences.end();
|
||
}
|
||
|
||
// ========================================
|
||
// RTC 함수
|
||
// ========================================
|
||
|
||
void initRTC() {
|
||
rtcWire.begin();
|
||
rtcWire.setClock(100000);
|
||
|
||
rtcWire.beginTransmission(DS3231_ADDRESS);
|
||
if (rtcWire.endTransmission() == 0) {
|
||
timeSyncStatus.rtcAvailable = true;
|
||
Serial.println("✓ RTC(DS3231) 감지");
|
||
} else {
|
||
timeSyncStatus.rtcAvailable = false;
|
||
Serial.println("! RTC(DS3231) 없음");
|
||
}
|
||
}
|
||
|
||
uint8_t bcdToDec(uint8_t val) {
|
||
return (val >> 4) * 10 + (val & 0x0F);
|
||
}
|
||
|
||
uint8_t decToBcd(uint8_t val) {
|
||
return ((val / 10) << 4) | (val % 10);
|
||
}
|
||
|
||
bool readRTC(struct tm *timeinfo) {
|
||
if (!timeSyncStatus.rtcAvailable) return false;
|
||
|
||
if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(50)) != pdTRUE) {
|
||
return false;
|
||
}
|
||
|
||
rtcWire.beginTransmission(DS3231_ADDRESS);
|
||
rtcWire.write(0x00);
|
||
if (rtcWire.endTransmission() != 0) {
|
||
xSemaphoreGive(rtcMutex);
|
||
return false;
|
||
}
|
||
|
||
if (rtcWire.requestFrom(DS3231_ADDRESS, 7) != 7) {
|
||
xSemaphoreGive(rtcMutex);
|
||
return false;
|
||
}
|
||
|
||
uint8_t buffer[7];
|
||
for (int i = 0; i < 7; i++) {
|
||
buffer[i] = rtcWire.read();
|
||
}
|
||
|
||
xSemaphoreGive(rtcMutex);
|
||
|
||
timeinfo->tm_sec = bcdToDec(buffer0 & 0x7F);
|
||
timeinfo->tm_min = bcdToDec(buffer[1] & 0x7F);
|
||
timeinfo->tm_hour = bcdToDec(buffer[2] & 0x3F);
|
||
timeinfo->tm_wday = bcdToDec(buffer[3] & 0x07) - 1;
|
||
timeinfo->tm_mday = bcdToDec(buffer[4] & 0x3F);
|
||
timeinfo->tm_mon = bcdToDec(buffer[5] & 0x1F) - 1;
|
||
timeinfo->tm_year = bcdToDec(buffer[6]) + 100;
|
||
|
||
return true;
|
||
}
|
||
|
||
bool writeRTC(const struct tm *timeinfo) {
|
||
if (!timeSyncStatus.rtcAvailable) return false;
|
||
|
||
if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(50)) != pdTRUE) {
|
||
return false;
|
||
}
|
||
|
||
rtcWire.beginTransmission(DS3231_ADDRESS);
|
||
rtcWire.write(0x00);
|
||
rtcWire.write(decToBcd(timeinfo->tm_sec));
|
||
rtcWire.write(decToBcd(timeinfo->tm_min));
|
||
rtcWire.write(decToBcd(timeinfo->tm_hour));
|
||
rtcWire.write(decToBcd(timeinfo->tm_wday + 1));
|
||
rtcWire.write(decToBcd(timeinfo->tm_mday));
|
||
rtcWire.write(decToBcd(timeinfo->tm_mon + 1));
|
||
rtcWire.write(decToBcd(timeinfo->tm_year - 100));
|
||
|
||
bool success = (rtcWire.endTransmission() == 0);
|
||
xSemaphoreGive(rtcMutex);
|
||
|
||
return success;
|
||
}
|
||
|
||
void timeSyncCallback(struct timeval *tv) {
|
||
Serial.println("✓ NTP 시간 동기화 완료");
|
||
|
||
timeSyncStatus.synchronized = true;
|
||
timeSyncStatus.lastSyncTime = (uint64_t)tv->tv_sec * 1000000ULL + tv->tv_usec;
|
||
timeSyncStatus.syncCount++;
|
||
|
||
if (timeSyncStatus.rtcAvailable) {
|
||
struct tm timeinfo;
|
||
time_t now = tv->tv_sec;
|
||
localtime_r(&now, &timeinfo);
|
||
|
||
if (writeRTC(&timeinfo)) {
|
||
timeSyncStatus.rtcSyncCount++;
|
||
}
|
||
}
|
||
}
|
||
|
||
void initNTP() {
|
||
configTime(9 * 3600, 0, "pool.ntp.org", "time.nist.gov", "time.google.com");
|
||
sntp_set_time_sync_notification_cb(timeSyncCallback);
|
||
}
|
||
|
||
void rtcSyncTask(void *parameter) {
|
||
const TickType_t xDelay = pdMS_TO_TICKS(RTC_SYNC_INTERVAL_MS);
|
||
|
||
while (1) {
|
||
if (timeSyncStatus.rtcAvailable) {
|
||
struct tm timeinfo;
|
||
if (readRTC(&timeinfo)) {
|
||
time_t now = mktime(&timeinfo);
|
||
struct timeval tv = { .tv_sec = now, .tv_usec = 0 };
|
||
settimeofday(&tv, NULL);
|
||
|
||
timeSyncStatus.synchronized = true;
|
||
timeSyncStatus.lastSyncTime = (uint64_t)now * 1000000ULL;
|
||
timeSyncStatus.rtcSyncCount++;
|
||
}
|
||
}
|
||
|
||
vTaskDelay(xDelay);
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// MCP2515 모드 설정
|
||
// ========================================
|
||
|
||
bool setMCP2515Mode(MCP2515Mode mode) {
|
||
const char* modeName;
|
||
MCP2515::ERROR result;
|
||
|
||
switch (mode) {
|
||
case MCP_MODE_NORMAL:
|
||
result = mcp2515.setNormalMode();
|
||
modeName = "Normal";
|
||
break;
|
||
case MCP_MODE_LISTEN_ONLY:
|
||
result = mcp2515.setListenOnlyMode();
|
||
modeName = "Listen-Only";
|
||
break;
|
||
case MCP_MODE_LOOPBACK:
|
||
result = mcp2515.setLoopbackMode();
|
||
modeName = "Loopback";
|
||
break;
|
||
case MCP_MODE_TRANSMIT:
|
||
result = mcp2515.setListenOnlyMode();
|
||
modeName = "Transmit-Only";
|
||
break;
|
||
default:
|
||
return false;
|
||
}
|
||
|
||
if (result == MCP2515::ERROR_OK) {
|
||
currentMcpMode = mode;
|
||
Serial.printf("✓ MCP2515 모드: %s\n", modeName);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// ========================================
|
||
// CAN 인터럽트 핸들러
|
||
// ========================================
|
||
|
||
void IRAM_ATTR canISR() {
|
||
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
||
if (canRxTaskHandle != NULL) {
|
||
vTaskNotifyGiveFromISR(canRxTaskHandle, &xHigherPriorityTaskWoken);
|
||
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// Serial RX Task (Priority 18)
|
||
// ========================================
|
||
|
||
void serialRxTask(void *parameter) {
|
||
SerialMessage serialMsg;
|
||
uint8_t lineBuffer[MAX_SERIAL_LINE_LEN];
|
||
uint16_t lineIndex = 0;
|
||
uint32_t lastActivity = millis();
|
||
|
||
Serial.println("✓ Serial RX Task 시작 (Priority 18)");
|
||
|
||
while (1) {
|
||
while (SerialComm.available()) {
|
||
uint8_t c = SerialComm.read();
|
||
lineBuffer[lineIndex++] = c;
|
||
lastActivity = millis();
|
||
|
||
if (c == '\n' || c == '\r' || lineIndex >= MAX_SERIAL_LINE_LEN - 1) {
|
||
if (lineIndex > 0) {
|
||
struct timeval tv;
|
||
gettimeofday(&tv, NULL);
|
||
serialMsg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||
serialMsg.length = lineIndex;
|
||
memcpy(serialMsg.data, lineBuffer, lineIndex);
|
||
serialMsg.isTx = false;
|
||
|
||
if (xQueueSend(serialQueue, &serialMsg, 0) == pdTRUE) {
|
||
totalSerialRxCount++;
|
||
}
|
||
|
||
lineIndex = 0;
|
||
}
|
||
}
|
||
|
||
if (lineIndex >= MAX_SERIAL_LINE_LEN - 1) {
|
||
lineIndex = 0;
|
||
}
|
||
}
|
||
|
||
if (lineIndex > 0 && (millis() - lastActivity > 100)) {
|
||
struct timeval tv;
|
||
gettimeofday(&tv, NULL);
|
||
serialMsg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||
serialMsg.length = lineIndex;
|
||
memcpy(serialMsg.data, lineBuffer, lineIndex);
|
||
serialMsg.isTx = false;
|
||
|
||
if (xQueueSend(serialQueue, &serialMsg, 0) == pdTRUE) {
|
||
totalSerialRxCount++;
|
||
}
|
||
|
||
lineIndex = 0;
|
||
}
|
||
|
||
vTaskDelay(1);
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// CAN RX Task (Priority 24 - 최고)
|
||
// ========================================
|
||
|
||
void canRxTask(void *parameter) {
|
||
struct can_frame frame;
|
||
CANMessage msg;
|
||
|
||
Serial.println("✓ CAN RX Task 시작 (Priority 24 - 최고)");
|
||
|
||
while (1) {
|
||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||
|
||
while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {
|
||
struct timeval tv;
|
||
gettimeofday(&tv, NULL);
|
||
msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||
msg.id = frame.can_id & 0x1FFFFFFF;
|
||
msg.dlc = frame.can_dlc;
|
||
memcpy(msg.data, frame.data, 8);
|
||
|
||
if (xQueueSend(canQueue, &msg, 0) == pdTRUE) {
|
||
totalMsgCount++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// SD Write Task (Priority 20 - 매우 높음)
|
||
// ========================================
|
||
|
||
void sdWriteTask(void *parameter) {
|
||
CANMessage canMsg;
|
||
uint32_t csvFlushCounter = 0;
|
||
|
||
Serial.println("✓ SD Write Task 시작 (Priority 20)");
|
||
|
||
while (1) {
|
||
bool hasWork = false;
|
||
|
||
int batchCount = 0;
|
||
while (batchCount < 100 && xQueueReceive(canQueue, &canMsg, 0) == pdTRUE) {
|
||
hasWork = true;
|
||
batchCount++;
|
||
|
||
// 실시간 모니터링 업데이트
|
||
bool found = false;
|
||
for (int i = 0; i < RECENT_MSG_COUNT; i++) {
|
||
if (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;
|
||
}
|
||
}
|
||
}
|
||
|
||
// CAN 로깅
|
||
if (loggingEnabled && sdCardReady) {
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
|
||
if (canLogFormatCSV) {
|
||
char csvLine[128];
|
||
uint64_t relativeTime = canMsg.timestamp_us - canLogStartTime;
|
||
|
||
char dataStr[32];
|
||
int dataLen = 0;
|
||
for (int i = 0; i < canMsg.dlc; i++) {
|
||
dataLen += sprintf(&dataStr[dataLen], "%02X", canMsg.data[i]);
|
||
if (i < canMsg.dlc - 1) dataStr[dataLen++] = ' ';
|
||
}
|
||
dataStr[dataLen] = '\0';
|
||
|
||
int lineLen = snprintf(csvLine, sizeof(csvLine),
|
||
"%llu,0x%X,%d,%s\n",
|
||
relativeTime, canMsg.id, canMsg.dlc, dataStr);
|
||
|
||
if (logFile) {
|
||
logFile.write((uint8_t*)csvLine, lineLen);
|
||
currentFileSize += lineLen;
|
||
|
||
if (++csvFlushCounter >= 500) {
|
||
logFile.flush();
|
||
csvFlushCounter = 0;
|
||
}
|
||
}
|
||
} else {
|
||
// BIN 형식
|
||
if (bufferIndex + sizeof(CANMessage) <= FILE_BUFFER_SIZE) {
|
||
memcpy(&fileBuffer[bufferIndex], &canMsg, sizeof(CANMessage));
|
||
bufferIndex += sizeof(CANMessage);
|
||
currentFileSize += sizeof(CANMessage);
|
||
}
|
||
|
||
if (bufferIndex >= FILE_BUFFER_SIZE * 0.9) {
|
||
if (logFile) {
|
||
logFile.write(fileBuffer, bufferIndex);
|
||
logFile.flush();
|
||
bufferIndex = 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!hasWork) {
|
||
vTaskDelay(1);
|
||
} else {
|
||
taskYIELD();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// SD Monitor Task (Priority 5)
|
||
// ========================================
|
||
|
||
void sdMonitorTask(void *parameter) {
|
||
const TickType_t xDelay = pdMS_TO_TICKS(1000);
|
||
uint32_t lastStatusPrint = 0;
|
||
|
||
Serial.println("✓ SD Monitor Task 시작 (Priority 5)");
|
||
|
||
while (1) {
|
||
uint32_t currentTime = millis();
|
||
|
||
// 메시지/초 계산
|
||
if (currentTime - lastMsgCountTime >= 1000) {
|
||
msgPerSecond = totalMsgCount - lastMsgCount;
|
||
lastMsgCount = totalMsgCount;
|
||
lastMsgCountTime = currentTime;
|
||
}
|
||
|
||
// 전압 체크
|
||
if (currentTime - powerStatus.lastCheck >= VOLTAGE_CHECK_INTERVAL_MS) {
|
||
float rawVoltage = analogRead(MONITORING_VOLT) * (3.3 / 4095.0);
|
||
powerStatus.voltage = rawVoltage * 1.0;
|
||
|
||
if (currentTime - powerStatus.lastMinReset >= 1000) {
|
||
powerStatus.minVoltage = powerStatus.voltage;
|
||
powerStatus.lastMinReset = currentTime;
|
||
} else {
|
||
if (powerStatus.voltage < powerStatus.minVoltage) {
|
||
powerStatus.minVoltage = powerStatus.voltage;
|
||
}
|
||
}
|
||
|
||
powerStatus.lowVoltage = (powerStatus.voltage < LOW_VOLTAGE_THRESHOLD);
|
||
powerStatus.lastCheck = currentTime;
|
||
}
|
||
|
||
// 10초마다 상태 출력
|
||
if (currentTime - lastStatusPrint >= 10000) {
|
||
uint32_t canQueueUsed = uxQueueMessagesWaiting(canQueue);
|
||
uint32_t serialQueueUsed = uxQueueMessagesWaiting(serialQueue);
|
||
float canQueuePercent = (float)canQueueUsed / CAN_QUEUE_SIZE * 100.0;
|
||
float serialQueuePercent = (float)serialQueueUsed / SERIAL_QUEUE_SIZE * 100.0;
|
||
|
||
Serial.printf("[상태] CAN: %u msg/s | CAN큐: %u/%u (%.1f%%) | Serial큐: %u/%u (%.1f%%) | PSRAM: %u KB\n",
|
||
msgPerSecond,
|
||
canQueueUsed, CAN_QUEUE_SIZE, canQueuePercent,
|
||
serialQueueUsed, SERIAL_QUEUE_SIZE, serialQueuePercent,
|
||
ESP.getFreePsram() / 1024);
|
||
|
||
if (canQueuePercent >= 80.0) {
|
||
Serial.printf("⚠️ 경고: CAN Queue 사용률 %.1f%%\n", canQueuePercent);
|
||
}
|
||
if (serialQueuePercent >= 80.0) {
|
||
Serial.printf("⚠️ 경고: Serial Queue 사용률 %.1f%%\n", serialQueuePercent);
|
||
}
|
||
|
||
lastStatusPrint = currentTime;
|
||
}
|
||
|
||
vTaskDelay(xDelay);
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// TX Task (Priority 15)
|
||
// ========================================
|
||
|
||
void txTask(void *parameter) {
|
||
struct can_frame frame;
|
||
|
||
Serial.println("✓ TX Task 시작 (Priority 15)");
|
||
|
||
while (1) {
|
||
uint32_t now = millis();
|
||
bool anyActive = false;
|
||
|
||
for (int i = 0; i < MAX_TX_MESSAGES; i++) {
|
||
if (txMessages[i].active && txMessages[i].interval > 0) {
|
||
anyActive = true;
|
||
|
||
if (now - txMessages[i].lastSent >= txMessages[i].interval) {
|
||
if (currentMcpMode == MCP_MODE_TRANSMIT) {
|
||
mcp2515.setNormalMode();
|
||
}
|
||
|
||
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 (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) {
|
||
totalTxCount++;
|
||
txMessages[i].lastSent = now;
|
||
}
|
||
|
||
if (currentMcpMode == MCP_MODE_TRANSMIT) {
|
||
mcp2515.setListenOnlyMode();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
vTaskDelay(pdMS_TO_TICKS(anyActive ? 1 : 10));
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// Sequence Task (Priority 12)
|
||
// ========================================
|
||
|
||
void sequenceTask(void *parameter) {
|
||
Serial.println("✓ Sequence Task 시작 (Priority 12)");
|
||
|
||
while (1) {
|
||
if (seqRuntime.running && seqRuntime.activeSequenceIndex >= 0 &&
|
||
seqRuntime.activeSequenceIndex < sequenceCount) {
|
||
|
||
CANSequence* seq = &sequences[seqRuntime.activeSequenceIndex];
|
||
uint32_t now = millis();
|
||
|
||
if (seqRuntime.currentStep < seq->stepCount) {
|
||
SequenceStep* step = &seq->steps[seqRuntime.currentStep];
|
||
|
||
if (now - seqRuntime.lastStepTime >= step->delayMs) {
|
||
if (currentMcpMode == MCP_MODE_TRANSMIT) {
|
||
mcp2515.setNormalMode();
|
||
}
|
||
|
||
struct can_frame frame;
|
||
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);
|
||
|
||
if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) {
|
||
totalTxCount++;
|
||
}
|
||
|
||
if (currentMcpMode == MCP_MODE_TRANSMIT) {
|
||
mcp2515.setListenOnlyMode();
|
||
}
|
||
|
||
seqRuntime.currentStep++;
|
||
seqRuntime.lastStepTime = now;
|
||
}
|
||
} else {
|
||
if (seq->repeatMode == 0) {
|
||
seqRuntime.running = false;
|
||
} else if (seq->repeatMode == 1) {
|
||
seqRuntime.currentRepeat++;
|
||
if (seqRuntime.currentRepeat >= seq->repeatCount) {
|
||
seqRuntime.running = false;
|
||
} else {
|
||
seqRuntime.currentStep = 0;
|
||
seqRuntime.lastStepTime = now;
|
||
}
|
||
} else if (seq->repeatMode == 2) {
|
||
seqRuntime.currentStep = 0;
|
||
seqRuntime.lastStepTime = now;
|
||
}
|
||
}
|
||
|
||
vTaskDelay(pdMS_TO_TICKS(1));
|
||
} else {
|
||
vTaskDelay(pdMS_TO_TICKS(10));
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// 파일 커멘트 관리
|
||
// ========================================
|
||
|
||
void saveFileComments() {
|
||
if (!sdCardReady) return;
|
||
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
File commentFile = SD.open("/comments.dat", FILE_WRITE);
|
||
if (commentFile) {
|
||
commentFile.write((uint8_t*)&commentCount, sizeof(commentCount));
|
||
commentFile.write((uint8_t*)fileComments, sizeof(FileComment) * commentCount);
|
||
commentFile.close();
|
||
}
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
|
||
void loadFileComments() {
|
||
if (!sdCardReady) return;
|
||
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
if (SD.exists("/comments.dat")) {
|
||
File commentFile = SD.open("/comments.dat", FILE_READ);
|
||
if (commentFile) {
|
||
commentFile.read((uint8_t*)&commentCount, sizeof(commentCount));
|
||
if (commentCount > MAX_FILE_COMMENTS) commentCount = MAX_FILE_COMMENTS;
|
||
commentFile.read((uint8_t*)fileComments, sizeof(FileComment) * commentCount);
|
||
commentFile.close();
|
||
}
|
||
}
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
|
||
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 "";
|
||
}
|
||
|
||
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);
|
||
fileComments[i].comment[MAX_COMMENT_LEN - 1] = '\0';
|
||
saveFileComments();
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (commentCount < MAX_FILE_COMMENTS) {
|
||
strncpy(fileComments[commentCount].filename, filename, MAX_FILENAME_LEN - 1);
|
||
fileComments[commentCount].filename[MAX_FILENAME_LEN - 1] = '\0';
|
||
strncpy(fileComments[commentCount].comment, comment, MAX_COMMENT_LEN - 1);
|
||
fileComments[commentCount].comment[MAX_COMMENT_LEN - 1] = '\0';
|
||
commentCount++;
|
||
saveFileComments();
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// 시퀀스 저장/로드
|
||
// ========================================
|
||
|
||
void saveSequences() {
|
||
if (!sdCardReady) return;
|
||
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
File seqFile = SD.open("/sequences.dat", FILE_WRITE);
|
||
if (seqFile) {
|
||
seqFile.write((uint8_t*)&sequenceCount, sizeof(sequenceCount));
|
||
seqFile.write((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount);
|
||
seqFile.close();
|
||
}
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
|
||
void loadSequences() {
|
||
if (!sdCardReady) return;
|
||
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
if (SD.exists("/sequences.dat")) {
|
||
File seqFile = SD.open("/sequences.dat", FILE_READ);
|
||
if (seqFile) {
|
||
seqFile.read((uint8_t*)&sequenceCount, sizeof(sequenceCount));
|
||
if (sequenceCount > MAX_SEQUENCES) sequenceCount = MAX_SEQUENCES;
|
||
seqFile.read((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount);
|
||
seqFile.close();
|
||
}
|
||
}
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// WebSocket 이벤트 처리
|
||
// ========================================
|
||
|
||
void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) {
|
||
if (type == WStype_TEXT) {
|
||
DynamicJsonDocument doc(2048);
|
||
DeserializationError error = deserializeJson(doc, payload);
|
||
|
||
if (error) return;
|
||
|
||
const char* cmd = doc["cmd"];
|
||
|
||
if (strcmp(cmd, "getSettings") == 0) {
|
||
DynamicJsonDocument response(1024);
|
||
response["type"] = "settings";
|
||
response["ssid"] = wifiSSID;
|
||
response["password"] = wifiPassword;
|
||
response["staEnable"] = enableSTAMode;
|
||
response["staSSID"] = staSSID;
|
||
response["staPassword"] = 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) {
|
||
const char* newSSID = doc["ssid"];
|
||
const char* newPassword = doc["password"];
|
||
bool newSTAEnable = doc["staEnable"];
|
||
const char* newSTASSID = doc["staSSID"];
|
||
const char* newSTAPassword = doc["staPassword"];
|
||
|
||
if (newSSID && strlen(newSSID) > 0) {
|
||
strncpy(wifiSSID, newSSID, sizeof(wifiSSID) - 1);
|
||
wifiSSID[sizeof(wifiSSID) - 1] = '\0';
|
||
}
|
||
|
||
if (newPassword) {
|
||
strncpy(wifiPassword, newPassword, sizeof(wifiPassword) - 1);
|
||
wifiPassword[sizeof(wifiPassword) - 1] = '\0';
|
||
}
|
||
|
||
enableSTAMode = newSTAEnable;
|
||
|
||
if (newSTASSID) {
|
||
strncpy(staSSID, newSTASSID, sizeof(staSSID) - 1);
|
||
staSSID[sizeof(staSSID) - 1] = '\0';
|
||
}
|
||
|
||
if (newSTAPassword) {
|
||
strncpy(staPassword, newSTAPassword, sizeof(staPassword) - 1);
|
||
staPassword[sizeof(staPassword) - 1] = '\0';
|
||
}
|
||
|
||
saveSettings();
|
||
|
||
DynamicJsonDocument response(256);
|
||
response["type"] = "settingsSaved";
|
||
response["success"] = true;
|
||
|
||
String json;
|
||
serializeJson(response, json);
|
||
webSocket.sendTXT(num, json);
|
||
}
|
||
else if (strcmp(cmd, "startLogging") == 0) {
|
||
if (!loggingEnabled && sdCardReady) {
|
||
const char* format = doc["format"];
|
||
if (format && strcmp(format, "csv") == 0) {
|
||
canLogFormatCSV = true;
|
||
} else {
|
||
canLogFormatCSV = false;
|
||
}
|
||
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
struct tm timeinfo;
|
||
time_t now;
|
||
time(&now);
|
||
localtime_r(&now, &timeinfo);
|
||
|
||
struct timeval tv;
|
||
gettimeofday(&tv, NULL);
|
||
canLogStartTime = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||
|
||
const char* ext = canLogFormatCSV ? "csv" : "bin";
|
||
snprintf(currentFilename, sizeof(currentFilename),
|
||
"/CAN_%04d%02d%02d_%02d%02d%02d.%s",
|
||
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
|
||
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, ext);
|
||
|
||
logFile = SD.open(currentFilename, FILE_WRITE);
|
||
|
||
if (logFile) {
|
||
if (canLogFormatCSV) {
|
||
logFile.println("Time_us,CAN_ID,DLC,Data");
|
||
}
|
||
|
||
loggingEnabled = true;
|
||
bufferIndex = 0;
|
||
currentFileSize = logFile.size();
|
||
Serial.printf("✓ CAN 로깅 시작: %s (%s)\n",
|
||
currentFilename, canLogFormatCSV ? "CSV" : "BIN");
|
||
}
|
||
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "stopLogging") == 0) {
|
||
if (loggingEnabled) {
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
if (bufferIndex > 0 && logFile) {
|
||
logFile.write(fileBuffer, bufferIndex);
|
||
bufferIndex = 0;
|
||
}
|
||
|
||
if (logFile) {
|
||
logFile.close();
|
||
}
|
||
|
||
loggingEnabled = false;
|
||
Serial.printf("✓ CAN 로깅 종료: %u bytes\n", currentFileSize);
|
||
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "startSerialLogging") == 0) {
|
||
if (!serialLoggingEnabled && sdCardReady) {
|
||
const char* format = doc["format"];
|
||
if (format && strcmp(format, "bin") == 0) {
|
||
serialLogFormatCSV = false;
|
||
} else {
|
||
serialLogFormatCSV = true;
|
||
}
|
||
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
struct tm timeinfo;
|
||
time_t now;
|
||
time(&now);
|
||
localtime_r(&now, &timeinfo);
|
||
|
||
struct timeval tv;
|
||
gettimeofday(&tv, NULL);
|
||
serialLogStartTime = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||
|
||
const char* ext = serialLogFormatCSV ? "csv" : "bin";
|
||
snprintf(currentSerialFilename, sizeof(currentSerialFilename),
|
||
"/SER_%04d%02d%02d_%02d%02d%02d.%s",
|
||
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
|
||
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, ext);
|
||
|
||
serialLogFile = SD.open(currentSerialFilename, FILE_WRITE);
|
||
|
||
if (serialLogFile) {
|
||
if (serialLogFormatCSV) {
|
||
serialLogFile.println("Time_us,Direction,Data");
|
||
}
|
||
serialLoggingEnabled = true;
|
||
serialCsvIndex = 0;
|
||
currentSerialFileSize = serialLogFile.size();
|
||
Serial.printf("✓ Serial 로깅 시작: %s\n", currentSerialFilename);
|
||
}
|
||
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "stopSerialLogging") == 0) {
|
||
if (serialLoggingEnabled) {
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
if (serialCsvIndex > 0 && serialLogFile) {
|
||
serialLogFile.write((uint8_t*)serialCsvBuffer, serialCsvIndex);
|
||
serialCsvIndex = 0;
|
||
}
|
||
|
||
if (serialLogFile) {
|
||
serialLogFile.close();
|
||
}
|
||
|
||
serialLoggingEnabled = false;
|
||
Serial.printf("✓ Serial 로깅 종료: %u bytes\n", currentSerialFileSize);
|
||
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "sendSerial") == 0) {
|
||
const char* data = doc["data"];
|
||
if (data && strlen(data) > 0) {
|
||
SerialComm.println(data);
|
||
|
||
SerialMessage serialMsg;
|
||
struct timeval tv;
|
||
gettimeofday(&tv, NULL);
|
||
serialMsg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||
serialMsg.length = strlen(data) + 2;
|
||
if (serialMsg.length > MAX_SERIAL_LINE_LEN - 1) {
|
||
serialMsg.length = MAX_SERIAL_LINE_LEN - 1;
|
||
}
|
||
|
||
snprintf((char*)serialMsg.data, MAX_SERIAL_LINE_LEN, "%s\r\n", data);
|
||
serialMsg.isTx = true;
|
||
|
||
if (xQueueSend(serialQueue, &serialMsg, pdMS_TO_TICKS(10)) == pdTRUE) {
|
||
totalSerialTxCount++;
|
||
}
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "setSerialConfig") == 0) {
|
||
serialSettings.baudRate = doc["baudRate"] | 115200;
|
||
serialSettings.dataBits = doc["dataBits"] | 8;
|
||
serialSettings.parity = doc["parity"] | 0;
|
||
serialSettings.stopBits = doc["stopBits"] | 1;
|
||
|
||
saveSerialSettings();
|
||
applySerialSettings();
|
||
}
|
||
else if (strcmp(cmd, "getSerialConfig") == 0) {
|
||
DynamicJsonDocument response(512);
|
||
response["type"] = "serialConfig";
|
||
response["baudRate"] = serialSettings.baudRate;
|
||
response["dataBits"] = serialSettings.dataBits;
|
||
response["parity"] = serialSettings.parity;
|
||
response["stopBits"] = serialSettings.stopBits;
|
||
|
||
String json;
|
||
serializeJson(response, json);
|
||
webSocket.sendTXT(num, json);
|
||
}
|
||
else if (strcmp(cmd, "setSpeed") == 0) {
|
||
int speedIndex = doc["speed"];
|
||
if (speedIndex >= 0 && speedIndex < 4) {
|
||
currentCanSpeed = canSpeedValues[speedIndex];
|
||
mcp2515.reset();
|
||
mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ);
|
||
setMCP2515Mode(currentMcpMode);
|
||
saveSettings();
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "setMcpMode") == 0) {
|
||
int mode = doc["mode"];
|
||
if (mode >= 0 && mode <= 3) {
|
||
setMCP2515Mode((MCP2515Mode)mode);
|
||
saveSettings();
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "syncTime") == 0) {
|
||
uint64_t phoneTime = doc["time"];
|
||
if (phoneTime > 0) {
|
||
time_t seconds = phoneTime / 1000;
|
||
suseconds_t microseconds = (phoneTime % 1000) * 1000;
|
||
|
||
struct timeval tv = {seconds, microseconds};
|
||
settimeofday(&tv, NULL);
|
||
|
||
timeSyncStatus.synchronized = true;
|
||
timeSyncStatus.lastSyncTime = phoneTime * 1000;
|
||
timeSyncStatus.syncCount++;
|
||
|
||
if (timeSyncStatus.rtcAvailable) {
|
||
struct tm timeinfo;
|
||
localtime_r(&seconds, &timeinfo);
|
||
writeRTC(&timeinfo);
|
||
}
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "getFiles") == 0) {
|
||
if (sdCardReady) {
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
DynamicJsonDocument response(6144);
|
||
response["type"] = "files";
|
||
JsonArray files = response.createNestedArray("list");
|
||
|
||
File root = SD.open("/");
|
||
File file = root.openNextFile();
|
||
|
||
while (file) {
|
||
if (!file.isDirectory()) {
|
||
const char* filename = file.name();
|
||
|
||
if (filename[0] != '.' &&
|
||
strcmp(filename, "System Volume Information") != 0) {
|
||
|
||
JsonObject fileObj = files.createNestedObject();
|
||
fileObj["name"] = filename;
|
||
fileObj["size"] = file.size();
|
||
|
||
const char* comment = getFileComment(filename);
|
||
if (strlen(comment) > 0) {
|
||
fileObj["comment"] = comment;
|
||
}
|
||
}
|
||
}
|
||
file = root.openNextFile();
|
||
}
|
||
|
||
xSemaphoreGive(sdMutex);
|
||
|
||
String json;
|
||
serializeJson(response, json);
|
||
webSocket.sendTXT(num, json);
|
||
}
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "deleteFile") == 0) {
|
||
const char* filename = doc["filename"];
|
||
|
||
if (filename && strlen(filename) > 0) {
|
||
String fullPath = "/" + String(filename);
|
||
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||
bool success = false;
|
||
|
||
if (SD.exists(fullPath)) {
|
||
if (SD.remove(fullPath)) {
|
||
success = true;
|
||
}
|
||
}
|
||
|
||
xSemaphoreGive(sdMutex);
|
||
|
||
DynamicJsonDocument response(256);
|
||
response["type"] = "deleteResult";
|
||
response["success"] = success;
|
||
|
||
String json;
|
||
serializeJson(response, json);
|
||
webSocket.sendTXT(num, json);
|
||
}
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "addComment") == 0) {
|
||
const char* filename = doc["filename"];
|
||
const char* comment = doc["comment"];
|
||
|
||
if (filename && comment) {
|
||
addFileComment(filename, comment);
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "addTx") == 0) {
|
||
for (int i = 0; i < MAX_TX_MESSAGES; i++) {
|
||
if (!txMessages[i].active) {
|
||
txMessages[i].id = strtoul(doc["id"], NULL, 16);
|
||
txMessages[i].extended = doc["ext"] | false;
|
||
txMessages[i].dlc = doc["dlc"] | 8;
|
||
|
||
JsonArray dataArray = doc["data"];
|
||
for (int j = 0; j < 8; j++) {
|
||
txMessages[i].data[j] = dataArray[j] | 0;
|
||
}
|
||
|
||
txMessages[i].interval = doc["interval"] | 1000;
|
||
txMessages[i].active = true;
|
||
txMessages[i].lastSent = 0;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "removeTx") == 0) {
|
||
uint32_t id = strtoul(doc["id"], NULL, 16);
|
||
|
||
for (int i = 0; i < MAX_TX_MESSAGES; i++) {
|
||
if (txMessages[i].active && txMessages[i].id == id) {
|
||
txMessages[i].active = false;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
else if (strcmp(cmd, "sendOnce") == 0) {
|
||
if (currentMcpMode == MCP_MODE_TRANSMIT) {
|
||
mcp2515.setNormalMode();
|
||
}
|
||
|
||
struct can_frame frame;
|
||
frame.can_id = strtoul(doc["id"], NULL, 16);
|
||
|
||
if (doc["ext"] | false) {
|
||
frame.can_id |= CAN_EFF_FLAG;
|
||
}
|
||
|
||
frame.can_dlc = doc["dlc"] | 8;
|
||
|
||
JsonArray dataArray = doc["data"];
|
||
for (int i = 0; i < 8; i++) {
|
||
frame.data[i] = dataArray[i] | 0;
|
||
}
|
||
|
||
if (mcp2515.sendMessage(&frame) == MCP2515::ERROR_OK) {
|
||
totalTxCount++;
|
||
}
|
||
|
||
if (currentMcpMode == MCP_MODE_TRANSMIT) {
|
||
mcp2515.setListenOnlyMode();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// Web Update Task (Priority 8)
|
||
// ========================================
|
||
|
||
void webUpdateTask(void *parameter) {
|
||
const TickType_t xDelay = pdMS_TO_TICKS(100);
|
||
|
||
Serial.println("✓ Web Update Task 시작 (Priority 8)");
|
||
|
||
while (1) {
|
||
webSocket.loop();
|
||
|
||
if (webSocket.connectedClients() > 0) {
|
||
DynamicJsonDocument doc(4096);
|
||
doc["type"] = "update";
|
||
doc["logging"] = loggingEnabled;
|
||
doc["serialLogging"] = serialLoggingEnabled;
|
||
doc["sdReady"] = sdCardReady;
|
||
doc["totalMsg"] = totalMsgCount;
|
||
doc["msgPerSec"] = msgPerSecond;
|
||
doc["totalTx"] = totalTxCount;
|
||
doc["totalSerialRx"] = totalSerialRxCount;
|
||
doc["totalSerialTx"] = totalSerialTxCount;
|
||
doc["fileSize"] = currentFileSize;
|
||
doc["serialFileSize"] = currentSerialFileSize;
|
||
doc["queueUsed"] = uxQueueMessagesWaiting(canQueue);
|
||
doc["queueSize"] = CAN_QUEUE_SIZE;
|
||
doc["serialQueueUsed"] = uxQueueMessagesWaiting(serialQueue);
|
||
doc["serialQueueSize"] = SERIAL_QUEUE_SIZE;
|
||
doc["timeSync"] = timeSyncStatus.synchronized;
|
||
doc["rtcAvail"] = timeSyncStatus.rtcAvailable;
|
||
doc["voltage"] = powerStatus.voltage;
|
||
doc["mcpMode"] = (int)currentMcpMode;
|
||
|
||
if (loggingEnabled && currentFilename[0] != '\0') {
|
||
doc["currentFile"] = String(currentFilename);
|
||
}
|
||
|
||
if (serialLoggingEnabled && currentSerialFilename[0] != '\0') {
|
||
doc["currentSerialFile"] = String(currentSerialFilename);
|
||
}
|
||
|
||
time_t now;
|
||
time(&now);
|
||
doc["timestamp"] = (uint64_t)now;
|
||
|
||
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.id;
|
||
msgObj["dlc"] = recentData[i].msg.dlc;
|
||
msgObj["count"] = recentData[i].count;
|
||
|
||
JsonArray dataArray = msgObj.createNestedArray("data");
|
||
for (int j = 0; j < recentData[i].msg.dlc; j++) {
|
||
dataArray.add(recentData[i].msg.data[j]);
|
||
}
|
||
}
|
||
}
|
||
|
||
SerialMessage serialMsg;
|
||
JsonArray serialMessages = doc.createNestedArray("serialMessages");
|
||
int serialCount = 0;
|
||
|
||
while (serialCount < 10 && xQueueReceive(serialQueue, &serialMsg, 0) == pdTRUE) {
|
||
JsonObject serMsgObj = serialMessages.createNestedObject();
|
||
serMsgObj["timestamp"] = serialMsg.timestamp_us;
|
||
serMsgObj["isTx"] = serialMsg.isTx;
|
||
|
||
char dataStr[MAX_SERIAL_LINE_LEN + 1];
|
||
memcpy(dataStr, serialMsg.data, serialMsg.length);
|
||
dataStr[serialMsg.length] = '\0';
|
||
serMsgObj["data"] = dataStr;
|
||
|
||
serialCount++;
|
||
|
||
if (serialLoggingEnabled && sdCardReady) {
|
||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
|
||
if (serialLogFormatCSV) {
|
||
uint64_t relativeTime = serialMsg.timestamp_us - serialLogStartTime;
|
||
char csvLine[256];
|
||
int lineLen = snprintf(csvLine, sizeof(csvLine),
|
||
"%llu,%s,\"%s\"\n",
|
||
relativeTime,
|
||
serialMsg.isTx ? "TX" : "RX",
|
||
dataStr);
|
||
|
||
if (serialCsvIndex + lineLen < SERIAL_CSV_BUFFER_SIZE) {
|
||
memcpy(&serialCsvBuffer[serialCsvIndex], csvLine, lineLen);
|
||
serialCsvIndex += lineLen;
|
||
currentSerialFileSize += lineLen;
|
||
}
|
||
|
||
if (serialCsvIndex >= SERIAL_CSV_BUFFER_SIZE * 0.9) {
|
||
if (serialLogFile) {
|
||
serialLogFile.write((uint8_t*)serialCsvBuffer, serialCsvIndex);
|
||
serialLogFile.flush();
|
||
serialCsvIndex = 0;
|
||
}
|
||
}
|
||
} else {
|
||
if (serialLogFile) {
|
||
serialLogFile.write((uint8_t*)&serialMsg, sizeof(SerialMessage));
|
||
currentSerialFileSize += sizeof(SerialMessage);
|
||
|
||
static int binFlushCounter = 0;
|
||
if (++binFlushCounter >= 50) {
|
||
serialLogFile.flush();
|
||
binFlushCounter = 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
xSemaphoreGive(sdMutex);
|
||
}
|
||
}
|
||
}
|
||
|
||
String json;
|
||
serializeJson(doc, json);
|
||
webSocket.broadcastTXT(json);
|
||
}
|
||
|
||
vTaskDelay(xDelay);
|
||
}
|
||
}
|
||
|
||
// ========================================
|
||
// Setup
|
||
// ========================================
|
||
|
||
void setup() {
|
||
Serial.begin(115200);
|
||
delay(1000);
|
||
|
||
Serial.println("\n========================================");
|
||
Serial.println(" Byun CAN Logger - PSRAM Edition");
|
||
Serial.println(" Version 2.5 - Full Optimization");
|
||
Serial.println("========================================\n");
|
||
|
||
// PSRAM 확인
|
||
if (!psramFound()) {
|
||
Serial.println("✗ PSRAM 없음!");
|
||
Serial.println(" Arduino IDE → Tools → PSRAM → OPI PSRAM");
|
||
while (1) delay(1000);
|
||
}
|
||
|
||
Serial.printf("✓ PSRAM 감지: %d MB\n", ESP.getPsramSize() / 1024 / 1024);
|
||
Serial.printf(" 초기 여유: %u KB\n", ESP.getFreePsram() / 1024);
|
||
|
||
// PSRAM 버퍼 할당
|
||
if (!allocatePSRAMBuffers()) {
|
||
Serial.println("\n✗ PSRAM 버퍼 할당 실패!");
|
||
while (1) delay(1000);
|
||
}
|
||
|
||
// 설정 로드
|
||
loadSettings();
|
||
|
||
// GPIO 초기화
|
||
pinMode(CAN_INT_PIN, INPUT_PULLUP);
|
||
analogSetPinAttenuation(MONITORING_VOLT, ADC_11db);
|
||
analogSetAttenuation(ADC_11db);
|
||
|
||
memset(recentData, 0, sizeof(recentData));
|
||
memset(txMessages, 0, sizeof(txMessages));
|
||
memset(fileComments, 0, sizeof(fileComments));
|
||
|
||
// SPI 초기화
|
||
Serial.println("========================================");
|
||
Serial.println("SPI 초기화...");
|
||
Serial.println("========================================");
|
||
|
||
hspi.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS);
|
||
hspi.setFrequency(20000000);
|
||
|
||
pinMode(VSPI_CS, OUTPUT);
|
||
digitalWrite(VSPI_CS, HIGH);
|
||
|
||
vspi.begin(VSPI_SCLK, VSPI_MISO, VSPI_MOSI, VSPI_CS);
|
||
vspi.setFrequency(40000000);
|
||
|
||
Serial.println("✓ SPI 초기화 완료\n");
|
||
|
||
// Watchdog 비활성화
|
||
esp_task_wdt_deinit();
|
||
|
||
// MCP2515 초기화
|
||
Serial.println("========================================");
|
||
Serial.println("MCP2515 초기화...");
|
||
Serial.println("========================================");
|
||
|
||
mcp2515.reset();
|
||
delay(50);
|
||
mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ);
|
||
setMCP2515Mode(currentMcpMode);
|
||
|
||
Serial.println("✓ MCP2515 초기화 완료\n");
|
||
|
||
// Serial 통신 초기화
|
||
applySerialSettings();
|
||
|
||
// Mutex 생성
|
||
Serial.println("========================================");
|
||
Serial.println("Mutex 생성...");
|
||
Serial.println("========================================");
|
||
|
||
sdMutex = xSemaphoreCreateMutex();
|
||
rtcMutex = xSemaphoreCreateMutex();
|
||
serialMutex = xSemaphoreCreateMutex();
|
||
|
||
if (!sdMutex || !rtcMutex || !serialMutex) {
|
||
Serial.println("✗ Mutex 생성 실패!");
|
||
while (1) delay(1000);
|
||
}
|
||
|
||
Serial.println("✓ Mutex 생성 완료 (DRAM)\n");
|
||
|
||
// Queue 생성
|
||
if (!createQueues()) {
|
||
Serial.println("✗ Queue 생성 실패!");
|
||
while (1) delay(1000);
|
||
}
|
||
|
||
// RTC 초기화
|
||
Serial.println("========================================");
|
||
Serial.println("RTC 초기화...");
|
||
Serial.println("========================================");
|
||
initRTC();
|
||
Serial.println();
|
||
|
||
// SD 카드 초기화
|
||
Serial.println("========================================");
|
||
Serial.println("SD 카드 초기화...");
|
||
Serial.println("========================================");
|
||
|
||
if (SD.begin(VSPI_CS, vspi)) {
|
||
sdCardReady = true;
|
||
Serial.println("✓ SD 카드 초기화 완료");
|
||
loadFileComments();
|
||
loadSequences();
|
||
} else {
|
||
Serial.println("✗ SD 카드 초기화 실패");
|
||
}
|
||
Serial.println();
|
||
|
||
// WiFi 설정
|
||
Serial.println("========================================");
|
||
Serial.println("WiFi 초기화...");
|
||
Serial.println("========================================");
|
||
|
||
if (enableSTAMode && strlen(staSSID) > 0) {
|
||
WiFi.mode(WIFI_AP_STA);
|
||
WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4);
|
||
WiFi.begin(staSSID, staPassword);
|
||
|
||
Serial.printf("AP SSID: %s\n", wifiSSID);
|
||
Serial.printf("AP IP: %s\n", WiFi.softAPIP().toString().c_str());
|
||
|
||
int attempts = 0;
|
||
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
|
||
delay(500);
|
||
Serial.print(".");
|
||
attempts++;
|
||
}
|
||
Serial.println();
|
||
|
||
if (WiFi.status() == WL_CONNECTED) {
|
||
Serial.printf("✓ STA 연결: %s\n", WiFi.localIP().toString().c_str());
|
||
initNTP();
|
||
} else {
|
||
Serial.println("! STA 연결 실패 (AP 모드는 정상)");
|
||
}
|
||
} else {
|
||
WiFi.mode(WIFI_AP);
|
||
WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4);
|
||
Serial.printf("AP SSID: %s\n", wifiSSID);
|
||
Serial.printf("AP IP: %s\n", WiFi.softAPIP().toString().c_str());
|
||
}
|
||
|
||
WiFi.setSleep(false);
|
||
esp_wifi_set_max_tx_power(84);
|
||
Serial.println();
|
||
|
||
// WebSocket & Server
|
||
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("/serial", HTTP_GET, []() {
|
||
server.send_P(200, "text/html", serial_terminal_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) {
|
||
server.sendHeader("Content-Disposition",
|
||
"attachment; filename=\"" + server.arg("file") + "\"");
|
||
server.streamFile(file, "application/octet-stream");
|
||
file.close();
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
server.begin();
|
||
|
||
// CAN 인터럽트
|
||
attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING);
|
||
|
||
// Task 생성
|
||
Serial.println("========================================");
|
||
Serial.println("Task 생성 (우선순위 최적화)...");
|
||
Serial.println("========================================");
|
||
|
||
xTaskCreatePinnedToCore(canRxTask, "CAN_RX", STACK_CAN_RX, NULL,
|
||
PRIORITY_CAN_RX, &canRxTaskHandle, 1);
|
||
|
||
xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", STACK_SD_WRITE, NULL,
|
||
PRIORITY_SD_WRITE, &sdWriteTaskHandle, 1);
|
||
|
||
xTaskCreatePinnedToCore(sequenceTask, "SEQ_TASK", STACK_SEQUENCE, NULL,
|
||
PRIORITY_SEQUENCE, NULL, 1);
|
||
|
||
xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", STACK_SERIAL_RX, NULL,
|
||
PRIORITY_SERIAL_RX, &serialRxTaskHandle, 0);
|
||
|
||
xTaskCreatePinnedToCore(txTask, "TX_TASK", STACK_TX_TASK, NULL,
|
||
PRIORITY_TX_TASK, NULL, 0);
|
||
|
||
xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", STACK_WEB_UPDATE, NULL,
|
||
PRIORITY_WEB_UPDATE, &webTaskHandle, 0);
|
||
|
||
xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", STACK_SD_MONITOR, NULL,
|
||
PRIORITY_SD_MONITOR, NULL, 0);
|
||
|
||
if (timeSyncStatus.rtcAvailable) {
|
||
xTaskCreatePinnedToCore(rtcSyncTask, "RTC_SYNC", STACK_RTC_SYNC, NULL,
|
||
PRIORITY_RTC_SYNC, &rtcTaskHandle, 0);
|
||
}
|
||
|
||
Serial.println("✓ 모든 Task 생성 완료\n");
|
||
|
||
// 최종 메모리 상태
|
||
Serial.println("========================================");
|
||
Serial.println("최종 메모리 상태");
|
||
Serial.println("========================================");
|
||
Serial.printf("PSRAM 사용: %u KB\n",
|
||
(ESP.getPsramSize() - ESP.getFreePsram()) / 1024);
|
||
Serial.printf("PSRAM 여유: %u KB\n", ESP.getFreePsram() / 1024);
|
||
Serial.printf("DRAM 여유: %u KB\n", ESP.getFreeHeap() / 1024);
|
||
Serial.println();
|
||
|
||
Serial.println("========================================");
|
||
Serial.println("시스템 준비 완료!");
|
||
Serial.println("========================================");
|
||
Serial.printf("접속: http://%s\n", WiFi.softAPIP().toString().c_str());
|
||
Serial.println("페이지:");
|
||
Serial.println(" - Monitor : /");
|
||
Serial.println(" - Transmit : /transmit");
|
||
Serial.println(" - Graph : /graph");
|
||
Serial.println(" - Settings : /settings");
|
||
Serial.println(" - Serial : /serial");
|
||
Serial.println("========================================\n");
|
||
}
|
||
|
||
void loop() {
|
||
server.handleClient();
|
||
vTaskDelay(pdMS_TO_TICKS(10));
|
||
|
||
static uint32_t lastPrint = 0;
|
||
if (millis() - lastPrint > 30000) {
|
||
Serial.printf("[30초 통계] CAN: %lu msg/s | Queue: %d/%d (%.1f%%) | PSRAM: %u KB\n",
|
||
msgPerSecond,
|
||
uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE,
|
||
(float)uxQueueMessagesWaiting(canQueue) / CAN_QUEUE_SIZE * 100.0,
|
||
ESP.getFreePsram() / 1024);
|
||
lastPrint = millis();
|
||
}
|
||
}
|