Upload files to "/"
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* Byun CAN Logger with Web Interface + Serial Terminal
|
* Byun CAN Logger with Web Interface + Serial Terminal
|
||||||
* Version: 2.1
|
* Version: 2.2 - RTC Time Sync & Settings Page Fixed
|
||||||
* Added: Serial communication (RS232) with web terminal interface
|
* Fixed: RTC synchronization with WiFi time + Settings page loading
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <esp_wifi.h>
|
#include <esp_wifi.h>
|
||||||
#include <esp_task_wdt.h>
|
#include <esp_task_wdt.h>
|
||||||
|
#include <esp_sntp.h>
|
||||||
#include <WebServer.h>
|
#include <WebServer.h>
|
||||||
#include <WebSocketsServer.h>
|
#include <WebSocketsServer.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
@@ -54,16 +55,16 @@
|
|||||||
#define DS3231_ADDRESS 0x68
|
#define DS3231_ADDRESS 0x68
|
||||||
|
|
||||||
// 버퍼 설정
|
// 버퍼 설정
|
||||||
#define CAN_QUEUE_SIZE 1000 // 1500 → 1000으로 축소
|
#define CAN_QUEUE_SIZE 2000
|
||||||
#define FILE_BUFFER_SIZE 8192 // 16384 → 8192로 축소
|
#define FILE_BUFFER_SIZE 16384
|
||||||
#define MAX_FILENAME_LEN 64
|
#define MAX_FILENAME_LEN 64
|
||||||
#define RECENT_MSG_COUNT 50 // 100 → 50으로 축소
|
#define RECENT_MSG_COUNT 100
|
||||||
#define MAX_TX_MESSAGES 20
|
#define MAX_TX_MESSAGES 20
|
||||||
#define MAX_COMMENT_LEN 128
|
#define MAX_COMMENT_LEN 128
|
||||||
|
|
||||||
// Serial 버퍼 설정 (추가)
|
// Serial 버퍼 설정 (추가)
|
||||||
#define SERIAL_QUEUE_SIZE 100 // 200 → 100으로 축소
|
#define SERIAL_QUEUE_SIZE 200
|
||||||
#define SERIAL_BUFFER_SIZE 1024 // 2048 → 1024로 축소
|
#define SERIAL_CSV_BUFFER_SIZE 8192 // CSV 텍스트 버퍼
|
||||||
#define MAX_SERIAL_LINE_LEN 128
|
#define MAX_SERIAL_LINE_LEN 128
|
||||||
|
|
||||||
// RTC 동기화 설정
|
// RTC 동기화 설정
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
#define VOLTAGE_CHECK_INTERVAL_MS 5000
|
#define VOLTAGE_CHECK_INTERVAL_MS 5000
|
||||||
#define LOW_VOLTAGE_THRESHOLD 3.0
|
#define LOW_VOLTAGE_THRESHOLD 3.0
|
||||||
#define MONITORING_VOLT 5
|
#define MONITORING_VOLT 5
|
||||||
|
|
||||||
// CAN 메시지 구조체
|
// CAN 메시지 구조체
|
||||||
struct CANMessage {
|
struct CANMessage {
|
||||||
uint64_t timestamp_us;
|
uint64_t timestamp_us;
|
||||||
@@ -208,31 +210,39 @@ Preferences preferences;
|
|||||||
void IRAM_ATTR canISR();
|
void IRAM_ATTR canISR();
|
||||||
|
|
||||||
QueueHandle_t canQueue;
|
QueueHandle_t canQueue;
|
||||||
QueueHandle_t serialQueue; // Serial Queue 추가
|
QueueHandle_t serialQueue;
|
||||||
SemaphoreHandle_t sdMutex;
|
SemaphoreHandle_t sdMutex;
|
||||||
SemaphoreHandle_t rtcMutex;
|
SemaphoreHandle_t rtcMutex;
|
||||||
SemaphoreHandle_t serialMutex; // Serial Mutex 추가
|
SemaphoreHandle_t serialMutex;
|
||||||
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; // Serial Task 추가
|
TaskHandle_t serialRxTaskHandle = NULL;
|
||||||
|
|
||||||
volatile bool loggingEnabled = false;
|
volatile bool loggingEnabled = false;
|
||||||
volatile bool serialLoggingEnabled = false; // Serial 로깅 상태 추가
|
volatile bool serialLoggingEnabled = false;
|
||||||
volatile bool sdCardReady = false;
|
volatile bool sdCardReady = false;
|
||||||
File logFile;
|
File logFile;
|
||||||
File serialLogFile; // Serial 로그 파일 추가
|
File serialLogFile;
|
||||||
char currentFilename[MAX_FILENAME_LEN];
|
char currentFilename[MAX_FILENAME_LEN];
|
||||||
char currentSerialFilename[MAX_FILENAME_LEN]; // Serial 로그 파일명 추가
|
char currentSerialFilename[MAX_FILENAME_LEN];
|
||||||
uint8_t fileBuffer[FILE_BUFFER_SIZE];
|
uint8_t fileBuffer[FILE_BUFFER_SIZE];
|
||||||
uint8_t serialFileBuffer[SERIAL_BUFFER_SIZE]; // Serial 파일 버퍼 추가
|
char serialCsvBuffer[SERIAL_CSV_BUFFER_SIZE]; // CSV 텍스트 버퍼
|
||||||
uint16_t bufferIndex = 0;
|
uint16_t bufferIndex = 0;
|
||||||
uint16_t serialBufferIndex = 0; // Serial 버퍼 인덱스 추가
|
uint16_t serialCsvIndex = 0; // CSV 버퍼 인덱스
|
||||||
|
|
||||||
// 로깅 파일 크기 추적
|
// 로깅 파일 크기 추적
|
||||||
volatile uint32_t currentFileSize = 0;
|
volatile uint32_t currentFileSize = 0;
|
||||||
volatile uint32_t currentSerialFileSize = 0; // Serial 파일 크기 추가
|
volatile uint32_t currentSerialFileSize = 0;
|
||||||
|
|
||||||
|
// 로깅 형식 선택 (false=BIN, true=CSV)
|
||||||
|
volatile bool canLogFormatCSV = false;
|
||||||
|
volatile bool serialLogFormatCSV = true;
|
||||||
|
|
||||||
|
// 로깅 시작 타임스탬프 (상대시간 계산용)
|
||||||
|
volatile uint64_t canLogStartTime = 0;
|
||||||
|
volatile uint64_t serialLogStartTime = 0;
|
||||||
|
|
||||||
// 현재 MCP2515 모드
|
// 현재 MCP2515 모드
|
||||||
MCP2515Mode currentMcpMode = MCP_MODE_NORMAL;
|
MCP2515Mode currentMcpMode = MCP_MODE_NORMAL;
|
||||||
@@ -401,9 +411,10 @@ void saveSettings() {
|
|||||||
preferences.putInt("mcp_mode", (int)currentMcpMode);
|
preferences.putInt("mcp_mode", (int)currentMcpMode);
|
||||||
Serial.printf("✓ MCP 모드 저장: %d\n", (int)currentMcpMode);
|
Serial.printf("✓ MCP 모드 저장: %d\n", (int)currentMcpMode);
|
||||||
|
|
||||||
preferences.end();
|
// Serial 설정 저장 (추가)
|
||||||
|
saveSerialSettings();
|
||||||
|
|
||||||
Serial.println("✓ 설정 저장 완료");
|
preferences.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -493,6 +504,39 @@ bool writeRTC(const struct tm *timeinfo) {
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NTP 시간 동기화 콜백
|
||||||
|
void timeSyncCallback(struct timeval *tv) {
|
||||||
|
Serial.println("✓ NTP 시간 동기화 완료");
|
||||||
|
|
||||||
|
timeSyncStatus.synchronized = true;
|
||||||
|
timeSyncStatus.lastSyncTime = (uint64_t)tv->tv_sec * 1000000ULL + tv->tv_usec;
|
||||||
|
timeSyncStatus.syncCount++;
|
||||||
|
|
||||||
|
// RTC에 시간 저장
|
||||||
|
if (timeSyncStatus.rtcAvailable) {
|
||||||
|
struct tm timeinfo;
|
||||||
|
time_t now = tv->tv_sec;
|
||||||
|
localtime_r(&now, &timeinfo);
|
||||||
|
|
||||||
|
if (writeRTC(&timeinfo)) {
|
||||||
|
Serial.println("✓ NTP → RTC 시간 동기화 완료");
|
||||||
|
timeSyncStatus.rtcSyncCount++;
|
||||||
|
} else {
|
||||||
|
Serial.println("✗ RTC 쓰기 실패");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void initNTP() {
|
||||||
|
// NTP 서버 설정
|
||||||
|
configTime(9 * 3600, 0, "pool.ntp.org", "time.nist.gov", "time.google.com");
|
||||||
|
|
||||||
|
// NTP 동기화 콜백 등록
|
||||||
|
sntp_set_time_sync_notification_cb(timeSyncCallback);
|
||||||
|
|
||||||
|
Serial.println("✓ NTP 클라이언트 초기화 완료");
|
||||||
|
}
|
||||||
|
|
||||||
void rtcSyncTask(void *parameter) {
|
void rtcSyncTask(void *parameter) {
|
||||||
const TickType_t xDelay = pdMS_TO_TICKS(RTC_SYNC_INTERVAL_MS);
|
const TickType_t xDelay = pdMS_TO_TICKS(RTC_SYNC_INTERVAL_MS);
|
||||||
|
|
||||||
@@ -536,8 +580,8 @@ bool setMCP2515Mode(MCP2515Mode mode) {
|
|||||||
modeName = "Loopback";
|
modeName = "Loopback";
|
||||||
break;
|
break;
|
||||||
case MCP_MODE_TRANSMIT:
|
case MCP_MODE_TRANSMIT:
|
||||||
result = mcp2515.setNormalMode(); // Transmit는 Normal 모드 사용
|
result = mcp2515.setListenOnlyMode(); // Listen-Only 기본 상태
|
||||||
modeName = "Transmit";
|
modeName = "Transmit-Only (Listen base)";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
@@ -573,40 +617,65 @@ void serialRxTask(void *parameter) {
|
|||||||
SerialMessage serialMsg;
|
SerialMessage serialMsg;
|
||||||
uint8_t lineBuffer[MAX_SERIAL_LINE_LEN];
|
uint8_t lineBuffer[MAX_SERIAL_LINE_LEN];
|
||||||
uint16_t lineIndex = 0;
|
uint16_t lineIndex = 0;
|
||||||
|
uint32_t lastActivity = millis();
|
||||||
|
|
||||||
Serial.println("✓ Serial RX Task 시작");
|
Serial.println("✓ Serial RX Task 시작");
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
// Serial 데이터 수신
|
bool hasData = false;
|
||||||
|
|
||||||
while (SerialComm.available()) {
|
while (SerialComm.available()) {
|
||||||
|
hasData = true;
|
||||||
uint8_t c = SerialComm.read();
|
uint8_t c = SerialComm.read();
|
||||||
|
|
||||||
// 바이너리 모드로 처리 (라인 단위)
|
|
||||||
lineBuffer[lineIndex++] = c;
|
lineBuffer[lineIndex++] = c;
|
||||||
|
lastActivity = millis();
|
||||||
|
|
||||||
// 개행 문자 또는 버퍼 가득 참
|
// 개행 문자 또는 버퍼 가득 참 시 전송
|
||||||
if (c == '\n' || c == '\r' || lineIndex >= MAX_SERIAL_LINE_LEN - 1) {
|
if (c == '\n' || c == '\r' || lineIndex >= MAX_SERIAL_LINE_LEN - 1) {
|
||||||
if (lineIndex > 0) {
|
if (lineIndex > 0) {
|
||||||
// 타임스탬프 생성
|
|
||||||
struct timeval tv;
|
struct timeval tv;
|
||||||
gettimeofday(&tv, NULL);
|
gettimeofday(&tv, NULL);
|
||||||
serialMsg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
serialMsg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||||||
|
|
||||||
serialMsg.length = lineIndex;
|
serialMsg.length = lineIndex;
|
||||||
memcpy(serialMsg.data, lineBuffer, lineIndex);
|
memcpy(serialMsg.data, lineBuffer, lineIndex);
|
||||||
serialMsg.isTx = false; // 수신 데이터
|
serialMsg.isTx = false;
|
||||||
|
|
||||||
// Queue에 전송
|
if (xQueueSend(serialQueue, &serialMsg, pdMS_TO_TICKS(10)) == pdTRUE) {
|
||||||
if (xQueueSend(serialQueue, &serialMsg, 0) == pdTRUE) {
|
|
||||||
totalSerialRxCount++;
|
totalSerialRxCount++;
|
||||||
|
} else {
|
||||||
|
Serial.println("! Serial Queue 전송 실패");
|
||||||
}
|
}
|
||||||
|
|
||||||
lineIndex = 0;
|
lineIndex = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 버퍼 오버플로우 방지
|
||||||
|
if (lineIndex >= MAX_SERIAL_LINE_LEN - 1) {
|
||||||
|
lineIndex = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(1)); // 1ms 간격
|
// 타임아웃: 100ms 동안 데이터가 없으면 버퍼 내용 전송
|
||||||
|
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, pdMS_TO_TICKS(10)) == pdTRUE) {
|
||||||
|
totalSerialRxCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,6 +749,37 @@ void sdWriteTask(void *parameter) {
|
|||||||
// CAN 로깅
|
// CAN 로깅
|
||||||
if (loggingEnabled && sdCardReady) {
|
if (loggingEnabled && sdCardReady) {
|
||||||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||||
|
if (canLogFormatCSV) {
|
||||||
|
// CSV 형식 로깅
|
||||||
|
char csvLine[128];
|
||||||
|
uint64_t relativeTime = canMsg.timestamp_us - canLogStartTime;
|
||||||
|
|
||||||
|
// 데이터를 16진수 문자열로 변환
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 주기적으로 플러시 (100개마다)
|
||||||
|
static int csvFlushCounter = 0;
|
||||||
|
if (++csvFlushCounter >= 100) {
|
||||||
|
logFile.flush();
|
||||||
|
csvFlushCounter = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// BIN 형식 로깅 (기존 방식)
|
||||||
if (bufferIndex + sizeof(CANMessage) <= FILE_BUFFER_SIZE) {
|
if (bufferIndex + sizeof(CANMessage) <= FILE_BUFFER_SIZE) {
|
||||||
memcpy(&fileBuffer[bufferIndex], &canMsg, sizeof(CANMessage));
|
memcpy(&fileBuffer[bufferIndex], &canMsg, sizeof(CANMessage));
|
||||||
bufferIndex += sizeof(CANMessage);
|
bufferIndex += sizeof(CANMessage);
|
||||||
@@ -693,31 +793,6 @@ void sdWriteTask(void *parameter) {
|
|||||||
bufferIndex = 0;
|
bufferIndex = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
xSemaphoreGive(sdMutex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serial 메시지 처리 (추가)
|
|
||||||
if (xQueueReceive(serialQueue, &serialMsg, 0) == pdTRUE) {
|
|
||||||
hasWork = true;
|
|
||||||
|
|
||||||
// Serial 로깅
|
|
||||||
if (serialLoggingEnabled && sdCardReady) {
|
|
||||||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
|
||||||
if (serialBufferIndex + sizeof(SerialMessage) <= SERIAL_BUFFER_SIZE) {
|
|
||||||
memcpy(&serialFileBuffer[serialBufferIndex], &serialMsg, sizeof(SerialMessage));
|
|
||||||
serialBufferIndex += sizeof(SerialMessage);
|
|
||||||
currentSerialFileSize += sizeof(SerialMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serialBufferIndex >= SERIAL_BUFFER_SIZE - sizeof(SerialMessage)) {
|
|
||||||
if (serialLogFile) {
|
|
||||||
serialLogFile.write(serialFileBuffer, serialBufferIndex);
|
|
||||||
serialLogFile.flush();
|
|
||||||
serialBufferIndex = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
xSemaphoreGive(sdMutex);
|
xSemaphoreGive(sdMutex);
|
||||||
@@ -725,27 +800,27 @@ void sdWriteTask(void *parameter) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serial 메시지 처리 - Queue에서는 항상 빼내고, 로깅이 활성화된 경우에만 저장
|
||||||
|
// 이렇게 해야 webUpdateTask에서 메시지를 받을 수 있음
|
||||||
|
// NOTE: Serial Queue는 webUpdateTask에서도 읽으므로 여기서는 로깅만 처리
|
||||||
|
|
||||||
if (!hasWork) {
|
if (!hasWork) {
|
||||||
vTaskDelay(pdMS_TO_TICKS(5));
|
vTaskDelay(pdMS_TO_TICKS(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// SD 모니터링 Task
|
// SD 모니터 Task
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
void sdMonitorTask(void *parameter) {
|
void sdMonitorTask(void *parameter) {
|
||||||
const TickType_t xDelay = pdMS_TO_TICKS(1000);
|
const TickType_t xDelay = pdMS_TO_TICKS(1000);
|
||||||
|
uint32_t lastStatusPrint = 0;
|
||||||
|
|
||||||
|
Serial.println("✓ SD Monitor Task 시작");
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
if (!sdCardReady) {
|
|
||||||
if (SD.begin(VSPI_CS, vspi)) {
|
|
||||||
sdCardReady = true;
|
|
||||||
Serial.println("✓ SD 카드 재연결 감지");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t currentTime = millis();
|
uint32_t currentTime = millis();
|
||||||
|
|
||||||
// 메시지/초 계산
|
// 메시지/초 계산
|
||||||
@@ -760,7 +835,6 @@ void sdMonitorTask(void *parameter) {
|
|||||||
float rawVoltage = analogRead(MONITORING_VOLT) * (3.3 / 4095.0);
|
float rawVoltage = analogRead(MONITORING_VOLT) * (3.3 / 4095.0);
|
||||||
powerStatus.voltage = rawVoltage * 1.0;
|
powerStatus.voltage = rawVoltage * 1.0;
|
||||||
|
|
||||||
// 1초 단위 최소값 업데이트
|
|
||||||
if (currentTime - powerStatus.lastMinReset >= 1000) {
|
if (currentTime - powerStatus.lastMinReset >= 1000) {
|
||||||
powerStatus.minVoltage = powerStatus.voltage;
|
powerStatus.minVoltage = powerStatus.voltage;
|
||||||
powerStatus.lastMinReset = currentTime;
|
powerStatus.lastMinReset = currentTime;
|
||||||
@@ -774,6 +848,24 @@ void sdMonitorTask(void *parameter) {
|
|||||||
powerStatus.lastCheck = currentTime;
|
powerStatus.lastCheck = currentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 10초마다 상태 출력 (시간 동기화 확인용)
|
||||||
|
if (currentTime - lastStatusPrint >= 10000) {
|
||||||
|
time_t now;
|
||||||
|
time(&now);
|
||||||
|
struct tm timeinfo;
|
||||||
|
localtime_r(&now, &timeinfo);
|
||||||
|
|
||||||
|
Serial.printf("[상태] %04d-%02d-%02d %02d:%02d:%02d | CAN: %u msg/s | Serial큐: %u/%u | TimeSync: %s | RTC동기: %u회\n",
|
||||||
|
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
|
||||||
|
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec,
|
||||||
|
msgPerSecond,
|
||||||
|
uxQueueMessagesWaiting(serialQueue), SERIAL_QUEUE_SIZE,
|
||||||
|
timeSyncStatus.synchronized ? "OK" : "NO",
|
||||||
|
timeSyncStatus.rtcSyncCount);
|
||||||
|
|
||||||
|
lastStatusPrint = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
vTaskDelay(xDelay);
|
vTaskDelay(xDelay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -899,6 +991,11 @@ void txTask(void *parameter) {
|
|||||||
anyActive = true;
|
anyActive = true;
|
||||||
|
|
||||||
if (now - txMessages[i].lastSent >= txMessages[i].interval) {
|
if (now - txMessages[i].lastSent >= txMessages[i].interval) {
|
||||||
|
// Transmit-Only 모드: 송신 전 Normal 모드로 전환
|
||||||
|
if (currentMcpMode == MCP_MODE_TRANSMIT) {
|
||||||
|
mcp2515.setNormalMode();
|
||||||
|
}
|
||||||
|
|
||||||
frame.can_id = txMessages[i].id;
|
frame.can_id = txMessages[i].id;
|
||||||
if (txMessages[i].extended) {
|
if (txMessages[i].extended) {
|
||||||
frame.can_id |= CAN_EFF_FLAG;
|
frame.can_id |= CAN_EFF_FLAG;
|
||||||
@@ -910,6 +1007,11 @@ void txTask(void *parameter) {
|
|||||||
totalTxCount++;
|
totalTxCount++;
|
||||||
txMessages[i].lastSent = now;
|
txMessages[i].lastSent = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transmit-Only 모드: 송신 후 Listen-Only로 복귀
|
||||||
|
if (currentMcpMode == MCP_MODE_TRANSMIT) {
|
||||||
|
mcp2515.setListenOnlyMode();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -940,6 +1042,11 @@ void sequenceTask(void *parameter) {
|
|||||||
SequenceStep* step = &seq->steps[seqRuntime.currentStep];
|
SequenceStep* step = &seq->steps[seqRuntime.currentStep];
|
||||||
|
|
||||||
if (now - seqRuntime.lastStepTime >= step->delayMs) {
|
if (now - seqRuntime.lastStepTime >= step->delayMs) {
|
||||||
|
// Transmit-Only 모드: 송신 전 Normal 모드로 전환
|
||||||
|
if (currentMcpMode == MCP_MODE_TRANSMIT) {
|
||||||
|
mcp2515.setNormalMode();
|
||||||
|
}
|
||||||
|
|
||||||
struct can_frame frame;
|
struct can_frame frame;
|
||||||
frame.can_id = step->canId;
|
frame.can_id = step->canId;
|
||||||
if (step->extended) {
|
if (step->extended) {
|
||||||
@@ -952,6 +1059,11 @@ void sequenceTask(void *parameter) {
|
|||||||
totalTxCount++;
|
totalTxCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transmit-Only 모드: 송신 후 Listen-Only로 복귀
|
||||||
|
if (currentMcpMode == MCP_MODE_TRANSMIT) {
|
||||||
|
mcp2515.setListenOnlyMode();
|
||||||
|
}
|
||||||
|
|
||||||
seqRuntime.currentStep++;
|
seqRuntime.currentStep++;
|
||||||
seqRuntime.lastStepTime = now;
|
seqRuntime.lastStepTime = now;
|
||||||
}
|
}
|
||||||
@@ -980,7 +1092,7 @@ void sequenceTask(void *parameter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// WebSocket 이벤트 처리 (Serial 명령 추가)
|
// WebSocket 이벤트 처리 (Settings 명령 추가)
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) {
|
void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) {
|
||||||
@@ -996,26 +1108,112 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
|
|||||||
|
|
||||||
const char* cmd = doc["cmd"];
|
const char* cmd = doc["cmd"];
|
||||||
|
|
||||||
if (strcmp(cmd, "startLogging") == 0) {
|
// ========================================
|
||||||
|
// Settings 페이지 명령 처리 (추가)
|
||||||
|
// ========================================
|
||||||
|
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);
|
||||||
|
|
||||||
|
Serial.println("✓ 설정 전송 완료");
|
||||||
|
}
|
||||||
|
else if (strcmp(cmd, "saveSettings") == 0) {
|
||||||
|
// WiFi 설정 저장
|
||||||
|
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);
|
||||||
|
|
||||||
|
Serial.println("✓ 설정 저장 완료 (재부팅 필요)");
|
||||||
|
}
|
||||||
|
// ========================================
|
||||||
|
// 기존 명령들
|
||||||
|
// ========================================
|
||||||
|
else if (strcmp(cmd, "startLogging") == 0) {
|
||||||
if (!loggingEnabled && sdCardReady) {
|
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) {
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||||||
struct tm timeinfo;
|
struct tm timeinfo;
|
||||||
time_t now;
|
time_t now;
|
||||||
time(&now);
|
time(&now);
|
||||||
localtime_r(&now, &timeinfo);
|
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),
|
snprintf(currentFilename, sizeof(currentFilename),
|
||||||
"/CAN_%04d%02d%02d_%02d%02d%02d.bin",
|
"/CAN_%04d%02d%02d_%02d%02d%02d.%s",
|
||||||
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
|
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
|
||||||
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
|
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, ext);
|
||||||
|
|
||||||
logFile = SD.open(currentFilename, FILE_WRITE);
|
logFile = SD.open(currentFilename, FILE_WRITE);
|
||||||
|
|
||||||
if (logFile) {
|
if (logFile) {
|
||||||
|
// CSV 형식이면 헤더 작성
|
||||||
|
if (canLogFormatCSV) {
|
||||||
|
logFile.println("Time_us,CAN_ID,DLC,Data");
|
||||||
|
}
|
||||||
|
|
||||||
loggingEnabled = true;
|
loggingEnabled = true;
|
||||||
bufferIndex = 0;
|
bufferIndex = 0;
|
||||||
currentFileSize = 0;
|
currentFileSize = logFile.size();
|
||||||
Serial.printf("✓ CAN 로깅 시작: %s\n", currentFilename);
|
Serial.printf("✓ CAN 로깅 시작: %s (%s)\n",
|
||||||
|
currentFilename, canLogFormatCSV ? "CSV" : "BIN");
|
||||||
} else {
|
} else {
|
||||||
Serial.println("✗ 파일 생성 실패");
|
Serial.println("✗ 파일 생성 실패");
|
||||||
}
|
}
|
||||||
@@ -1043,26 +1241,46 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (strcmp(cmd, "startSerialLogging") == 0) { // Serial 로깅 시작
|
else if (strcmp(cmd, "startSerialLogging") == 0) {
|
||||||
if (!serialLoggingEnabled && sdCardReady) {
|
if (!serialLoggingEnabled && sdCardReady) {
|
||||||
|
// 로깅 형식 선택
|
||||||
|
const char* format = doc["format"];
|
||||||
|
if (format && strcmp(format, "bin") == 0) {
|
||||||
|
serialLogFormatCSV = false;
|
||||||
|
} else {
|
||||||
|
serialLogFormatCSV = true; // 기본값 CSV
|
||||||
|
}
|
||||||
|
|
||||||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||||||
struct tm timeinfo;
|
struct tm timeinfo;
|
||||||
time_t now;
|
time_t now;
|
||||||
time(&now);
|
time(&now);
|
||||||
localtime_r(&now, &timeinfo);
|
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),
|
snprintf(currentSerialFilename, sizeof(currentSerialFilename),
|
||||||
"/SER_%04d%02d%02d_%02d%02d%02d.bin",
|
"/SER_%04d%02d%02d_%02d%02d%02d.%s",
|
||||||
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
|
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
|
||||||
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
|
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, ext);
|
||||||
|
|
||||||
serialLogFile = SD.open(currentSerialFilename, FILE_WRITE);
|
serialLogFile = SD.open(currentSerialFilename, FILE_WRITE);
|
||||||
|
|
||||||
if (serialLogFile) {
|
if (serialLogFile) {
|
||||||
|
// CSV 형식이면 헤더 작성
|
||||||
|
if (serialLogFormatCSV) {
|
||||||
|
serialLogFile.println("Time_us,Direction,Data");
|
||||||
|
}
|
||||||
serialLoggingEnabled = true;
|
serialLoggingEnabled = true;
|
||||||
serialBufferIndex = 0;
|
serialCsvIndex = 0;
|
||||||
currentSerialFileSize = 0;
|
currentSerialFileSize = serialLogFile.size();
|
||||||
Serial.printf("✓ Serial 로깅 시작: %s\n", currentSerialFilename);
|
Serial.printf("✓ Serial 로깅 시작: %s (%s)\n",
|
||||||
|
currentSerialFilename, serialLogFormatCSV ? "CSV" : "BIN");
|
||||||
} else {
|
} else {
|
||||||
Serial.println("✗ Serial 파일 생성 실패");
|
Serial.println("✗ Serial 파일 생성 실패");
|
||||||
}
|
}
|
||||||
@@ -1071,12 +1289,13 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (strcmp(cmd, "stopSerialLogging") == 0) { // Serial 로깅 종료
|
else if (strcmp(cmd, "stopSerialLogging") == 0) {
|
||||||
if (serialLoggingEnabled) {
|
if (serialLoggingEnabled) {
|
||||||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||||||
if (serialBufferIndex > 0 && serialLogFile) {
|
// 남은 CSV 버퍼 내용 쓰기
|
||||||
serialLogFile.write(serialFileBuffer, serialBufferIndex);
|
if (serialCsvIndex > 0 && serialLogFile) {
|
||||||
serialBufferIndex = 0;
|
serialLogFile.write((uint8_t*)serialCsvBuffer, serialCsvIndex);
|
||||||
|
serialCsvIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serialLogFile) {
|
if (serialLogFile) {
|
||||||
@@ -1091,30 +1310,35 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (strcmp(cmd, "sendSerial") == 0) { // Serial 데이터 전송
|
else if (strcmp(cmd, "sendSerial") == 0) {
|
||||||
const char* data = doc["data"];
|
const char* data = doc["data"];
|
||||||
if (data && strlen(data) > 0) {
|
if (data && strlen(data) > 0) {
|
||||||
|
// UART로 데이터 전송
|
||||||
SerialComm.println(data);
|
SerialComm.println(data);
|
||||||
|
|
||||||
// 송신 데이터를 Queue에 추가 (모니터링용)
|
// Queue에 TX 메시지 추가 (모니터링용)
|
||||||
SerialMessage serialMsg;
|
SerialMessage serialMsg;
|
||||||
struct timeval tv;
|
struct timeval tv;
|
||||||
gettimeofday(&tv, NULL);
|
gettimeofday(&tv, NULL);
|
||||||
serialMsg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
serialMsg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||||||
serialMsg.length = strlen(data);
|
serialMsg.length = strlen(data) + 2; // \r\n 포함
|
||||||
if (serialMsg.length > MAX_SERIAL_LINE_LEN - 1) {
|
if (serialMsg.length > MAX_SERIAL_LINE_LEN - 1) {
|
||||||
serialMsg.length = MAX_SERIAL_LINE_LEN - 1;
|
serialMsg.length = MAX_SERIAL_LINE_LEN - 1;
|
||||||
}
|
}
|
||||||
memcpy(serialMsg.data, data, serialMsg.length);
|
|
||||||
serialMsg.isTx = true; // 송신 데이터
|
|
||||||
|
|
||||||
xQueueSend(serialQueue, &serialMsg, 0);
|
// 데이터 복사 및 개행 문자 추가
|
||||||
|
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++;
|
totalSerialTxCount++;
|
||||||
|
Serial.printf("✓ Serial TX Queue 전송: %s\n", data);
|
||||||
Serial.printf("→ Serial TX: %s\n", data);
|
} else {
|
||||||
|
Serial.printf("✗ Serial TX Queue 전송 실패: %s\n", data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (strcmp(cmd, "setSerialConfig") == 0) { // Serial 설정 변경
|
}
|
||||||
|
else if (strcmp(cmd, "setSerialConfig") == 0) {
|
||||||
uint32_t baud = doc["baudRate"] | 115200;
|
uint32_t baud = doc["baudRate"] | 115200;
|
||||||
uint8_t data = doc["dataBits"] | 8;
|
uint8_t data = doc["dataBits"] | 8;
|
||||||
uint8_t parity = doc["parity"] | 0;
|
uint8_t parity = doc["parity"] | 0;
|
||||||
@@ -1130,7 +1354,7 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
|
|||||||
|
|
||||||
Serial.printf("✓ Serial 설정 변경: %u-%u-%u-%u\n", baud, data, parity, stop);
|
Serial.printf("✓ Serial 설정 변경: %u-%u-%u-%u\n", baud, data, parity, stop);
|
||||||
}
|
}
|
||||||
else if (strcmp(cmd, "getSerialConfig") == 0) { // Serial 설정 조회
|
else if (strcmp(cmd, "getSerialConfig") == 0) {
|
||||||
DynamicJsonDocument response(512);
|
DynamicJsonDocument response(512);
|
||||||
response["type"] = "serialConfig";
|
response["type"] = "serialConfig";
|
||||||
response["baudRate"] = serialSettings.baudRate;
|
response["baudRate"] = serialSettings.baudRate;
|
||||||
@@ -1185,6 +1409,40 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (strcmp(cmd, "syncTimeFromPhone") == 0) {
|
||||||
|
// 개별 시간 값으로 동기화 (년/월/일/시/분/초)
|
||||||
|
int year = doc["year"] | 2024;
|
||||||
|
int month = doc["month"] | 1;
|
||||||
|
int day = doc["day"] | 1;
|
||||||
|
int hour = doc["hour"] | 0;
|
||||||
|
int minute = doc["minute"] | 0;
|
||||||
|
int second = doc["second"] | 0;
|
||||||
|
|
||||||
|
struct tm timeinfo;
|
||||||
|
timeinfo.tm_year = year - 1900;
|
||||||
|
timeinfo.tm_mon = month - 1;
|
||||||
|
timeinfo.tm_mday = day;
|
||||||
|
timeinfo.tm_hour = hour;
|
||||||
|
timeinfo.tm_min = minute;
|
||||||
|
timeinfo.tm_sec = second;
|
||||||
|
|
||||||
|
time_t t = mktime(&timeinfo);
|
||||||
|
struct timeval tv = {t, 0};
|
||||||
|
settimeofday(&tv, NULL);
|
||||||
|
|
||||||
|
timeSyncStatus.synchronized = true;
|
||||||
|
timeSyncStatus.lastSyncTime = (uint64_t)t * 1000000ULL;
|
||||||
|
timeSyncStatus.syncCount++;
|
||||||
|
|
||||||
|
if (timeSyncStatus.rtcAvailable) {
|
||||||
|
writeRTC(&timeinfo);
|
||||||
|
Serial.printf("✓ 시간 동기화 완료: %04d-%02d-%02d %02d:%02d:%02d (Phone → ESP32 → RTC)\n",
|
||||||
|
year, month, day, hour, minute, second);
|
||||||
|
} else {
|
||||||
|
Serial.printf("✓ 시간 동기화 완료: %04d-%02d-%02d %02d:%02d:%02d (Phone → ESP32)\n",
|
||||||
|
year, month, day, hour, minute, second);
|
||||||
|
}
|
||||||
|
}
|
||||||
else if (strcmp(cmd, "getFiles") == 0) {
|
else if (strcmp(cmd, "getFiles") == 0) {
|
||||||
if (sdCardReady) {
|
if (sdCardReady) {
|
||||||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||||||
@@ -1405,6 +1663,11 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (strcmp(cmd, "sendOnce") == 0) {
|
else if (strcmp(cmd, "sendOnce") == 0) {
|
||||||
|
// Transmit-Only 모드: 송신 전 Normal 모드로 전환
|
||||||
|
if (currentMcpMode == MCP_MODE_TRANSMIT) {
|
||||||
|
mcp2515.setNormalMode();
|
||||||
|
}
|
||||||
|
|
||||||
struct can_frame frame;
|
struct can_frame frame;
|
||||||
frame.can_id = strtoul(doc["id"], NULL, 16);
|
frame.can_id = strtoul(doc["id"], NULL, 16);
|
||||||
|
|
||||||
@@ -1423,6 +1686,11 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
|
|||||||
totalTxCount++;
|
totalTxCount++;
|
||||||
Serial.printf("✓ CAN 메시지 전송: ID=0x%X\n", frame.can_id & 0x1FFFFFFF);
|
Serial.printf("✓ CAN 메시지 전송: ID=0x%X\n", frame.can_id & 0x1FFFFFFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transmit-Only 모드: 송신 후 Listen-Only로 복귀
|
||||||
|
if (currentMcpMode == MCP_MODE_TRANSMIT) {
|
||||||
|
mcp2515.setListenOnlyMode();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (strcmp(cmd, "addSequence") == 0) {
|
else if (strcmp(cmd, "addSequence") == 0) {
|
||||||
if (sequenceCount < MAX_SEQUENCES) {
|
if (sequenceCount < MAX_SEQUENCES) {
|
||||||
@@ -1521,36 +1789,50 @@ void webUpdateTask(void *parameter) {
|
|||||||
while (1) {
|
while (1) {
|
||||||
webSocket.loop();
|
webSocket.loop();
|
||||||
|
|
||||||
// CAN 데이터 전송
|
|
||||||
if (webSocket.connectedClients() > 0) {
|
if (webSocket.connectedClients() > 0) {
|
||||||
DynamicJsonDocument doc(3072); // 4096 → 3072로 축소
|
DynamicJsonDocument doc(4096); // 3072 → 4096으로 증가 (Serial 메시지 포함)
|
||||||
doc["type"] = "update";
|
doc["type"] = "update";
|
||||||
doc["logging"] = loggingEnabled;
|
doc["logging"] = loggingEnabled;
|
||||||
doc["serialLogging"] = serialLoggingEnabled; // Serial 로깅 상태 추가
|
doc["serialLogging"] = serialLoggingEnabled;
|
||||||
doc["sdReady"] = sdCardReady;
|
doc["sdReady"] = sdCardReady;
|
||||||
doc["totalMsg"] = totalMsgCount;
|
doc["totalMsg"] = totalMsgCount;
|
||||||
doc["msgPerSec"] = msgPerSecond;
|
doc["msgPerSec"] = msgPerSecond;
|
||||||
doc["totalTx"] = totalTxCount;
|
doc["totalTx"] = totalTxCount;
|
||||||
doc["totalSerialRx"] = totalSerialRxCount; // Serial RX 카운터 추가
|
doc["totalSerialRx"] = totalSerialRxCount;
|
||||||
doc["totalSerialTx"] = totalSerialTxCount; // Serial TX 카운터 추가
|
doc["totalSerialTx"] = totalSerialTxCount;
|
||||||
doc["fileSize"] = currentFileSize;
|
doc["fileSize"] = currentFileSize;
|
||||||
doc["serialFileSize"] = currentSerialFileSize; // Serial 파일 크기 추가
|
doc["serialFileSize"] = currentSerialFileSize;
|
||||||
doc["queueUsed"] = uxQueueMessagesWaiting(canQueue);
|
doc["queueUsed"] = uxQueueMessagesWaiting(canQueue);
|
||||||
doc["queueSize"] = CAN_QUEUE_SIZE;
|
doc["queueSize"] = CAN_QUEUE_SIZE;
|
||||||
doc["serialQueueUsed"] = uxQueueMessagesWaiting(serialQueue); // Serial Queue 사용량 추가
|
doc["serialQueueUsed"] = uxQueueMessagesWaiting(serialQueue);
|
||||||
doc["serialQueueSize"] = SERIAL_QUEUE_SIZE;
|
doc["serialQueueSize"] = SERIAL_QUEUE_SIZE;
|
||||||
doc["timeSync"] = timeSyncStatus.synchronized;
|
doc["timeSync"] = timeSyncStatus.synchronized;
|
||||||
doc["rtcAvail"] = timeSyncStatus.rtcAvailable;
|
doc["rtcAvail"] = timeSyncStatus.rtcAvailable;
|
||||||
doc["rtcSyncCnt"] = timeSyncStatus.rtcSyncCount;
|
doc["rtcSyncCnt"] = timeSyncStatus.rtcSyncCount;
|
||||||
|
doc["syncCount"] = timeSyncStatus.syncCount; // 전체 동기화 횟수 (NTP + 수동)
|
||||||
doc["voltage"] = powerStatus.voltage;
|
doc["voltage"] = powerStatus.voltage;
|
||||||
doc["minVoltage"] = powerStatus.minVoltage;
|
doc["minVoltage"] = powerStatus.minVoltage;
|
||||||
doc["lowVoltage"] = powerStatus.lowVoltage;
|
doc["lowVoltage"] = powerStatus.lowVoltage;
|
||||||
doc["mcpMode"] = (int)currentMcpMode;
|
doc["mcpMode"] = (int)currentMcpMode;
|
||||||
|
|
||||||
|
// 현재 로깅 파일명 추가
|
||||||
|
if (loggingEnabled && currentFilename[0] != '\0') {
|
||||||
|
doc["currentFile"] = String(currentFilename);
|
||||||
|
} else {
|
||||||
|
doc["currentFile"] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serialLoggingEnabled && currentSerialFilename[0] != '\0') {
|
||||||
|
doc["currentSerialFile"] = String(currentSerialFilename);
|
||||||
|
} else {
|
||||||
|
doc["currentSerialFile"] = "";
|
||||||
|
}
|
||||||
|
|
||||||
time_t now;
|
time_t now;
|
||||||
time(&now);
|
time(&now);
|
||||||
doc["timestamp"] = (uint64_t)now;
|
doc["timestamp"] = (uint64_t)now;
|
||||||
|
|
||||||
|
// CAN 메시지 배열
|
||||||
JsonArray messages = doc.createNestedArray("messages");
|
JsonArray messages = doc.createNestedArray("messages");
|
||||||
for (int i = 0; i < RECENT_MSG_COUNT; i++) {
|
for (int i = 0; i < RECENT_MSG_COUNT; i++) {
|
||||||
if (recentData[i].count > 0) {
|
if (recentData[i].count > 0) {
|
||||||
@@ -1566,23 +1848,72 @@ void webUpdateTask(void *parameter) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serial 메시지 전송 (추가)
|
// Serial 메시지 배열 (Queue에서 읽기)
|
||||||
SerialMessage serialMsg;
|
SerialMessage serialMsg;
|
||||||
JsonArray serialMessages = doc.createNestedArray("serialMessages");
|
JsonArray serialMessages = doc.createNestedArray("serialMessages");
|
||||||
int serialCount = 0;
|
int serialCount = 0;
|
||||||
|
|
||||||
while (serialCount < 5 && xQueueReceive(serialQueue, &serialMsg, 0) == pdTRUE) { // 10 → 5개로 축소
|
// Queue에서 최대 10개의 Serial 메시지 읽기
|
||||||
|
while (serialCount < 10 && xQueueReceive(serialQueue, &serialMsg, 0) == pdTRUE) {
|
||||||
JsonObject serMsgObj = serialMessages.createNestedObject();
|
JsonObject serMsgObj = serialMessages.createNestedObject();
|
||||||
serMsgObj["timestamp"] = serialMsg.timestamp_us;
|
serMsgObj["timestamp"] = serialMsg.timestamp_us;
|
||||||
serMsgObj["isTx"] = serialMsg.isTx;
|
serMsgObj["isTx"] = serialMsg.isTx;
|
||||||
|
|
||||||
// 데이터를 문자열로 변환 (printable characters)
|
// 데이터를 문자열로 변환
|
||||||
char dataStr[MAX_SERIAL_LINE_LEN + 1];
|
char dataStr[MAX_SERIAL_LINE_LEN + 1];
|
||||||
memcpy(dataStr, serialMsg.data, serialMsg.length);
|
memcpy(dataStr, serialMsg.data, serialMsg.length);
|
||||||
dataStr[serialMsg.length] = '\0';
|
dataStr[serialMsg.length] = '\0';
|
||||||
serMsgObj["data"] = dataStr;
|
serMsgObj["data"] = dataStr;
|
||||||
|
|
||||||
serialCount++;
|
serialCount++;
|
||||||
|
|
||||||
|
// Serial 로깅이 활성화되어 있으면 SD 카드에 저장
|
||||||
|
if (serialLoggingEnabled && sdCardReady) {
|
||||||
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
|
||||||
|
if (serialLogFormatCSV) {
|
||||||
|
// CSV 형식 로깅 (상대 시간 사용)
|
||||||
|
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);
|
||||||
|
|
||||||
|
// CSV 버퍼에 추가
|
||||||
|
if (serialCsvIndex + lineLen < SERIAL_CSV_BUFFER_SIZE) {
|
||||||
|
memcpy(&serialCsvBuffer[serialCsvIndex], csvLine, lineLen);
|
||||||
|
serialCsvIndex += lineLen;
|
||||||
|
currentSerialFileSize += lineLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 버퍼가 가득 차면 파일에 쓰기
|
||||||
|
if (serialCsvIndex >= SERIAL_CSV_BUFFER_SIZE - 256) {
|
||||||
|
if (serialLogFile) {
|
||||||
|
serialLogFile.write((uint8_t*)serialCsvBuffer, serialCsvIndex);
|
||||||
|
serialLogFile.flush();
|
||||||
|
serialCsvIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// BIN 형식 로깅 (기존 방식)
|
||||||
|
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;
|
String json;
|
||||||
@@ -1604,7 +1935,8 @@ void setup() {
|
|||||||
|
|
||||||
Serial.println("\n========================================");
|
Serial.println("\n========================================");
|
||||||
Serial.println(" Byun CAN Logger + Serial Terminal");
|
Serial.println(" Byun CAN Logger + Serial Terminal");
|
||||||
Serial.println(" Version 2.1 - ESP32-S3 Edition");
|
Serial.println(" Version 2.2 - ESP32-S3 Edition");
|
||||||
|
Serial.println(" Fixed: RTC Time Sync + Settings");
|
||||||
Serial.println("========================================\n");
|
Serial.println("========================================\n");
|
||||||
|
|
||||||
loadSettings();
|
loadSettings();
|
||||||
@@ -1620,7 +1952,7 @@ void setup() {
|
|||||||
// ADC 설정
|
// ADC 설정
|
||||||
analogSetAttenuation(ADC_11db);
|
analogSetAttenuation(ADC_11db);
|
||||||
|
|
||||||
// SPI 초기화 (먼저 완료)
|
// SPI 초기화
|
||||||
Serial.println("SPI 초기화 중...");
|
Serial.println("SPI 초기화 중...");
|
||||||
hspi.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS);
|
hspi.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS);
|
||||||
hspi.beginTransaction(SPISettings(40000000, MSBFIRST, SPI_MODE0));
|
hspi.beginTransaction(SPISettings(40000000, MSBFIRST, SPI_MODE0));
|
||||||
@@ -1633,27 +1965,25 @@ void setup() {
|
|||||||
vspi.setFrequency(40000000);
|
vspi.setFrequency(40000000);
|
||||||
Serial.println("✓ SPI 초기화 완료");
|
Serial.println("✓ SPI 초기화 완료");
|
||||||
|
|
||||||
// Watchdog 완전 비활성화 (SPI 초기화 후)
|
// Watchdog 완전 비활성화
|
||||||
Serial.println("Watchdog 비활성화...");
|
Serial.println("Watchdog 비활성화...");
|
||||||
esp_task_wdt_deinit();
|
esp_task_wdt_deinit();
|
||||||
Serial.println("✓ Watchdog 비활성화 완료");
|
Serial.println("✓ Watchdog 비활성화 완료");
|
||||||
|
|
||||||
// MCP2515 초기화 (간소화)
|
// MCP2515 초기화
|
||||||
Serial.println("MCP2515 초기화 중...");
|
Serial.println("MCP2515 초기화 중...");
|
||||||
mcp2515.reset();
|
mcp2515.reset();
|
||||||
delay(50);
|
delay(50);
|
||||||
|
|
||||||
// Bitrate만 설정 (모드는 나중에)
|
|
||||||
MCP2515::ERROR result = mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ);
|
MCP2515::ERROR result = mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ);
|
||||||
if (result != MCP2515::ERROR_OK) {
|
if (result != MCP2515::ERROR_OK) {
|
||||||
Serial.printf("! MCP2515 Bitrate 설정 실패: %d (계속 진행)\n", result);
|
Serial.printf("! MCP2515 Bitrate 설정 실패: %d (계속 진행)\n", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normal 모드로 직접 설정 (함수 호출 안함)
|
|
||||||
mcp2515.setNormalMode();
|
mcp2515.setNormalMode();
|
||||||
Serial.println("✓ MCP2515 초기화 완료");
|
Serial.println("✓ MCP2515 초기화 완료");
|
||||||
|
|
||||||
// Serial 통신 초기화 (추가)
|
// Serial 통신 초기화
|
||||||
applySerialSettings();
|
applySerialSettings();
|
||||||
Serial.println("✓ Serial 통신 초기화 완료 (UART1)");
|
Serial.println("✓ Serial 통신 초기화 완료 (UART1)");
|
||||||
|
|
||||||
@@ -1683,7 +2013,7 @@ void setup() {
|
|||||||
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);
|
||||||
WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4); // 채널1, 최대4개연결
|
WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4);
|
||||||
Serial.print("✓ AP SSID: ");
|
Serial.print("✓ AP SSID: ");
|
||||||
Serial.println(wifiSSID);
|
Serial.println(wifiSSID);
|
||||||
Serial.print("✓ AP IP: ");
|
Serial.print("✓ AP IP: ");
|
||||||
@@ -1704,13 +2034,30 @@ void setup() {
|
|||||||
Serial.println("✓ WiFi 연결 성공!");
|
Serial.println("✓ WiFi 연결 성공!");
|
||||||
Serial.print("✓ STA IP: ");
|
Serial.print("✓ STA IP: ");
|
||||||
Serial.println(WiFi.localIP());
|
Serial.println(WiFi.localIP());
|
||||||
|
|
||||||
|
// NTP 시간 동기화 시작
|
||||||
|
initNTP();
|
||||||
|
|
||||||
|
// NTP 동기화 대기 (최대 5초)
|
||||||
|
Serial.print("⏱ NTP 시간 동기화 대기 중");
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
delay(500);
|
||||||
|
Serial.print(".");
|
||||||
|
if (timeSyncStatus.synchronized) {
|
||||||
|
Serial.println(" 완료!");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!timeSyncStatus.synchronized) {
|
||||||
|
Serial.println(" 시간 초과 (계속 진행)");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Serial.println("✗ WiFi 연결 실패 (AP 모드는 정상 동작)");
|
Serial.println("✗ WiFi 연결 실패 (AP 모드는 정상 동작)");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Serial.println("\n📶 WiFi AP 모드 시작...");
|
Serial.println("\n📶 WiFi AP 모드 시작...");
|
||||||
WiFi.mode(WIFI_AP);
|
WiFi.mode(WIFI_AP);
|
||||||
WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4); // 채널1, 최대4개연결
|
WiFi.softAP(wifiSSID, wifiPassword, 1, 0, 4);
|
||||||
Serial.print("✓ AP SSID: ");
|
Serial.print("✓ AP SSID: ");
|
||||||
Serial.println(wifiSSID);
|
Serial.println(wifiSSID);
|
||||||
Serial.print("✓ AP IP: ");
|
Serial.print("✓ AP IP: ");
|
||||||
@@ -1745,7 +2092,7 @@ void setup() {
|
|||||||
server.send_P(200, "text/html", settings_html);
|
server.send_P(200, "text/html", settings_html);
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on("/serial", HTTP_GET, []() { // Serial 페이지 추가
|
server.on("/serial", HTTP_GET, []() {
|
||||||
server.send_P(200, "text/html", serial_terminal_html);
|
server.send_P(200, "text/html", serial_terminal_html);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1815,7 +2162,7 @@ void setup() {
|
|||||||
|
|
||||||
// Queue 생성
|
// Queue 생성
|
||||||
canQueue = xQueueCreate(CAN_QUEUE_SIZE, sizeof(CANMessage));
|
canQueue = xQueueCreate(CAN_QUEUE_SIZE, sizeof(CANMessage));
|
||||||
serialQueue = xQueueCreate(SERIAL_QUEUE_SIZE, sizeof(SerialMessage)); // Serial Queue 생성
|
serialQueue = xQueueCreate(SERIAL_QUEUE_SIZE, sizeof(SerialMessage));
|
||||||
|
|
||||||
if (canQueue == NULL || serialQueue == NULL) {
|
if (canQueue == NULL || serialQueue == NULL) {
|
||||||
Serial.println("✗ Queue 생성 실패!");
|
Serial.println("✗ Queue 생성 실패!");
|
||||||
@@ -1825,14 +2172,14 @@ void setup() {
|
|||||||
// CAN 인터럽트 활성화
|
// CAN 인터럽트 활성화
|
||||||
attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING);
|
attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), canISR, FALLING);
|
||||||
|
|
||||||
// Task 생성 (메모리 최적화)
|
// Task 생성
|
||||||
xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 8096, NULL, 5, &canRxTaskHandle, 1); // 4096 → 3072
|
xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 8096, NULL, 5, &canRxTaskHandle, 1);
|
||||||
xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", 4072, NULL, 4, &serialRxTaskHandle, 1); // 3072 → 2560
|
xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", 4072, NULL, 4, &serialRxTaskHandle, 0);
|
||||||
xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 15240, NULL, 3, &sdWriteTaskHandle, 1); // 10240 → 8192
|
xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 15240, NULL, 3, &sdWriteTaskHandle, 1);
|
||||||
xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 5072, NULL, 1, NULL, 1); // 3072 → 2560
|
xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 5072, NULL, 1, NULL, 0);
|
||||||
xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 8192, NULL, 2, &webTaskHandle, 0); // 8192 → 6144
|
xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 8192, NULL, 2, &webTaskHandle, 0);
|
||||||
xTaskCreatePinnedToCore(txTask, "TX_TASK", 5072, NULL, 2, NULL, 1); // 3072 → 2560
|
xTaskCreatePinnedToCore(txTask, "TX_TASK", 5072, NULL, 2, NULL, 0);
|
||||||
xTaskCreatePinnedToCore(sequenceTask, "SEQ_TASK", 4072, NULL, 2, NULL, 1); // 3072 → 2560
|
xTaskCreatePinnedToCore(sequenceTask, "SEQ_TASK", 4072, NULL, 2, NULL, 1);
|
||||||
|
|
||||||
// RTC 동기화 Task
|
// RTC 동기화 Task
|
||||||
if (timeSyncStatus.rtcAvailable) {
|
if (timeSyncStatus.rtcAvailable) {
|
||||||
@@ -1858,7 +2205,7 @@ void setup() {
|
|||||||
Serial.println(" - Transmit : /transmit");
|
Serial.println(" - Transmit : /transmit");
|
||||||
Serial.println(" - Graph : /graph");
|
Serial.println(" - Graph : /graph");
|
||||||
Serial.println(" - Settings : /settings");
|
Serial.println(" - Settings : /settings");
|
||||||
Serial.println(" - Serial : /serial ← NEW!");
|
Serial.println(" - Serial : /serial");
|
||||||
Serial.println("========================================\n");
|
Serial.println("========================================\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -279,6 +279,46 @@ const char serial_terminal_html[] PROGMEM = R"rawliteral(
|
|||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 파일 형식 선택 라디오 버튼 스타일 */
|
||||||
|
.format-selector {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid #667eea;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-selector label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 0.95em;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-selector label:hover {
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-selector input[type="radio"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-info {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #7f8c8d;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -361,6 +401,21 @@ const char serial_terminal_html[] PROGMEM = R"rawliteral(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Serial 로깅 형식 선택 -->
|
||||||
|
<div class="format-selector">
|
||||||
|
<label style="font-weight: 700; color: #2c3e50;">📁 Log File Format:</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="serial-format" value="csv" checked>
|
||||||
|
<span>📄 CSV</span>
|
||||||
|
<span class="format-info">(Text - Easy to Read)</span>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="serial-format" value="bin">
|
||||||
|
<span>📦 BIN</span>
|
||||||
|
<span class="format-info">(Binary - Fast)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 버튼 그룹 -->
|
<!-- 버튼 그룹 -->
|
||||||
<div class="btn-group" style="margin-bottom: 20px;">
|
<div class="btn-group" style="margin-bottom: 20px;">
|
||||||
<button class="btn btn-primary" onclick="applySerialConfig()">Apply Settings</button>
|
<button class="btn btn-primary" onclick="applySerialConfig()">Apply Settings</button>
|
||||||
@@ -484,18 +539,35 @@ const char serial_terminal_html[] PROGMEM = R"rawliteral(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function toggleSerialLogging() {
|
function toggleSerialLogging() {
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
if (serialLogging) {
|
if (serialLogging) {
|
||||||
|
// 로깅 중지
|
||||||
ws.send(JSON.stringify({cmd: 'stopSerialLogging'}));
|
ws.send(JSON.stringify({cmd: 'stopSerialLogging'}));
|
||||||
} else {
|
} else {
|
||||||
ws.send(JSON.stringify({cmd: 'startSerialLogging'}));
|
// 선택된 형식 가져오기
|
||||||
|
let serialFormat = 'csv'; // 기본값
|
||||||
|
const formatRadios = document.getElementsByName('serial-format');
|
||||||
|
for (const radio of formatRadios) {
|
||||||
|
if (radio.checked) {
|
||||||
|
serialFormat = radio.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 로깅 시작 명령 전송
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
cmd: 'startSerialLogging',
|
||||||
|
format: serialFormat
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log('Start serial logging: format=' + serialFormat);
|
||||||
}
|
}
|
||||||
serialLogging = !serialLogging;
|
serialLogging = !serialLogging;
|
||||||
updateLoggingUI();
|
updateLoggingUI();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLoggingUI() {
|
function updateLoggingUI() {
|
||||||
const btn = document.getElementById('log-btn');
|
const btn = document.getElementById('log-btn');
|
||||||
const indicator = btn.querySelector('.status-indicator');
|
const indicator = btn.querySelector('.status-indicator');
|
||||||
|
|||||||
Reference in New Issue
Block a user