This commit is contained in:
2025-11-13 06:22:25 +00:00
parent 7e612696e0
commit 0620d035bf

View File

@@ -1,7 +1,7 @@
/*
* Byun CAN Logger with Web Interface + RTC Time Synchronization
* Version: 2.0
* Added: Phone time sync to RTC, File comments, MCP2515 mode control
* Version: 3.0
* Added: Phone time sync to RTC, File comments, MCP2515 mode control, Serial Terminal
*/
#include <Arduino.h>
@@ -25,10 +25,15 @@
#include "graph.h"
#include "graph_viewer.h"
#include "settings.h"
#include "serial_terminal.h"
// GPIO 핀 정의
#define CAN_INT_PIN 27
// Serial2 핀 정의 (RS232 통신용)
#define SERIAL_RX_PIN 16
#define SERIAL_TX_PIN 17
// HSPI 핀 (CAN)
#define HSPI_MISO 12
#define HSPI_MOSI 13
@@ -54,6 +59,11 @@
#define MAX_TX_MESSAGES 20
#define MAX_COMMENT_LEN 128
// Serial 버퍼 설정
#define SERIAL_RX_BUFFER_SIZE 4096
#define SERIAL_TX_QUEUE_SIZE 100
#define SERIAL_LOG_BUFFER_SIZE 8192
// RTC 동기화 설정
#define RTC_SYNC_INTERVAL_MS 60000 // 1분마다 RTC와 동기화
@@ -138,6 +148,40 @@ struct PowerStatus {
uint32_t lastMinReset; // 최소값 리셋 시간
} powerStatus = {0.0, 999.9, false, 0, 0};
// ========================================
// Serial 통신 관련 구조체
// ========================================
// Serial 설정 구조체
struct SerialConfig {
uint32_t baudRate;
uint8_t dataBits; // 5, 6, 7, 8
uint8_t parity; // 0=None, 1=Even, 2=Odd
uint8_t stopBits; // 1, 2
bool flowControl; // RTS/CTS
} serialConfig = {115200, 8, 0, 1, false};
// Serial 데이터 구조체 (로깅용)
struct SerialLogData {
uint64_t timestamp_us;
bool isTx; // true=송신, false=수신
uint16_t length;
uint8_t data[256];
} __attribute__((packed));
// Serial 상태
struct SerialStatus {
bool logging;
uint32_t rxCount;
uint32_t txCount;
uint32_t rxBytesPerSec;
uint32_t txBytesPerSec;
uint32_t lastRxBytes;
uint32_t lastTxBytes;
uint32_t lastStatTime;
} serialStatus = {false, 0, 0, 0, 0, 0, 0, 0};
// MCP2515 레지스터 주소 정의 (라이브러리에 없는 경우)
#ifndef MCP_CANCTRL
#define MCP_CANCTRL 0x0F
@@ -171,18 +215,27 @@ MCP2515 mcp2515(HSPI_CS, 20000000, &hspi); // 20MHz로 증가 (10MHz → 20MHz)
WebServer server(80);
WebSocketsServer webSocket = WebSocketsServer(81);
WebSocketsServer serialWebSocket = WebSocketsServer(82);
Preferences preferences;
// Forward declaration
void IRAM_ATTR canISR();
void serialWebSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length);
QueueHandle_t canQueue;
QueueHandle_t serialTxQueue;
QueueHandle_t serialLogQueue;
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;
TaskHandle_t serialTxTaskHandle = NULL;
TaskHandle_t serialLogTaskHandle = NULL;
TaskHandle_t serialWebTaskHandle = NULL;
volatile bool loggingEnabled = false;
volatile bool sdCardReady = false;
@@ -231,6 +284,378 @@ SequenceRuntime seqRuntime = {false, 0, 0, 0, -1};
FileComment fileComments[MAX_FILE_COMMENTS];
int commentCount = 0;
// Serial 로그 파일
File serialLogFile;
char currentSerialFilename[MAX_FILENAME_LEN];
uint8_t serialLogBuffer[SERIAL_LOG_BUFFER_SIZE];
uint16_t serialLogBufferIndex = 0;
volatile uint32_t currentSerialFileSize = 0;
// ========================================
// Serial 설정 저장/로드 함수
// ========================================
void saveSerialSettings() {
preferences.begin("serial-cfg", false);
preferences.putUInt("baudrate", serialConfig.baudRate);
preferences.putUChar("databits", serialConfig.dataBits);
preferences.putUChar("parity", serialConfig.parity);
preferences.putUChar("stopbits", serialConfig.stopBits);
preferences.putBool("flowctrl", serialConfig.flowControl);
preferences.end();
Serial.println("✓ Serial 설정 저장 완료");
}
void loadSerialSettings() {
preferences.begin("serial-cfg", true);
serialConfig.baudRate = preferences.getUInt("baudrate", 115200);
serialConfig.dataBits = preferences.getUChar("databits", 8);
serialConfig.parity = preferences.getUChar("parity", 0);
serialConfig.stopBits = preferences.getUChar("stopbits", 1);
serialConfig.flowControl = preferences.getBool("flowctrl", false);
preferences.end();
Serial.printf("✓ Serial 설정 로드: %u bps, %dN%d\n",
serialConfig.baudRate, serialConfig.dataBits, serialConfig.stopBits);
}
void applySerialConfig() {
Serial2.end();
uint32_t config = SERIAL_8N1;
if (serialConfig.dataBits == 5) config = SERIAL_5N1;
else if (serialConfig.dataBits == 6) config = SERIAL_6N1;
else if (serialConfig.dataBits == 7) config = SERIAL_7N1;
else config = SERIAL_8N1;
if (serialConfig.parity == 1) {
if (serialConfig.dataBits == 5) config = SERIAL_5E1;
else if (serialConfig.dataBits == 6) config = SERIAL_6E1;
else if (serialConfig.dataBits == 7) config = SERIAL_7E1;
else config = SERIAL_8E1;
} else if (serialConfig.parity == 2) {
if (serialConfig.dataBits == 5) config = SERIAL_5O1;
else if (serialConfig.dataBits == 6) config = SERIAL_6O1;
else if (serialConfig.dataBits == 7) config = SERIAL_7O1;
else config = SERIAL_8O1;
}
if (serialConfig.stopBits == 2) {
config |= 0x3000;
}
Serial2.begin(serialConfig.baudRate, config, SERIAL_RX_PIN, SERIAL_TX_PIN);
Serial2.setRxBufferSize(SERIAL_RX_BUFFER_SIZE);
Serial.printf("✓ Serial2 재구성: %u bps\n", serialConfig.baudRate);
}
// ========================================
// Serial 로깅 함수
// ========================================
void flushSerialLogBuffer() {
if (serialLogBufferIndex > 0 && serialLogFile) {
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
serialLogFile.write(serialLogBuffer, serialLogBufferIndex);
currentSerialFileSize += serialLogBufferIndex;
serialLogBufferIndex = 0;
xSemaphoreGive(sdMutex);
}
}
}
void startSerialLogging() {
if (!sdCardReady) {
Serial.println("✗ SD 카드 없음 - Serial 로깅 불가");
return;
}
if (serialStatus.logging) {
Serial.println("⚠ Serial 로깅이 이미 실행 중입니다");
return;
}
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
sprintf(currentSerialFilename, "/SERIAL_%lu.log", millis());
} else {
sprintf(currentSerialFilename, "/SERIAL_%04d%02d%02d_%02d%02d%02d.log",
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
}
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
serialLogFile = SD.open(currentSerialFilename, FILE_WRITE);
if (serialLogFile) {
serialStatus.logging = true;
currentSerialFileSize = 0;
serialLogBufferIndex = 0;
char header[256];
snprintf(header, sizeof(header),
"Serial Log - BaudRate: %u, DataBits: %d, Parity: %d, StopBits: %d\n",
serialConfig.baudRate, serialConfig.dataBits,
serialConfig.parity, serialConfig.stopBits);
serialLogFile.print(header);
Serial.printf("✓ Serial 로깅 시작: %s\n", currentSerialFilename);
} else {
Serial.println("✗ Serial 로그 파일 생성 실패");
}
xSemaphoreGive(sdMutex);
}
}
void stopSerialLogging() {
if (!serialStatus.logging) return;
serialStatus.logging = false;
flushSerialLogBuffer();
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
if (serialLogFile) {
serialLogFile.close();
Serial.printf("✓ Serial 로깅 종료: %s (크기: %u bytes)\n",
currentSerialFilename, currentSerialFileSize);
}
xSemaphoreGive(sdMutex);
}
}
// ========================================
// Serial Task 함수들
// ========================================
void serialRxTask(void* parameter) {
SerialLogData logData;
uint8_t rxBuffer[256];
Serial.println("✓ Serial RX Task 시작");
while (true) {
if (Serial2.available()) {
int len = Serial2.readBytes(rxBuffer, sizeof(rxBuffer));
if (len > 0) {
serialStatus.rxCount += len;
String hexStr = "";
String asciiStr = "";
for (int i = 0; i < len; i++) {
char hexBuf[4];
sprintf(hexBuf, "%02X ", rxBuffer[i]);
hexStr += hexBuf;
if (rxBuffer[i] >= 32 && rxBuffer[i] < 127) {
asciiStr += (char)rxBuffer[i];
} else {
asciiStr += '.';
}
}
String wsData = "RX:" + hexStr + "|" + asciiStr;
serialWebSocket.broadcastTXT(wsData);
if (serialStatus.logging) {
logData.timestamp_us = esp_timer_get_time();
logData.isTx = false;
logData.length = len;
memcpy(logData.data, rxBuffer, len);
xQueueSend(serialLogQueue, &logData, 0);
}
}
}
vTaskDelay(pdMS_TO_TICKS(5));
}
}
void serialTxTask(void* parameter) {
uint8_t txData[256];
int txLen;
Serial.println("✓ Serial TX Task 시작");
while (true) {
if (xQueueReceive(serialTxQueue, txData, pdMS_TO_TICKS(100))) {
txLen = txData[0];
if (txLen > 0 && txLen < 256) {
Serial2.write(&txData[1], txLen);
serialStatus.txCount += txLen;
if (serialStatus.logging) {
SerialLogData logData;
logData.timestamp_us = esp_timer_get_time();
logData.isTx = true;
logData.length = txLen;
memcpy(logData.data, &txData[1], txLen);
xQueueSend(serialLogQueue, &logData, 0);
}
String hexStr = "TX:";
for (int i = 0; i < txLen; i++) {
char hexBuf[4];
sprintf(hexBuf, "%02X ", txData[i + 1]);
hexStr += hexBuf;
}
serialWebSocket.broadcastTXT(hexStr);
}
}
vTaskDelay(pdMS_TO_TICKS(1));
}
}
void serialLogTask(void* parameter) {
SerialLogData logData;
char logLine[512];
Serial.println("✓ Serial Log Task 시작");
while (true) {
if (xQueueReceive(serialLogQueue, &logData, pdMS_TO_TICKS(100))) {
if (serialStatus.logging && serialLogFile) {
int offset = snprintf(logLine, sizeof(logLine),
"[%llu] %s: ", logData.timestamp_us, logData.isTx ? "TX" : "RX");
for (int i = 0; i < logData.length && offset < sizeof(logLine) - 10; i++) {
offset += snprintf(logLine + offset, sizeof(logLine) - offset,
"%02X ", logData.data[i]);
}
offset += snprintf(logLine + offset, sizeof(logLine) - offset, " | ");
for (int i = 0; i < logData.length && offset < sizeof(logLine) - 5; i++) {
if (logData.data[i] >= 32 && logData.data[i] < 127) {
logLine[offset++] = logData.data[i];
} else {
logLine[offset++] = '.';
}
}
logLine[offset++] = '\n';
logLine[offset] = '\0';
int lineLen = strlen(logLine);
if (serialLogBufferIndex + lineLen >= SERIAL_LOG_BUFFER_SIZE) {
flushSerialLogBuffer();
}
memcpy(&serialLogBuffer[serialLogBufferIndex], logLine, lineLen);
serialLogBufferIndex += lineLen;
}
} else {
if (serialLogBufferIndex > 0) {
flushSerialLogBuffer();
}
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
void serialWebSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) {
if (type == WStype_TEXT) {
String msg = String((char*)payload);
if (msg.startsWith("CONFIG:")) {
int idx2 = msg.indexOf(',');
int idx3 = msg.indexOf(',', idx2 + 1);
int idx4 = msg.indexOf(',', idx3 + 1);
serialConfig.baudRate = msg.substring(7, idx2).toInt();
serialConfig.dataBits = msg.substring(idx2 + 1, idx3).toInt();
serialConfig.parity = msg.substring(idx3 + 1, idx4).toInt();
serialConfig.stopBits = msg.substring(idx4 + 1).toInt();
applySerialConfig();
saveSerialSettings();
serialWebSocket.sendTXT(num, "CONFIG_OK");
} else if (msg == "START_LOG") {
startSerialLogging();
serialWebSocket.sendTXT(num, serialStatus.logging ? "LOG_STARTED" : "LOG_FAILED");
} else if (msg == "STOP_LOG") {
stopSerialLogging();
serialWebSocket.sendTXT(num, "LOG_STOPPED");
} else if (msg == "GET_CONFIG") {
char cfgStr[128];
snprintf(cfgStr, sizeof(cfgStr), "CFG:%u,%d,%d,%d,%d",
serialConfig.baudRate, serialConfig.dataBits, serialConfig.parity,
serialConfig.stopBits, serialConfig.flowControl ? 1 : 0);
serialWebSocket.sendTXT(num, cfgStr);
} else if (msg == "GET_STATUS") {
char statStr[256];
snprintf(statStr, sizeof(statStr), "STAT:%d,%u,%u,%u,%u,%s",
serialStatus.logging ? 1 : 0,
serialStatus.rxCount, serialStatus.txCount,
serialStatus.rxBytesPerSec, serialStatus.txBytesPerSec,
currentSerialFilename);
serialWebSocket.sendTXT(num, statStr);
} else if (msg.startsWith("TX:")) {
String hexData = msg.substring(3);
hexData.trim();
uint8_t txBuffer[256];
int txLen = 0;
for (int i = 0; i < hexData.length() && txLen < 255; i += 2) {
if (i + 1 < hexData.length()) {
String hexByte = hexData.substring(i, i + 2);
txBuffer[txLen + 1] = (uint8_t)strtol(hexByte.c_str(), NULL, 16);
txLen++;
}
}
if (txLen > 0) {
txBuffer[0] = txLen;
xQueueSend(serialTxQueue, txBuffer, 0);
}
} else if (msg.startsWith("TXT:")) {
String text = msg.substring(4);
uint8_t txBuffer[256];
int txLen = text.length();
if (txLen > 0 && txLen < 255) {
txBuffer[0] = txLen;
memcpy(&txBuffer[1], text.c_str(), txLen);
xQueueSend(serialTxQueue, txBuffer, 0);
}
}
}
}
void serialWebUpdateTask(void* parameter) {
Serial.println("✓ Serial Web Update Task 시작");
while (true) {
serialWebSocket.loop();
uint32_t now = millis();
if (now - serialStatus.lastStatTime >= 1000) {
serialStatus.rxBytesPerSec = serialStatus.rxCount - serialStatus.lastRxBytes;
serialStatus.txBytesPerSec = serialStatus.txCount - serialStatus.lastTxBytes;
serialStatus.lastRxBytes = serialStatus.rxCount;
serialStatus.lastTxBytes = serialStatus.txCount;
serialStatus.lastStatTime = now;
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// ========================================
// 설정 저장/로드 함수
// ========================================
@@ -1633,6 +2058,7 @@ void setup() {
// 설정 로드
loadSettings();
loadSerialSettings();
// 설정값 표시
Serial.println("\n📋 현재 설정값:");
@@ -1648,6 +2074,10 @@ void setup() {
pinMode(CAN_INT_PIN, INPUT_PULLUP);
// Serial2 초기화
applySerialConfig();
Serial.println("✓ Serial2 초기화 완료");
// ADC 설정 (전압 모니터링용)
analogSetAttenuation(ADC_11db); // 0-3.3V 범위
@@ -1669,8 +2099,9 @@ void setup() {
// Mutex 생성 (다른 초기화보다 먼저!)
sdMutex = xSemaphoreCreateMutex();
rtcMutex = xSemaphoreCreateMutex();
serialMutex = xSemaphoreCreateMutex();
if (sdMutex == NULL || rtcMutex == NULL) {
if (sdMutex == NULL || rtcMutex == NULL || serialMutex == NULL) {
Serial.println("✗ Mutex 생성 실패!");
while (1) delay(1000);
}
@@ -1747,6 +2178,11 @@ void setup() {
webSocket.begin();
webSocket.onEvent(webSocketEvent);
// Serial WebSocket 시작
serialWebSocket.begin();
serialWebSocket.onEvent(serialWebSocketEvent);
Serial.println("✓ Serial WebSocket 시작 (포트 82)");
// 웹 서버 라우팅
server.on("/", HTTP_GET, []() {
server.send_P(200, "text/html", index_html);
@@ -1768,6 +2204,10 @@ void setup() {
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");
@@ -1838,11 +2278,14 @@ void setup() {
// Queue 생성
canQueue = xQueueCreate(CAN_QUEUE_SIZE, sizeof(CANMessage));
serialTxQueue = xQueueCreate(SERIAL_TX_QUEUE_SIZE, 256);
serialLogQueue = xQueueCreate(200, sizeof(SerialLogData));
if (canQueue == NULL) {
if (canQueue == NULL || serialTxQueue == NULL || serialLogQueue == NULL) {
Serial.println("✗ Queue 생성 실패!");
while (1) delay(1000);
}
Serial.println("✓ Serial Queue 생성 완료");
// CAN 인터럽트 활성화
attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING);
@@ -1855,6 +2298,13 @@ void setup() {
xTaskCreatePinnedToCore(txTask, "TX_TASK", 4096, NULL, 2, NULL, 1);
xTaskCreatePinnedToCore(sequenceTask, "SEQ_TASK", 4096, NULL, 2, NULL, 1); // 시퀀스 Task 추가
// Serial Task 생성
xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", 4096, NULL, 4, &serialRxTaskHandle, 1);
xTaskCreatePinnedToCore(serialTxTask, "SERIAL_TX", 4096, NULL, 3, &serialTxTaskHandle, 1);
xTaskCreatePinnedToCore(serialLogTask, "SERIAL_LOG", 8192, NULL, 2, &serialLogTaskHandle, 1);
xTaskCreatePinnedToCore(serialWebUpdateTask, "SERIAL_WEB", 4096, NULL, 2, &serialWebTaskHandle, 0);
Serial.println("✓ Serial Task 시작 완료");
// RTC 동기화 Task
if (timeSyncStatus.rtcAvailable) {
xTaskCreatePinnedToCore(rtcSyncTask, "RTC_SYNC", 4096, NULL, 0, &rtcTaskHandle, 0);
@@ -1879,6 +2329,7 @@ void setup() {
Serial.println(" - Transmit : /transmit");
Serial.println(" - Graph : /graph");
Serial.println(" - Settings : /settings");
Serial.println(" - Serial : /serial");
Serial.println("========================================\n");
}
@@ -1888,18 +2339,15 @@ void loop() {
static uint32_t lastPrint = 0;
if (millis() - lastPrint > 10000) {
Serial.printf("[상태] 큐: %d/%d | 로깅: %s | SD: %s | RX: %lu | TX: %lu | 파일크기: %u | 시간: %s | RTC: %s(%u) | 전압: %.2fV%s | 모드: %d\n",
Serial.printf("[상태] CAN큐: %d/%d | CAN: %s | Serial: RX=%u TX=%u LOG=%s | SD: %s | 시간: %s | 전압: %.2fV%s\n",
uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE,
loggingEnabled ? "ON " : "OFF",
serialStatus.rxCount, serialStatus.txCount,
serialStatus.logging ? "ON" : "OFF",
sdCardReady ? "OK" : "NO",
totalMsgCount, totalTxCount,
currentFileSize,
timeSyncStatus.synchronized ? "OK" : "NO",
timeSyncStatus.rtcAvailable ? "OK" : "NO",
timeSyncStatus.rtcSyncCount,
powerStatus.voltage,
powerStatus.lowVoltage ? " ⚠️" : "",
currentMcpMode);
powerStatus.lowVoltage ? " ⚠️" : "");
lastPrint = millis();
}
}