Files
250928_esp32_spi_sdcard_ads…/index.h
byun 37da387904 파일 커멘트 추가, can mode 항목 추가,
현재 고쳐햐 할 점으로 can 중지 시 que가 적산됨

2025년 11월 6일 오전 3:30 GMT+9 작성 내용 기반으로 

esp32 로 mcp2515, sdcard 를 spi 로 연결하여 can 최대 속도를 빠짐 없이 실시간 로깅하는 코드야 RTC DS3231로 i2c 연결(softwire 라이브러리)하여 esp32의 시간데이터를 보정하는 기능으로 보다 신뢰성 있는 로깅을 해주지, wifi ap로 연결하면 monitoring페이지가 있고 모니터링 페이지에 초기 접속 시(로깅하지 않은상태) 핸드폰의 시간을 rtc에 저장 하고 시스템 시간을 핸드폰시간으로 맞춰줘 그리고 log files 항목에 파일 리스트를 보여주는데 사용자가 커멘트입력하여 차후 어떤 파일인지 알 수 있게 해줘 또한 컨트롤 패널 항목에 추가로 MCP2515의 CAN 모드를 넣어 MCP2515컨트롤 loop-back, normal, listen-only모드 등을 넣어 사용자가 상황에 맞게 mcp2515를 동작시키게 해줘,  settings 페이지에서는 timezone은 불필요한것 같아 삭제 해줘, 이 요구사항을 우선 첨부한 코드를 분석하고 해당 요구사항을 적용하여 수정해줘, 참고로 transmit 페이지는 파일용량이 커서 첨부에 뺐으니 빼고 수정해줘(수정과 상관 없는 graph.h, graph_viewer.h,transmit.h 는 첨부에 뺐음)
2025-11-07 09:54:14 +00:00

