Files
2025-12-03 08:35:08 +00:00

925 lines
34 KiB
C++
Raw Permalink 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 GRAPH_H
#define GRAPH_H
const char graph_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>CAN Signal Graph Setup - Byun 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, #43cea2 0%, #185a9d 100%);
min-height: 100vh;
padding: 10px;
}
.container {
max-width: 1600px;
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, #43cea2 0%, #185a9d 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;
}
.nav a:hover { background: #34495e; }
.nav a.active { background: #3498db; }
.content { padding: 15px; }
.dbc-upload {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin-bottom: 15px;
border: 2px solid #e0e0e0;
}
.upload-area {
border: 3px dashed #43cea2;
border-radius: 10px;
padding: 30px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
background: white;
}
.upload-area:hover {
background: #f0f9ff;
border-color: #185a9d;
transform: translateY(-2px);
}
.upload-area input { display: none; }
.upload-icon {
font-size: 3em;
margin-bottom: 10px;
}
.signal-selector {
background: #f8f9fa;
padding: 15px;
border-radius: 10px;
margin-bottom: 15px;
border: 2px solid #e0e0e0;
}
.search-container {
margin-bottom: 15px;
position: relative;
}
.search-box {
width: 100%;
padding: 12px 12px 12px 40px;
border: 2px solid #43cea2;
border-radius: 8px;
font-size: 0.95em;
transition: all 0.3s;
}
.search-box:focus {
outline: none;
border-color: #185a9d;
box-shadow: 0 0 0 3px rgba(67, 206, 162, 0.1);
}
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #43cea2;
font-size: 1.2em;
}
.search-info {
margin-top: 8px;
padding: 8px 12px;
background: #e3f2fd;
border-radius: 5px;
font-size: 0.85em;
color: #185a9d;
font-weight: 600;
}
.no-results {
text-align: center;
padding: 40px 20px;
color: #666;
font-size: 0.95em;
}
.no-results-icon {
font-size: 3em;
margin-bottom: 10px;
opacity: 0.5;
}
.sort-controls {
display: flex;
gap: 10px;
margin-bottom: 15px;
align-items: center;
flex-wrap: wrap;
}
.sort-label {
font-weight: 600;
color: #333;
font-size: 0.9em;
}
.sort-btn {
padding: 8px 15px;
border: 2px solid #43cea2;
background: white;
border-radius: 5px;
font-size: 0.85em;
cursor: pointer;
transition: all 0.3s;
font-weight: 600;
}
.sort-btn:hover {
background: #f0f9ff;
transform: translateY(-2px);
}
.sort-btn.active {
background: #43cea2;
color: white;
}
.signal-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 10px;
margin-top: 15px;
}
.signal-item {
background: white;
padding: 12px;
border-radius: 8px;
border: 2px solid #ddd;
cursor: pointer;
transition: all 0.3s;
position: relative;
}
.signal-item:hover {
border-color: #43cea2;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.signal-item.selected {
border-color: #185a9d;
background: linear-gradient(135deg, rgba(67, 206, 162, 0.1) 0%, rgba(24, 90, 157, 0.1) 100%);
}
.signal-item.selected::before {
content: '';
position: absolute;
top: 8px;
right: 8px;
background: #185a9d;
color: white;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.9em;
}
.signal-name {
font-weight: 600;
color: #333;
margin-bottom: 5px;
font-size: 0.95em;
padding-right: 30px;
}
.signal-info {
font-size: 0.8em;
color: #666;
line-height: 1.5;
}
.signal-info-row {
margin-bottom: 3px;
}
.highlight {
background-color: #ffeb3b;
padding: 2px 0;
font-weight: 600;
}
.controls {
display: flex;
gap: 8px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 5px;
font-size: 0.9em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary { background: linear-gradient(135deg, #43cea2 0%, #185a9d 100%); color: white; }
.btn-success { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); color: white; }
.btn-danger { background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%); color: white; }
.btn-secondary { background: linear-gradient(135deg, #bdc3c7 0%, #95a5a6 100%); color: white; }
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
h2 {
color: #333;
margin: 20px 0 15px 0;
padding-bottom: 8px;
border-bottom: 3px solid #43cea2;
font-size: 1.3em;
}
.status {
padding: 12px 15px;
border-radius: 8px;
margin-bottom: 15px;
font-size: 0.9em;
border-left: 4px solid;
}
.status.success {
background: #d4edda;
color: #155724;
border-color: #28a745;
}
.status.error {
background: #f8d7da;
color: #721c24;
border-color: #dc3545;
}
.status.info {
background: #d1ecf1;
color: #0c5460;
border-color: #17a2b8;
}
.selection-info {
background: linear-gradient(135deg, #e3f2fd 0%, #f0f9ff 100%);
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 15px;
border-left: 4px solid #185a9d;
font-size: 0.95em;
color: #333;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.selection-info strong {
color: #185a9d;
font-size: 1.1em;
}
.selection-count {
background: #185a9d;
color: white;
padding: 8px 16px;
border-radius: 20px;
font-weight: 700;
font-size: 1em;
}
.info-box {
background: linear-gradient(135deg, #e3f2fd 0%, #f0f9ff 100%);
padding: 15px 20px;
border-radius: 8px;
margin-top: 15px;
border-left: 4px solid #185a9d;
}
.info-box p {
color: #333;
font-size: 0.9em;
margin: 0;
line-height: 1.6;
}
.info-box strong {
color: #185a9d;
}
@media (max-width: 768px) {
body { padding: 5px; }
.header h1 { font-size: 1.5em; }
.content { padding: 10px; }
.signal-grid {
grid-template-columns: 1fr;
gap: 8px;
}
h2 { font-size: 1.1em; }
.controls {
flex-direction: column;
}
.btn {
width: 100%;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 CAN Signal Graph Setup</h1>
<p>Configure real-time signal visualization (Max 20 signals)</p>
</div>
<div class="nav">
<a href="/">📊 Monitor</a>
<a href="/transmit">📤 Transmit</a>
<a href="/graph" class="active">📈 Graph</a>
<a href="/graph-view">📊 Graph View</a>
<a href="/settings"> Settings</a>
<a href="/serial">📟 Serial</a>
</div>
<div class="content">
<div id="status" class="status" style="display:none;"></div>
<h2>📁 Upload DBC File</h2>
<div class="dbc-upload">
<div class="upload-area" onclick="document.getElementById('dbc-file').click()">
<input type="file" id="dbc-file" accept=".dbc" onchange="loadDBCFile(event)">
<div class="upload-icon">📄</div>
<p style="font-size: 1.1em; margin-bottom: 8px; font-weight: 600;">Click or Drag & Drop DBC File</p>
<p style="color: #666; font-size: 0.85em;" id="dbc-status">No file loaded</p>
</div>
</div>
<div id="signal-section" style="display:none;">
<h2>🎯 Select Signals (Max 20)</h2>
<div class="selection-info">
<div>
<strong>Selected Signals</strong>
</div>
<div class="selection-count">
<span id="selected-count">0</span> / 20
</div>
</div>
<div class="controls">
<button class="btn btn-success" onclick="startGraphing()">▶️ Start Graph Viewer</button>
<button class="btn btn-danger" onclick="stopGraphing()">⏹️ Stop Logging</button>
<button class="btn btn-secondary" onclick="clearSelection()">🗑️ Clear Selection</button>
<button class="btn btn-primary" onclick="debugDBCInfo()">🐛 Debug Info</button>
</div>
<div class="signal-selector">
<div class="search-container">
<span class="search-icon">🔍</span>
<input type="text"
id="search-box"
class="search-box"
placeholder="Search by signal name, CAN ID (hex/dec), message name, or unit..."
oninput="filterSignals()">
<div id="search-info" class="search-info" style="display:none;"></div>
</div>
<div class="sort-controls">
<span class="sort-label">📊 Sort by:</span>
<button class="sort-btn active" id="sort-selection" onclick="setSortMode('selection')">
Selection Order
</button>
<button class="sort-btn" id="sort-name-asc" onclick="setSortMode('name-asc')">
🔼 Name (AZ)
</button>
<button class="sort-btn" id="sort-name-desc" onclick="setSortMode('name-desc')">
🔽 Name (ZA)
</button>
<button class="sort-btn" id="sort-id-asc" onclick="setSortMode('id-asc')">
🔼 CAN ID
</button>
</div>
<div id="signal-list" class="signal-grid"></div>
</div>
<div class="info-box">
<p>
<strong> How to use:</strong>
</p>
<p style="margin-top: 8px;">
1. Select signals from the list above (click to toggle)<br>
2. Click <strong>"▶️ Start Graph Viewer"</strong> to open a new window<br>
3. The graph will display real-time data from the selected signals<br>
4. Your selections are automatically saved in browser storage
</p>
</div>
</div>
</div>
</div>
<script>
let ws;
let dbcData = { messages: {}, valueTables: {} };
let selectedSignals = [];
let allSignals = [];
let filteredSignals = [];
let sortMode = 'selection';
let searchQuery = '';
const MAX_SIGNALS = 20;
function initWebSocket() {
ws = new WebSocket('ws://' + window.location.hostname + ':81');
ws.onopen = function() {
console.log(' WebSocket connected');
};
ws.onclose = function() {
console.log(' WebSocket disconnected');
setTimeout(initWebSocket, 3000);
};
ws.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
// 필요시 처리
} catch(e) {
console.error('Parse error:', e);
}
};
}
function loadDBCFile(event) {
const file = event.target.files[0];
if (!file) return;
showStatus('Loading DBC file: ' + file.name, 'info');
const reader = new FileReader();
reader.onload = function(e) {
const content = e.target.result;
parseDBCContent(content);
document.getElementById('dbc-status').textContent = ' ' + file.name;
saveDBCToLocalStorage(content, file.name);
};
reader.readAsText(file);
}
function saveDBCToLocalStorage(content, filename) {
try {
localStorage.setItem('dbc_content', content);
localStorage.setItem('dbc_filename', filename);
console.log(' DBC saved to localStorage:', filename);
} catch(e) {
console.error(' Failed to save DBC:', e);
showStatus('Failed to save DBC to storage', 'error');
}
}
function loadDBCFromLocalStorage() {
try {
const content = localStorage.getItem('dbc_content');
const filename = localStorage.getItem('dbc_filename');
if (content && filename) {
parseDBCContent(content);
document.getElementById('dbc-status').textContent = ' ' + filename + ' (restored)';
showStatus('DBC file restored: ' + filename, 'success');
console.log(' DBC restored:', filename);
return true;
}
} catch(e) {
console.error(' Failed to load DBC:', e);
}
return false;
}
function parseDBCContent(content) {
console.log('=== Parsing DBC File ===');
dbcData = { messages: {}, valueTables: {} };
allSignals = [];
const lines = content.split('\n');
let currentMessage = null;
// 1단계: VAL_ (Value Table) 파싱
for (let line of lines) {
line = line.trim();
if (line.startsWith('VAL_ ')) {
const match = line.match(/VAL_\s+(\d+)\s+(\w+)\s+(.+);/);
if (match) {
const msgId = parseInt(match[1]);
const sigName = match[2];
const valuesStr = match[3];
const normalizedMsgId = (msgId & 0x80000000) ? (msgId & 0x1FFFFFFF) : msgId;
const key = normalizedMsgId + '_' + sigName;
dbcData.valueTables[key] = {};
const valueMatches = valuesStr.matchAll(/(\d+)\s+"([^"]+)"/g);
for (let vm of valueMatches) {
dbcData.valueTables[key][parseInt(vm[1])] = vm[2];
}
console.log(' Value Table: ' + key, dbcData.valueTables[key]);
}
}
}
// 2단계: BO_ (Message) 및 SG_ (Signal) 파싱
for (let line of lines) {
line = line.trim();
if (line.startsWith('BO_ ')) {
const match = line.match(/BO_\s+(\d+)\s+(\w+)\s*:\s*(\d+)/);
if (match) {
let id = parseInt(match[1]);
const name = match[2];
const dlc = parseInt(match[3]);
if (id & 0x80000000) {
id = id & 0x1FFFFFFF;
}
currentMessage = {
id: id,
name: name,
dlc: dlc,
signals: []
};
dbcData.messages[id] = currentMessage;
console.log('Message: ID=' + id + ' (0x' + id.toString(16).toUpperCase() + ') Name=' + name + ' DLC=' + dlc);
}
}
else if (line.startsWith('SG_ ') && currentMessage) {
const match = line.match(/SG_\s+(\w+)\s*:\s*(\d+)\|(\d+)@([01])([+-])\s*$([^,]+),([^)]+)$\s*$$([^$$]+)$$\s*"([^"]*)"/);
if (match) {
const signalName = match[1];
const signal = {
name: signalName,
startBit: parseInt(match[2]),
bitLength: parseInt(match[3]),
byteOrder: match[4] === '0' ? 'motorola' : 'intel',
signed: match[5] === '-',
factor: parseFloat(match[6]),
offset: parseFloat(match[7]),
minMax: match[8],
unit: match[9],
messageId: currentMessage.id,
messageName: currentMessage.name,
messageDlc: currentMessage.dlc
};
const vtKey = currentMessage.id + '_' + signalName;
if (dbcData.valueTables[vtKey]) {
signal.valueTable = dbcData.valueTables[vtKey];
console.log(' Signal (with VT): ' + signalName);
} else {
console.log(' Signal: ' + signalName);
}
currentMessage.signals.push(signal);
allSignals.push(signal);
}
}
}
console.log('=== Parsing Complete ===');
console.log('Total Messages: ' + Object.keys(dbcData.messages).length);
console.log('Total Signals: ' + allSignals.length);
console.log('Total Value Tables: ' + Object.keys(dbcData.valueTables).length);
filteredSignals = [...allSignals];
displaySignals();
showStatus('DBC loaded: ' + Object.keys(dbcData.messages).length + ' messages, ' + allSignals.length + ' signals', 'success');
}
function filterSignals() {
searchQuery = document.getElementById('search-box').value.toLowerCase().trim();
if (searchQuery === '') {
filteredSignals = [...allSignals];
document.getElementById('search-info').style.display = 'none';
} else {
filteredSignals = allSignals.filter(signal => {
const nameMatch = signal.name.toLowerCase().includes(searchQuery);
const idDecMatch = signal.messageId.toString().includes(searchQuery);
const idHex = signal.messageId.toString(16).toLowerCase();
const idHexMatch = idHex.includes(searchQuery) || idHex.includes(searchQuery.replace('0x', ''));
const msgNameMatch = signal.messageName.toLowerCase().includes(searchQuery);
const unitMatch = signal.unit && signal.unit.toLowerCase().includes(searchQuery);
return nameMatch || idDecMatch || idHexMatch || msgNameMatch || unitMatch;
});
const searchInfo = document.getElementById('search-info');
searchInfo.textContent = '🔍 Showing ' + filteredSignals.length + ' of ' + allSignals.length + ' signals';
searchInfo.style.display = 'block';
}
displaySignals();
}
function highlightText(text, query) {
if (!query) return text;
const regex = new RegExp('(' + query.replace(/[.*+?^${}()|[$$\$$/g, '\\$&') + ')', 'gi');
return text.replace(regex, '<span class="highlight">$1</span>');
}
function setSortMode(mode) {
sortMode = mode;
document.querySelectorAll('.sort-btn').forEach(btn => btn.classList.remove('active'));
document.getElementById('sort-' + mode).classList.add('active');
displaySignals();
console.log('Sort mode:', mode);
}
function sortSignals(signals) {
if (sortMode === 'name-asc') {
return signals.sort((a, b) => a.name.localeCompare(b.name));
} else if (sortMode === 'name-desc') {
return signals.sort((a, b) => b.name.localeCompare(a.name));
} else if (sortMode === 'id-asc') {
return signals.sort((a, b) => a.messageId - b.messageId);
} else {
const selected = signals.filter(s => isSignalSelected(s));
const notSelected = signals.filter(s => !isSignalSelected(s));
return [...selected, ...notSelected];
}
}
function isSignalSelected(signal) {
return selectedSignals.some(s =>
s.messageId === signal.messageId && s.name === signal.name);
}
function displaySignals() {
const signalList = document.getElementById('signal-list');
signalList.innerHTML = '';
const sortedSignals = sortSignals([...filteredSignals]);
if (sortedSignals.length === 0) {
signalList.innerHTML =
'<div class="no-results">' +
'<div class="no-results-icon">🔍</div>' +
'<div><strong>No signals found</strong></div>' +
'<div style="margin-top: 8px; color: #999;">Try a different search term</div>' +
'</div>';
return;
}
sortedSignals.forEach(signal => {
const item = document.createElement('div');
item.className = 'signal-item';
if (isSignalSelected(signal)) {
item.classList.add('selected');
}
item.onclick = () => toggleSignal(signal, item);
const canIdHex = '0x' + signal.messageId.toString(16).toUpperCase().padStart(3, '0');
const canIdDec = signal.messageId;
const highlightedName = highlightText(signal.name, searchQuery);
const highlightedId = highlightText(canIdHex, searchQuery);
const highlightedMsgName = highlightText(signal.messageName, searchQuery);
const unitText = signal.unit ? highlightText(signal.unit, searchQuery) : 'no unit';
item.innerHTML =
'<div class="signal-name">' + highlightedName + '</div>' +
'<div class="signal-info">' +
'<div class="signal-info-row">📍 CAN ID: ' + highlightedId + ' (' + canIdDec + ')</div>' +
'<div class="signal-info-row">📦 Message: ' + highlightedMsgName + '</div>' +
'<div class="signal-info-row">📊 Bit: ' + signal.startBit + '|' + signal.bitLength + ' | Unit: ' + unitText + '</div>' +
'</div>';
signalList.appendChild(item);
});
document.getElementById('signal-section').style.display = 'block';
updateSelectionCount();
}
function updateSelectionCount() {
document.getElementById('selected-count').textContent = selectedSignals.length;
}
function toggleSignal(signal, element) {
const index = selectedSignals.findIndex(s =>
s.messageId === signal.messageId && s.name === signal.name);
if (index >= 0) {
selectedSignals.splice(index, 1);
element.classList.remove('selected');
} else {
if (selectedSignals.length >= MAX_SIGNALS) {
showStatus(' Maximum ' + MAX_SIGNALS + ' signals allowed!', 'error');
return;
}
selectedSignals.push(signal);
element.classList.add('selected');
}
updateSelectionCount();
saveSelectedSignals();
if (sortMode === 'selection') {
displaySignals();
}
}
function clearSelection() {
selectedSignals = [];
document.querySelectorAll('.signal-item').forEach(item => {
item.classList.remove('selected');
});
updateSelectionCount();
saveSelectedSignals();
if (sortMode === 'selection') {
displaySignals();
}
showStatus('Selection cleared', 'info');
}
function saveSelectedSignals() {
try {
localStorage.setItem('selected_signals', JSON.stringify(selectedSignals));
localStorage.setItem('sort_mode', sortMode);
console.log(' Saved ' + selectedSignals.length + ' signals');
} catch(e) {
console.error(' Failed to save signals:', e);
}
}
function loadSelectedSignals() {
try {
const saved = localStorage.getItem('selected_signals');
const savedSortMode = localStorage.getItem('sort_mode');
if (savedSortMode) {
sortMode = savedSortMode;
document.querySelectorAll('.sort-btn').forEach(btn => btn.classList.remove('active'));
const btn = document.getElementById('sort-' + sortMode);
if (btn) btn.classList.add('active');
}
if (saved) {
const signals = JSON.parse(saved);
signals.forEach(savedSignal => {
for (let msgId in dbcData.messages) {
const msg = dbcData.messages[msgId];
const signal = msg.signals.find(s =>
s.messageId === savedSignal.messageId && s.name === savedSignal.name);
if (signal) {
selectedSignals.push(signal);
break;
}
}
});
displaySignals();
if (selectedSignals.length > 0) {
showStatus(' Restored ' + selectedSignals.length + ' selected signals', 'success');
}
}
} catch(e) {
console.error(' Failed to load signals:', e);
}
}
function startGraphing() {
if (selectedSignals.length === 0) {
showStatus(' Please select at least one signal!', 'error');
return;
}
saveSelectedSignals();
const width = 1400;
const height = 900;
const left = (screen.width - width) / 2;
const top = (screen.height - height) / 2;
window.open(
'/graph-view',
'CAN_Graph_Viewer',
'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top + ',resizable=yes,scrollbars=yes'
);
showStatus(' Graph viewer opened in new window', 'success');
}
function stopGraphing() {
showStatus(' Use the stop button in the graph viewer window', 'info');
}
function debugDBCInfo() {
console.log('=== DBC Debug Info ===');
console.log('Messages:', Object.keys(dbcData.messages).length);
console.log('Signals:', allSignals.length);
console.log('Value Tables:', Object.keys(dbcData.valueTables).length);
console.log('Selected Signals:', selectedSignals.length);
console.log('\n=== Messages ===');
Object.values(dbcData.messages).forEach(msg => {
console.log('ID: 0x' + msg.id.toString(16).toUpperCase() + ' | ' + msg.name + ' | Signals: ' + msg.signals.length);
});
console.log('\n=== Selected Signals ===');
selectedSignals.forEach((sig, idx) => {
console.log((idx + 1) + '. ' + sig.name + ' (ID: 0x' + sig.messageId.toString(16).toUpperCase() + ')');
});
showStatus(' Debug info printed to console (F12)', 'info');
}
function showStatus(message, type) {
const status = document.getElementById('status');
status.textContent = message;
status.className = 'status ' + type;
status.style.display = 'block';
setTimeout(() => { status.style.display = 'none'; }, 5000);
}
// Drag & Drop
const uploadArea = document.querySelector('.upload-area');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, e => {
e.preventDefault();
e.stopPropagation();
});
});
['dragenter', 'dragover'].forEach(eventName => {
uploadArea.addEventListener(eventName, () => {
uploadArea.style.borderColor = '#185a9d';
uploadArea.style.background = '#f0f9ff';
});
});
['dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, () => {
uploadArea.style.borderColor = '#43cea2';
uploadArea.style.background = 'white';
});
});
uploadArea.addEventListener('drop', function(e) {
const file = e.dataTransfer.files[0];
if (file && file.name.endsWith('.dbc')) {
const reader = new FileReader();
reader.onload = function(ev) {
parseDBCContent(ev.target.result);
document.getElementById('dbc-status').textContent = ' ' + file.name;
saveDBCToLocalStorage(ev.target.result, file.name);
};
reader.readAsText(file);
} else {
showStatus(' Please drop a .dbc file', 'error');
}
});
// 페이지 로드 시 복원
window.addEventListener('load', function() {
if (loadDBCFromLocalStorage()) {
setTimeout(() => loadSelectedSignals(), 100);
}
});
initWebSocket();
</script>
</body>
</html>
)rawliteral";
#endif