Files
esp32s3_canlogger_mcp2515/index.h
2026-01-02 21:37:48 +00:00

2044 lines
79 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.

This file contains Unicode characters that might be confused with other characters. 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: #f8f9fa;
padding: 0;
border-bottom: 2px solid #e0e0e0;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
.nav a {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 15px 20px;
text-decoration: none;
color: #666;
font-weight: 500;
font-size: 0.95em;
border-bottom: 3px solid transparent;
transition: all 0.3s;
white-space: nowrap;
min-width: 120px;
}
.nav a:hover {
background: #e9ecef;
color: #667eea;
transform: translateY(-2px);
}
.nav a.active {
color: #764ba2;
border-bottom-color: #764ba2;
background: white;
font-weight: 600;
}
.content { padding: 15px; }
/* 전력 경고 배너 */
.power-warning {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: white; /* ⭐ #666 → 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: #666;
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.5);
}
.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, #667eea 0%, #764ba2 100%);
opacity: 0.7;
}
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; /* ⭐ #666 → 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; /* ⭐ #666 → 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;
}
tbody 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;
border: 1px solid #e0e0e0;
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-item.selected {
border: 2px solid #667eea;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
}
.file-checkbox {
width: 20px;
height: 20px;
cursor: pointer;
}
.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: #666;
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; /* ⭐ #666 → white */
}
.download-btn:hover {
background: linear-gradient(135deg, #5568d3 0%, #66409e 100%);
}
.comment-btn {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white; /* ⭐ #666 → white */
}
.comment-btn:hover {
background: linear-gradient(135deg, #e77fe8 0%, #e44459 100%);
}
.delete-btn {
background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);
color: white; /* ⭐ #666 → 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: #666;
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: #666;
}
.btn-modal-cancel {
background: #ddd;
color: #333;
}
/* 파일 형식 선택 라디오 버튼 스타일 */
.format-selector {
display: flex;
align-items: center;
gap: 15px;
background: white;
padding: 8px 15px;
border-radius: 8px;
border: 2px solid #667eea;
}
.format-selector label {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
font-weight: 600;
color: #2c3e50;
font-size: 0.9em;
transition: all 0.3s;
}
.format-selector label:hover {
color: #667eea;
}
.format-selector input[type="radio"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #667eea;
}
.format-info {
font-size: 0.75em;
color: #7f8c8d;
margin-left: 3px;
}
/* ========== 반응형 디자인 ========== */
@media (max-width: 768px) {
body {
padding: 5px;
}
.container {
border-radius: 10px;
}
.header {
padding: 15px;
flex-direction: column;
text-align: center;
gap: 10px;
}
.header h1 {
font-size: 1.4em;
}
.nav {
padding: 5px;
}
.nav a {
padding: 12px 15px;
font-size: 0.85em;
}
.content {
padding: 15px;
}
.btn {
padding: 10px 20px;
font-size: 13px;
}
}
@media (min-width: 769px) and (max-width: 1024px) {
.header h1 {
font-size: 1.6em;
}
.nav a {
padding: 13px 18px;
font-size: 0.9em;
}
.content {
padding: 25px;
}
}
@media (min-width: 1025px) {
.content {
padding: 30px;
}
}
/* 🎯 Auto Trigger 스타일 */
.auto-trigger-section {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.auto-trigger-section h3 {
margin-top: 0;
margin-bottom: 15px;
color: #667eea;
font-size: 1.3em;
}
.trigger-control {
margin-bottom: 20px;
padding: 10px;
background: #f8f9fa;
border-radius: 8px;
}
.trigger-control label {
font-size: 1.1em;
font-weight: 500;
cursor: pointer;
}
.trigger-group {
margin-bottom: 25px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.trigger-group h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-size: 1.1em;
}
.trigger-group select {
margin-bottom: 15px;
padding: 8px;
border-radius: 6px;
border: 1px solid #ddd;
}
.trigger-card {
background: white;
padding: 12px;
margin-bottom: 10px;
border-radius: 8px;
border: 1px solid #e0e0e0;
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.trigger-card input[type="text"],
.trigger-card input[type="number"] {
padding: 6px 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.9em;
}
.trigger-card input[type="text"] {
width: 100px;
}
.trigger-card input[type="number"] {
width: 80px;
}
.trigger-card select {
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.9em;
}
.trigger-card .btn-delete {
background: #dc3545;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
}
.trigger-card .btn-delete:hover {
background: #c82333;
}
.btn-add-trigger {
background: #28a745;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 1em;
margin-top: 10px;
}
.btn-add-trigger:hover {
background: #218838;
}
.btn-save-triggers {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 30px;
border-radius: 8px;
cursor: pointer;
font-size: 1.1em;
width: 100%;
margin-top: 15px;
}
.btn-save-triggers:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.trigger-status {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.85em;
font-weight: 500;
margin-left: 10px;
}
.trigger-status.active {
background: #d4edda;
color: #155724;
}
.trigger-status.inactive {
background: #f8d7da;
color: #721c24;
}
</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>
<a href="/serial">📟 Serial1</a>
<a href="/serial2">📟 Serial2</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 status-off" 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 class="status-card status-off" id="serial1-logging-status">
<h3>SERIAL1 LOG</h3>
<div class="value" id="serial1-file" style="font-size: 0.75em;">OFF</div>
</div>
<div class="status-card status-off" id="serial2-logging-status">
<h3>SERIAL2 LOG</h3>
<div class="value" id="serial2-file" style="font-size: 0.75em;">OFF</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 (TX/RX + ACK)</option>
<option value="1">Listen-Only (RX only, no ACK)</option>
<option value="2">Loopback (Self-test)</option>
<option value="3">Transmit Only (no ACK, auto mode switch)</option>
</select>
<button onclick="setMcpMode()">Apply</button>
<span id="mode-status" style="color: #11998e; font-size: 0.85em; font-weight: 600;"></span>
</div>
<!-- CAN -->
<div class="control-row">
<label style="font-weight: 600; color: #333;">📁 File Format:</label>
<div class="format-selector">
<label>
<input type="radio" name="can-format" value="bin" checked>
<span>📦 BIN</span>
<span class="format-info">(Binary - Fast, Small)</span>
</label>
<label>
<input type="radio" name="can-format" value="csv">
<span>📄 CSV</span>
<span class="format-info">(Text - Excel Ready)</span>
</label>
</div>
</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="hardwareReset()" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); color: white;">🔄 Hardware Reset</button>
<button onclick="refreshFiles()">Refresh Files</button>
<button onclick="clearMessages()">Clear Display</button>
</div>
</div>
<!-- 🎯 Auto Trigger -->
<div class="auto-trigger-section">
<h3>
🎯 Auto Trigger
<span id="autoTriggerStatus" class="trigger-status inactive">Disabled</span>
</h3>
<!-- Enable/Disable -->
<div class="trigger-control">
<label>
<input type="checkbox" id="autoTriggerEnabled" onchange="toggleAutoTrigger()">
Enable Auto Trigger
</label>
</div>
<!-- 🆕 Auto Trigger -->
<div class="trigger-group" id="autoTriggerFormatGroup" style="display: none;">
<h4>📁 Log Format</h4>
<select id="autoTriggerFormat" style="width: 100%; padding: 8px; border-radius: 5px; border: 1px solid #444; background: #2a2a2a; color: white;">
<option value="bin">📦 Binary (BIN) - High Speed</option>
<option value="csv">📄 CSV - Human Readable</option>
</select>
</div>
<!-- -->
<div class="trigger-group" id="startTriggerGroup" style="display: none;">
<h4> Start Logging Conditions</h4>
<select id="startLogic">
<option value="OR">OR (Any condition matches)</option>
<option value="AND">AND (All conditions match)</option>
</select>
<div id="startTriggersList">
<!-- -->
</div>
<button class="btn-add-trigger" onclick="addStartTrigger()"> Add Start Condition</button>
</div>
<!-- -->
<div class="trigger-group" id="stopTriggerGroup" style="display: none;">
<h4> Stop Logging Conditions</h4>
<select id="stopLogic">
<option value="OR">OR (Any condition matches)</option>
<option value="AND">AND (All conditions match)</option>
</select>
<div id="stopTriggersList">
<!-- -->
</div>
<button class="btn-add-trigger" onclick="addStopTrigger()"> Add Stop Condition</button>
</div>
<button class="btn-save-triggers" onclick="saveAutoTriggers()" style="display: none;" id="saveTriggerBtn">
💾 Save Auto Trigger Settings
</button>
</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="control-row" style="margin-bottom: 10px;">
<button onclick="selectAllFiles()" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">Select All</button>
<button onclick="deselectAllFiles()" style="background: linear-gradient(135deg, #bdc3c7 0%, #95a5a6 100%);">Deselect All</button>
<button onclick="downloadSelectedFiles()" style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);">Download Selected</button>
<button onclick="deleteSelectedFiles()" style="background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);">Delete Selected</button>
</div>
<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 = {};
let lastCanCounts = {}; // ★ 각 CAN ID별 마지막 count 저장
const speedNames = {0: '125K', 1: '250K', 2: '500K', 3: '1M'};
const modeNames = {0: 'NORMAL (TX/RX+ACK)', 1: 'LISTEN-ONLY (RX)', 2: 'LOOPBACK', 3: 'TRANSMIT-ONLY (TX)'};
let currentLoggingFile = '';
let commentingFile = '';
// hasInitialSync 제거 - 매번 자동 동기화
// 🎯 Auto Trigger 전역 변수
let startTriggers = [];
let stopTriggers = [];
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';
// ⭐⭐⭐ 자동 시간 동기화 (페이지 로드 시 항상 실행)
setTimeout(function() {
syncTimeFromPhone();
console.log(' Auto time sync on page load');
}, 500); // WebSocket 안정화 대기
// ⭐ WebSocket 연결되면 즉시 파일 목록 요청
setTimeout(function() {
refreshFiles();
}, 1000);
// 🎯 Auto Trigger 설정 로드
setTimeout(loadAutoTriggers, 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) {
// 🆕 바이너리/비정상 데이터 필터링
if (typeof event.data !== 'string') {
console.warn('Non-string WebSocket data received, ignoring');
return;
}
// 🆕 빈 데이터 필터링
if (!event.data || event.data.trim() === '') {
return;
}
try {
const data = JSON.parse(event.data);
// 🆕 유효한 JSON 객체인지 확인
if (!data || typeof data !== 'object') {
console.warn('Invalid JSON object received');
return;
}
console.log('WebSocket message received:', data.type);
if (data.type === 'status' || data.type === 'update') {
updateStatus(data);
if (data.messages && data.messages.length > 0) {
updateCanMessages(data.messages);
}
} else if (data.type === 'canBatch') {
updateCanBatch(data.messages);
} else if (data.type === 'files') {
console.log('Files received:', data.files ? data.files.length : 0, 'files');
console.log('Files data:', data.files);
updateFileList(data.files || data.list);
} 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');
}
} else if (data.type === 'hwReset') {
console.log(' - ESP32 ...');
}
// 🎯 Auto Trigger 메시지 처리 - try 블록 내부로 이동!
else if (data.type === 'autoTriggers') {
document.getElementById('autoTriggerEnabled').checked = data.enabled;
document.getElementById('startLogic').value = data.startLogic;
document.getElementById('stopLogic').value = data.stopLogic;
// 🆕 로그 형식 설정
if (data.logFormat) {
document.getElementById('autoTriggerFormat').value = data.logFormat;
}
startTriggers = data.startTriggers || [];
stopTriggers = data.stopTriggers || [];
if (data.enabled) {
document.getElementById('autoTriggerFormatGroup').style.display = 'block';
document.getElementById('startTriggerGroup').style.display = 'block';
document.getElementById('stopTriggerGroup').style.display = 'block';
document.getElementById('saveTriggerBtn').style.display = 'block';
document.getElementById('autoTriggerStatus').textContent = 'Enabled';
document.getElementById('autoTriggerStatus').className = 'trigger-status active';
}
renderStartTriggers();
renderStopTriggers();
}
else if (data.type === 'startTriggersSet') {
console.log(' Start triggers saved:', data.count);
}
else if (data.type === 'stopTriggersSet') {
console.log(' Stop triggers saved:', data.count);
}
// Auto Trigger 상태 업데이트 (update 메시지에서)
if (data.autoTriggerEnabled !== undefined) {
const status = document.getElementById('autoTriggerStatus');
if (data.autoTriggerActive) {
status.textContent = 'Active (Triggered)';
status.className = 'trigger-status active';
} else if (data.autoTriggerEnabled) {
status.textContent = 'Enabled (Waiting)';
status.className = 'trigger-status active';
} else {
status.textContent = 'Disabled';
status.className = 'trigger-status inactive';
}
}
} 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';
}
// 큐 상태 - ★ 수정: queueUsed/queueSize 또는 queueSize/queueMax 둘 다 지원
const queueUsed = data.queueUsed !== undefined ? data.queueUsed : data.queueSize;
const queueMax = data.queueSize !== undefined && data.queueMax !== undefined ? data.queueMax :
data.queueSize !== undefined ? 1000 : 1000;
const actualMax = data.queueMax || data.queueSize || 1000;
const actualUsed = data.queueUsed !== undefined ? data.queueUsed : 0;
const queuePercent = (actualUsed / actualMax) * 100;
document.getElementById('queue-bar').style.width = queuePercent + '%';
document.getElementById('queue-text').textContent = actualUsed + ' / ' + actualMax;
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');
}
// 메시지 카운트
if (data.totalMsg !== undefined) {
document.getElementById('msg-count').textContent = data.totalMsg.toLocaleString();
}
if (data.msgPerSec !== undefined) {
document.getElementById('msg-speed').textContent = data.msgPerSec + '/s';
}
// 시간 동기화 - ★ 수정: timeSync 또는 timeSynced 둘 다 지원
const timeSyncCard = document.getElementById('time-sync-card');
const timeSynced = data.timeSynced !== undefined ? data.timeSynced : data.timeSync;
if (timeSynced) {
timeSyncCard.classList.add('status-on');
timeSyncCard.classList.remove('status-off');
document.getElementById('sync-count').textContent = data.rtcSyncCnt || data.rtcSyncCount || 0;
} 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');
}
// ⭐ Serial1 로깅 상태
const serial1Card = document.getElementById('serial1-logging-status');
const serial1File = document.getElementById('serial1-file');
if (data.serialLogging && data.currentSerialFile) {
serial1Card.classList.remove('status-off');
serial1Card.classList.add('status-on');
serial1File.textContent = data.currentSerialFile;
} else {
serial1Card.classList.remove('status-on');
serial1Card.classList.add('status-off');
serial1File.textContent = 'OFF';
}
// ⭐ Serial2 로깅 상태
const serial2Card = document.getElementById('serial2-logging-status');
const serial2File = document.getElementById('serial2-file');
if (data.serial2Logging && data.currentSerial2File) {
serial2Card.classList.remove('status-off');
serial2Card.classList.add('status-on');
serial2File.textContent = data.currentSerial2File;
} else {
serial2Card.classList.remove('status-on');
serial2Card.classList.add('status-off');
serial2File.textContent = 'OFF';
}
}
// ★ 추가: update 타입에서 오는 messages 배열 처리
// count가 증가한 경우에만 업데이트 (새로운 CAN 메시지가 있을 때만)
function updateCanMessages(messages) {
let hasNewData = false;
messages.forEach(msg => {
if (msg.count > 0) {
const canId = msg.id.toString(16).toUpperCase();
const prevCount = lastCanCounts[canId] || 0;
// count가 증가한 경우에만 업데이트
if (msg.count > prevCount) {
if (!canMessages[canId]) {
messageOrder.push(canId);
}
// 데이터 배열을 16진수 문자열로 변환
let dataStr = '';
if (msg.data && msg.data.length > 0) {
dataStr = msg.data.map(b => b.toString(16).toUpperCase().padStart(2, '0')).join(' ');
}
canMessages[canId] = {
timestamp: Date.now(),
dlc: msg.dlc,
data: dataStr,
updateCount: msg.count
};
lastCanCounts[canId] = msg.count;
hasNewData = true;
}
}
});
// 새 데이터가 있을 때만 테이블 업데이트
if (hasNewData) {
updateCanTable();
}
}
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) {
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) {
console.log('updateFileList called, files:', files); // ⭐ 디버그
const fileList = document.getElementById('file-list');
if (!files || files.length === 0) {
console.log('No files to display'); // ⭐ 디버그
fileList.innerHTML = '<p style="text-align: center; color: #666; font-size: 0.9em;">No log files</p>';
return;
}
console.log('Displaying', files.length, 'files'); // ⭐ 디버그
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 =
'<input type="checkbox" class="file-checkbox" data-filename="' + file.name + '" ' +
'onchange="toggleFileSelection(this)" ' +
(isLogging ? 'disabled' : '') + '>' +
'<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) {
// 선택된 형식 가져오기
let canFormat = 'bin'; // 기본값
const formatRadios = document.getElementsByName('can-format');
for (const radio of formatRadios) {
if (radio.checked) {
canFormat = radio.value;
break;
}
}
// 로깅 시작 명령 전송
ws.send(JSON.stringify({
cmd: 'startLogging',
format: canFormat
}));
console.log('Start logging: format=' + canFormat);
setTimeout(() => {
refreshFiles();
}, 1000);
}
}
function stopLogging() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({cmd: 'stopLogging'}));
console.log('Stop logging command sent');
// ⭐⭐⭐ 로깅 종료 시 Messages 카운트 리셋
document.getElementById('msg-count').textContent = '0';
document.getElementById('msg-speed').textContent = '0/s';
console.log(' Messages count reset to 0');
setTimeout(() => {
refreshFiles();
}, 1000);
}
}
function hardwareReset() {
if (!confirm('ESP32를 ?\n\n WebSocket .\n재부팅 .')) {
return;
}
if (ws && ws.readyState === WebSocket.OPEN) {
console.log('🔄 ...');
ws.send(JSON.stringify({cmd: 'hwReset'}));
// 메시지 카운터 리셋
document.getElementById('msg-count').textContent = '0';
document.getElementById('msg-speed').textContent = '0/s';
console.log(' ');
// 3초 후 알림
setTimeout(() => {
alert('ESP32가 ...\n\n10초 !');
}, 1000);
} else {
alert('WebSocket이 !');
console.error('WebSocket not connected');
}
}
function refreshFiles() {
console.log('Requesting file list...'); // ⭐ 디버그
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({cmd: 'getFiles'}));
console.log('getFiles command sent'); // ⭐ 디버그
} else {
console.error('WebSocket not connected, readyState:', ws ? ws.readyState : 'null'); // ⭐ 디버그
}
}
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();
}
}
function toggleFileSelection(checkbox) {
const fileItem = checkbox.closest('.file-item');
if (checkbox.checked) {
fileItem.classList.add('selected');
} else {
fileItem.classList.remove('selected');
}
}
function selectAllFiles() {
const checkboxes = document.querySelectorAll('.file-checkbox:not(:disabled)');
checkboxes.forEach(cb => {
cb.checked = true;
cb.closest('.file-item').classList.add('selected');
});
}
function deselectAllFiles() {
const checkboxes = document.querySelectorAll('.file-checkbox');
checkboxes.forEach(cb => {
cb.checked = false;
cb.closest('.file-item').classList.remove('selected');
});
}
function getSelectedFiles() {
const selected = [];
const checkboxes = document.querySelectorAll('.file-checkbox:checked');
checkboxes.forEach(cb => {
selected.push(cb.dataset.filename);
});
return selected;
}
function downloadSelectedFiles() {
const selected = getSelectedFiles();
if (selected.length === 0) {
alert('Please select files to download.');
return;
}
selected.forEach((filename, index) => {
setTimeout(() => {
downloadFile(filename);
}, index * 500);
});
console.log('Downloading files:', selected);
}
function deleteSelectedFiles() {
const selected = getSelectedFiles();
if (selected.length === 0) {
alert('Please select files to delete.');
return;
}
const message = 'Are you sure you want to delete ' + selected.length + ' file(s)?\n\n' +
'Files:\n' + selected.join('\n') + '\n\n' +
'This action cannot be undone.';
if (!confirm(message)) {
return;
}
selected.forEach(filename => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({cmd: 'deleteFile', filename: filename}));
}
});
console.log('Deleting files:', selected);
setTimeout(() => {
deselectAllFiles();
}, 1000);
}
window.addEventListener('load', function() {
loadCanSpeed();
loadMcpMode();
});
initWebSocket();
// ============================================
// 🎯 Auto Trigger 관련 함수
// ============================================
// Auto Trigger 토글
function toggleAutoTrigger() {
const enabled = document.getElementById('autoTriggerEnabled').checked;
const formatGroup = document.getElementById('autoTriggerFormatGroup');
const startGroup = document.getElementById('startTriggerGroup');
const stopGroup = document.getElementById('stopTriggerGroup');
const saveBtn = document.getElementById('saveTriggerBtn');
const status = document.getElementById('autoTriggerStatus');
if (enabled) {
formatGroup.style.display = 'block';
startGroup.style.display = 'block';
stopGroup.style.display = 'block';
saveBtn.style.display = 'block';
status.textContent = 'Enabled';
status.className = 'trigger-status active';
if (startTriggers.length === 0) addStartTrigger();
if (stopTriggers.length === 0) addStopTrigger();
} else {
formatGroup.style.display = 'none';
startGroup.style.display = 'none';
stopGroup.style.display = 'none';
saveBtn.style.display = 'none';
status.textContent = 'Disabled';
status.className = 'trigger-status inactive';
}
const logFormat = document.getElementById('autoTriggerFormat').value;
ws.send(JSON.stringify({ cmd: 'setAutoTrigger', enabled: enabled, logFormat: logFormat }));
}
function addStartTrigger() {
const trigger = {
canId: '0x100',
startBit: 0,
bitLength: 8,
op: '==',
value: 0,
enabled: true
};
startTriggers.push(trigger);
renderStartTriggers();
}
function addStopTrigger() {
const trigger = {
canId: '0x100',
startBit: 0,
bitLength: 8,
op: '==',
value: 0,
enabled: true
};
stopTriggers.push(trigger);
renderStopTriggers();
}
function renderStartTriggers() {
const container = document.getElementById('startTriggersList');
container.innerHTML = '';
startTriggers.forEach((trigger, index) => {
const card = document.createElement('div');
card.className = 'trigger-card';
card.innerHTML = `
<label>
<input type="checkbox" ${trigger.enabled ? 'checked' : ''}
onchange="startTriggers[${index}].enabled = this.checked">
Enable
</label>
<input type="text" placeholder="CAN ID (0x100)" value="${trigger.canId}"
onchange="startTriggers[${index}].canId = this.value">
<input type="number" placeholder="Start Bit" min="0" max="63" value="${trigger.startBit}"
onchange="startTriggers[${index}].startBit = parseInt(this.value)">
<input type="number" placeholder="Bit Length" min="1" max="64" value="${trigger.bitLength}"
onchange="startTriggers[${index}].bitLength = parseInt(this.value)">
<select onchange="startTriggers[${index}].op = this.value">
<option value="==" ${trigger.op === '==' ? 'selected' : ''}> == </option>
<option value="!=" ${trigger.op === '!=' ? 'selected' : ''}> != </option>
<option value=">" ${trigger.op === '>' ? 'selected' : ''}> &gt; </option>
<option value="<" ${trigger.op === '<' ? 'selected' : ''}> &lt; </option>
<option value=">=" ${trigger.op === '>=' ? 'selected' : ''}> &gt;= </option>
<option value="<=" ${trigger.op === '<=' ? 'selected' : ''}> &lt;= </option>
</select>
<input type="number" placeholder="Value" value="${trigger.value}"
onchange="startTriggers[${index}].value = parseInt(this.value)">
<button class="btn-delete" onclick="removeStartTrigger(${index})">🗑️</button>
`;
container.appendChild(card);
});
}
function renderStopTriggers() {
const container = document.getElementById('stopTriggersList');
container.innerHTML = '';
stopTriggers.forEach((trigger, index) => {
const card = document.createElement('div');
card.className = 'trigger-card';
card.innerHTML = `
<label>
<input type="checkbox" ${trigger.enabled ? 'checked' : ''}
onchange="stopTriggers[${index}].enabled = this.checked">
Enable
</label>
<input type="text" placeholder="CAN ID (0x100)" value="${trigger.canId}"
onchange="stopTriggers[${index}].canId = this.value">
<input type="number" placeholder="Start Bit" min="0" max="63" value="${trigger.startBit}"
onchange="stopTriggers[${index}].startBit = parseInt(this.value)">
<input type="number" placeholder="Bit Length" min="1" max="64" value="${trigger.bitLength}"
onchange="stopTriggers[${index}].bitLength = parseInt(this.value)">
<select onchange="stopTriggers[${index}].op = this.value">
<option value="==" ${trigger.op === '==' ? 'selected' : ''}> == </option>
<option value="!=" ${trigger.op === '!=' ? 'selected' : ''}> != </option>
<option value=">" ${trigger.op === '>' ? 'selected' : ''}> &gt; </option>
<option value="<" ${trigger.op === '<' ? 'selected' : ''}> &lt; </option>
<option value=">=" ${trigger.op === '>=' ? 'selected' : ''}> &gt;= </option>
<option value="<=" ${trigger.op === '<=' ? 'selected' : ''}> &lt;= </option>
</select>
<input type="number" placeholder="Value" value="${trigger.value}"
onchange="stopTriggers[${index}].value = parseInt(this.value)">
<button class="btn-delete" onclick="removeStopTrigger(${index})">🗑️</button>
`;
container.appendChild(card);
});
}
function removeStartTrigger(index) {
if (confirm(' ?')) {
startTriggers.splice(index, 1);
renderStartTriggers();
}
}
function removeStopTrigger(index) {
if (confirm(' ?')) {
stopTriggers.splice(index, 1);
renderStopTriggers();
}
}
function saveAutoTriggers() {
const startLogic = document.getElementById('startLogic').value;
const stopLogic = document.getElementById('stopLogic').value;
const logFormat = document.getElementById('autoTriggerFormat').value;
// 로그 형식 설정 먼저 전송
ws.send(JSON.stringify({ cmd: 'setAutoTriggerFormat', logFormat: logFormat }));
ws.send(JSON.stringify({ cmd: 'setStartTriggers', logic: startLogic, triggers: startTriggers }));
ws.send(JSON.stringify({ cmd: 'setStopTriggers', logic: stopLogic, triggers: stopTriggers }));
alert(' Auto Trigger !');
}
function loadAutoTriggers() {
ws.send(JSON.stringify({ cmd: 'getAutoTriggers' }));
}
</script>
</body>
</html>
)rawliteral";
#endif