시리얼 모드버스 추가

This commit is contained in:
2025-12-07 20:35:25 +00:00
parent 0cb159acd4
commit 1743eb37f9
4 changed files with 2747 additions and 574 deletions

View File

@@ -39,11 +39,15 @@
#include "graph_viewer.h" #include "graph_viewer.h"
#include "settings.h" #include "settings.h"
#include "serial_terminal.h" #include "serial_terminal.h"
#include "serial2_terminal.h" // ⭐ Serial2 페이지 추가
// GPIO 핀 정의 // GPIO 핀 정의
#define CAN_INT_PIN 4 #define CAN_INT_PIN 4
#define SERIAL_TX_PIN 17 #define SERIAL_TX_PIN 17
#define SERIAL_RX_PIN 18 #define SERIAL_RX_PIN 18
// UART2 (Serial Logger 2) ⭐ 추가
#define SERIAL2_TX_PIN 6
#define SERIAL2_RX_PIN 7
// HSPI 핀 (CAN) // HSPI 핀 (CAN)
#define HSPI_MISO 13 #define HSPI_MISO 13
@@ -68,7 +72,10 @@
#define CAN_QUEUE_SIZE 6000 // 1000 → 6000 (PSRAM 사용) #define CAN_QUEUE_SIZE 6000 // 1000 → 6000 (PSRAM 사용)
#define FILE_BUFFER_SIZE 65536 // 16KB → 64KB (PSRAM 사용) #define FILE_BUFFER_SIZE 65536 // 16KB → 64KB (PSRAM 사용)
#define SERIAL_QUEUE_SIZE 1200 // 200 → 1200 (PSRAM 사용) #define SERIAL_QUEUE_SIZE 1200 // 200 → 1200 (PSRAM 사용)
#define SERIAL_CSV_BUFFER_SIZE 32768 // 8KB → 32KB (PSRAM 사용) #define SERIAL_CSV_BUFFER_SIZE 32768
#define SERIAL2_QUEUE_SIZE 1200 // ⭐ Serial2 추가
#define SERIAL2_CSV_BUFFER_SIZE 32768 // ⭐ Serial2 추가 // 8KB → 32KB (PSRAM 사용)
#define MAX_FILENAME_LEN 64 #define MAX_FILENAME_LEN 64
#define RECENT_MSG_COUNT 100 #define RECENT_MSG_COUNT 100
@@ -105,7 +112,7 @@ struct SerialSettings {
uint8_t dataBits; uint8_t dataBits;
uint8_t parity; uint8_t parity;
uint8_t stopBits; uint8_t stopBits;
} serialSettings = {115200, 8, 0, 1}; };
struct RecentCANData { struct RecentCANData {
CANMessage msg; CANMessage msg;
@@ -180,6 +187,7 @@ enum MCP2515Mode {
// ======================================== // ========================================
uint8_t *fileBuffer = nullptr; uint8_t *fileBuffer = nullptr;
char *serialCsvBuffer = nullptr; char *serialCsvBuffer = nullptr;
char *serial2CsvBuffer = nullptr; // ⭐ Serial2 추가
RecentCANData *recentData = nullptr; RecentCANData *recentData = nullptr;
TxMessage *txMessages = nullptr; TxMessage *txMessages = nullptr;
CANSequence *sequences = nullptr; CANSequence *sequences = nullptr;
@@ -188,8 +196,10 @@ FileComment *fileComments = nullptr;
// Queue 저장소 (PSRAM) // Queue 저장소 (PSRAM)
StaticQueue_t *canQueueBuffer = nullptr; StaticQueue_t *canQueueBuffer = nullptr;
StaticQueue_t *serialQueueBuffer = nullptr; StaticQueue_t *serialQueueBuffer = nullptr;
StaticQueue_t *serial2QueueBuffer = nullptr; // ⭐ Serial2
uint8_t *canQueueStorage = nullptr; uint8_t *canQueueStorage = nullptr;
uint8_t *serialQueueStorage = nullptr; uint8_t *serialQueueStorage = nullptr;
uint8_t *serial2QueueStorage = nullptr; // ⭐ Serial2
// WiFi 설정 (내부 SRAM) // WiFi 설정 (내부 SRAM)
char wifiSSID[32] = "Byun_CAN_Logger"; char wifiSSID[32] = "Byun_CAN_Logger";
@@ -198,11 +208,18 @@ bool enableSTAMode = false;
char staSSID[32] = ""; char staSSID[32] = "";
char staPassword[64] = ""; char staPassword[64] = "";
// ========================================
// Serial 설정 (2개)
// ========================================
SerialSettings serialSettings = {115200, 8, 0, 1}; // Serial1
SerialSettings serial2Settings = {115200, 8, 0, 1}; // ⭐ Serial2 추가
// 전역 객체 (내부 SRAM) // 전역 객체 (내부 SRAM)
SPIClass hspi(HSPI); SPIClass hspi(HSPI);
SPIClass vspi(FSPI); SPIClass vspi(FSPI);
MCP2515 mcp2515(HSPI_CS, 20000000, &hspi); MCP2515 mcp2515(HSPI_CS, 20000000, &hspi);
HardwareSerial SerialComm(1); HardwareSerial SerialComm(1); // UART1
HardwareSerial Serial2Comm(2); // ⭐ UART2 추가
WebServer server(80); WebServer server(80);
WebSocketsServer webSocket = WebSocketsServer(81); WebSocketsServer webSocket = WebSocketsServer(81);
Preferences preferences; Preferences preferences;
@@ -210,31 +227,41 @@ Preferences preferences;
// FreeRTOS 핸들 // FreeRTOS 핸들
QueueHandle_t canQueue = NULL; QueueHandle_t canQueue = NULL;
QueueHandle_t serialQueue = NULL; QueueHandle_t serialQueue = NULL;
QueueHandle_t serial2Queue = NULL; // ⭐ Serial2 추가
SemaphoreHandle_t sdMutex = NULL; SemaphoreHandle_t sdMutex = NULL;
SemaphoreHandle_t rtcMutex = NULL; SemaphoreHandle_t rtcMutex = NULL;
SemaphoreHandle_t serialMutex = NULL; SemaphoreHandle_t serialMutex = NULL;
SemaphoreHandle_t serial2Mutex = NULL; // ⭐ Serial2 추가
TaskHandle_t canRxTaskHandle = NULL; TaskHandle_t canRxTaskHandle = NULL;
TaskHandle_t sdWriteTaskHandle = NULL; TaskHandle_t sdWriteTaskHandle = NULL;
TaskHandle_t webTaskHandle = NULL; TaskHandle_t webTaskHandle = NULL;
TaskHandle_t rtcTaskHandle = NULL; TaskHandle_t rtcTaskHandle = NULL;
TaskHandle_t serialRxTaskHandle = NULL; TaskHandle_t serialRxTaskHandle = NULL;
TaskHandle_t serial2RxTaskHandle = NULL; // ⭐ Serial2 추가
// 로깅 변수 // 로깅 변수
volatile bool loggingEnabled = false; volatile bool loggingEnabled = false;
volatile bool serialLoggingEnabled = false; volatile bool serialLoggingEnabled = false;
volatile bool serial2LoggingEnabled = false; // ⭐ Serial2 추가
volatile bool sdCardReady = false; volatile bool sdCardReady = false;
File logFile; File logFile;
File serialLogFile; File serialLogFile;
File serial2LogFile; // ⭐ Serial2 추가
char currentFilename[MAX_FILENAME_LEN]; char currentFilename[MAX_FILENAME_LEN];
char currentSerialFilename[MAX_FILENAME_LEN]; char currentSerialFilename[MAX_FILENAME_LEN];
char currentSerial2Filename[MAX_FILENAME_LEN]; // ⭐ Serial2 추가
uint16_t bufferIndex = 0; uint16_t bufferIndex = 0;
uint16_t serialCsvIndex = 0; uint16_t serialCsvIndex = 0;
uint16_t serial2CsvIndex = 0; // ⭐ Serial2 추가
volatile uint32_t currentFileSize = 0; volatile uint32_t currentFileSize = 0;
volatile uint32_t currentSerialFileSize = 0; volatile uint32_t currentSerialFileSize = 0;
volatile uint32_t currentSerial2FileSize = 0; // ⭐ Serial2 추가
volatile bool canLogFormatCSV = false; volatile bool canLogFormatCSV = false;
volatile bool serialLogFormatCSV = true; volatile bool serialLogFormatCSV = true;
volatile bool serial2LogFormatCSV = true; // ⭐ Serial2 추가
volatile uint64_t canLogStartTime = 0; volatile uint64_t canLogStartTime = 0;
volatile uint64_t serialLogStartTime = 0; volatile uint64_t serialLogStartTime = 0;
volatile uint64_t serial2LogStartTime = 0; // ⭐ Serial2 추가
// 기타 전역 변수 // 기타 전역 변수
MCP2515Mode currentMcpMode = MCP_MODE_NORMAL; MCP2515Mode currentMcpMode = MCP_MODE_NORMAL;
@@ -250,6 +277,8 @@ uint32_t lastMsgCountTime = 0;
uint32_t lastMsgCount = 0; uint32_t lastMsgCount = 0;
volatile uint32_t totalSerialRxCount = 0; volatile uint32_t totalSerialRxCount = 0;
volatile uint32_t totalSerialTxCount = 0; volatile uint32_t totalSerialTxCount = 0;
volatile uint32_t totalSerial2RxCount = 0; // ⭐ Serial2 추가
volatile uint32_t totalSerial2TxCount = 0; // ⭐ Serial2 추가
uint32_t totalTxCount = 0; uint32_t totalTxCount = 0;
uint8_t sequenceCount = 0; uint8_t sequenceCount = 0;
SequenceRuntime seqRuntime = {false, 0, 0, 0, -1}; SequenceRuntime seqRuntime = {false, 0, 0, 0, -1};
@@ -292,6 +321,14 @@ bool initPSRAM() {
} }
Serial.printf("✓ serialCsvBuffer: %d KB\n", SERIAL_CSV_BUFFER_SIZE / 1024); Serial.printf("✓ serialCsvBuffer: %d KB\n", SERIAL_CSV_BUFFER_SIZE / 1024);
// ⭐ Serial2 CSV Buffer
serial2CsvBuffer = (char*)ps_malloc(SERIAL2_CSV_BUFFER_SIZE);
if (!serial2CsvBuffer) {
Serial.println("✗ serial2CsvBuffer 할당 실패");
return false;
}
Serial.printf("✓ serial2CsvBuffer: %d KB\n", SERIAL2_CSV_BUFFER_SIZE / 1024);
recentData = (RecentCANData*)ps_calloc(RECENT_MSG_COUNT, sizeof(RecentCANData)); recentData = (RecentCANData*)ps_calloc(RECENT_MSG_COUNT, sizeof(RecentCANData));
if (!recentData) { if (!recentData) {
Serial.println("✗ recentData 할당 실패"); Serial.println("✗ recentData 할당 실패");
@@ -343,6 +380,17 @@ bool initPSRAM() {
SERIAL_QUEUE_SIZE, sizeof(SerialMessage), SERIAL_QUEUE_SIZE, sizeof(SerialMessage),
(float)(SERIAL_QUEUE_SIZE * sizeof(SerialMessage)) / 1024.0); (float)(SERIAL_QUEUE_SIZE * sizeof(SerialMessage)) / 1024.0);
// ⭐ Serial2 Queue
serial2QueueBuffer = (StaticQueue_t*)ps_malloc(sizeof(StaticQueue_t));
serial2QueueStorage = (uint8_t*)ps_malloc(SERIAL2_QUEUE_SIZE * sizeof(SerialMessage));
if (!serial2QueueBuffer || !serial2QueueStorage) {
Serial.println("✗ Serial2 Queue 저장소 할당 실패");
return false;
}
Serial.printf("✓ Serial2 Queue: %d 개 × %d bytes = %.2f KB\n",
SERIAL2_QUEUE_SIZE, sizeof(SerialMessage),
(float)(SERIAL2_QUEUE_SIZE * sizeof(SerialMessage)) / 1024.0);
Serial.println("========================================"); Serial.println("========================================");
Serial.printf("✓ PSRAM 남은 용량: %.2f KB\n", (float)ESP.getFreePsram() / 1024.0); Serial.printf("✓ PSRAM 남은 용량: %.2f KB\n", (float)ESP.getFreePsram() / 1024.0);
Serial.println("========================================\n"); Serial.println("========================================\n");
@@ -377,7 +425,21 @@ bool createQueues() {
Serial.println("✗ Serial Queue 생성 실패"); Serial.println("✗ Serial Queue 생성 실패");
return false; return false;
} }
Serial.printf("✓ Serial Queue: %d 개\n\n", SERIAL_QUEUE_SIZE); Serial.printf("✓ Serial Queue: %d 개\n", SERIAL_QUEUE_SIZE);
// ⭐ Serial2 Queue 생성 (중요!)
serial2Queue = xQueueCreateStatic(
SERIAL2_QUEUE_SIZE,
sizeof(SerialMessage),
serial2QueueStorage,
serial2QueueBuffer
);
if (serial2Queue == NULL) {
Serial.println("✗ Serial2 Queue 생성 실패");
return false;
}
Serial.printf("✓ Serial2 Queue: %d 개\n\n", SERIAL2_QUEUE_SIZE);
return true; return true;
} }
@@ -390,6 +452,12 @@ void loadSerialSettings() {
serialSettings.dataBits = preferences.getUChar("ser_data", 8); serialSettings.dataBits = preferences.getUChar("ser_data", 8);
serialSettings.parity = preferences.getUChar("ser_parity", 0); serialSettings.parity = preferences.getUChar("ser_parity", 0);
serialSettings.stopBits = preferences.getUChar("ser_stop", 1); serialSettings.stopBits = preferences.getUChar("ser_stop", 1);
// ⭐ Serial2
serial2Settings.baudRate = preferences.getUInt("ser2_baud", 115200);
serial2Settings.dataBits = preferences.getUChar("ser2_data", 8);
serial2Settings.parity = preferences.getUChar("ser2_parity", 0);
serial2Settings.stopBits = preferences.getUChar("ser2_stop", 1);
} }
void saveSerialSettings() { void saveSerialSettings() {
@@ -397,6 +465,12 @@ void saveSerialSettings() {
preferences.putUChar("ser_data", serialSettings.dataBits); preferences.putUChar("ser_data", serialSettings.dataBits);
preferences.putUChar("ser_parity", serialSettings.parity); preferences.putUChar("ser_parity", serialSettings.parity);
preferences.putUChar("ser_stop", serialSettings.stopBits); preferences.putUChar("ser_stop", serialSettings.stopBits);
// ⭐ Serial2
preferences.putUInt("ser2_baud", serial2Settings.baudRate);
preferences.putUChar("ser2_data", serial2Settings.dataBits);
preferences.putUChar("ser2_parity", serial2Settings.parity);
preferences.putUChar("ser2_stop", serial2Settings.stopBits);
} }
void applySerialSettings() { void applySerialSettings() {
@@ -426,6 +500,31 @@ void applySerialSettings() {
SerialComm.begin(serialSettings.baudRate, config, SERIAL_RX_PIN, SERIAL_TX_PIN); SerialComm.begin(serialSettings.baudRate, config, SERIAL_RX_PIN, SERIAL_TX_PIN);
SerialComm.setRxBufferSize(2048); SerialComm.setRxBufferSize(2048);
// ⭐ Serial2 설정
uint32_t config2 = SERIAL_8N1;
if (serial2Settings.dataBits == 5) {
if (serial2Settings.parity == 0) config2 = SERIAL_5N1;
else if (serial2Settings.parity == 1) config2 = SERIAL_5E1;
else if (serial2Settings.parity == 2) config2 = SERIAL_5O1;
} else if (serial2Settings.dataBits == 6) {
if (serial2Settings.parity == 0) config2 = SERIAL_6N1;
else if (serial2Settings.parity == 1) config2 = SERIAL_6E1;
else if (serial2Settings.parity == 2) config2 = SERIAL_6O1;
} else if (serial2Settings.dataBits == 7) {
if (serial2Settings.parity == 0) config2 = SERIAL_7N1;
else if (serial2Settings.parity == 1) config2 = SERIAL_7E1;
else if (serial2Settings.parity == 2) config2 = SERIAL_7O1;
} else {
if (serial2Settings.parity == 0) config2 = SERIAL_8N1;
else if (serial2Settings.parity == 1) config2 = SERIAL_8E1;
else if (serial2Settings.parity == 2) config2 = SERIAL_8O1;
}
if (serial2Settings.stopBits == 2) config2 |= 0x3000;
Serial2Comm.begin(serial2Settings.baudRate, config2, SERIAL2_RX_PIN, SERIAL2_TX_PIN);
Serial2Comm.setRxBufferSize(2048);
} }
void loadSettings() { void loadSettings() {
@@ -683,6 +782,57 @@ void serialRxTask(void *parameter) {
} }
} }
// ⭐ Serial2 RX Task (우선순위 5)
void serial2RxTask(void *parameter) {
SerialMessage serialMsg;
uint8_t lineBuffer[MAX_SERIAL_LINE_LEN];
uint16_t lineIndex = 0;
uint32_t lastActivity = millis();
while (1) {
while (Serial2Comm.available()) {
uint8_t c = Serial2Comm.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 (serial2Queue && xQueueSend(serial2Queue, &serialMsg, pdMS_TO_TICKS(10)) == pdTRUE) { // ⭐ NULL 체크
totalSerial2RxCount++;
}
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 (serial2Queue && xQueueSend(serial2Queue, &serialMsg, pdMS_TO_TICKS(10)) == pdTRUE) { // ⭐ NULL 체크
totalSerial2RxCount++;
}
lineIndex = 0;
}
vTaskDelay(pdMS_TO_TICKS(1));
}
}
void canRxTask(void *parameter) { void canRxTask(void *parameter) {
struct can_frame frame; struct can_frame frame;
CANMessage msg; CANMessage msg;
@@ -1266,6 +1416,121 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
saveSettings(); saveSettings();
} }
} }
else if (strcmp(cmd, "startSerial2Logging") == 0) {
if (!serial2LoggingEnabled && sdCardReady) {
const char* format = doc["format"];
if (format && strcmp(format, "bin") == 0) {
serial2LogFormatCSV = false;
} else {
serial2LogFormatCSV = 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);
serial2LogStartTime = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
const char* ext = serial2LogFormatCSV ? "csv" : "bin";
snprintf(currentSerial2Filename, sizeof(currentSerial2Filename),
"/SER2_%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);
serial2LogFile = SD.open(currentSerial2Filename, FILE_WRITE);
if (serial2LogFile) {
if (serial2LogFormatCSV) {
serial2LogFile.println("Time_us,Direction,Data");
}
serial2LoggingEnabled = true;
serial2CsvIndex = 0;
currentSerial2FileSize = serial2LogFile.size();
}
xSemaphoreGive(sdMutex);
}
}
}
else if (strcmp(cmd, "stopSerial2Logging") == 0) {
if (serial2LoggingEnabled) {
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
if (serial2CsvIndex > 0 && serial2LogFile) {
serial2LogFile.write((uint8_t*)serial2CsvBuffer, serial2CsvIndex);
serial2CsvIndex = 0;
}
if (serial2LogFile) {
serial2LogFile.close();
}
serial2LoggingEnabled = false;
xSemaphoreGive(sdMutex);
}
}
}
else if (strcmp(cmd, "sendSerial2") == 0) {
const char* data = doc["data"];
if (data && strlen(data) > 0) {
Serial2Comm.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 (serial2Queue && xQueueSend(serial2Queue, &serialMsg, pdMS_TO_TICKS(10)) == pdTRUE) { // ⭐ NULL 체크
totalSerial2TxCount++;
}
}
}
else if (strcmp(cmd, "setSerial2Config") == 0) {
uint32_t baud = doc["baudRate"] | 115200;
uint8_t data = doc["dataBits"] | 8;
uint8_t parity = doc["parity"] | 0;
uint8_t stop = doc["stopBits"] | 1;
serial2Settings.baudRate = baud;
serial2Settings.dataBits = data;
serial2Settings.parity = parity;
serial2Settings.stopBits = stop;
saveSerialSettings();
applySerialSettings();
}
else if (strcmp(cmd, "getSerial2Config") == 0) {
DynamicJsonDocument response(512);
response["type"] = "serial2Config";
response["baudRate"] = serial2Settings.baudRate;
response["dataBits"] = serial2Settings.dataBits;
response["parity"] = serial2Settings.parity;
response["stopBits"] = serial2Settings.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) { else if (strcmp(cmd, "setMcpMode") == 0) {
int mode = doc["mode"]; int mode = doc["mode"];
if (mode >= 0 && mode <= 3) { if (mode >= 0 && mode <= 3) {
@@ -1306,37 +1571,76 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
DynamicJsonDocument response(6144); DynamicJsonDocument response(6144);
response["type"] = "files"; response["type"] = "files";
JsonArray files = response.createNestedArray("list"); JsonArray files = response.createNestedArray("files");
File root = SD.open("/"); File root = SD.open("/");
File file = root.openNextFile(); if (root) {
File file = root.openNextFile();
int fileCount = 0;
while (file) { while (file && fileCount < 50) {
if (!file.isDirectory()) { if (!file.isDirectory()) {
const char* filename = file.name(); const char* filename = file.name();
if (filename[0] != '.' && // ⭐ 파일명이 '/'로 시작하면 건너뛰기
strcmp(filename, "System Volume Information") != 0) { if (filename[0] == '/') {
filename++; // 슬래시 제거
}
JsonObject fileObj = files.createNestedObject(); // 숨김 파일과 시스템 폴더 제외
fileObj["name"] = filename; if (filename[0] != '.' &&
fileObj["size"] = file.size(); strcmp(filename, "System Volume Information") != 0 &&
strlen(filename) > 0) {
const char* comment = getFileComment(filename); JsonObject fileObj = files.createNestedObject();
if (strlen(comment) > 0) { fileObj["name"] = filename;
fileObj["comment"] = comment; fileObj["size"] = file.size();
const char* comment = getFileComment(filename);
if (strlen(comment) > 0) {
fileObj["comment"] = comment;
}
fileCount++;
} }
} }
file.close();
file = root.openNextFile();
} }
file = root.openNextFile();
root.close();
// ⭐ 디버그 로그
Serial.printf("getFiles: Found %d files\n", fileCount);
} else {
Serial.println("getFiles: Failed to open root directory");
} }
xSemaphoreGive(sdMutex); xSemaphoreGive(sdMutex);
String json;
size_t jsonSize = serializeJson(response, json);
Serial.printf("getFiles: JSON size = %d bytes\n", jsonSize);
webSocket.sendTXT(num, json);
} else {
Serial.println("getFiles: Failed to acquire sdMutex");
// Mutex 실패 시에도 응답 전송
DynamicJsonDocument response(256);
response["type"] = "files";
response["error"] = "SD busy";
JsonArray files = response.createNestedArray("files");
String json; String json;
serializeJson(response, json); serializeJson(response, json);
webSocket.sendTXT(num, json); webSocket.sendTXT(num, json);
} }
} else {
Serial.println("getFiles: SD card not ready");
// SD 카드 없을 때 빈 목록 전송
DynamicJsonDocument response(256);
response["type"] = "files";
JsonArray files = response.createNestedArray("files");
String json;
serializeJson(response, json);
webSocket.sendTXT(num, json);
} }
} }
else if (strcmp(cmd, "deleteFile") == 0) { else if (strcmp(cmd, "deleteFile") == 0) {
@@ -1425,16 +1729,29 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
// Web Update Task // Web Update Task
// ======================================== // ========================================
void webUpdateTask(void *parameter) { void webUpdateTask(void *parameter) {
const TickType_t xDelay = pdMS_TO_TICKS(100); const TickType_t xDelay = pdMS_TO_TICKS(200); // ⭐ 100ms → 200ms (WiFi 안정성 향상)
while (1) { while (1) {
webSocket.loop(); webSocket.loop();
if (webSocket.connectedClients() > 0) { if (webSocket.connectedClients() > 0) {
DynamicJsonDocument doc(4096); DynamicJsonDocument doc(8192); // ⭐ 4096 → 8192로 증가
doc["type"] = "update"; doc["type"] = "update";
doc["logging"] = loggingEnabled; doc["logging"] = loggingEnabled;
doc["serialLogging"] = serialLoggingEnabled; doc["serialLogging"] = serialLoggingEnabled;
doc["serial2Logging"] = serial2LoggingEnabled;
doc["totalSerial2Rx"] = totalSerial2RxCount;
doc["totalSerial2Tx"] = totalSerial2TxCount;
doc["serial2QueueUsed"] = serial2Queue ? uxQueueMessagesWaiting(serial2Queue) : 0; // ⭐ NULL 체크
doc["serial2QueueSize"] = SERIAL2_QUEUE_SIZE;
doc["serial2FileSize"] = currentSerial2FileSize;
if (serial2LoggingEnabled && currentSerial2Filename[0] != '\0') {
doc["currentSerial2File"] = String(currentSerial2Filename);
} else {
doc["currentSerial2File"] = "";
}
doc["sdReady"] = sdCardReady; doc["sdReady"] = sdCardReady;
doc["totalMsg"] = totalMsgCount; doc["totalMsg"] = totalMsgCount;
doc["msgPerSec"] = msgPerSecond; doc["msgPerSec"] = msgPerSecond;
@@ -1472,9 +1789,10 @@ void webUpdateTask(void *parameter) {
time(&now); time(&now);
doc["timestamp"] = (uint64_t)now; doc["timestamp"] = (uint64_t)now;
// CAN 메시지 배열 // CAN 메시지 배열 (최근 20개만 전송)
JsonArray messages = doc.createNestedArray("messages"); JsonArray messages = doc.createNestedArray("messages");
for (int i = 0; i < RECENT_MSG_COUNT; i++) { int msgCount = 0;
for (int i = 0; i < RECENT_MSG_COUNT && msgCount < 20; i++) { // ⭐ 최대 20개
if (recentData[i].count > 0) { if (recentData[i].count > 0) {
JsonObject msgObj = messages.createNestedObject(); JsonObject msgObj = messages.createNestedObject();
msgObj["id"] = recentData[i].msg.id; msgObj["id"] = recentData[i].msg.id;
@@ -1485,6 +1803,7 @@ void webUpdateTask(void *parameter) {
for (int j = 0; j < recentData[i].msg.dlc; j++) { for (int j = 0; j < recentData[i].msg.dlc; j++) {
dataArray.add(recentData[i].msg.data[j]); dataArray.add(recentData[i].msg.data[j]);
} }
msgCount++;
} }
} }
@@ -1549,9 +1868,77 @@ void webUpdateTask(void *parameter) {
} }
} }
// ⭐ Serial2 메시지 배열 처리
SerialMessage serial2Msg;
JsonArray serial2Messages = doc.createNestedArray("serial2Messages");
int serial2Count = 0;
while (serial2Queue && serial2Count < 10 && xQueueReceive(serial2Queue, &serial2Msg, 0) == pdTRUE) { // ⭐ NULL 체크
JsonObject serMsgObj = serial2Messages.createNestedObject();
serMsgObj["timestamp"] = serial2Msg.timestamp_us;
serMsgObj["isTx"] = serial2Msg.isTx;
char dataStr[MAX_SERIAL_LINE_LEN + 1];
memcpy(dataStr, serial2Msg.data, serial2Msg.length);
dataStr[serial2Msg.length] = '\0';
serMsgObj["data"] = dataStr;
serial2Count++;
// Serial2 로깅
if (serial2LoggingEnabled && sdCardReady) {
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
if (serial2LogFormatCSV) {
uint64_t relativeTime = serial2Msg.timestamp_us - serial2LogStartTime;
char csvLine[256];
int lineLen = snprintf(csvLine, sizeof(csvLine),
"%llu,%s,\"%s\"\n",
relativeTime,
serial2Msg.isTx ? "TX" : "RX",
dataStr);
if (serial2CsvIndex + lineLen < SERIAL2_CSV_BUFFER_SIZE) {
memcpy(&serial2CsvBuffer[serial2CsvIndex], csvLine, lineLen);
serial2CsvIndex += lineLen;
currentSerial2FileSize += lineLen;
}
if (serial2CsvIndex >= SERIAL2_CSV_BUFFER_SIZE - 256) {
if (serial2LogFile) {
serial2LogFile.write((uint8_t*)serial2CsvBuffer, serial2CsvIndex);
serial2LogFile.flush();
serial2CsvIndex = 0;
}
}
} else {
// BIN 형식
if (serial2LogFile) {
serial2LogFile.write((uint8_t*)&serial2Msg, sizeof(SerialMessage));
currentSerial2FileSize += sizeof(SerialMessage);
static int binFlushCounter2 = 0;
if (++binFlushCounter2 >= 50) {
serial2LogFile.flush();
binFlushCounter2 = 0;
}
}
}
xSemaphoreGive(sdMutex);
}
}
}
String json; String json;
serializeJson(doc, json); size_t jsonSize = serializeJson(doc, json);
webSocket.broadcastTXT(json);
// JSON 크기 확인 (8KB 이하만 전송)
if (jsonSize > 0 && jsonSize < 8192) {
webSocket.broadcastTXT(json);
} else {
Serial.printf("! JSON 크기 초과: %d bytes\n", jsonSize);
}
} }
vTaskDelay(xDelay); vTaskDelay(xDelay);
@@ -1614,12 +2001,14 @@ void setup() {
// Serial 통신 초기화 // Serial 통신 초기화
applySerialSettings(); applySerialSettings();
Serial.println("✓ Serial 통신 초기화 완료"); Serial.println("✓ Serial1 통신 초기화 (GPIO 17/18)");
Serial.println("✓ Serial2 통신 초기화 (GPIO 6/7)"); // ⭐ Serial2
// Mutex 생성 // Mutex 생성
sdMutex = xSemaphoreCreateMutex(); sdMutex = xSemaphoreCreateMutex();
rtcMutex = xSemaphoreCreateMutex(); rtcMutex = xSemaphoreCreateMutex();
serialMutex = xSemaphoreCreateMutex(); serialMutex = xSemaphoreCreateMutex();
serial2Mutex = xSemaphoreCreateMutex(); // ⭐ Serial2
if (!sdMutex || !rtcMutex || !serialMutex) { if (!sdMutex || !rtcMutex || !serialMutex) {
Serial.println("✗ Mutex 생성 실패!"); Serial.println("✗ Mutex 생성 실패!");
@@ -1640,6 +2029,8 @@ void setup() {
} }
// WiFi 설정 // WiFi 설정
WiFi.setSleep(false); // ⭐ WiFi 절전 모드 비활성화 (연결 안정성 향상)
if (enableSTAMode && strlen(staSSID) > 0) { if (enableSTAMode && strlen(staSSID) > 0) {
Serial.println("\n📶 WiFi APSTA 모드..."); Serial.println("\n📶 WiFi APSTA 모드...");
WiFi.mode(WIFI_AP_STA); WiFi.mode(WIFI_AP_STA);
@@ -1700,6 +2091,10 @@ void setup() {
server.send_P(200, "text/html", serial_terminal_html); server.send_P(200, "text/html", serial_terminal_html);
}); });
server.on("/serial2", HTTP_GET, []() {
server.send_P(200,"text/html", serial2_terminal_html);
});
server.on("/download", HTTP_GET, []() { server.on("/download", HTTP_GET, []() {
if (server.hasArg("file")) { if (server.hasArg("file")) {
String filename = "/" + server.arg("file"); String filename = "/" + server.arg("file");
@@ -1741,8 +2136,9 @@ void setup() {
xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 24576, NULL, 4, &sdWriteTaskHandle, 1); xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 24576, NULL, 4, &sdWriteTaskHandle, 1);
xTaskCreatePinnedToCore(sequenceTask, "SEQ", 4096, NULL, 2, NULL, 1); xTaskCreatePinnedToCore(sequenceTask, "SEQ", 4096, NULL, 2, NULL, 1);
xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", 6144, NULL, 5, &serialRxTaskHandle, 0); xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", 6144, NULL, 5, &serialRxTaskHandle, 0);
xTaskCreatePinnedToCore(serial2RxTask, "SERIAL2_RX", 6144, NULL, 5, &serial2RxTaskHandle, 0); // ⭐ Serial2
xTaskCreatePinnedToCore(txTask, "TX", 4096, NULL, 3, NULL, 0); xTaskCreatePinnedToCore(txTask, "TX", 4096, NULL, 3, NULL, 0);
xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 10240, NULL, 2, &webTaskHandle, 0); xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 16384, NULL, 2, &webTaskHandle, 0); // ⭐ 10240 → 16384
xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 0); xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 0);
if (timeSyncStatus.rtcAvailable) { if (timeSyncStatus.rtcAvailable) {
@@ -1771,9 +2167,10 @@ void loop() {
static uint32_t lastPrint = 0; static uint32_t lastPrint = 0;
if (millis() - lastPrint > 30000) { if (millis() - lastPrint > 30000) {
Serial.printf("[상태] CAN: %d/%d | Serial큐: %d/%d | PSRAM: %d KB\n", Serial.printf("[상태] CAN: %d/%d | S1: %d/%d | S2: %d/%d | PSRAM: %d KB\n",
uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE, uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE,
uxQueueMessagesWaiting(serialQueue), SERIAL_QUEUE_SIZE, uxQueueMessagesWaiting(serialQueue), SERIAL_QUEUE_SIZE,
uxQueueMessagesWaiting(serial2Queue), SERIAL2_QUEUE_SIZE,
ESP.getFreePsram() / 1024); ESP.getFreePsram() / 1024);
lastPrint = millis(); lastPrint = millis();
} }

54
index.h
View File

@@ -645,6 +645,7 @@ const char index_html[] PROGMEM = R"rawliteral(
<a href="/graph-view">📊 Graph View</a> <a href="/graph-view">📊 Graph View</a>
<a href="/settings"> Settings</a> <a href="/settings"> Settings</a>
<a href="/serial">📟 Serial</a> <a href="/serial">📟 Serial</a>
<a href="/serial2">📟 Serial2</a>
</div> </div>
<div class="content"> <div class="content">
@@ -723,6 +724,14 @@ const char index_html[] PROGMEM = R"rawliteral(
<h3>FILE SIZE</h3> <h3>FILE SIZE</h3>
<div class="value" id="current-file-size">0 B</div> <div class="value" id="current-file-size">0 B</div>
</div> </div>
<div class="status-card status-off" id="serial1-logging-status">
<h3>SERIAL1 LOG</h3>
<div class="value" id="serial1-file" style="font-size: 0.75em;">OFF</div>
</div>
<div class="status-card status-off" id="serial2-logging-status">
<h3>SERIAL2 LOG</h3>
<div class="value" id="serial2-file" style="font-size: 0.75em;">OFF</div>
</div>
</div> </div>
<div class="control-panel"> <div class="control-panel">
@@ -894,6 +903,11 @@ const char index_html[] PROGMEM = R"rawliteral(
hasInitialSync = true; hasInitialSync = true;
}, 500); }, 500);
} }
// ⭐ WebSocket 연결되면 즉시 파일 목록 요청
setTimeout(function() {
refreshFiles();
}, 1000);
}; };
ws.onclose = function() { ws.onclose = function() {
@@ -911,6 +925,9 @@ const char index_html[] PROGMEM = R"rawliteral(
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
// ⭐ 디버그: 받은 메시지 타입 출력
console.log('WebSocket message received:', data.type);
// ★ 수정: 'update' 타입 추가 (서버에서 보내는 타입과 일치) // ★ 수정: 'update' 타입 추가 (서버에서 보내는 타입과 일치)
if (data.type === 'status' || data.type === 'update') { if (data.type === 'status' || data.type === 'update') {
updateStatus(data); updateStatus(data);
@@ -921,6 +938,8 @@ const char index_html[] PROGMEM = R"rawliteral(
} else if (data.type === 'canBatch') { } else if (data.type === 'canBatch') {
updateCanBatch(data.messages); updateCanBatch(data.messages);
} else if (data.type === 'files') { } else if (data.type === 'files') {
console.log('Files received:', data.files ? data.files.length : 0, 'files'); // ⭐ 디버그
console.log('Files data:', data.files); // ⭐ 디버그
updateFileList(data.files || data.list); // ★ 수정: 'list' 키도 확인 updateFileList(data.files || data.list); // ★ 수정: 'list' 키도 확인
} else if (data.type === 'deleteResult') { } else if (data.type === 'deleteResult') {
handleDeleteResult(data); handleDeleteResult(data);
@@ -1036,6 +1055,32 @@ const char index_html[] PROGMEM = R"rawliteral(
} else { } else {
document.getElementById('power-status').classList.remove('low'); document.getElementById('power-status').classList.remove('low');
} }
// ⭐ Serial1 로깅 상태
const serial1Card = document.getElementById('serial1-logging-status');
const serial1File = document.getElementById('serial1-file');
if (data.serialLogging && data.currentSerialFile) {
serial1Card.classList.remove('status-off');
serial1Card.classList.add('status-on');
serial1File.textContent = data.currentSerialFile;
} else {
serial1Card.classList.remove('status-on');
serial1Card.classList.add('status-off');
serial1File.textContent = 'OFF';
}
// ⭐ Serial2 로깅 상태
const serial2Card = document.getElementById('serial2-logging-status');
const serial2File = document.getElementById('serial2-file');
if (data.serial2Logging && data.currentSerial2File) {
serial2Card.classList.remove('status-off');
serial2Card.classList.add('status-on');
serial2File.textContent = data.currentSerial2File;
} else {
serial2Card.classList.remove('status-on');
serial2Card.classList.add('status-off');
serial2File.textContent = 'OFF';
}
} }
// ★ 추가: update 타입에서 오는 messages 배열 처리 // ★ 추가: update 타입에서 오는 messages 배열 처리
@@ -1166,13 +1211,17 @@ const char index_html[] PROGMEM = R"rawliteral(
} }
function updateFileList(files) { function updateFileList(files) {
console.log('updateFileList called, files:', files); // ⭐ 디버그
const fileList = document.getElementById('file-list'); const fileList = document.getElementById('file-list');
if (!files || files.length === 0) { if (!files || files.length === 0) {
console.log('No files to display'); // ⭐ 디버그
fileList.innerHTML = '<p style="text-align: center; color: #666; font-size: 0.9em;">No log files</p>'; fileList.innerHTML = '<p style="text-align: center; color: #666; font-size: 0.9em;">No log files</p>';
return; return;
} }
console.log('Displaying', files.length, 'files'); // ⭐ 디버그
files.sort((a, b) => { files.sort((a, b) => {
return b.name.localeCompare(a.name); return b.name.localeCompare(a.name);
}); });
@@ -1298,8 +1347,12 @@ const char index_html[] PROGMEM = R"rawliteral(
} }
function refreshFiles() { function refreshFiles() {
console.log('Requesting file list...'); // ⭐ 디버그
if (ws && ws.readyState === WebSocket.OPEN) { if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({cmd: 'getFiles'})); ws.send(JSON.stringify({cmd: 'getFiles'}));
console.log('getFiles command sent'); // ⭐ 디버그
} else {
console.error('WebSocket not connected, readyState:', ws ? ws.readyState : 'null'); // ⭐ 디버그
} }
} }
@@ -1456,7 +1509,6 @@ const char index_html[] PROGMEM = R"rawliteral(
}); });
initWebSocket(); initWebSocket();
setTimeout(() => { refreshFiles(); }, 2000);
</script> </script>
</body> </body>
</html> </html>

1217
serial2_terminal.h Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff