This commit is contained in:
2025-11-13 03:37:17 +00:00
parent e10db0ff4b
commit a153f924b2

View File

@@ -25,6 +25,7 @@
#include "graph.h"
#include "graph_viewer.h"
#include "settings.h"
#include "serial_terminal.h"
// GPIO 핀 정의
#define CAN_INT_PIN 27
@@ -46,6 +47,10 @@
#define RTC_SCL 26
#define DS3231_ADDRESS 0x68
// Serial2 핀 (RS232 통신)
#define SERIAL2_RX 16
#define SERIAL2_TX 17
// 버퍼 설정
#define CAN_QUEUE_SIZE 2000 // 1000 → 2000으로 증가
#define FILE_BUFFER_SIZE 16384 // 8192 → 16384 (16KB)로 증가
@@ -53,6 +58,8 @@
#define RECENT_MSG_COUNT 100
#define MAX_TX_MESSAGES 20
#define MAX_COMMENT_LEN 128
#define SERIAL_BUFFER_SIZE 1024
#define SERIAL_LOG_BUFFER_SIZE 32768
// RTC 동기화 설정
#define RTC_SYNC_INTERVAL_MS 60000 // 1분마다 RTC와 동기화
@@ -119,6 +126,29 @@ struct FileComment {
char comment[MAX_COMMENT_LEN];
};
// Serial 설정 구조체
struct SerialTermConfig {
uint32_t baudRate;
uint8_t dataBits;
uint8_t parity;
uint8_t stopBits;
bool enabled;
bool echoEnabled;
bool timestampEnabled;
bool autoSaveEnabled;
char logFilename[MAX_FILENAME_LEN];
};
// 시리얼 데이터 버퍼
struct SerialDataBuffer {
char data[SERIAL_LOG_BUFFER_SIZE];
uint32_t writeIndex;
uint32_t readIndex;
bool overflow;
SemaphoreHandle_t mutex;
};
// 시간 동기화 상태
struct TimeSyncStatus {
bool synchronized;
@@ -173,6 +203,24 @@ WebServer server(80);
WebSocketsServer webSocket = WebSocketsServer(81);
Preferences preferences;
// 시리얼 터미널 전역 변수
SerialTermConfig serialTermConfig = {
9600, 8, 0, 1, false, true, true, false, "serial_log.txt"
};
SerialDataBuffer serialBuffer;
WebSocketsServer serialWebSocket = WebSocketsServer(82);
QueueHandle_t serialQueue;
TaskHandle_t serialTaskHandle = NULL;
File serialLogFile;
volatile uint32_t serialRxCount = 0;
volatile uint32_t serialTxCount = 0;
volatile uint32_t serialRxPerSecond = 0;
volatile uint32_t serialTxPerSecond = 0;
uint32_t lastSerialCountTime = 0;
uint32_t lastSerialRxCount = 0;
uint32_t lastSerialTxCount = 0;
// Forward declaration
void IRAM_ATTR canISR();
@@ -275,6 +323,9 @@ void loadSettings() {
if (enableSTAMode && strlen(staSSID) > 0) {
Serial.printf("✓ WiFi STA 모드: 활성화 (SSID: %s)\n", staSSID);
}
// 시리얼 설정 로드
loadSerialSettings();
}
void saveSettings() {
@@ -305,6 +356,430 @@ void saveSettings() {
Serial.println("⚠️ 재부팅 후 WiFi 설정이 적용됩니다.");
}
// ========================================
// 시리얼 터미널 함수들
// ========================================
void loadSerialSettings() {
preferences.begin("serial-term", false);
serialTermConfig.baudRate = preferences.getUInt("baud", 9600);
serialTermConfig.dataBits = preferences.getUChar("databits", 8);
serialTermConfig.parity = preferences.getUChar("parity", 0);
serialTermConfig.stopBits = preferences.getUChar("stopbits", 1);
serialTermConfig.enabled = preferences.getBool("enabled", false);
serialTermConfig.echoEnabled = preferences.getBool("echo", true);
serialTermConfig.timestampEnabled = preferences.getBool("timestamp", true);
serialTermConfig.autoSaveEnabled = preferences.getBool("autosave", false);
preferences.getString("logfile", serialTermConfig.logFilename, sizeof(serialTermConfig.logFilename));
if (strlen(serialTermConfig.logFilename) == 0) {
strcpy(serialTermConfig.logFilename, "serial_log.txt");
}
preferences.end();
Serial.printf("✓ 시리얼 설정 로드: %d-%d-%c-%d\n",
serialTermConfig.baudRate,
serialTermConfig.dataBits,
serialTermConfig.parity == 0 ? 'N' : (serialTermConfig.parity == 1 ? 'E' : 'O'),
serialTermConfig.stopBits);
}
void saveSerialSettings() {
preferences.begin("serial-term", false);
preferences.putUInt("baud", serialTermConfig.baudRate);
preferences.putUChar("databits", serialTermConfig.dataBits);
preferences.putUChar("parity", serialTermConfig.parity);
preferences.putUChar("stopbits", serialTermConfig.stopBits);
preferences.putBool("enabled", serialTermConfig.enabled);
preferences.putBool("echo", serialTermConfig.echoEnabled);
preferences.putBool("timestamp", serialTermConfig.timestampEnabled);
preferences.putBool("autosave", serialTermConfig.autoSaveEnabled);
preferences.putString("logfile", serialTermConfig.logFilename);
preferences.end();
Serial.println("✓ 시리얼 설정 저장 완료");
}
void configureSerial2() {
Serial2.end();
uint32_t config = SERIAL_8N1;
switch(serialTermConfig.dataBits) {
case 5: config = SERIAL_5N1; break;
case 6: config = SERIAL_6N1; break;
case 7: config = SERIAL_7N1; break;
case 8: default: config = SERIAL_8N1; break;
}
if (serialTermConfig.parity == 1) {
switch(serialTermConfig.dataBits) {
case 5: config = SERIAL_5E1; break;
case 6: config = SERIAL_6E1; break;
case 7: config = SERIAL_7E1; break;
case 8: default: config = SERIAL_8E1; break;
}
} else if (serialTermConfig.parity == 2) {
switch(serialTermConfig.dataBits) {
case 5: config = SERIAL_5O1; break;
case 6: config = SERIAL_6O1; break;
case 7: config = SERIAL_7O1; break;
case 8: default: config = SERIAL_8O1; break;
}
}
if (serialTermConfig.stopBits == 2) {
switch(serialTermConfig.dataBits) {
case 5:
if (serialTermConfig.parity == 0) config = SERIAL_5N2;
else if (serialTermConfig.parity == 1) config = SERIAL_5E2;
else config = SERIAL_5O2;
break;
case 6:
if (serialTermConfig.parity == 0) config = SERIAL_6N2;
else if (serialTermConfig.parity == 1) config = SERIAL_6E2;
else config = SERIAL_6O2;
break;
case 7:
if (serialTermConfig.parity == 0) config = SERIAL_7N2;
else if (serialTermConfig.parity == 1) config = SERIAL_7E2;
else config = SERIAL_7O2;
break;
case 8: default:
if (serialTermConfig.parity == 0) config = SERIAL_8N2;
else if (serialTermConfig.parity == 1) config = SERIAL_8E2;
else config = SERIAL_8O2;
break;
}
}
Serial2.begin(serialTermConfig.baudRate, config, SERIAL2_RX, SERIAL2_TX);
Serial.printf("✓ Serial2 구성: %d baud, config=0x%X\n", serialTermConfig.baudRate, config);
}
void addToSerialBuffer(const char* data, size_t len, bool isTx) {
if (!serialBuffer.mutex) return;
xSemaphoreTake(serialBuffer.mutex, portMAX_DELAY);
if (serialTermConfig.timestampEnabled) {
char timestamp[32];
struct timeval tv;
gettimeofday(&tv, NULL);
struct tm *tm_info = localtime(&tv.tv_sec);
snprintf(timestamp, sizeof(timestamp), "[%02d:%02d:%02d.%03ld] %s: ",
tm_info->tm_hour, tm_info->tm_min, tm_info->tm_sec,
tv.tv_usec / 1000, isTx ? "TX" : "RX");
size_t tsLen = strlen(timestamp);
for (size_t i = 0; i < tsLen; i++) {
serialBuffer.data[serialBuffer.writeIndex] = timestamp[i];
serialBuffer.writeIndex = (serialBuffer.writeIndex + 1) % SERIAL_LOG_BUFFER_SIZE;
if (serialBuffer.writeIndex == serialBuffer.readIndex) {
serialBuffer.overflow = true;
serialBuffer.readIndex = (serialBuffer.readIndex + 1) % SERIAL_LOG_BUFFER_SIZE;
}
}
}
for (size_t i = 0; i < len; i++) {
serialBuffer.data[serialBuffer.writeIndex] = data[i];
serialBuffer.writeIndex = (serialBuffer.writeIndex + 1) % SERIAL_LOG_BUFFER_SIZE;
if (serialBuffer.writeIndex == serialBuffer.readIndex) {
serialBuffer.overflow = true;
serialBuffer.readIndex = (serialBuffer.readIndex + 1) % SERIAL_LOG_BUFFER_SIZE;
}
}
xSemaphoreGive(serialBuffer.mutex);
}
String getSerialBufferData() {
if (!serialBuffer.mutex) return "";
String result;
xSemaphoreTake(serialBuffer.mutex, portMAX_DELAY);
uint32_t idx = serialBuffer.readIndex;
while (idx != serialBuffer.writeIndex) {
result += serialBuffer.data[idx];
idx = (idx + 1) % SERIAL_LOG_BUFFER_SIZE;
}
xSemaphoreGive(serialBuffer.mutex);
return result;
}
void clearSerialBuffer() {
if (!serialBuffer.mutex) return;
xSemaphoreTake(serialBuffer.mutex, portMAX_DELAY);
serialBuffer.readIndex = serialBuffer.writeIndex;
serialBuffer.overflow = false;
xSemaphoreGive(serialBuffer.mutex);
}
bool saveSerialLog() {
if (!sdCardReady) return false;
String data = getSerialBufferData();
if (data.length() == 0) return true;
xSemaphoreTake(sdMutex, portMAX_DELAY);
File file = SD.open(serialTermConfig.logFilename, FILE_APPEND);
if (!file) {
xSemaphoreGive(sdMutex);
return false;
}
size_t written = file.print(data);
file.close();
xSemaphoreGive(sdMutex);
if (written > 0) {
clearSerialBuffer();
return true;
}
return false;
}
void serialTask(void *pvParameters) {
char rxBuffer[SERIAL_BUFFER_SIZE];
size_t rxIndex = 0;
Serial.println("✓ Serial Task 시작 (Core 0, Priority 1)");
while (true) {
if (serialTermConfig.enabled && Serial2.available()) {
while (Serial2.available() && rxIndex < SERIAL_BUFFER_SIZE - 1) {
char c = Serial2.read();
rxBuffer[rxIndex++] = c;
serialRxCount++;
if (serialTermConfig.echoEnabled) {
Serial2.write(c);
serialTxCount++;
}
}
if (rxIndex > 0) {
addToSerialBuffer(rxBuffer, rxIndex, false);
String data;
for (size_t i = 0; i < rxIndex; i++) {
if (rxBuffer[i] >= 32 && rxBuffer[i] < 127) {
data += rxBuffer[i];
} else if (rxBuffer[i] == '\r' || rxBuffer[i] == '\n') {
data += rxBuffer[i];
} else {
char hex[5];
snprintf(hex, sizeof(hex), "<%02X>", (uint8_t)rxBuffer[i]);
data += hex;
}
}
serialWebSocket.broadcastTXT(data);
if (serialTermConfig.autoSaveEnabled && rxIndex > 100) {
saveSerialLog();
}
rxIndex = 0;
}
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
void handleSerialTerminal() {
server.send_P(200, "text/html", SERIAL_TERMINAL_HTML);
}
void handleSerialConfig() {
String json = "{";
json += "\"baudRate\":" + String(serialTermConfig.baudRate) + ",";
json += "\"dataBits\":" + String(serialTermConfig.dataBits) + ",";
json += "\"parity\":" + String(serialTermConfig.parity) + ",";
json += "\"stopBits\":" + String(serialTermConfig.stopBits) + ",";
json += "\"enabled\":" + String(serialTermConfig.enabled ? "true" : "false") + ",";
json += "\"echo\":" + String(serialTermConfig.echoEnabled ? "true" : "false") + ",";
json += "\"timestamp\":" + String(serialTermConfig.timestampEnabled ? "true" : "false") + ",";
json += "\"autosave\":" + String(serialTermConfig.autoSaveEnabled ? "true" : "false") + ",";
json += "\"logFilename\":\"" + String(serialTermConfig.logFilename) + "\",";
json += "\"rxCount\":" + String(serialRxCount) + ",";
json += "\"txCount\":" + String(serialTxCount) + ",";
json += "\"rxPerSec\":" + String(serialRxPerSecond) + ",";
json += "\"txPerSec\":" + String(serialTxPerSecond);
json += "}";
server.send(200, "application/json", json);
}
void handleSerialSetConfig() {
if (server.hasArg("plain")) {
String body = server.arg("plain");
if (body.indexOf("baudRate") >= 0) {
int idx = body.indexOf("baudRate\":") + 10;
serialTermConfig.baudRate = body.substring(idx, body.indexOf(',', idx)).toInt();
}
if (body.indexOf("dataBits") >= 0) {
int idx = body.indexOf("dataBits\":") + 10;
serialTermConfig.dataBits = body.substring(idx, body.indexOf(',', idx)).toInt();
}
if (body.indexOf("parity") >= 0) {
int idx = body.indexOf("parity\":") + 8;
serialTermConfig.parity = body.substring(idx, body.indexOf(',', idx)).toInt();
}
if (body.indexOf("stopBits") >= 0) {
int idx = body.indexOf("stopBits\":") + 10;
serialTermConfig.stopBits = body.substring(idx, body.indexOf(',', idx)).toInt();
}
if (body.indexOf("enabled") >= 0) {
serialTermConfig.enabled = body.indexOf("\"enabled\":true") >= 0;
}
if (body.indexOf("echo") >= 0) {
serialTermConfig.echoEnabled = body.indexOf("\"echo\":true") >= 0;
}
if (body.indexOf("timestamp") >= 0) {
serialTermConfig.timestampEnabled = body.indexOf("\"timestamp\":true") >= 0;
}
if (body.indexOf("autosave") >= 0) {
serialTermConfig.autoSaveEnabled = body.indexOf("\"autosave\":true") >= 0;
}
if (body.indexOf("logFilename") >= 0) {
int idx = body.indexOf("logFilename\":\"") + 14;
String filename = body.substring(idx, body.indexOf('\"', idx));
filename.toCharArray(serialTermConfig.logFilename, sizeof(serialTermConfig.logFilename));
}
configureSerial2();
saveSerialSettings();
server.send(200, "application/json", "{\"status\":\"ok\"}");
} else {
server.send(400, "application/json", "{\"status\":\"error\"}");
}
}
void handleSerialSend() {
if (server.hasArg("data")) {
String data = server.arg("data");
if (serialTermConfig.enabled) {
data.replace("\\r", "\r");
data.replace("\\n", "\n");
Serial2.print(data);
serialTxCount += data.length();
addToSerialBuffer(data.c_str(), data.length(), true);
server.send(200, "application/json", "{\"status\":\"ok\"}");
} else {
server.send(400, "application/json", "{\"status\":\"error\",\"message\":\"Serial not enabled\"}");
}
} else {
server.send(400, "application/json", "{\"status\":\"error\",\"message\":\"No data\"}");
}
}
void handleSerialSaveLog() {
if (saveSerialLog()) {
server.send(200, "application/json", "{\"status\":\"ok\"}");
} else {
server.send(500, "application/json", "{\"status\":\"error\"}");
}
}
void handleSerialClearBuffer() {
clearSerialBuffer();
server.send(200, "application/json", "{\"status\":\"ok\"}");
}
void serialWebSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
switch(type) {
case WStype_DISCONNECTED:
Serial.printf("[%u] Serial WebSocket Disconnected\n", num);
break;
case WStype_CONNECTED: {
IPAddress ip = serialWebSocket.remoteIP(num);
Serial.printf("[%u] Serial WebSocket Connected from %d.%d.%d.%d\n",
num, ip[0], ip[1], ip[2], ip[3]);
String data = getSerialBufferData();
if (data.length() > 0) {
serialWebSocket.sendTXT(num, data);
}
break;
}
case WStype_TEXT:
if (serialTermConfig.enabled && length > 0) {
Serial2.write(payload, length);
serialTxCount += length;
addToSerialBuffer((const char*)payload, length, true);
}
break;
default:
break;
}
}
void setupSerial() {
serialBuffer.writeIndex = 0;
serialBuffer.readIndex = 0;
serialBuffer.overflow = false;
serialBuffer.mutex = xSemaphoreCreateMutex();
configureSerial2();
serialQueue = xQueueCreate(100, sizeof(char) * SERIAL_BUFFER_SIZE);
xTaskCreatePinnedToCore(
serialTask,
"SerialTask",
4096,
NULL,
1,
&serialTaskHandle,
0
);
serialWebSocket.begin();
serialWebSocket.onEvent(serialWebSocketEvent);
server.on("/serial", HTTP_GET, handleSerialTerminal);
server.on("/serial/config", HTTP_GET, handleSerialConfig);
server.on("/serial/setconfig", HTTP_POST, handleSerialSetConfig);
server.on("/serial/send", HTTP_POST, handleSerialSend);
server.on("/serial/savelog", HTTP_POST, handleSerialSaveLog);
server.on("/serial/clear", HTTP_POST, handleSerialClearBuffer);
Serial.println("✓ 시리얼 터미널 초기화 완료 (CAN과 독립 동작)");
}
void updateSerialStats() {
uint32_t currentTime = millis();
if (currentTime - lastSerialCountTime >= 1000) {
serialRxPerSecond = serialRxCount - lastSerialRxCount;
serialTxPerSecond = serialTxCount - lastSerialTxCount;
lastSerialRxCount = serialRxCount;
lastSerialTxCount = serialTxCount;
lastSerialCountTime = currentTime;
}
}
void saveCANSettings() {
preferences.begin("can-logger", false);
@@ -1834,6 +2309,9 @@ void setup() {
}
});
// 시리얼 터미널 초기화
setupSerial();
server.begin();
// Queue 생성
@@ -1884,6 +2362,8 @@ void setup() {
void loop() {
server.handleClient();
serialWebSocket.loop();
updateSerialStats();
vTaskDelay(pdMS_TO_TICKS(10));
static uint32_t lastPrint = 0;