버젼 1.5 로깅데이터 모니터링, 파일삭제기능 추가
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Byun CAN Logger with Web Interface + RTC Time Synchronization + Timezone Settings
|
||||
* Version: 1.4
|
||||
* Added: Timezone configuration, WiFi settings, Power monitoring
|
||||
* Version: 1.5
|
||||
* Added: File delete function, Real-time file size monitoring
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
@@ -132,6 +132,9 @@ char currentFilename[MAX_FILENAME_LEN];
|
||||
uint8_t fileBuffer[FILE_BUFFER_SIZE];
|
||||
uint16_t bufferIndex = 0;
|
||||
|
||||
// 로깅 파일 크기 추적 (실시간 모니터링용)
|
||||
volatile uint32_t currentFileSize = 0;
|
||||
|
||||
// RTC 관련
|
||||
SoftWire rtcWire(RTC_SDA, RTC_SCL);
|
||||
char rtcSyncBuffer[20];
|
||||
@@ -247,12 +250,8 @@ float readVoltage() {
|
||||
}
|
||||
|
||||
float avgReading = sum / (float)samples;
|
||||
|
||||
// ADC 값을 전압으로 변환 (ESP32는 12-bit ADC, 0-4095)
|
||||
// 기본 참조 전압은 3.3V
|
||||
// 실제 전압 = (ADC값 / 4095) * 3.3V * 보정계수
|
||||
// 보정계수는 실제 하드웨어에 따라 조정 필요
|
||||
float voltage = (avgReading / 4095.0) * 3.3 * 1.1; // 1.1은 보정계수
|
||||
// ADC 값을 전압으로 변환 (0-4095 -> 0-3.3V)
|
||||
float voltage = (avgReading / 4095.0) * 3.3;
|
||||
|
||||
return voltage;
|
||||
}
|
||||
@@ -263,150 +262,140 @@ void updatePowerStatus() {
|
||||
if (now - powerStatus.lastCheck >= VOLTAGE_CHECK_INTERVAL_MS) {
|
||||
powerStatus.voltage = readVoltage();
|
||||
|
||||
// 1초마다 최소 전압 리셋
|
||||
// 1초 단위 최소값 추적
|
||||
if (now - powerStatus.lastMinReset >= 1000) {
|
||||
powerStatus.minVoltage = powerStatus.voltage;
|
||||
powerStatus.lastMinReset = now;
|
||||
} else {
|
||||
// 1초 내에서 최소값 추적
|
||||
if (powerStatus.voltage < powerStatus.minVoltage) {
|
||||
powerStatus.minVoltage = powerStatus.voltage;
|
||||
}
|
||||
}
|
||||
|
||||
powerStatus.lowVoltage = (powerStatus.minVoltage < LOW_VOLTAGE_THRESHOLD);
|
||||
powerStatus.lastCheck = now;
|
||||
// 저전압 경고
|
||||
powerStatus.lowVoltage = (powerStatus.voltage < LOW_VOLTAGE_THRESHOLD);
|
||||
|
||||
if (powerStatus.lowVoltage) {
|
||||
Serial.printf("⚠️ 전압 경고: 현재=%.2fV, 최소(1s)=%.2fV (임계값: %.2fV)\n",
|
||||
powerStatus.voltage, powerStatus.minVoltage, LOW_VOLTAGE_THRESHOLD);
|
||||
}
|
||||
powerStatus.lastCheck = now;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// RTC 관련 함수
|
||||
// RTC (DS3231) 함수
|
||||
// ========================================
|
||||
|
||||
// BCD to Decimal 변환
|
||||
uint8_t bcd2dec(uint8_t val) {
|
||||
void initRTC() {
|
||||
rtcWire.begin();
|
||||
rtcWire.setClock(100000);
|
||||
|
||||
rtcWire.beginTransmission(DS3231_ADDRESS);
|
||||
uint8_t error = rtcWire.endTransmission();
|
||||
|
||||
if (error == 0) {
|
||||
timeSyncStatus.rtcAvailable = true;
|
||||
Serial.println("✓ RTC DS3231 감지됨");
|
||||
|
||||
// RTC에서 시간 읽어서 시스템 시간 초기화
|
||||
struct tm timeinfo;
|
||||
if (readRTC(&timeinfo)) {
|
||||
// RTC의 로컬 시간을 UTC로 변환
|
||||
time_t localTime = mktime(&timeinfo);
|
||||
time_t utcTime = localTime - (timezoneOffset * 3600);
|
||||
|
||||
struct timeval tv = { .tv_sec = utcTime, .tv_usec = 0 };
|
||||
settimeofday(&tv, NULL);
|
||||
|
||||
timeSyncStatus.synchronized = true;
|
||||
timeSyncStatus.rtcSyncCount++;
|
||||
|
||||
char timeStr[64];
|
||||
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
|
||||
Serial.printf("✓ RTC에서 시간 로드: %s (로컬 시간, UTC%+d)\n", timeStr, timezoneOffset);
|
||||
}
|
||||
} else {
|
||||
timeSyncStatus.rtcAvailable = false;
|
||||
Serial.println("⚠️ RTC DS3231 없음 - 웹에서 시간 동기화 필요");
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t bcdToDec(uint8_t val) {
|
||||
return ((val / 16 * 10) + (val % 16));
|
||||
}
|
||||
|
||||
// Decimal to BCD 변환
|
||||
uint8_t dec2bcd(uint8_t val) {
|
||||
uint8_t decToBcd(uint8_t val) {
|
||||
return ((val / 10 * 16) + (val % 10));
|
||||
}
|
||||
|
||||
// DS3231에서 시간 읽기 (Non-blocking, SoftWire 사용)
|
||||
bool readRTC(struct tm* timeinfo) {
|
||||
if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(10)) != pdTRUE) {
|
||||
return false; // Mutex를 얻지 못하면 실패 반환
|
||||
}
|
||||
|
||||
rtcWire.beginTransmission(DS3231_ADDRESS);
|
||||
rtcWire.write(0x00); // 초 레지스터부터 시작
|
||||
|
||||
if (rtcWire.endTransmission() != 0) {
|
||||
xSemaphoreGive(rtcMutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
rtcWire.requestFrom(DS3231_ADDRESS, 7);
|
||||
|
||||
if (rtcWire.available() < 7) {
|
||||
xSemaphoreGive(rtcMutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t seconds = bcd2dec(rtcWire.read() & 0x7F);
|
||||
uint8_t minutes = bcd2dec(rtcWire.read());
|
||||
uint8_t hours = bcd2dec(rtcWire.read() & 0x3F);
|
||||
rtcWire.read(); // day of week (skip)
|
||||
uint8_t day = bcd2dec(rtcWire.read());
|
||||
uint8_t month = bcd2dec(rtcWire.read());
|
||||
uint8_t year = bcd2dec(rtcWire.read());
|
||||
|
||||
xSemaphoreGive(rtcMutex);
|
||||
|
||||
timeinfo->tm_sec = seconds;
|
||||
timeinfo->tm_min = minutes;
|
||||
timeinfo->tm_hour = hours;
|
||||
timeinfo->tm_mday = day;
|
||||
timeinfo->tm_mon = month - 1; // 0-11
|
||||
timeinfo->tm_year = year + 100; // years since 1900
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// DS3231에 시간 설정 (Non-blocking, SoftWire 사용)
|
||||
bool writeRTC(const struct tm* timeinfo) {
|
||||
if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(10)) != pdTRUE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rtcWire.beginTransmission(DS3231_ADDRESS);
|
||||
rtcWire.write(0x00); // 초 레지스터부터 시작
|
||||
rtcWire.write(dec2bcd(timeinfo->tm_sec));
|
||||
rtcWire.write(dec2bcd(timeinfo->tm_min));
|
||||
rtcWire.write(dec2bcd(timeinfo->tm_hour));
|
||||
rtcWire.write(dec2bcd(1)); // day of week (1-7)
|
||||
rtcWire.write(dec2bcd(timeinfo->tm_mday));
|
||||
rtcWire.write(dec2bcd(timeinfo->tm_mon + 1));
|
||||
rtcWire.write(dec2bcd(timeinfo->tm_year - 100));
|
||||
|
||||
bool success = (rtcWire.endTransmission() == 0);
|
||||
|
||||
xSemaphoreGive(rtcMutex);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
// RTC 초기화 확인
|
||||
bool initRTC() {
|
||||
rtcWire.begin();
|
||||
rtcWire.setTimeout(100); // 100ms timeout
|
||||
if (!timeSyncStatus.rtcAvailable) return false;
|
||||
|
||||
if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(100)) != pdTRUE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rtcWire.beginTransmission(DS3231_ADDRESS);
|
||||
bool available = (rtcWire.endTransmission() == 0);
|
||||
rtcWire.write(0x00);
|
||||
uint8_t error = rtcWire.endTransmission();
|
||||
|
||||
if (error != 0) {
|
||||
xSemaphoreGive(rtcMutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rtcWire.requestFrom(DS3231_ADDRESS, 7) != 7) {
|
||||
xSemaphoreGive(rtcMutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t second = bcdToDec(rtcWire.read() & 0x7F);
|
||||
uint8_t minute = bcdToDec(rtcWire.read());
|
||||
uint8_t hour = bcdToDec(rtcWire.read() & 0x3F);
|
||||
rtcWire.read();
|
||||
uint8_t day = bcdToDec(rtcWire.read());
|
||||
uint8_t month = bcdToDec(rtcWire.read());
|
||||
uint8_t year = bcdToDec(rtcWire.read());
|
||||
|
||||
xSemaphoreGive(rtcMutex);
|
||||
|
||||
if (available) {
|
||||
Serial.println("✓ DS3231 RTC 감지됨");
|
||||
|
||||
// RTC에서 시간 읽어서 시스템 시간 설정
|
||||
// RTC는 로컬 시간(예: 서울 시간)을 저장한다고 가정
|
||||
struct tm rtcTime;
|
||||
if (readRTC(&rtcTime)) {
|
||||
// RTC 로컬 시간을 UTC로 변환
|
||||
time_t localTime = mktime(&rtcTime);
|
||||
time_t utcTime = localTime - (timezoneOffset * 3600); // UTC로 변환
|
||||
|
||||
struct timeval tv = { .tv_sec = utcTime, .tv_usec = 0 };
|
||||
settimeofday(&tv, NULL);
|
||||
|
||||
timeSyncStatus.synchronized = true;
|
||||
timeSyncStatus.rtcAvailable = true;
|
||||
timeSyncStatus.lastSyncTime = getMicrosecondTimestamp();
|
||||
timeSyncStatus.rtcSyncCount++;
|
||||
|
||||
char timeStr[64];
|
||||
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &rtcTime);
|
||||
Serial.printf("✓ RTC 시간 동기화 완료: RTC=%s (로컬), UTC%+d 기준\n", timeStr, timezoneOffset);
|
||||
}
|
||||
} else {
|
||||
Serial.println("✗ DS3231 RTC를 찾을 수 없음");
|
||||
}
|
||||
timeinfo->tm_sec = second;
|
||||
timeinfo->tm_min = minute;
|
||||
timeinfo->tm_hour = hour;
|
||||
timeinfo->tm_mday = day;
|
||||
timeinfo->tm_mon = month - 1;
|
||||
timeinfo->tm_year = year + 100;
|
||||
timeinfo->tm_wday = 0;
|
||||
timeinfo->tm_yday = 0;
|
||||
timeinfo->tm_isdst = -1;
|
||||
|
||||
return available;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 마이크로초 단위 Unix 타임스탬프 획득
|
||||
bool writeRTC(struct tm* timeinfo) {
|
||||
if (!timeSyncStatus.rtcAvailable) return false;
|
||||
|
||||
if (xSemaphoreTake(rtcMutex, pdMS_TO_TICKS(100)) != pdTRUE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rtcWire.beginTransmission(DS3231_ADDRESS);
|
||||
rtcWire.write(0x00);
|
||||
rtcWire.write(decToBcd(timeinfo->tm_sec));
|
||||
rtcWire.write(decToBcd(timeinfo->tm_min));
|
||||
rtcWire.write(decToBcd(timeinfo->tm_hour));
|
||||
rtcWire.write(decToBcd(0));
|
||||
rtcWire.write(decToBcd(timeinfo->tm_mday));
|
||||
rtcWire.write(decToBcd(timeinfo->tm_mon + 1));
|
||||
rtcWire.write(decToBcd(timeinfo->tm_year - 100));
|
||||
|
||||
uint8_t error = rtcWire.endTransmission();
|
||||
xSemaphoreGive(rtcMutex);
|
||||
|
||||
return (error == 0);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 시간 관련 함수
|
||||
// ========================================
|
||||
|
||||
uint64_t getMicrosecondTimestamp() {
|
||||
struct timeval tv;
|
||||
gettimeofday(&tv, NULL);
|
||||
@@ -414,45 +403,40 @@ uint64_t getMicrosecondTimestamp() {
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CAN 인터럽트 핸들러
|
||||
// CAN 관련 함수
|
||||
// ========================================
|
||||
|
||||
void IRAM_ATTR canISR() {
|
||||
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
||||
if (canRxTaskHandle != NULL) {
|
||||
vTaskNotifyGiveFromISR(canRxTaskHandle, &xHigherPriorityTaskWoken);
|
||||
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
|
||||
if (xHigherPriorityTaskWoken) {
|
||||
portYIELD_FROM_ISR();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CAN 수신 Task
|
||||
// ========================================
|
||||
|
||||
void canRxTask(void* parameter) {
|
||||
Serial.println("✓ CAN RX Task 시작");
|
||||
|
||||
void canRxTask(void *parameter) {
|
||||
CANMessage msg;
|
||||
struct can_frame frame;
|
||||
CANMessage canMsg;
|
||||
|
||||
while (1) {
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
|
||||
while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {
|
||||
canMsg.timestamp_us = getMicrosecondTimestamp();
|
||||
canMsg.id = frame.can_id;
|
||||
canMsg.dlc = frame.can_dlc;
|
||||
memcpy(canMsg.data, frame.data, 8);
|
||||
|
||||
xQueueSend(canQueue, &canMsg, 0);
|
||||
msg.timestamp_us = getMicrosecondTimestamp();
|
||||
msg.id = frame.can_id;
|
||||
msg.dlc = frame.can_dlc;
|
||||
memcpy(msg.data, frame.data, 8);
|
||||
|
||||
xQueueSend(canQueue, &msg, 0);
|
||||
totalMsgCount++;
|
||||
|
||||
// 실시간 모니터링용 데이터 업데이트
|
||||
// 실시간 모니터링 업데이트
|
||||
bool found = false;
|
||||
for (int i = 0; i < RECENT_MSG_COUNT; i++) {
|
||||
if (recentData[i].msg.id == canMsg.id || recentData[i].msg.timestamp_us == 0) {
|
||||
recentData[i].msg = canMsg;
|
||||
if (recentData[i].msg.id == msg.id) {
|
||||
recentData[i].msg = msg;
|
||||
recentData[i].count++;
|
||||
found = true;
|
||||
break;
|
||||
@@ -460,123 +444,105 @@ void canRxTask(void* parameter) {
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
uint32_t oldestIdx = 0;
|
||||
uint64_t oldestTime = recentData[0].msg.timestamp_us;
|
||||
for (int i = 1; i < RECENT_MSG_COUNT; i++) {
|
||||
if (recentData[i].msg.timestamp_us < oldestTime) {
|
||||
oldestTime = recentData[i].msg.timestamp_us;
|
||||
oldestIdx = i;
|
||||
for (int i = 0; i < RECENT_MSG_COUNT; i++) {
|
||||
if (recentData[i].msg.timestamp_us == 0) {
|
||||
recentData[i].msg = msg;
|
||||
recentData[i].count = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
recentData[oldestIdx].msg = canMsg;
|
||||
recentData[oldestIdx].count = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SD 카드 쓰기 Task
|
||||
// SD 카드 관련 함수
|
||||
// ========================================
|
||||
|
||||
void flushBuffer() {
|
||||
if (bufferIndex > 0 && logFile) {
|
||||
if (bufferIndex > 0 && loggingEnabled && logFile) {
|
||||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
logFile.write(fileBuffer, bufferIndex);
|
||||
bufferIndex = 0;
|
||||
size_t written = logFile.write(fileBuffer, bufferIndex);
|
||||
if (written == bufferIndex) {
|
||||
currentFileSize += bufferIndex; // 파일 크기 업데이트
|
||||
bufferIndex = 0;
|
||||
}
|
||||
xSemaphoreGive(sdMutex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void sdWriteTask(void* parameter) {
|
||||
Serial.println("✓ SD Write Task 시작");
|
||||
|
||||
void sdWriteTask(void *parameter) {
|
||||
CANMessage msg;
|
||||
uint32_t lastFlushTime = millis();
|
||||
const uint32_t FLUSH_INTERVAL = 1000;
|
||||
|
||||
while (1) {
|
||||
if (loggingEnabled && xQueueReceive(canQueue, &msg, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
if (logFile) {
|
||||
size_t writeSize = sizeof(CANMessage);
|
||||
|
||||
if (bufferIndex + writeSize > FILE_BUFFER_SIZE) {
|
||||
if (xQueueReceive(canQueue, &msg, pdMS_TO_TICKS(10)) == pdTRUE) {
|
||||
if (loggingEnabled) {
|
||||
if (bufferIndex + sizeof(CANMessage) > FILE_BUFFER_SIZE) {
|
||||
flushBuffer();
|
||||
}
|
||||
|
||||
memcpy(&fileBuffer[bufferIndex], &msg, writeSize);
|
||||
bufferIndex += writeSize;
|
||||
memcpy(&fileBuffer[bufferIndex], &msg, sizeof(CANMessage));
|
||||
bufferIndex += sizeof(CANMessage);
|
||||
}
|
||||
} else {
|
||||
if (loggingEnabled && bufferIndex > 0) {
|
||||
flushBuffer();
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
|
||||
if (millis() - lastFlushTime >= FLUSH_INTERVAL) {
|
||||
flushBuffer();
|
||||
lastFlushTime = millis();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SD 카드 모니터링 Task
|
||||
// ========================================
|
||||
|
||||
void sdMonitorTask(void* parameter) {
|
||||
Serial.println("✓ SD Monitor Task 시작");
|
||||
void sdMonitorTask(void *parameter) {
|
||||
const uint32_t FLUSH_INTERVAL = 1000;
|
||||
uint32_t lastFlush = 0;
|
||||
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||
uint32_t now = millis();
|
||||
|
||||
if (!SD.begin(VSPI_CS, vspi)) {
|
||||
if (sdCardReady) {
|
||||
Serial.println("✗ SD 카드 연결 끊김!");
|
||||
sdCardReady = false;
|
||||
|
||||
if (loggingEnabled) {
|
||||
loggingEnabled = false;
|
||||
if (logFile) {
|
||||
flushBuffer();
|
||||
logFile.close();
|
||||
}
|
||||
}
|
||||
if (loggingEnabled && (now - lastFlush >= FLUSH_INTERVAL)) {
|
||||
if (bufferIndex > 0) {
|
||||
flushBuffer();
|
||||
}
|
||||
} else {
|
||||
if (!sdCardReady) {
|
||||
Serial.println("✓ SD 카드 재연결됨");
|
||||
sdCardReady = true;
|
||||
|
||||
// 주기적으로 sync 호출하여 SD 카드에 완전히 기록
|
||||
if (logFile && xSemaphoreTake(sdMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
||||
logFile.flush();
|
||||
xSemaphoreGive(sdMutex);
|
||||
}
|
||||
|
||||
lastFlush = now;
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// RTC 동기화 Task
|
||||
// RTC 동기화 태스크
|
||||
// ========================================
|
||||
|
||||
void rtcSyncTask(void* parameter) {
|
||||
Serial.println("✓ RTC Sync Task 시작");
|
||||
|
||||
void rtcSyncTask(void *parameter) {
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(RTC_SYNC_INTERVAL_MS));
|
||||
|
||||
if (timeSyncStatus.rtcAvailable) {
|
||||
struct tm rtcTime;
|
||||
if (readRTC(&rtcTime)) {
|
||||
// RTC 로컬 시간을 UTC로 변환
|
||||
time_t localTime = mktime(&rtcTime);
|
||||
struct tm timeinfo;
|
||||
if (readRTC(&timeinfo)) {
|
||||
// RTC의 로컬 시간을 UTC로 변환
|
||||
time_t localTime = mktime(&timeinfo);
|
||||
time_t utcTime = localTime - (timezoneOffset * 3600);
|
||||
|
||||
struct timeval tv = { .tv_sec = utcTime, .tv_usec = 0 };
|
||||
settimeofday(&tv, NULL);
|
||||
|
||||
timeSyncStatus.synchronized = true;
|
||||
timeSyncStatus.lastSyncTime = getMicrosecondTimestamp();
|
||||
timeSyncStatus.rtcSyncCount++;
|
||||
|
||||
Serial.printf("✓ RTC 자동 동기화 완료 (UTC%+d) [#%u]\n",
|
||||
timezoneOffset, timeSyncStatus.rtcSyncCount);
|
||||
char timeStr[64];
|
||||
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
|
||||
Serial.printf("🕐 RTC 자동 동기화: %s (로컬 시간)\n", timeStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -586,8 +552,15 @@ void rtcSyncTask(void* parameter) {
|
||||
// WebSocket 이벤트 핸들러
|
||||
// ========================================
|
||||
|
||||
void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) {
|
||||
if (type == WStype_TEXT) {
|
||||
void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) {
|
||||
if (type == WStype_DISCONNECTED) {
|
||||
Serial.printf("[%u] WebSocket 연결 끊김\n", num);
|
||||
}
|
||||
else if (type == WStype_CONNECTED) {
|
||||
IPAddress ip = webSocket.remoteIP(num);
|
||||
Serial.printf("[%u] WebSocket 연결됨: %s\n", num, ip.toString().c_str());
|
||||
}
|
||||
else if (type == WStype_TEXT) {
|
||||
String message = String((char*)payload);
|
||||
|
||||
// JSON 파싱
|
||||
@@ -613,6 +586,7 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length)
|
||||
if (logFile) {
|
||||
loggingEnabled = true;
|
||||
bufferIndex = 0;
|
||||
currentFileSize = 0; // 파일 크기 초기화
|
||||
Serial.printf("✓ 로깅 시작: %s (UTC%+d)\n", currentFilename, timezoneOffset);
|
||||
}
|
||||
xSemaphoreGive(sdMutex);
|
||||
@@ -626,7 +600,8 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length)
|
||||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||||
if (logFile) {
|
||||
logFile.close();
|
||||
Serial.println("✓ 로깅 중지");
|
||||
Serial.printf("✓ 로깅 중지 (최종 크기: %u bytes)\n", currentFileSize);
|
||||
currentFileSize = 0;
|
||||
}
|
||||
xSemaphoreGive(sdMutex);
|
||||
}
|
||||
@@ -698,6 +673,51 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length)
|
||||
webSocket.sendTXT(num, fileList);
|
||||
}
|
||||
}
|
||||
else if (cmd == "deleteFile") {
|
||||
// 파일 삭제 명령 처리
|
||||
int fileStart = message.indexOf("\"filename\":\"") + 12;
|
||||
int fileEnd = message.indexOf("\"", fileStart);
|
||||
String filename = message.substring(fileStart, fileEnd);
|
||||
|
||||
// 로깅 중인 파일은 삭제 불가
|
||||
if (loggingEnabled && currentFilename[0] != '\0') {
|
||||
String currentFileStr = String(currentFilename);
|
||||
if (currentFileStr.startsWith("/")) currentFileStr = currentFileStr.substring(1);
|
||||
|
||||
if (filename == currentFileStr) {
|
||||
String response = "{\"type\":\"deleteResult\",\"success\":false,\"message\":\"Cannot delete file currently being logged\"}";
|
||||
webSocket.sendTXT(num, response);
|
||||
Serial.printf("⚠️ 파일 삭제 실패: 로깅 중인 파일입니다 (%s)\n", filename.c_str());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 파일 삭제 수행
|
||||
String fullPath = "/" + filename;
|
||||
bool deleteSuccess = false;
|
||||
|
||||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||||
if (SD.exists(fullPath)) {
|
||||
deleteSuccess = SD.remove(fullPath);
|
||||
if (deleteSuccess) {
|
||||
Serial.printf("✓ 파일 삭제 완료: %s\n", filename.c_str());
|
||||
} else {
|
||||
Serial.printf("✗ 파일 삭제 실패: %s\n", filename.c_str());
|
||||
}
|
||||
} else {
|
||||
Serial.printf("✗ 파일이 존재하지 않음: %s\n", filename.c_str());
|
||||
}
|
||||
xSemaphoreGive(sdMutex);
|
||||
}
|
||||
|
||||
String response = "{\"type\":\"deleteResult\",\"success\":" + String(deleteSuccess ? "true" : "false") +
|
||||
",\"message\":\"" + (deleteSuccess ? "File deleted successfully" : "Failed to delete file") + "\"}";
|
||||
webSocket.sendTXT(num, response);
|
||||
|
||||
// 파일 목록 자동 갱신
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
webSocketEvent(num, WStype_TEXT, (uint8_t*)"{\"cmd\":\"getFiles\"}", 17);
|
||||
}
|
||||
else if (cmd == "setSpeed") {
|
||||
int speedStart = message.indexOf("\"speed\":") + 8;
|
||||
int speedValue = message.substring(speedStart, message.indexOf("}", speedStart)).toInt();
|
||||
@@ -722,65 +742,145 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length)
|
||||
else if (cmd == "saveSettings") {
|
||||
int ssidStart = message.indexOf("\"ssid\":\"") + 8;
|
||||
int ssidEnd = message.indexOf("\"", ssidStart);
|
||||
String newSSID = message.substring(ssidStart, ssidEnd);
|
||||
|
||||
int passStart = message.indexOf("\"password\":\"") + 12;
|
||||
int passEnd = message.indexOf("\"", passStart);
|
||||
String newPassword = message.substring(passStart, passEnd);
|
||||
|
||||
int tzStart = message.indexOf("\"timezone\":") + 11;
|
||||
|
||||
String newSSID = message.substring(ssidStart, ssidEnd);
|
||||
String newPassword = message.substring(passStart, passEnd);
|
||||
int newTimezone = message.substring(tzStart, message.indexOf("}", tzStart)).toInt();
|
||||
|
||||
// 설정 저장
|
||||
newSSID.toCharArray(wifiSSID, sizeof(wifiSSID));
|
||||
newPassword.toCharArray(wifiPassword, sizeof(wifiPassword));
|
||||
timezoneOffset = newTimezone;
|
||||
|
||||
saveSettings();
|
||||
|
||||
// 설정 저장 성공 응답
|
||||
String response = "{\"type\":\"settingsSaved\",\"success\":true}";
|
||||
String response = "{\"type\":\"settingsResult\",\"success\":true}";
|
||||
webSocket.sendTXT(num, response);
|
||||
|
||||
Serial.println("✓ 설정이 저장되었습니다. 재부팅 후 적용됩니다.");
|
||||
}
|
||||
// CAN 송신 관련 명령어
|
||||
else if (cmd == "sendCAN") {
|
||||
// 송신 처리 (기존 코드 유지)
|
||||
int idStart = message.indexOf("\"id\":\"") + 6;
|
||||
int idEnd = message.indexOf("\"", idStart);
|
||||
String idStr = message.substring(idStart, idEnd);
|
||||
|
||||
int dataStart = message.indexOf("\"data\":\"") + 8;
|
||||
int dataEnd = message.indexOf("\"", dataStart);
|
||||
String dataStr = message.substring(dataStart, dataEnd);
|
||||
|
||||
uint32_t canId = strtoul(idStr.c_str(), NULL, 16);
|
||||
|
||||
struct can_frame frame;
|
||||
frame.can_id = canId;
|
||||
frame.can_dlc = 0;
|
||||
|
||||
dataStr.replace(" ", "");
|
||||
for (int i = 0; i < dataStr.length() && i < 16; i += 2) {
|
||||
String byteStr = dataStr.substring(i, i + 2);
|
||||
frame.data[frame.can_dlc++] = strtoul(byteStr.c_str(), NULL, 16);
|
||||
}
|
||||
|
||||
MCP2515::ERROR result = mcp2515.sendMessage(&frame);
|
||||
if (result == MCP2515::ERROR_OK) {
|
||||
totalTxCount++;
|
||||
Serial.printf("✓ CAN 송신: ID=0x%X, DLC=%d\n", canId, frame.can_dlc);
|
||||
}
|
||||
}
|
||||
else if (cmd == "addTxMessage") {
|
||||
int slot = -1;
|
||||
for (int i = 0; i < MAX_TX_MESSAGES; i++) {
|
||||
if (!txMessages[i].active) {
|
||||
slot = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (slot >= 0) {
|
||||
int idStart = message.indexOf("\"id\":\"") + 6;
|
||||
int idEnd = message.indexOf("\"", idStart);
|
||||
String idStr = message.substring(idStart, idEnd);
|
||||
|
||||
int dataStart = message.indexOf("\"data\":\"") + 8;
|
||||
int dataEnd = message.indexOf("\"", dataStart);
|
||||
String dataStr = message.substring(dataStart, dataEnd);
|
||||
|
||||
int intervalStart = message.indexOf("\"interval\":") + 11;
|
||||
int intervalEnd = message.indexOf(",", intervalStart);
|
||||
if (intervalEnd < 0) intervalEnd = message.indexOf("}", intervalStart);
|
||||
int interval = message.substring(intervalStart, intervalEnd).toInt();
|
||||
|
||||
txMessages[slot].id = strtoul(idStr.c_str(), NULL, 16);
|
||||
txMessages[slot].extended = false;
|
||||
txMessages[slot].interval = interval;
|
||||
txMessages[slot].lastSent = 0;
|
||||
txMessages[slot].active = true;
|
||||
|
||||
dataStr.replace(" ", "");
|
||||
txMessages[slot].dlc = 0;
|
||||
for (int i = 0; i < dataStr.length() && i < 16; i += 2) {
|
||||
String byteStr = dataStr.substring(i, i + 2);
|
||||
txMessages[slot].data[txMessages[slot].dlc++] = strtoul(byteStr.c_str(), NULL, 16);
|
||||
}
|
||||
|
||||
Serial.printf("✓ 주기 송신 추가: Slot=%d, ID=0x%X, Interval=%dms\n",
|
||||
slot, txMessages[slot].id, interval);
|
||||
}
|
||||
}
|
||||
else if (cmd == "removeTxMessage") {
|
||||
int slotStart = message.indexOf("\"slot\":") + 7;
|
||||
int slot = message.substring(slotStart, message.indexOf("}", slotStart)).toInt();
|
||||
|
||||
if (slot >= 0 && slot < MAX_TX_MESSAGES) {
|
||||
txMessages[slot].active = false;
|
||||
Serial.printf("✓ 주기 송신 제거: Slot=%d\n", slot);
|
||||
}
|
||||
}
|
||||
else if (cmd == "getTxMessages") {
|
||||
String txList = "{\"type\":\"txMessages\",\"messages\":[";
|
||||
bool first = true;
|
||||
|
||||
for (int i = 0; i < MAX_TX_MESSAGES; i++) {
|
||||
if (txMessages[i].active) {
|
||||
if (!first) txList += ",";
|
||||
first = false;
|
||||
|
||||
char idStr[16], dataStr[32];
|
||||
sprintf(idStr, "%08X", txMessages[i].id);
|
||||
|
||||
dataStr[0] = '\0';
|
||||
for (int j = 0; j < txMessages[i].dlc; j++) {
|
||||
char byteStr[4];
|
||||
sprintf(byteStr, "%02X", txMessages[i].data[j]);
|
||||
strcat(dataStr, byteStr);
|
||||
if (j < txMessages[i].dlc - 1) strcat(dataStr, " ");
|
||||
}
|
||||
|
||||
txList += "{\"slot\":" + String(i) +
|
||||
",\"id\":\"" + String(idStr) +
|
||||
"\",\"data\":\"" + String(dataStr) +
|
||||
"\",\"interval\":" + String(txMessages[i].interval) + "}";
|
||||
}
|
||||
}
|
||||
|
||||
txList += "]}";
|
||||
webSocket.sendTXT(num, txList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 웹 업데이트 Task
|
||||
// 주기 송신 태스크
|
||||
// ========================================
|
||||
|
||||
void webUpdateTask(void* parameter) {
|
||||
Serial.println("✓ Web Update Task 시작");
|
||||
|
||||
const uint32_t STATUS_UPDATE_INTERVAL = 1000;
|
||||
const uint32_t CAN_UPDATE_INTERVAL = 200;
|
||||
|
||||
uint32_t lastStatusUpdate = 0;
|
||||
uint32_t lastCanUpdate = 0;
|
||||
uint32_t lastTxStatusUpdate = 0;
|
||||
|
||||
void txTask(void *parameter) {
|
||||
while (1) {
|
||||
webSocket.loop();
|
||||
uint32_t now = millis();
|
||||
|
||||
// 전력 상태 업데이트
|
||||
updatePowerStatus();
|
||||
|
||||
// CAN 송신 처리 (기존 코드 유지)
|
||||
for (int i = 0; i < MAX_TX_MESSAGES; i++) {
|
||||
if (txMessages[i].active && txMessages[i].interval > 0) {
|
||||
if (txMessages[i].active) {
|
||||
if (now - txMessages[i].lastSent >= txMessages[i].interval) {
|
||||
struct can_frame frame;
|
||||
frame.can_id = txMessages[i].id;
|
||||
if (txMessages[i].extended) {
|
||||
frame.can_id |= CAN_EFF_FLAG;
|
||||
}
|
||||
frame.can_dlc = txMessages[i].dlc;
|
||||
memcpy(frame.data, txMessages[i].data, 8);
|
||||
|
||||
@@ -792,21 +892,37 @@ void webUpdateTask(void* parameter) {
|
||||
}
|
||||
}
|
||||
|
||||
// TX 상태 업데이트
|
||||
if (now - lastTxStatusUpdate >= 1000) {
|
||||
String txStatus = "{\"type\":\"txStatus\",\"count\":" + String(totalTxCount) + "}";
|
||||
webSocket.broadcastTXT(txStatus);
|
||||
lastTxStatusUpdate = now;
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 웹 업데이트 태스크
|
||||
// ========================================
|
||||
|
||||
void webUpdateTask(void *parameter) {
|
||||
const uint32_t STATUS_UPDATE_INTERVAL = 500;
|
||||
const uint32_t CAN_UPDATE_INTERVAL = 200;
|
||||
|
||||
uint32_t lastStatusUpdate = 0;
|
||||
uint32_t lastCanUpdate = 0;
|
||||
uint32_t lastMsgSpeedCalc = 0;
|
||||
|
||||
while (1) {
|
||||
webSocket.loop();
|
||||
updatePowerStatus();
|
||||
|
||||
uint32_t now = millis();
|
||||
|
||||
// 메시지 속도 계산
|
||||
if (now - lastMsgSpeedCalc >= 1000) {
|
||||
msgPerSecond = totalMsgCount - lastMsgCount;
|
||||
lastMsgCount = totalMsgCount;
|
||||
lastMsgSpeedCalc = now;
|
||||
}
|
||||
|
||||
// 상태 업데이트 (전력 상태 포함)
|
||||
// 상태 업데이트
|
||||
if (now - lastStatusUpdate >= STATUS_UPDATE_INTERVAL) {
|
||||
if (now - lastMsgCountTime >= 1000) {
|
||||
msgPerSecond = totalMsgCount - lastMsgCount;
|
||||
lastMsgCount = totalMsgCount;
|
||||
lastMsgCountTime = now;
|
||||
}
|
||||
|
||||
String status = "{\"type\":\"status\",";
|
||||
status += "\"logging\":" + String(loggingEnabled ? "true" : "false") + ",";
|
||||
status += "\"sdReady\":" + String(sdCardReady ? "true" : "false") + ",";
|
||||
@@ -823,9 +939,11 @@ void webUpdateTask(void* parameter) {
|
||||
status += "\"queueSize\":" + String(CAN_QUEUE_SIZE) + ",";
|
||||
|
||||
if (loggingEnabled && logFile) {
|
||||
status += "\"currentFile\":\"" + String(currentFilename) + "\"";
|
||||
status += "\"currentFile\":\"" + String(currentFilename) + "\",";
|
||||
status += "\"currentFileSize\":" + String(currentFileSize);
|
||||
} else {
|
||||
status += "\"currentFile\":\"\"";
|
||||
status += "\"currentFile\":\"\",";
|
||||
status += "\"currentFileSize\":0";
|
||||
}
|
||||
status += "}";
|
||||
|
||||
@@ -885,7 +1003,8 @@ void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
Serial.println("\n========================================");
|
||||
Serial.println(" ESP32 CAN Logger v1.4 with Timezone ");
|
||||
Serial.println(" ESP32 CAN Logger v1.5 ");
|
||||
Serial.println(" + File Delete & Size Monitor ");
|
||||
Serial.println("========================================");
|
||||
|
||||
// 설정 로드
|
||||
@@ -991,6 +1110,46 @@ void setup() {
|
||||
}
|
||||
});
|
||||
|
||||
// 파일 삭제 HTTP 엔드포인트 추가 (백업용 - 주로 WebSocket 사용)
|
||||
server.on("/delete", HTTP_GET, []() {
|
||||
if (server.hasArg("file")) {
|
||||
String filename = server.arg("file");
|
||||
|
||||
// 로깅 중인 파일은 삭제 불가
|
||||
if (loggingEnabled && currentFilename[0] != '\0') {
|
||||
String currentFileStr = String(currentFilename);
|
||||
if (currentFileStr.startsWith("/")) currentFileStr = currentFileStr.substring(1);
|
||||
|
||||
if (filename == currentFileStr) {
|
||||
server.send(403, "text/plain", "Cannot delete file currently being logged");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
String fullPath = "/" + filename;
|
||||
|
||||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||||
if (SD.exists(fullPath)) {
|
||||
if (SD.remove(fullPath)) {
|
||||
xSemaphoreGive(sdMutex);
|
||||
server.send(200, "text/plain", "File deleted successfully");
|
||||
Serial.printf("✓ HTTP 파일 삭제: %s\n", filename.c_str());
|
||||
} else {
|
||||
xSemaphoreGive(sdMutex);
|
||||
server.send(500, "text/plain", "Failed to delete file");
|
||||
}
|
||||
} else {
|
||||
xSemaphoreGive(sdMutex);
|
||||
server.send(404, "text/plain", "File not found");
|
||||
}
|
||||
} else {
|
||||
server.send(503, "text/plain", "SD card busy");
|
||||
}
|
||||
} else {
|
||||
server.send(400, "text/plain", "Bad request");
|
||||
}
|
||||
});
|
||||
|
||||
server.begin();
|
||||
|
||||
// Queue 생성
|
||||
@@ -1009,6 +1168,7 @@ void setup() {
|
||||
xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 12288, NULL, 3, &sdWriteTaskHandle, 1);
|
||||
xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 0);
|
||||
xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 8192, NULL, 2, &webTaskHandle, 0);
|
||||
xTaskCreatePinnedToCore(txTask, "TX_TASK", 4096, NULL, 2, NULL, 1);
|
||||
|
||||
// RTC 동기화 Task
|
||||
if (timeSyncStatus.rtcAvailable) {
|
||||
@@ -1042,11 +1202,12 @@ void loop() {
|
||||
|
||||
static uint32_t lastPrint = 0;
|
||||
if (millis() - lastPrint > 10000) {
|
||||
Serial.printf("[상태] 큐: %d/%d | 로깅: %s | SD: %s | RX: %lu | TX: %lu | 시간: %s | RTC: %s(%u) | 전압: %.2fV%s\n",
|
||||
Serial.printf("[상태] 큐: %d/%d | 로깅: %s | SD: %s | RX: %lu | TX: %lu | 파일크기: %u | 시간: %s | RTC: %s(%u) | 전압: %.2fV%s\n",
|
||||
uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE,
|
||||
loggingEnabled ? "ON " : "OFF",
|
||||
sdCardReady ? "OK" : "NO",
|
||||
totalMsgCount, totalTxCount,
|
||||
currentFileSize,
|
||||
timeSyncStatus.synchronized ? "OK" : "NO",
|
||||
timeSyncStatus.rtcAvailable ? "OK" : "NO",
|
||||
timeSyncStatus.rtcSyncCount,
|
||||
|
||||
254
index.h
254
index.h
@@ -349,6 +349,10 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.file-item:hover { transform: translateX(5px); box-shadow: 0 3px 10px rgba(0,0,0,0.1); }
|
||||
.file-item.logging {
|
||||
border: 2px solid #11998e;
|
||||
background: linear-gradient(to right, rgba(17, 153, 142, 0.05), white);
|
||||
}
|
||||
.file-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -361,18 +365,46 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
color: #333;
|
||||
font-size: 0.9em;
|
||||
word-break: break-all;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.logging-badge {
|
||||
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75em;
|
||||
font-weight: 700;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
.file-size {
|
||||
color: #666;
|
||||
font-size: 0.85em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.download-btn {
|
||||
.file-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.download-btn, .delete-btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 0.85em;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
min-width: 100px;
|
||||
min-width: 80px;
|
||||
}
|
||||
.delete-btn {
|
||||
background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);
|
||||
}
|
||||
.delete-btn:hover {
|
||||
background: linear-gradient(135deg, #d32f3f 0%, #e53935 100%);
|
||||
}
|
||||
.delete-btn:disabled {
|
||||
background: #cccccc;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -399,29 +431,16 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
.time-value {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* 파일 목록 모바일 최적화 */
|
||||
.file-item {
|
||||
padding: 10px;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.file-info {
|
||||
.btn-time-sync {
|
||||
width: 100%;
|
||||
margin-bottom: 5px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
.file-name {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
.file-size {
|
||||
font-size: 0.8em;
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.download-btn {
|
||||
.file-actions {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
min-width: auto;
|
||||
justify-content: stretch;
|
||||
}
|
||||
.download-btn, .delete-btn {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -429,70 +448,67 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Byun CAN Logger</h1>
|
||||
<p>Real-time CAN Bus Monitor & Data Logger with Time Sync</p>
|
||||
<h1>🚗 Byun CAN Logger v1.5</h1>
|
||||
<p>Real-time CAN Bus Monitor & Logger + File Management</p>
|
||||
</div>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/" class="active">Monitor</a>
|
||||
<a href="/transmit">Transmit</a>
|
||||
<a href="/graph">Graph</a>
|
||||
<a href="/" class="active">📊 Monitor</a>
|
||||
<a href="/transmit">📤 Transmit</a>
|
||||
<a href="/graph">📈 Graph</a>
|
||||
<a href="/graph-view">📊 Graph View</a>
|
||||
<a href="/settings">⚙️ Settings</a>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- 큐 상태 표시 -->
|
||||
<div class="queue-status" id="queue-status">
|
||||
<div class="queue-info">
|
||||
<span style="font-size: 1.3em;">📊</span>
|
||||
<span style="font-weight: 600; font-size: 0.85em;">CAN Queue</span>
|
||||
</div>
|
||||
<div style="flex: 1; min-width: 200px;">
|
||||
<div class="queue-bar-container">
|
||||
<div class="queue-bar" id="queue-bar" style="width: 0%"></div>
|
||||
<div class="queue-text" id="queue-text">0/1000</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 전력 상태 표시 -->
|
||||
<div class="power-status" id="power-status">
|
||||
<div class="power-status-label">
|
||||
<span id="power-icon">⚡</span>
|
||||
<span id="power-text">전원 상태</span>
|
||||
</div>
|
||||
<div class="power-status-values">
|
||||
<div class="power-status-item">
|
||||
<span class="power-status-item-label">현재</span>
|
||||
<span class="power-status-value" id="power-value">--V</span>
|
||||
</div>
|
||||
<div class="power-status-item">
|
||||
<span class="power-status-item-label">최소(1s)</span>
|
||||
<span class="power-status-value" id="power-min-value">--V</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="time-sync-banner">
|
||||
<div class="time-sync-info">
|
||||
<div class="time-info-item">
|
||||
<span class="time-label">⏰ 시간 동기화 상태</span>
|
||||
<span class="time-value" id="sync-status">대기 중...</span>
|
||||
<div class="time-label">CURRENT TIME</div>
|
||||
<div class="time-value" id="current-time">--:--:--</div>
|
||||
</div>
|
||||
<div class="time-info-item">
|
||||
<span class="time-label">🕐 현재 시간</span>
|
||||
<span class="time-value" id="current-time">--:--:--</span>
|
||||
<div class="time-label">CONNECTION</div>
|
||||
<div class="time-value" id="sync-status">연결 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-time-sync" onclick="syncTime()">⏰ 시간 동기화</button>
|
||||
<button class="btn-time-sync" onclick="syncTime()">🕐 Sync Time</button>
|
||||
</div>
|
||||
|
||||
<div class="power-status" id="power-status">
|
||||
<div class="power-status-label">
|
||||
<span>⚡</span>
|
||||
<span>POWER STATUS</span>
|
||||
</div>
|
||||
<div class="power-status-values">
|
||||
<div class="power-status-item">
|
||||
<div class="power-status-item-label">CURRENT</div>
|
||||
<div class="power-status-value" id="voltage-current">-.--V</div>
|
||||
</div>
|
||||
<div class="power-status-item">
|
||||
<div class="power-status-item-label">MIN (1s)</div>
|
||||
<div class="power-status-value" id="voltage-min">-.--V</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="queue-status" id="queue-status">
|
||||
<div class="queue-info">
|
||||
<span style="font-size: 1.2em;">📦</span>
|
||||
<span style="font-weight: 700; font-size: 0.9em;">QUEUE STATUS</span>
|
||||
</div>
|
||||
<div class="queue-bar-container">
|
||||
<div class="queue-bar" id="queue-bar" style="width: 0%;"></div>
|
||||
<div class="queue-text" id="queue-text">0 / 1000</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-grid">
|
||||
<div class="status-card" id="logging-status">
|
||||
<div class="status-card status-off" id="logging-status">
|
||||
<h3>LOGGING</h3>
|
||||
<div class="value">OFF</div>
|
||||
</div>
|
||||
<div class="status-card" id="sd-status">
|
||||
<div class="status-card status-off" id="sd-status">
|
||||
<h3>SD CARD</h3>
|
||||
<div class="value">NOT READY</div>
|
||||
</div>
|
||||
@@ -512,6 +528,10 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
<h3>CURRENT FILE</h3>
|
||||
<div class="value" id="current-file" style="font-size: 0.85em;">-</div>
|
||||
</div>
|
||||
<div class="status-card" id="filesize-status">
|
||||
<h3>FILE SIZE</h3>
|
||||
<div class="value" id="current-file-size">0 B</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-panel">
|
||||
@@ -564,6 +584,7 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
let messageOrder = [];
|
||||
let lastMessageData = {};
|
||||
const speedNames = {0: '125K', 1: '250K', 2: '500K', 3: '1M'};
|
||||
let currentLoggingFile = '';
|
||||
|
||||
function updateCurrentTime() {
|
||||
const now = new Date();
|
||||
@@ -636,6 +657,8 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
updateCanBatch(data.messages);
|
||||
} else if (data.type === 'files') {
|
||||
updateFileList(data.files);
|
||||
} else if (data.type === 'deleteResult') {
|
||||
handleDeleteResult(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Parse error:', e);
|
||||
@@ -648,6 +671,7 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
const sdCard = document.getElementById('sd-status');
|
||||
const timeSyncCard = document.getElementById('time-sync-card');
|
||||
const fileCard = document.getElementById('file-status');
|
||||
const filesizeCard = document.getElementById('filesize-status');
|
||||
|
||||
if (data.logging) {
|
||||
loggingCard.classList.add('status-on');
|
||||
@@ -684,10 +708,26 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
if (data.currentFile && data.currentFile !== '') {
|
||||
fileCard.classList.add('status-on');
|
||||
fileCard.classList.remove('status-off');
|
||||
document.getElementById('current-file').textContent = data.currentFile;
|
||||
const filename = data.currentFile.startsWith('/') ? data.currentFile.substring(1) : data.currentFile;
|
||||
document.getElementById('current-file').textContent = filename;
|
||||
currentLoggingFile = filename;
|
||||
} else {
|
||||
fileCard.classList.remove('status-on', 'status-off');
|
||||
document.getElementById('current-file').textContent = '-';
|
||||
currentLoggingFile = '';
|
||||
}
|
||||
|
||||
// 실시간 파일 크기 표시
|
||||
if (data.currentFileSize !== undefined) {
|
||||
const sizeStr = formatBytes(data.currentFileSize);
|
||||
document.getElementById('current-file-size').textContent = sizeStr;
|
||||
|
||||
if (data.currentFileSize > 0) {
|
||||
filesizeCard.classList.add('status-on');
|
||||
filesizeCard.classList.remove('status-off');
|
||||
} else {
|
||||
filesizeCard.classList.remove('status-on', 'status-off');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('msg-count').textContent = data.msgCount.toLocaleString();
|
||||
@@ -699,42 +739,29 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
const queueBar = document.getElementById('queue-bar');
|
||||
const queueText = document.getElementById('queue-text');
|
||||
|
||||
const queuePercent = (data.queueUsed / data.queueSize) * 100;
|
||||
queueBar.style.width = queuePercent + '%';
|
||||
queueText.textContent = data.queueUsed + '/' + data.queueSize;
|
||||
const percentage = (data.queueUsed / data.queueSize) * 100;
|
||||
queueBar.style.width = percentage + '%';
|
||||
queueText.textContent = data.queueUsed + ' / ' + data.queueSize;
|
||||
|
||||
// 큐 상태에 따른 색상 변경
|
||||
queueStatus.classList.remove('warning', 'critical');
|
||||
if (queuePercent >= 90) {
|
||||
if (percentage >= 90) {
|
||||
queueStatus.classList.add('critical');
|
||||
} else if (queuePercent >= 70) {
|
||||
} else if (percentage >= 70) {
|
||||
queueStatus.classList.add('warning');
|
||||
}
|
||||
}
|
||||
|
||||
// 전력 상태 업데이트
|
||||
if (data.voltage !== undefined) {
|
||||
const powerStatus = document.getElementById('power-status');
|
||||
const powerValue = document.getElementById('power-value');
|
||||
const powerMinValue = document.getElementById('power-min-value');
|
||||
const powerIcon = document.getElementById('power-icon');
|
||||
const powerText = document.getElementById('power-text');
|
||||
|
||||
powerValue.textContent = data.voltage.toFixed(2) + 'V';
|
||||
|
||||
if (data.minVoltage !== undefined) {
|
||||
powerMinValue.textContent = data.minVoltage.toFixed(2) + 'V';
|
||||
}
|
||||
|
||||
if (data.lowVoltage) {
|
||||
powerStatus.classList.add('low');
|
||||
powerIcon.textContent = '⚠️';
|
||||
powerText.textContent = '전력 부족 경고!';
|
||||
} else {
|
||||
powerStatus.classList.remove('low');
|
||||
powerIcon.textContent = '⚡';
|
||||
powerText.textContent = '전원 정상';
|
||||
}
|
||||
document.getElementById('voltage-current').textContent = data.voltage.toFixed(2) + 'V';
|
||||
}
|
||||
if (data.minVoltage !== undefined) {
|
||||
document.getElementById('voltage-min').textContent = data.minVoltage.toFixed(2) + 'V';
|
||||
}
|
||||
if (data.lowVoltage !== undefined && data.lowVoltage) {
|
||||
document.getElementById('power-status').classList.add('low');
|
||||
} else {
|
||||
document.getElementById('power-status').classList.remove('low');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -839,14 +866,26 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
|
||||
fileList.innerHTML = '';
|
||||
files.forEach(file => {
|
||||
const isLogging = (currentLoggingFile && file.name === currentLoggingFile);
|
||||
const fileItem = document.createElement('div');
|
||||
fileItem.className = 'file-item';
|
||||
fileItem.className = 'file-item' + (isLogging ? ' logging' : '');
|
||||
|
||||
let nameHtml = '<div class="file-name">' + file.name;
|
||||
if (isLogging) {
|
||||
nameHtml += '<span class="logging-badge">LOGGING</span>';
|
||||
}
|
||||
nameHtml += '</div>';
|
||||
|
||||
fileItem.innerHTML =
|
||||
'<div class="file-info">' +
|
||||
'<div class="file-name">' + file.name + '</div>' +
|
||||
nameHtml +
|
||||
'<div class="file-size">' + formatBytes(file.size) + '</div>' +
|
||||
'</div>' +
|
||||
'<button class="download-btn" onclick="downloadFile(\'' + file.name + '\')">Download</button>';
|
||||
'<div class="file-actions">' +
|
||||
'<button class="download-btn" onclick="downloadFile(\'' + file.name + '\')">Download</button>' +
|
||||
'<button class="delete-btn" onclick="deleteFile(\'' + file.name + '\')" ' +
|
||||
(isLogging ? 'disabled title="Cannot delete file being logged"' : '') + '>Delete</button>' +
|
||||
'</div>';
|
||||
fileList.appendChild(fileItem);
|
||||
});
|
||||
}
|
||||
@@ -909,6 +948,27 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
window.location.href = '/download?file=' + encodeURIComponent(filename);
|
||||
}
|
||||
|
||||
function deleteFile(filename) {
|
||||
if (!confirm('Are you sure you want to delete "' + filename + '"?\n\nThis action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({cmd: 'deleteFile', filename: filename}));
|
||||
console.log('Delete file command sent:', filename);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteResult(data) {
|
||||
if (data.success) {
|
||||
console.log('File deleted successfully');
|
||||
// 파일 목록은 서버에서 자동으로 갱신됨
|
||||
} else {
|
||||
alert('Failed to delete file: ' + data.message);
|
||||
console.error('Delete failed:', data.message);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('load', function() {
|
||||
loadCanSpeed();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user