시리얼 모드버스 추가

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 "settings.h"
#include "serial_terminal.h"
#include "serial2_terminal.h" // ⭐ Serial2 페이지 추가
// GPIO 핀 정의
#define CAN_INT_PIN 4
#define SERIAL_TX_PIN 17
#define SERIAL_RX_PIN 18
// UART2 (Serial Logger 2) ⭐ 추가
#define SERIAL2_TX_PIN 6
#define SERIAL2_RX_PIN 7
// HSPI 핀 (CAN)
#define HSPI_MISO 13
@@ -68,7 +72,10 @@
#define CAN_QUEUE_SIZE 6000 // 1000 → 6000 (PSRAM 사용)
#define FILE_BUFFER_SIZE 65536 // 16KB → 64KB (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 RECENT_MSG_COUNT 100
@@ -105,7 +112,7 @@ struct SerialSettings {
uint8_t dataBits;
uint8_t parity;
uint8_t stopBits;
} serialSettings = {115200, 8, 0, 1};
};
struct RecentCANData {
CANMessage msg;
@@ -180,6 +187,7 @@ enum MCP2515Mode {
// ========================================
uint8_t *fileBuffer = nullptr;
char *serialCsvBuffer = nullptr;
char *serial2CsvBuffer = nullptr; // ⭐ Serial2 추가
RecentCANData *recentData = nullptr;
TxMessage *txMessages = nullptr;
CANSequence *sequences = nullptr;
@@ -188,8 +196,10 @@ FileComment *fileComments = nullptr;
// Queue 저장소 (PSRAM)
StaticQueue_t *canQueueBuffer = nullptr;
StaticQueue_t *serialQueueBuffer = nullptr;
StaticQueue_t *serial2QueueBuffer = nullptr; // ⭐ Serial2
uint8_t *canQueueStorage = nullptr;
uint8_t *serialQueueStorage = nullptr;
uint8_t *serial2QueueStorage = nullptr; // ⭐ Serial2
// WiFi 설정 (내부 SRAM)
char wifiSSID[32] = "Byun_CAN_Logger";
@@ -198,11 +208,18 @@ bool enableSTAMode = false;
char staSSID[32] = "";
char staPassword[64] = "";
// ========================================
// Serial 설정 (2개)
// ========================================
SerialSettings serialSettings = {115200, 8, 0, 1}; // Serial1
SerialSettings serial2Settings = {115200, 8, 0, 1}; // ⭐ Serial2 추가
// 전역 객체 (내부 SRAM)
SPIClass hspi(HSPI);
SPIClass vspi(FSPI);
MCP2515 mcp2515(HSPI_CS, 20000000, &hspi);
HardwareSerial SerialComm(1);
HardwareSerial SerialComm(1); // UART1
HardwareSerial Serial2Comm(2); // ⭐ UART2 추가
WebServer server(80);
WebSocketsServer webSocket = WebSocketsServer(81);
Preferences preferences;
@@ -210,31 +227,41 @@ Preferences preferences;
// FreeRTOS 핸들
QueueHandle_t canQueue = NULL;
QueueHandle_t serialQueue = NULL;
QueueHandle_t serial2Queue = NULL; // ⭐ Serial2 추가
SemaphoreHandle_t sdMutex = NULL;
SemaphoreHandle_t rtcMutex = NULL;
SemaphoreHandle_t serialMutex = NULL;
SemaphoreHandle_t serial2Mutex = NULL; // ⭐ Serial2 추가
TaskHandle_t canRxTaskHandle = NULL;
TaskHandle_t sdWriteTaskHandle = NULL;
TaskHandle_t webTaskHandle = NULL;
TaskHandle_t rtcTaskHandle = NULL;
TaskHandle_t serialRxTaskHandle = NULL;
TaskHandle_t serial2RxTaskHandle = NULL; // ⭐ Serial2 추가
// 로깅 변수
volatile bool loggingEnabled = false;
volatile bool serialLoggingEnabled = false;
volatile bool serial2LoggingEnabled = false; // ⭐ Serial2 추가
volatile bool sdCardReady = false;
File logFile;
File serialLogFile;
File serial2LogFile; // ⭐ Serial2 추가
char currentFilename[MAX_FILENAME_LEN];
char currentSerialFilename[MAX_FILENAME_LEN];
char currentSerial2Filename[MAX_FILENAME_LEN]; // ⭐ Serial2 추가
uint16_t bufferIndex = 0;
uint16_t serialCsvIndex = 0;
uint16_t serial2CsvIndex = 0; // ⭐ Serial2 추가
volatile uint32_t currentFileSize = 0;
volatile uint32_t currentSerialFileSize = 0;
volatile uint32_t currentSerial2FileSize = 0; // ⭐ Serial2 추가
volatile bool canLogFormatCSV = false;
volatile bool serialLogFormatCSV = true;
volatile bool serial2LogFormatCSV = true; // ⭐ Serial2 추가
volatile uint64_t canLogStartTime = 0;
volatile uint64_t serialLogStartTime = 0;
volatile uint64_t serial2LogStartTime = 0; // ⭐ Serial2 추가
// 기타 전역 변수
MCP2515Mode currentMcpMode = MCP_MODE_NORMAL;
@@ -250,6 +277,8 @@ uint32_t lastMsgCountTime = 0;
uint32_t lastMsgCount = 0;
volatile uint32_t totalSerialRxCount = 0;
volatile uint32_t totalSerialTxCount = 0;
volatile uint32_t totalSerial2RxCount = 0; // ⭐ Serial2 추가
volatile uint32_t totalSerial2TxCount = 0; // ⭐ Serial2 추가
uint32_t totalTxCount = 0;
uint8_t sequenceCount = 0;
SequenceRuntime seqRuntime = {false, 0, 0, 0, -1};
@@ -292,6 +321,14 @@ bool initPSRAM() {
}
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));
if (!recentData) {
Serial.println("✗ recentData 할당 실패");
@@ -343,6 +380,17 @@ bool initPSRAM() {
SERIAL_QUEUE_SIZE, sizeof(SerialMessage),
(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.printf("✓ PSRAM 남은 용량: %.2f KB\n", (float)ESP.getFreePsram() / 1024.0);
Serial.println("========================================\n");
@@ -377,7 +425,21 @@ bool createQueues() {
Serial.println("✗ Serial Queue 생성 실패");
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;
}
@@ -390,6 +452,12 @@ void loadSerialSettings() {
serialSettings.dataBits = preferences.getUChar("ser_data", 8);
serialSettings.parity = preferences.getUChar("ser_parity", 0);
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() {
@@ -397,6 +465,12 @@ void saveSerialSettings() {
preferences.putUChar("ser_data", serialSettings.dataBits);
preferences.putUChar("ser_parity", serialSettings.parity);
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() {
@@ -426,6 +500,31 @@ void applySerialSettings() {
SerialComm.begin(serialSettings.baudRate, config, SERIAL_RX_PIN, SERIAL_TX_PIN);
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() {
@@ -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) {
struct can_frame frame;
CANMessage msg;
@@ -1266,6 +1416,121 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
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) {
int mode = doc["mode"];
if (mode >= 0 && mode <= 3) {
@@ -1306,17 +1571,26 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
DynamicJsonDocument response(6144);
response["type"] = "files";
JsonArray files = response.createNestedArray("list");
JsonArray files = response.createNestedArray("files");
File root = SD.open("/");
if (root) {
File file = root.openNextFile();
int fileCount = 0;
while (file) {
while (file && fileCount < 50) {
if (!file.isDirectory()) {
const char* filename = file.name();
// ⭐ 파일명이 '/'로 시작하면 건너뛰기
if (filename[0] == '/') {
filename++; // 슬래시 제거
}
// 숨김 파일과 시스템 폴더 제외
if (filename[0] != '.' &&
strcmp(filename, "System Volume Information") != 0) {
strcmp(filename, "System Volume Information") != 0 &&
strlen(filename) > 0) {
JsonObject fileObj = files.createNestedObject();
fileObj["name"] = filename;
@@ -1326,17 +1600,47 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
if (strlen(comment) > 0) {
fileObj["comment"] = comment;
}
fileCount++;
}
}
file.close();
file = root.openNextFile();
}
root.close();
// ⭐ 디버그 로그
Serial.printf("getFiles: Found %d files\n", fileCount);
} else {
Serial.println("getFiles: Failed to open root directory");
}
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;
serializeJson(response, 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) {
@@ -1425,16 +1729,29 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
// Web Update Task
// ========================================
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) {
webSocket.loop();
if (webSocket.connectedClients() > 0) {
DynamicJsonDocument doc(4096);
DynamicJsonDocument doc(8192); // ⭐ 4096 → 8192로 증가
doc["type"] = "update";
doc["logging"] = loggingEnabled;
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["totalMsg"] = totalMsgCount;
doc["msgPerSec"] = msgPerSecond;
@@ -1472,9 +1789,10 @@ void webUpdateTask(void *parameter) {
time(&now);
doc["timestamp"] = (uint64_t)now;
// CAN 메시지 배열
// CAN 메시지 배열 (최근 20개만 전송)
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) {
JsonObject msgObj = messages.createNestedObject();
msgObj["id"] = recentData[i].msg.id;
@@ -1485,6 +1803,7 @@ void webUpdateTask(void *parameter) {
for (int j = 0; j < recentData[i].msg.dlc; 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;
serializeJson(doc, json);
size_t jsonSize = serializeJson(doc, json);
// JSON 크기 확인 (8KB 이하만 전송)
if (jsonSize > 0 && jsonSize < 8192) {
webSocket.broadcastTXT(json);
} else {
Serial.printf("! JSON 크기 초과: %d bytes\n", jsonSize);
}
}
vTaskDelay(xDelay);
@@ -1614,12 +2001,14 @@ void setup() {
// Serial 통신 초기화
applySerialSettings();
Serial.println("✓ Serial 통신 초기화 완료");
Serial.println("✓ Serial1 통신 초기화 (GPIO 17/18)");
Serial.println("✓ Serial2 통신 초기화 (GPIO 6/7)"); // ⭐ Serial2
// Mutex 생성
sdMutex = xSemaphoreCreateMutex();
rtcMutex = xSemaphoreCreateMutex();
serialMutex = xSemaphoreCreateMutex();
serial2Mutex = xSemaphoreCreateMutex(); // ⭐ Serial2
if (!sdMutex || !rtcMutex || !serialMutex) {
Serial.println("✗ Mutex 생성 실패!");
@@ -1640,6 +2029,8 @@ void setup() {
}
// WiFi 설정
WiFi.setSleep(false); // ⭐ WiFi 절전 모드 비활성화 (연결 안정성 향상)
if (enableSTAMode && strlen(staSSID) > 0) {
Serial.println("\n📶 WiFi APSTA 모드...");
WiFi.mode(WIFI_AP_STA);
@@ -1700,6 +2091,10 @@ void setup() {
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, []() {
if (server.hasArg("file")) {
String filename = "/" + server.arg("file");
@@ -1741,8 +2136,9 @@ void setup() {
xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 24576, NULL, 4, &sdWriteTaskHandle, 1);
xTaskCreatePinnedToCore(sequenceTask, "SEQ", 4096, NULL, 2, NULL, 1);
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(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);
if (timeSyncStatus.rtcAvailable) {
@@ -1771,9 +2167,10 @@ void loop() {
static uint32_t lastPrint = 0;
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(serialQueue), SERIAL_QUEUE_SIZE,
uxQueueMessagesWaiting(serial2Queue), SERIAL2_QUEUE_SIZE,
ESP.getFreePsram() / 1024);
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="/settings"> Settings</a>
<a href="/serial">📟 Serial</a>
<a href="/serial2">📟 Serial2</a>
</div>
<div class="content">
@@ -723,6 +724,14 @@ const char index_html[] PROGMEM = R"rawliteral(
<h3>FILE SIZE</h3>
<div class="value" id="current-file-size">0 B</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 class="control-panel">
@@ -894,6 +903,11 @@ const char index_html[] PROGMEM = R"rawliteral(
hasInitialSync = true;
}, 500);
}
// ⭐ WebSocket 연결되면 즉시 파일 목록 요청
setTimeout(function() {
refreshFiles();
}, 1000);
};
ws.onclose = function() {
@@ -911,6 +925,9 @@ const char index_html[] PROGMEM = R"rawliteral(
try {
const data = JSON.parse(event.data);
// ⭐ 디버그: 받은 메시지 타입 출력
console.log('WebSocket message received:', data.type);
// ★ 수정: 'update' 타입 추가 (서버에서 보내는 타입과 일치)
if (data.type === 'status' || data.type === 'update') {
updateStatus(data);
@@ -921,6 +938,8 @@ const char index_html[] PROGMEM = R"rawliteral(
} else if (data.type === 'canBatch') {
updateCanBatch(data.messages);
} 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' 키도 확인
} else if (data.type === 'deleteResult') {
handleDeleteResult(data);
@@ -1036,6 +1055,32 @@ const char index_html[] PROGMEM = R"rawliteral(
} else {
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 배열 처리
@@ -1166,13 +1211,17 @@ const char index_html[] PROGMEM = R"rawliteral(
}
function updateFileList(files) {
console.log('updateFileList called, files:', files); // ⭐ 디버그
const fileList = document.getElementById('file-list');
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>';
return;
}
console.log('Displaying', files.length, 'files'); // ⭐ 디버그
files.sort((a, b) => {
return b.name.localeCompare(a.name);
});
@@ -1298,8 +1347,12 @@ const char index_html[] PROGMEM = R"rawliteral(
}
function refreshFiles() {
console.log('Requesting file list...'); // ⭐ 디버그
if (ws && ws.readyState === WebSocket.OPEN) {
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();
setTimeout(() => { refreshFiles(); }, 2000);
</script>
</body>
</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