1237 lines
45 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#ifndef INDEX_H
#define INDEX_H
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Byun CAN Logger</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 10px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
text-align: center;
}
.header h1 { font-size: 1.8em; margin-bottom: 5px; }
.header p { opacity: 0.9; font-size: 0.9em; }
.nav {
background: #2c3e50;
padding: 10px;
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
.nav a {
color: white;
text-decoration: none;
padding: 10px 15px;
border-radius: 5px;
transition: all 0.3s;
font-size: 0.9em;
white-space: nowrap;
}
.nav a:hover { background: #34495e; }
.nav a.active { background: #3498db; }
.content { padding: 15px; }
/* 전력 경고 배너 */
.power-warning {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: white;
padding: 12px 20px;
border-radius: 8px;
margin-bottom: 15px;
display: none;
align-items: center;
gap: 10px;
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4);
animation: pulse 2s ease-in-out infinite;
}
.power-warning.show { display: flex; }
.power-warning-icon { font-size: 1.5em; }
.power-warning-text { flex: 1; font-weight: 600; }
.power-voltage {
font-family: 'Courier New', monospace;
font-size: 1.2em;
font-weight: 700;
background: rgba(255,255,255,0.2);
padding: 5px 12px;
border-radius: 5px;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.85; }
}
/* 전력 상태 표시 */
.power-status {
background: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%);
color: white;
padding: 10px 15px;
border-radius: 8px;
margin-bottom: 15px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 3px 10px rgba(86, 171, 47, 0.3);
flex-wrap: wrap;
gap: 10px;
}
.power-status.low {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
box-shadow: 0 3px 10px rgba(255, 107, 107, 0.3);
}
.power-status-label {
font-size: 0.85em;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.power-status-values {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.power-status-item {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.power-status-item-label {
font-size: 0.7em;
opacity: 0.9;
}
.power-status-value {
font-family: 'Courier New', monospace;
font-size: 1.2em;
font-weight: 700;
}
/* 큐 상태 표시 */
.queue-status {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 10px 15px;
border-radius: 8px;
margin-bottom: 15px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.3);
}
.queue-status.warning {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
box-shadow: 0 3px 10px rgba(240, 147, 251, 0.3);
}
.queue-status.critical {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
box-shadow: 0 3px 10px rgba(255, 107, 107, 0.3);
animation: pulse 2s ease-in-out infinite;
}
.queue-info {
display: flex;
align-items: center;
gap: 10px;
}
.queue-bar-container {
flex: 1;
min-width: 150px;
height: 20px;
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
overflow: hidden;
position: relative;
}
.queue-bar {
height: 100%;
background: rgba(255, 255, 255, 0.8);
border-radius: 10px;
transition: width 0.3s ease;
}
.queue-text {
position: absolute;
width: 100%;
text-align: center;
line-height: 20px;
font-size: 0.75em;
font-weight: 700;
color: white;
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
}
.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;
white-space: nowrap;
}
.btn-time-sync:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
margin-bottom: 20px;
}
.status-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px;
border-radius: 10px;
text-align: center;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
transition: all 0.3s;
}
.status-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
}
.status-card h3 {
font-size: 0.75em;
opacity: 0.9;
margin-bottom: 8px;
letter-spacing: 1px;
}
.status-card .value {
font-size: 1.5em;
font-weight: 700;
font-family: 'Courier New', monospace;
}
.status-card.status-on {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.status-card.status-off {
background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);
}
h2 {
color: #333;
margin: 20px 0 10px 0;
font-size: 1.3em;
border-bottom: 3px solid #667eea;
padding-bottom: 8px;
}
.control-panel {
background: #f8f9fa;
padding: 15px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.control-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.control-row:last-child {
margin-bottom: 0;
}
.control-row label {
font-weight: 600;
color: #333;
white-space: nowrap;
}
.control-row select {
padding: 8px 12px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 0.95em;
transition: all 0.3s;
background: white;
}
.control-row select:focus {
outline: none;
border-color: #667eea;
}
.control-row button {
padding: 8px 16px;
border: none;
border-radius: 5px;
font-size: 0.95em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.control-row button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.can-table-container {
overflow-x: auto;
background: white;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
min-width: 600px;
}
thead {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
th {
padding: 12px 8px;
text-align: left;
font-weight: 600;
font-size: 0.85em;
letter-spacing: 0.5px;
}
td {
padding: 10px 8px;
border-bottom: 1px solid #eee;
font-size: 0.9em;
}
tr:hover {
background: #f8f9fa;
}
.mono {
font-family: 'Courier New', monospace;
font-weight: 500;
}
@keyframes flash {
0%, 100% { background-color: transparent; }
50% { background-color: #fff3cd; }
}
.flash-row {
animation: flash 0.3s ease-in-out;
}
.file-list {
background: #f8f9fa;
border-radius: 10px;
padding: 15px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.file-item {
background: white;
padding: 12px;
margin-bottom: 10px;
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
transition: all 0.3s;
flex-wrap: wrap;
gap: 10px;
}
.file-item:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.file-item:last-child {
margin-bottom: 0;
}
.file-item.logging {
border: 2px solid #11998e;
background: linear-gradient(135deg, rgba(17, 153, 142, 0.05) 0%, rgba(56, 239, 125, 0.05) 100%);
}
.file-info {
flex: 1;
min-width: 200px;
}
.file-name {
font-weight: 600;
color: #333;
margin-bottom: 4px;
font-family: 'Courier New', monospace;
font-size: 0.95em;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.file-comment {
font-size: 0.85em;
color: #666;
font-style: italic;
margin-top: 4px;
}
.file-size {
color: #666;
font-size: 0.85em;
}
.file-actions {
display: flex;
gap: 8px;
}
.logging-badge {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
color: white;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.75em;
font-weight: 700;
letter-spacing: 0.5px;
}
.download-btn, .delete-btn, .comment-btn {
padding: 6px 12px;
border: none;
border-radius: 5px;
font-size: 0.85em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
white-space: nowrap;
}
.download-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.download-btn:hover {
background: linear-gradient(135deg, #5568d3 0%, #66409e 100%);
}
.comment-btn {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
.comment-btn:hover {
background: linear-gradient(135deg, #e77fe8 0%, #e44459 100%);
}
.delete-btn {
background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);
color: white;
}
.delete-btn:hover {
background: linear-gradient(135deg, #d32f3f 0%, #e53935 100%);
}
.delete-btn:disabled, .comment-btn:disabled {
background: #cccccc;
cursor: not-allowed;
opacity: 0.6;
}
/* 모달 스타일 */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: white;
margin: 15% auto;
padding: 25px;
border-radius: 10px;
width: 90%;
max-width: 500px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
}
.modal-header {
font-size: 1.3em;
font-weight: 700;
color: #333;
margin-bottom: 15px;
}
.modal-body {
margin-bottom: 20px;
}
.modal-body label {
display: block;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.modal-body input, .modal-body textarea {
width: 100%;
padding: 10px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 1em;
font-family: inherit;
}
.modal-body input:focus, .modal-body textarea:focus {
outline: none;
border-color: #667eea;
}
.modal-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.modal-buttons button {
padding: 10px 20px;
border: none;
border-radius: 5px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-modal-save {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
color: white;
}
.btn-modal-cancel {
background: #ddd;
color: #333;
}
@media (max-width: 768px) {
body { padding: 5px; }
.header h1 { font-size: 1.5em; }
.header p { font-size: 0.85em; }
.content { padding: 10px; }
.status-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; }
.status-card { padding: 10px; }
.status-card h3 { font-size: 0.7em; }
.status-card .value { font-size: 1.2em; }
h2 { font-size: 1.1em; }
.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;
}
.btn-time-sync {
width: 100%;
padding: 10px 20px;
}
.file-actions {
width: 100%;
justify-content: stretch;
}
.download-btn, .delete-btn, .comment-btn {
flex: 1;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚗 Byun CAN Logger v2.0</h1>
<p>Real-time CAN Bus Monitor & Logger + Phone Time Sync + MCP2515 Mode Control</p>
</div>
<div class="nav">
<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="time-sync-banner">
<div class="time-sync-info">
<div class="time-info-item">
<div class="time-label">CURRENT TIME</div>
<div class="time-value" id="current-time">--:--:--</div>
</div>
<div class="time-info-item">
<div class="time-label">CONNECTION</div>
<div class="time-value" id="sync-status">연결 중...</div>
</div>
</div>
<button class="btn-time-sync" onclick="syncTimeFromPhone()">📱 Sync from Phone</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 status-off" id="logging-status">
<h3>LOGGING</h3>
<div class="value">OFF</div>
</div>
<div class="status-card status-off" id="sd-status">
<h3>SD CARD</h3>
<div class="value">NOT READY</div>
</div>
<div class="status-card">
<h3>MESSAGES</h3>
<div class="value" id="msg-count">0</div>
</div>
<div class="status-card">
<h3>SPEED</h3>
<div class="value" id="msg-speed">0/s</div>
</div>
<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="mcp-mode-card">
<h3>MCP MODE</h3>
<div class="value" id="mcp-mode-display">NORMAL</div>
</div>
<div class="status-card" id="file-status">
<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">
<h2>Control Panel</h2>
<div class="control-row">
<label for="can-speed">CAN Speed:</label>
<select id="can-speed">
<option value="0">125 Kbps</option>
<option value="1">250 Kbps</option>
<option value="2">500 Kbps</option>
<option value="3" selected>1 Mbps</option>
</select>
<button onclick="setCanSpeed()">Apply</button>
<span id="speed-status" style="color: #11998e; font-size: 0.85em; font-weight: 600;"></span>
</div>
<div class="control-row">
<label for="mcp-mode">MCP2515 Mode:</label>
<select id="mcp-mode">
<option value="0" selected>Normal</option>
<option value="1">Listen-Only</option>
<option value="2">Loopback</option>
</select>
<button onclick="setMcpMode()">Apply</button>
<span id="mode-status" style="color: #11998e; font-size: 0.85em; font-weight: 600;"></span>
</div>
<div class="control-row">
<button onclick="startLogging()" style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);">Start Logging</button>
<button onclick="stopLogging()" style="background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);">Stop Logging</button>
<button onclick="refreshFiles()">Refresh Files</button>
<button onclick="clearMessages()">Clear Display</button>
</div>
</div>
<h2>CAN Messages (by ID)</h2>
<div class="can-table-container">
<table>
<thead>
<tr>
<th>ID</th>
<th>DLC</th>
<th>Data</th>
<th>Count</th>
<th>Time(ms)</th>
</tr>
</thead>
<tbody id="can-messages"></tbody>
</table>
</div>
<h2>Log Files</h2>
<div class="file-list" id="file-list">
<p style="text-align: center; color: #666; font-size: 0.9em;">Loading...</p>
</div>
</div>
</div>
<!-- -->
<div id="commentModal" class="modal">
<div class="modal-content">
<div class="modal-header">Add File Comment</div>
<div class="modal-body">
<label>File: <span id="comment-filename" style="font-family: 'Courier New', monospace;"></span></label>
<label for="comment-input" style="margin-top: 10px;">Comment:</label>
<textarea id="comment-input" rows="3" maxlength="128" placeholder="Enter a description for this file..."></textarea>
</div>
<div class="modal-buttons">
<button class="btn-modal-cancel" onclick="closeCommentModal()">Cancel</button>
<button class="btn-modal-save" onclick="saveComment()">Save</button>
</div>
</div>
</div>
<script>
let ws;
let canMessages = {};
let messageOrder = [];
let lastMessageData = {};
const speedNames = {0: '125K', 1: '250K', 2: '500K', 3: '1M'};
const modeNames = {0: 'NORMAL', 1: 'LISTEN-ONLY', 2: 'LOOPBACK'};
let currentLoggingFile = '';
let commentingFile = '';
let hasInitialSync = false; // 초기 동기화 완료 여부
function updateCurrentTime() {
const now = new Date();
const timeStr = now.toLocaleTimeString('ko-KR', {hour12: false});
document.getElementById('current-time').textContent = timeStr;
}
setInterval(updateCurrentTime, 1000);
updateCurrentTime();
function saveCanSpeed() {
const speed = document.getElementById('can-speed').value;
localStorage.setItem('canSpeed', speed);
}
function loadCanSpeed() {
const savedSpeed = localStorage.getItem('canSpeed');
if (savedSpeed !== null) {
document.getElementById('can-speed').value = savedSpeed;
}
}
function saveMcpMode() {
const mode = document.getElementById('mcp-mode').value;
localStorage.setItem('mcpMode', mode);
}
function loadMcpMode() {
const savedMode = localStorage.getItem('mcpMode');
if (savedMode !== null) {
document.getElementById('mcp-mode').value = savedMode;
}
}
function syncTimeFromPhone() {
const now = new Date();
const timeData = {
cmd: 'syncTimeFromPhone',
year: now.getFullYear(),
month: now.getMonth() + 1,
day: now.getDate(),
hour: now.getHours(),
minute: now.getMinutes(),
second: now.getSeconds()
};
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(timeData));
console.log('Phone time sync command sent:', timeData);
}
}
function initWebSocket() {
ws = new WebSocket('ws://' + window.location.hostname + ':81/');
ws.onopen = function() {
console.log('WebSocket connected');
document.getElementById('sync-status').textContent = '';
document.getElementById('sync-status').style.color = '#38ef7d';
// 초기 접속 시 자동 시간 동기화
if (!hasInitialSync) {
setTimeout(function() {
syncTimeFromPhone();
hasInitialSync = true;
}, 500);
}
};
ws.onclose = function() {
console.log('WebSocket disconnected');
document.getElementById('sync-status').textContent = ' ';
document.getElementById('sync-status').style.color = '#f45c43';
setTimeout(initWebSocket, 3000);
};
ws.onerror = function(error) {
console.error('WebSocket error:', error);
};
ws.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'status') {
updateStatus(data);
} else if (data.type === 'canBatch') {
updateCanBatch(data.messages);
} else if (data.type === 'files') {
updateFileList(data.files);
} else if (data.type === 'deleteResult') {
handleDeleteResult(data);
} else if (data.type === 'timeSyncResult') {
if (data.success) {
console.log('Time sync successful');
}
} else if (data.type === 'commentResult') {
if (data.success) {
console.log('Comment saved successfully');
}
}
} catch (e) {
console.error('Parse error:', e);
}
};
}
function updateStatus(data) {
// 로깅 상태
const loggingCard = document.getElementById('logging-status');
if (data.logging) {
loggingCard.classList.remove('status-off');
loggingCard.classList.add('status-on');
loggingCard.querySelector('.value').textContent = 'ON';
} else {
loggingCard.classList.remove('status-on');
loggingCard.classList.add('status-off');
loggingCard.querySelector('.value').textContent = 'OFF';
}
// SD 카드 상태
const sdCard = document.getElementById('sd-status');
if (data.sdReady) {
sdCard.classList.remove('status-off');
sdCard.classList.add('status-on');
sdCard.querySelector('.value').textContent = 'READY';
} else {
sdCard.classList.remove('status-on');
sdCard.classList.add('status-off');
sdCard.querySelector('.value').textContent = 'NOT READY';
}
// 큐 상태
const queuePercent = (data.queueSize / data.queueMax) * 100;
document.getElementById('queue-bar').style.width = queuePercent + '%';
document.getElementById('queue-text').textContent = data.queueSize + ' / ' + data.queueMax;
const queueStatus = document.getElementById('queue-status');
queueStatus.classList.remove('warning', 'critical');
if (queuePercent > 90) {
queueStatus.classList.add('critical');
} else if (queuePercent > 70) {
queueStatus.classList.add('warning');
}
// 메시지 카운트
document.getElementById('msg-count').textContent = data.totalMsg.toLocaleString();
document.getElementById('msg-speed').textContent = data.msgPerSec + '/s';
// 시간 동기화
const timeSyncCard = document.getElementById('time-sync-card');
if (data.timeSynced) {
timeSyncCard.classList.add('status-on');
timeSyncCard.classList.remove('status-off');
document.getElementById('sync-count').textContent = data.rtcSyncCount;
} else {
timeSyncCard.classList.remove('status-on');
timeSyncCard.classList.add('status-off');
document.getElementById('sync-count').textContent = '0';
}
// MCP 모드
if (data.mcpMode !== undefined) {
document.getElementById('mcp-mode-display').textContent = modeNames[data.mcpMode];
}
// 현재 파일
currentLoggingFile = data.currentFile || '';
if (data.currentFile) {
document.getElementById('current-file').textContent = data.currentFile;
} else {
document.getElementById('current-file').textContent = '-';
}
// 파일 크기
if (data.fileSize > 0) {
document.getElementById('current-file-size').textContent = formatBytes(data.fileSize);
} else {
document.getElementById('current-file-size').textContent = '0 B';
}
// 전압 상태
if (data.voltage !== undefined) {
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');
}
}
function addCanMessage(data) {
const canId = data.id;
if (!canMessages[canId]) {
messageOrder.push(canId);
}
canMessages[canId] = {
timestamp: data.timestamp,
dlc: data.dlc,
data: data.data,
updateCount: data.count
};
}
function updateCanBatch(messages) {
messages.forEach(msg => {
const canId = msg.id;
if (!canMessages[canId]) {
messageOrder.push(canId);
}
canMessages[canId] = {
timestamp: msg.timestamp,
dlc: msg.dlc,
data: msg.data,
updateCount: msg.count
};
});
updateCanTable();
}
function updateCanTable() {
const tbody = document.getElementById('can-messages');
const existingRows = new Map();
Array.from(tbody.rows).forEach(row => {
existingRows.set(row.dataset.canId, row);
});
messageOrder.forEach(canId => {
const msg = canMessages[canId];
let row = existingRows.get(canId);
const prevData = lastMessageData[canId];
const hasChanged = !prevData ||
prevData.data !== msg.data ||
prevData.dlc !== msg.dlc ||
prevData.timestamp !== msg.timestamp;
// Extended CAN ID 처리 (bit 31 제거)
let displayId = parseInt(canId, 16);
if (displayId & 0x80000000) {
displayId = displayId & 0x1FFFFFFF;
}
const displayIdStr = '0x' + displayId.toString(16).toUpperCase().padStart(8, '0');
if (row) {
// ID 셀 업데이트
row.cells[0].textContent = displayIdStr;
row.cells[1].textContent = msg.dlc;
row.cells[2].textContent = msg.data;
row.cells[3].textContent = msg.updateCount;
row.cells[4].textContent = msg.timestamp;
if (hasChanged) {
row.classList.add('flash-row');
setTimeout(() => row.classList.remove('flash-row'), 300);
}
} else {
row = tbody.insertRow();
row.dataset.canId = canId;
row.innerHTML =
'<td class="mono">' + displayIdStr + '</td>' +
'<td>' + msg.dlc + '</td>' +
'<td class="mono">' + msg.data + '</td>' +
'<td>' + msg.updateCount + '</td>' +
'<td>' + msg.timestamp + '</td>';
row.classList.add('flash-row');
setTimeout(() => row.classList.remove('flash-row'), 300);
}
lastMessageData[canId] = {
data: msg.data,
dlc: msg.dlc,
timestamp: msg.timestamp,
updateCount: msg.updateCount
};
});
}
function updateFileList(files) {
const fileList = document.getElementById('file-list');
if (!files || files.length === 0) {
fileList.innerHTML = '<p style="text-align: center; color: #666; font-size: 0.9em;">No log files</p>';
return;
}
files.sort((a, b) => {
return b.name.localeCompare(a.name);
});
fileList.innerHTML = '';
files.forEach(file => {
const isLogging = (currentLoggingFile && file.name === currentLoggingFile);
const fileItem = document.createElement('div');
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>';
// 커멘트 추가
let commentHtml = '';
if (file.comment) {
commentHtml = '<div class="file-comment">' + file.comment + '</div>';
}
fileItem.innerHTML =
'<div class="file-info">' +
nameHtml +
'<div class="file-size">' + formatBytes(file.size) + '</div>' +
commentHtml +
'</div>' +
'<div class="file-actions">' +
'<button class="comment-btn" onclick="openCommentModal(\'' + file.name + '\')" ' +
(isLogging ? 'disabled title="Cannot add comment while logging"' : '') + '>Comment</button>' +
'<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);
});
}
function formatBytes(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
}
function setCanSpeed() {
const speed = document.getElementById('can-speed').value;
const speedName = speedNames[speed];
ws.send(JSON.stringify({cmd: 'setSpeed', speed: parseInt(speed)}));
saveCanSpeed();
const statusSpan = document.getElementById('speed-status');
if (statusSpan) {
statusSpan.textContent = ' Applied: ' + speedName;
statusSpan.style.color = '#11998e';
setTimeout(() => {
statusSpan.textContent = '';
}, 3000);
}
console.log('CAN speed set to:', speedName);
}
function setMcpMode() {
const mode = document.getElementById('mcp-mode').value;
const modeName = modeNames[mode];
ws.send(JSON.stringify({cmd: 'setMcpMode', mode: parseInt(mode)}));
saveMcpMode();
const statusSpan = document.getElementById('mode-status');
if (statusSpan) {
statusSpan.textContent = ' Applied: ' + modeName;
statusSpan.style.color = '#11998e';
setTimeout(() => {
statusSpan.textContent = '';
}, 3000);
}
console.log('MCP2515 mode set to:', modeName);
}
function startLogging() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({cmd: 'startLogging'}));
console.log('Start logging command sent');
}
}
function stopLogging() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({cmd: 'stopLogging'}));
console.log('Stop logging command sent');
}
}
function refreshFiles() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({cmd: 'getFiles'}));
}
}
function clearMessages() {
canMessages = {};
messageOrder = [];
lastMessageData = {};
document.getElementById('can-messages').innerHTML = '';
}
function downloadFile(filename) {
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);
}
}
function openCommentModal(filename) {
commentingFile = filename;
document.getElementById('comment-filename').textContent = filename;
document.getElementById('comment-input').value = '';
document.getElementById('commentModal').style.display = 'block';
}
function closeCommentModal() {
document.getElementById('commentModal').style.display = 'none';
commentingFile = '';
}
function saveComment() {
const comment = document.getElementById('comment-input').value.trim();
if (comment.length === 0) {
alert('Please enter a comment.');
return;
}
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
cmd: 'addComment',
filename: commentingFile,
comment: comment
}));
console.log('Comment added for:', commentingFile);
}
closeCommentModal();
}
// 모달 외부 클릭 시 닫기
window.onclick = function(event) {
const modal = document.getElementById('commentModal');
if (event.target == modal) {
closeCommentModal();
}
}
window.addEventListener('load', function() {
loadCanSpeed();
loadMcpMode();
});
initWebSocket();
setTimeout(() => { refreshFiles(); }, 2000);
</script>
</body>
</html>
)rawliteral";
#endif