웹디바이스로 시간 동기화

This commit is contained in:
2025-10-09 20:41:29 +00:00
parent 5e2da19075
commit 3e0e0286e7
2 changed files with 260 additions and 85 deletions

170
index.h
View File

@@ -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 = '';