웹디바이스로 시간 동기화
This commit is contained in:
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