웹디바이스로 시간 동기화
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Byun CAN Logger with Web Interface
|
||||
* Version: 1.1
|
||||
* Byun CAN Logger with Web Interface + Time Synchronization
|
||||
* Version: 1.2
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
@@ -14,11 +14,14 @@
|
||||
#include <freertos/task.h>
|
||||
#include <freertos/queue.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
#include "index.h"
|
||||
#include "transmit.h"
|
||||
#include "graph.h"
|
||||
#include "graph_viewer.h"
|
||||
|
||||
|
||||
// GPIO 핀 정의
|
||||
#define CAN_INT_PIN 27
|
||||
|
||||
@@ -37,13 +40,13 @@
|
||||
// 버퍼 설정
|
||||
#define CAN_QUEUE_SIZE 1000
|
||||
#define FILE_BUFFER_SIZE 8192
|
||||
#define MAX_FILENAME_LEN 32
|
||||
#define MAX_FILENAME_LEN 64
|
||||
#define RECENT_MSG_COUNT 100
|
||||
#define MAX_TX_MESSAGES 20
|
||||
|
||||
// CAN 메시지 구조체
|
||||
// CAN 메시지 구조체 - 마이크로초 단위 타임스탬프
|
||||
struct CANMessage {
|
||||
uint32_t timestamp;
|
||||
uint64_t timestamp_us; // 마이크로초 단위 Unix timestamp
|
||||
uint32_t id;
|
||||
uint8_t dlc;
|
||||
uint8_t data[8];
|
||||
@@ -66,6 +69,14 @@ struct TxMessage {
|
||||
bool active;
|
||||
};
|
||||
|
||||
// 시간 동기화 상태
|
||||
struct TimeSyncStatus {
|
||||
bool synchronized;
|
||||
uint64_t lastSyncTime;
|
||||
int32_t offsetUs;
|
||||
uint32_t syncCount;
|
||||
} timeSyncStatus = {false, 0, 0, 0};
|
||||
|
||||
// WiFi AP 설정
|
||||
const char* ssid = "Byun_CAN_Logger";
|
||||
const char* password = "12345678";
|
||||
@@ -90,7 +101,6 @@ File logFile;
|
||||
char currentFilename[MAX_FILENAME_LEN];
|
||||
uint8_t fileBuffer[FILE_BUFFER_SIZE];
|
||||
uint16_t bufferIndex = 0;
|
||||
uint32_t fileCounter = 0;
|
||||
|
||||
// CAN 속도 설정
|
||||
CAN_SPEED currentCanSpeed = CAN_1000KBPS;
|
||||
@@ -108,9 +118,37 @@ uint32_t lastMsgCount = 0;
|
||||
TxMessage txMessages[MAX_TX_MESSAGES];
|
||||
uint32_t totalTxCount = 0;
|
||||
|
||||
// 정밀한 현재 시간 가져오기 (마이크로초)
|
||||
uint64_t getMicrosecondTimestamp() {
|
||||
struct timeval tv;
|
||||
gettimeofday(&tv, NULL);
|
||||
return (uint64_t)tv.tv_sec * 1000000ULL + (uint64_t)tv.tv_usec;
|
||||
}
|
||||
|
||||
// 시간 동기화 설정
|
||||
void setSystemTime(uint64_t timestampMs) {
|
||||
struct timeval tv;
|
||||
tv.tv_sec = timestampMs / 1000;
|
||||
tv.tv_usec = (timestampMs % 1000) * 1000;
|
||||
settimeofday(&tv, NULL);
|
||||
|
||||
timeSyncStatus.synchronized = true;
|
||||
timeSyncStatus.lastSyncTime = getMicrosecondTimestamp();
|
||||
timeSyncStatus.syncCount++;
|
||||
|
||||
// 현재 시간 출력
|
||||
time_t now = tv.tv_sec;
|
||||
struct tm timeinfo;
|
||||
localtime_r(&now, &timeinfo);
|
||||
char timeStr[64];
|
||||
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
|
||||
|
||||
Serial.printf("⏰ 시간 동기화 완료: %s.%03d (동기화 횟수: %u)\n",
|
||||
timeStr, (int)(tv.tv_usec / 1000), timeSyncStatus.syncCount);
|
||||
}
|
||||
|
||||
// 함수 선언
|
||||
void changeCanSpeed(CAN_SPEED newSpeed);
|
||||
void scanExistingFiles();
|
||||
bool createNewLogFile();
|
||||
bool flushBuffer();
|
||||
void startLogging();
|
||||
@@ -123,6 +161,7 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length
|
||||
void handleCanTransmit(String msg);
|
||||
void handleStartMessage(String msg);
|
||||
void handleStopMessage(String msg);
|
||||
void handleTimeSync(String msg);
|
||||
void webUpdateTask(void *pvParameters);
|
||||
|
||||
// CAN 인터럽트 핸들러
|
||||
@@ -145,60 +184,7 @@ void changeCanSpeed(CAN_SPEED newSpeed) {
|
||||
Serial.printf("CAN 속도 변경: %s\n", canSpeedNames[newSpeed]);
|
||||
}
|
||||
|
||||
// 기존 파일 번호 스캔
|
||||
void scanExistingFiles() {
|
||||
if (!sdCardReady) {
|
||||
fileCounter = 0;
|
||||
Serial.println("SD 카드 없음 - 파일 카운터 0으로 시작");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t maxFileNumber = 0;
|
||||
bool foundFiles = false;
|
||||
|
||||
File root = SD.open("/");
|
||||
if (!root) {
|
||||
Serial.println("루트 디렉토리 열기 실패");
|
||||
fileCounter = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
File file = root.openNextFile();
|
||||
while (file) {
|
||||
if (!file.isDirectory()) {
|
||||
String name = file.name();
|
||||
if (name.startsWith("/")) name = name.substring(1);
|
||||
|
||||
if (name.startsWith("canlog_") && (name.endsWith(".bin") || name.endsWith(".BIN"))) {
|
||||
int startIdx = 7;
|
||||
int endIdx = name.lastIndexOf('.');
|
||||
|
||||
if (endIdx > startIdx) {
|
||||
String numStr = name.substring(startIdx, endIdx);
|
||||
uint32_t fileNum = numStr.toInt();
|
||||
|
||||
if (fileNum > maxFileNumber) {
|
||||
maxFileNumber = fileNum;
|
||||
}
|
||||
foundFiles = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
file = root.openNextFile();
|
||||
}
|
||||
root.close();
|
||||
|
||||
if (foundFiles) {
|
||||
fileCounter = maxFileNumber + 1;
|
||||
Serial.printf("기존 파일 발견 - 다음 파일 번호: %lu\n", fileCounter);
|
||||
} else {
|
||||
fileCounter = 0;
|
||||
Serial.println("기존 파일 없음 - 파일 카운터 0으로 시작");
|
||||
}
|
||||
}
|
||||
|
||||
// 새 로그 파일 생성
|
||||
// 새 로그 파일 생성 - 시간 기반 파일명
|
||||
bool createNewLogFile() {
|
||||
if (logFile) {
|
||||
logFile.flush();
|
||||
@@ -206,8 +192,20 @@ bool createNewLogFile() {
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
|
||||
// 현재 시간으로 파일명 생성
|
||||
time_t now;
|
||||
struct tm timeinfo;
|
||||
time(&now);
|
||||
localtime_r(&now, &timeinfo);
|
||||
|
||||
char filename[MAX_FILENAME_LEN];
|
||||
snprintf(filename, MAX_FILENAME_LEN, "/canlog_%05lu.bin", fileCounter++);
|
||||
snprintf(filename, MAX_FILENAME_LEN, "/canlog_%04d%02d%02d_%02d%02d%02d.bin",
|
||||
timeinfo.tm_year + 1900,
|
||||
timeinfo.tm_mon + 1,
|
||||
timeinfo.tm_mday,
|
||||
timeinfo.tm_hour,
|
||||
timeinfo.tm_min,
|
||||
timeinfo.tm_sec);
|
||||
|
||||
logFile = SD.open(filename, FILE_WRITE);
|
||||
|
||||
@@ -218,6 +216,12 @@ bool createNewLogFile() {
|
||||
|
||||
strncpy(currentFilename, filename, MAX_FILENAME_LEN);
|
||||
Serial.printf("새 로그 파일 생성: %s\n", currentFilename);
|
||||
|
||||
// 시간 동기화 경고
|
||||
if (!timeSyncStatus.synchronized) {
|
||||
Serial.println("⚠️ 경고: 시간이 동기화되지 않았습니다! 웹페이지에서 시간 동기화를 실행하세요.");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -297,7 +301,8 @@ void canRxTask(void *pvParameters) {
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
|
||||
while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {
|
||||
msg.timestamp = millis();
|
||||
// 마이크로초 단위 타임스탬프
|
||||
msg.timestamp_us = getMicrosecondTimestamp();
|
||||
msg.id = frame.can_id;
|
||||
msg.dlc = frame.can_dlc;
|
||||
memcpy(msg.data, frame.data, 8);
|
||||
@@ -313,7 +318,7 @@ void canRxTask(void *pvParameters) {
|
||||
// 최근 메시지 저장 및 카운트 증가
|
||||
bool found = false;
|
||||
for (int i = 0; i < RECENT_MSG_COUNT; i++) {
|
||||
if (recentData[i].msg.id == msg.id && recentData[i].msg.timestamp > 0) {
|
||||
if (recentData[i].msg.id == msg.id && recentData[i].msg.timestamp_us > 0) {
|
||||
recentData[i].msg = msg;
|
||||
recentData[i].count++;
|
||||
found = true;
|
||||
@@ -323,7 +328,7 @@ void canRxTask(void *pvParameters) {
|
||||
|
||||
if (!found) {
|
||||
for (int i = 0; i < RECENT_MSG_COUNT; i++) {
|
||||
if (recentData[i].msg.timestamp == 0) {
|
||||
if (recentData[i].msg.timestamp_us == 0) {
|
||||
recentData[i].msg = msg;
|
||||
recentData[i].count = 1;
|
||||
found = true;
|
||||
@@ -383,7 +388,6 @@ void sdMonitorTask(void *pvParameters) {
|
||||
|
||||
if (sdCardReady) {
|
||||
Serial.println("SD 카드 준비됨");
|
||||
scanExistingFiles();
|
||||
} else {
|
||||
Serial.println("SD 카드 없음");
|
||||
if (loggingEnabled) {
|
||||
@@ -526,6 +530,19 @@ void handleStopMessage(String msg) {
|
||||
}
|
||||
}
|
||||
|
||||
// 시간 동기화 처리
|
||||
void handleTimeSync(String msg) {
|
||||
int timestampIdx = msg.indexOf("\"timestamp\":") + 12;
|
||||
String timestampStr = msg.substring(timestampIdx);
|
||||
timestampStr = timestampStr.substring(0, timestampStr.indexOf("}"));
|
||||
|
||||
uint64_t clientTimestamp = strtoull(timestampStr.c_str(), NULL, 10);
|
||||
|
||||
if (clientTimestamp > 0) {
|
||||
setSystemTime(clientTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
// 웹소켓 이벤트 핸들러
|
||||
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
|
||||
switch(type) {
|
||||
@@ -539,6 +556,12 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length
|
||||
Serial.printf("WebSocket #%u 연결: %d.%d.%d.%d\n",
|
||||
num, ip[0], ip[1], ip[2], ip[3]);
|
||||
sendFileList(num);
|
||||
|
||||
// 시간 동기화 상태 전송
|
||||
String syncStatus = "{\"type\":\"timeSyncStatus\",\"synchronized\":";
|
||||
syncStatus += timeSyncStatus.synchronized ? "true" : "false";
|
||||
syncStatus += ",\"syncCount\":" + String(timeSyncStatus.syncCount) + "}";
|
||||
webSocket.sendTXT(num, syncStatus);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -577,6 +600,9 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length
|
||||
txMessages[i].active = false;
|
||||
}
|
||||
}
|
||||
else if (msg.indexOf("\"cmd\":\"syncTime\"") >= 0) {
|
||||
handleTimeSync(msg);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -632,6 +658,8 @@ void webUpdateTask(void *pvParameters) {
|
||||
status += "\"sdReady\":" + String(sdCardReady ? "true" : "false") + ",";
|
||||
status += "\"msgCount\":" + String(totalMsgCount) + ",";
|
||||
status += "\"msgSpeed\":" + String(msgPerSecond) + ",";
|
||||
status += "\"timeSync\":" + String(timeSyncStatus.synchronized ? "true" : "false") + ",";
|
||||
status += "\"syncCount\":" + String(timeSyncStatus.syncCount) + ",";
|
||||
|
||||
if (loggingEnabled && logFile) {
|
||||
status += "\"currentFile\":\"" + String(currentFilename) + "\"";
|
||||
@@ -650,7 +678,7 @@ void webUpdateTask(void *pvParameters) {
|
||||
bool first = true;
|
||||
|
||||
for (int i = 0; i < RECENT_MSG_COUNT; i++) {
|
||||
if (recentData[i].msg.timestamp > 0) {
|
||||
if (recentData[i].msg.timestamp_us > 0) {
|
||||
CANMessage* msg = &recentData[i].msg;
|
||||
|
||||
if (!first) canBatch += ",";
|
||||
@@ -670,7 +698,9 @@ void webUpdateTask(void *pvParameters) {
|
||||
if (j < msg->dlc - 1) canBatch += " ";
|
||||
}
|
||||
|
||||
canBatch += "\",\"timestamp\":" + String(msg->timestamp);
|
||||
// 마이크로초 타임스탬프를 밀리초로 변환하여 전송
|
||||
uint64_t timestamp_ms = msg->timestamp_us / 1000;
|
||||
canBatch += "\",\"timestamp\":" + String((uint32_t)timestamp_ms);
|
||||
canBatch += ",\"count\":" + String(recentData[i].count) + "}";
|
||||
}
|
||||
}
|
||||
@@ -692,7 +722,7 @@ void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
Serial.println("\n========================================");
|
||||
Serial.println(" ESP32 CAN Logger with Web Interface ");
|
||||
Serial.println(" ESP32 CAN Logger with Time Sync ");
|
||||
Serial.println("========================================");
|
||||
|
||||
memset(recentData, 0, sizeof(recentData));
|
||||
@@ -711,7 +741,6 @@ void setup() {
|
||||
if (SD.begin(VSPI_CS, vspi)) {
|
||||
sdCardReady = true;
|
||||
Serial.println("✓ SD 카드 초기화 완료");
|
||||
scanExistingFiles();
|
||||
} else {
|
||||
Serial.println("✗ SD 카드 초기화 실패");
|
||||
}
|
||||
@@ -793,6 +822,7 @@ void setup() {
|
||||
Serial.println(" - Transmit: /transmit");
|
||||
Serial.println(" - Graph: /graph");
|
||||
Serial.println("========================================\n");
|
||||
Serial.println("⚠️ 시간 동기화를 위해 웹페이지에서 '⏰ 시간 동기화' 버튼을 클릭하세요");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
@@ -801,11 +831,12 @@ void loop() {
|
||||
|
||||
static uint32_t lastPrint = 0;
|
||||
if (millis() - lastPrint > 10000) {
|
||||
Serial.printf("[상태] 큐: %d/%d | 로깅: %s | SD: %s | RX: %lu | TX: %lu\n",
|
||||
Serial.printf("[상태] 큐: %d/%d | 로깅: %s | SD: %s | RX: %lu | TX: %lu | 시간동기: %s\n",
|
||||
uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE,
|
||||
loggingEnabled ? "ON " : "OFF",
|
||||
sdCardReady ? "OK" : "NO",
|
||||
totalMsgCount, totalTxCount);
|
||||
totalMsgCount, totalTxCount,
|
||||
timeSyncStatus.synchronized ? "OK" : "NO");
|
||||
lastPrint = millis();
|
||||
}
|
||||
}
|
||||
170
index.h
170
index.h
@@ -52,6 +52,61 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
.nav a:hover { background: #34495e; }
|
||||
.nav a.active { background: #3498db; }
|
||||
.content { padding: 15px; }
|
||||
|
||||
.time-sync-banner {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: white;
|
||||
padding: 15px 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
box-shadow: 0 4px 15px rgba(240, 147, 251, 0.4);
|
||||
}
|
||||
.time-sync-info {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.time-info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
.time-label {
|
||||
font-size: 0.75em;
|
||||
opacity: 0.9;
|
||||
font-weight: 600;
|
||||
}
|
||||
.time-value {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 1.1em;
|
||||
font-weight: 700;
|
||||
}
|
||||
.btn-time-sync {
|
||||
background: white;
|
||||
color: #f5576c;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1em;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 3px 10px rgba(0,0,0,0.2);
|
||||
}
|
||||
.btn-time-sync:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
|
||||
}
|
||||
.btn-time-sync:active {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
@@ -187,6 +242,17 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
.nav a { padding: 8px 12px; font-size: 0.85em; }
|
||||
table { min-width: 400px; }
|
||||
th, td { padding: 6px 4px; font-size: 0.75em; }
|
||||
.time-sync-banner {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 12px 15px;
|
||||
}
|
||||
.time-sync-info {
|
||||
gap: 10px;
|
||||
}
|
||||
.time-value {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -194,7 +260,7 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Byun CAN Logger</h1>
|
||||
<p>Real-time CAN Bus Monitor & Data Logger</p>
|
||||
<p>Real-time CAN Bus Monitor & Data Logger with Time Sync</p>
|
||||
</div>
|
||||
|
||||
<div class="nav">
|
||||
@@ -204,6 +270,20 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<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>
|
||||
<div class="time-info-item">
|
||||
<span class="time-label">🕐 현재 시간</span>
|
||||
<span class="time-value" id="current-time">--:--:--</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-time-sync" onclick="syncTime()">⏰ 시간 동기화</button>
|
||||
</div>
|
||||
|
||||
<div class="status-grid">
|
||||
<div class="status-card" id="logging-status">
|
||||
<h3>LOGGING</h3>
|
||||
@@ -221,9 +301,13 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
<h3>SPEED</h3>
|
||||
<div class="value" id="msg-speed">0/s</div>
|
||||
</div>
|
||||
<div class="status-card" id="file-status" style="grid-column: span 2;">
|
||||
<div class="status-card" id="time-sync-card">
|
||||
<h3>TIME SYNC</h3>
|
||||
<div class="value" id="sync-count">0</div>
|
||||
</div>
|
||||
<div class="status-card" id="file-status">
|
||||
<h3>CURRENT FILE</h3>
|
||||
<div class="value" id="current-file" style="font-size: 1em;">-</div>
|
||||
<div class="value" id="current-file" style="font-size: 0.85em;">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -276,16 +360,54 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
let reconnectInterval;
|
||||
let canMessages = {};
|
||||
let messageOrder = [];
|
||||
|
||||
// 마지막 업데이트 추적용
|
||||
let lastMessageData = {};
|
||||
|
||||
const speedNames = ['125 Kbps', '250 Kbps', '500 Kbps', '1 Mbps'];
|
||||
|
||||
// 현재 시간 업데이트
|
||||
function updateCurrentTime() {
|
||||
const now = new Date();
|
||||
const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
|
||||
now.getMinutes().toString().padStart(2, '0') + ':' +
|
||||
now.getSeconds().toString().padStart(2, '0');
|
||||
document.getElementById('current-time').textContent = timeStr;
|
||||
}
|
||||
|
||||
setInterval(updateCurrentTime, 1000);
|
||||
updateCurrentTime();
|
||||
|
||||
// 시간 동기화 함수
|
||||
function syncTime() {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
const timestamp = Date.now();
|
||||
ws.send(JSON.stringify({
|
||||
cmd: 'syncTime',
|
||||
timestamp: timestamp
|
||||
}));
|
||||
|
||||
document.getElementById('sync-status').textContent = '동기화 중...';
|
||||
|
||||
setTimeout(() => {
|
||||
const now = new Date();
|
||||
const dateStr = now.getFullYear() + '-' +
|
||||
(now.getMonth() + 1).toString().padStart(2, '0') + '-' +
|
||||
now.getDate().toString().padStart(2, '0') + ' ' +
|
||||
now.getHours().toString().padStart(2, '0') + ':' +
|
||||
now.getMinutes().toString().padStart(2, '0') + ':' +
|
||||
now.getSeconds().toString().padStart(2, '0');
|
||||
document.getElementById('sync-status').textContent = '✓ ' + dateStr;
|
||||
}, 200);
|
||||
|
||||
console.log('시간 동기화 전송:', new Date(timestamp).toLocaleString());
|
||||
} else {
|
||||
alert('WebSocket이 연결되지 않았습니다!');
|
||||
}
|
||||
}
|
||||
|
||||
function saveCanSpeed() {
|
||||
const speed = document.getElementById('can-speed').value;
|
||||
try {
|
||||
localStorage.setItem('canSpeed', speed);
|
||||
window.localStorage.setItem('canSpeed', speed);
|
||||
console.log('Saved CAN speed:', speedNames[speed]);
|
||||
} catch(e) {
|
||||
console.error('Failed to save CAN speed:', e);
|
||||
@@ -294,7 +416,7 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
|
||||
function loadCanSpeed() {
|
||||
try {
|
||||
const savedSpeed = localStorage.getItem('canSpeed');
|
||||
const savedSpeed = window.localStorage.getItem('canSpeed');
|
||||
if (savedSpeed !== null) {
|
||||
document.getElementById('can-speed').value = savedSpeed;
|
||||
console.log('Restored CAN speed:', speedNames[savedSpeed]);
|
||||
@@ -319,11 +441,18 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
console.log('WebSocket connected');
|
||||
clearInterval(reconnectInterval);
|
||||
setTimeout(() => { refreshFiles(); }, 500);
|
||||
|
||||
// 연결 직후 자동 시간 동기화
|
||||
setTimeout(() => {
|
||||
syncTime();
|
||||
console.log('자동 시간 동기화 실행');
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
ws.onclose = function() {
|
||||
console.log('WebSocket disconnected');
|
||||
reconnectInterval = setInterval(initWebSocket, 3000);
|
||||
document.getElementById('sync-status').textContent = '연결 끊김';
|
||||
};
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
@@ -342,6 +471,13 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
} else {
|
||||
updateFileList(data.files);
|
||||
}
|
||||
} else if (data.type === 'timeSyncStatus') {
|
||||
if (data.synchronized) {
|
||||
document.getElementById('sync-count').textContent = data.syncCount;
|
||||
const card = document.getElementById('time-sync-card');
|
||||
card.classList.add('status-on');
|
||||
card.classList.remove('status-off');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -350,6 +486,7 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
const loggingCard = document.getElementById('logging-status');
|
||||
const sdCard = document.getElementById('sd-status');
|
||||
const fileCard = document.getElementById('file-status');
|
||||
const timeSyncCard = document.getElementById('time-sync-card');
|
||||
|
||||
if (data.logging) {
|
||||
loggingCard.classList.add('status-on');
|
||||
@@ -371,6 +508,18 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
sdCard.querySelector('.value').textContent = 'NOT READY';
|
||||
}
|
||||
|
||||
if (data.timeSync) {
|
||||
timeSyncCard.classList.add('status-on');
|
||||
timeSyncCard.classList.remove('status-off');
|
||||
} else {
|
||||
timeSyncCard.classList.add('status-off');
|
||||
timeSyncCard.classList.remove('status-on');
|
||||
}
|
||||
|
||||
if (data.syncCount !== undefined) {
|
||||
document.getElementById('sync-count').textContent = data.syncCount;
|
||||
}
|
||||
|
||||
if (data.currentFile && data.currentFile !== '') {
|
||||
fileCard.classList.add('status-on');
|
||||
fileCard.classList.remove('status-off');
|
||||
@@ -424,7 +573,6 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
const msg = canMessages[canId];
|
||||
let row = existingRows.get(canId);
|
||||
|
||||
// 이전 데이터와 비교하여 실제 변경사항 확인
|
||||
const prevData = lastMessageData[canId];
|
||||
const hasChanged = !prevData ||
|
||||
prevData.data !== msg.data ||
|
||||
@@ -437,7 +585,6 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
row.cells[3].textContent = msg.updateCount;
|
||||
row.cells[4].textContent = msg.timestamp;
|
||||
|
||||
// 실제로 변경된 경우에만 flash 효과
|
||||
if (hasChanged) {
|
||||
row.classList.add('flash-row');
|
||||
setTimeout(() => row.classList.remove('flash-row'), 300);
|
||||
@@ -455,7 +602,6 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
setTimeout(() => row.classList.remove('flash-row'), 300);
|
||||
}
|
||||
|
||||
// 현재 데이터 저장
|
||||
lastMessageData[canId] = {
|
||||
data: msg.data,
|
||||
dlc: msg.dlc,
|
||||
@@ -474,9 +620,7 @@ const char index_html[] PROGMEM = R"rawliteral(
|
||||
}
|
||||
|
||||
files.sort((a, b) => {
|
||||
const numA = parseInt(a.name.match(/\d+/)?.[0] || '0');
|
||||
const numB = parseInt(b.name.match(/\d+/)?.[0] || '0');
|
||||
return numB - numA;
|
||||
return b.name.localeCompare(a.name);
|
||||
});
|
||||
|
||||
fileList.innerHTML = '';
|
||||
|
||||
Reference in New Issue
Block a user