Files
esp32s3_canlogger_mcp2515/graph.h
2026-03-12 21:14:40 +00:00

820 lines
37 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 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</title>
<style>
:root {
--bg: #0e1117;
--panel: #161b24;
--card: #1c2230;
--border: #2d3748;
--accent: #43cea2;
--green: #3fb950;
--blue: #58a6ff;
--red: #f85149;
--yellow: #e3b341;
--purple: #bc8cff;
--text: #e6edf3;
--muted: #8b949e;
--r: 8px;
}
* { margin:0; padding:0; box-sizing:border-box; }
html, body { min-height:100%; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg); color: var(--text);
overflow-x: hidden; font-size: 14px;
}
/* Header */
.header {
background: linear-gradient(135deg,#1a2744 0%,#1e1a3a 100%);
padding: 11px 16px; border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
.header h1 {
font-size: 1.0em; font-weight: 700; color: var(--accent);
display: flex; align-items: center; gap: 8px;
}
.header p { font-size: 0.78em; color: var(--muted); margin:0; }
.badge {
background: rgba(67,206,162,.15); color: var(--accent);
padding: 2px 8px; border-radius: 12px; font-size: 0.65em;
font-weight: 600; border: 1px solid rgba(67,206,162,.3);
white-space: nowrap;
}
.header-spacer { flex:1; }
/* Nav */
.nav {
background: var(--panel); border-bottom: 1px solid var(--border);
display: flex; overflow-x: auto; -webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.nav::-webkit-scrollbar { display:none; }
.nav a {
display: inline-flex; align-items: center;
padding: 10px 13px; text-decoration: none; color: var(--muted);
font-size: 0.78em; font-weight: 500;
border-bottom: 2px solid transparent; white-space: nowrap;
transition: color .2s, border-color .2s;
}
.nav a:hover { color: var(--text); }
.nav a.active { color: var(--accent); border-bottom-color: var(--accent); }
/* Content */
.content { padding: 12px; }
/* Headings */
h2 {
color: var(--accent); margin: 14px 0 10px;
font-size: 0.82em; font-weight: 700; text-transform: uppercase;
letter-spacing: .5px; padding-bottom: 6px;
border-bottom: 1px solid var(--border);
}
h3 {
color: var(--text); font-size: 0.85em; font-weight: 600;
margin-bottom: 10px; padding-bottom: 6px;
border-bottom: 1px solid var(--border);
}
/* Buttons */
.btn, button {
padding: 6px 13px; border: 1px solid var(--border);
border-radius: var(--r); background: var(--bg); color: var(--muted);
font-size: 0.8em; font-weight: 600; cursor: pointer;
font-family: inherit; transition: all .15s; white-space: nowrap;
-webkit-tap-highlight-color: transparent; touch-action: manipulation;
}
.btn:hover, button:hover { border-color: var(--accent); color: var(--accent); }
.btn:active, button:active { transform: scale(.97); }
.btn-primary { border-color:var(--blue); color:var(--blue); }
.btn-success { border-color:var(--accent); color:var(--accent); }
.btn-danger { border-color:var(--red); color:var(--red); }
.btn-warning { border-color:var(--yellow); color:var(--yellow); }
.btn-secondary{ border-color:var(--muted); color:var(--muted); }
.btn-primary:hover { background:rgba(88,166,255,.10); }
.btn-success:hover { background:rgba(67,206,162,.10); }
.btn-danger:hover { background:rgba(248,81,73,.10); }
.btn-warning:hover { background:rgba(227,179,65,.10); }
.btn-secondary:hover{ background:rgba(139,148,158,.10); }
.btn-small { padding:4px 9px; font-size:.75em; }
.button-group, .btn-group { display:flex; gap:6px; flex-wrap:wrap; margin-top:10px; }
/* Forms */
label {
display:block; font-weight:600; color:var(--muted);
margin-bottom:4px; font-size:.78em;
text-transform:uppercase; letter-spacing:.3px;
}
input[type="text"], input[type="password"], input[type="number"],
select, textarea {
width:100%; padding:7px 10px;
border:1px solid var(--border); border-radius:var(--r);
font-size:.85em; font-family:inherit;
background:var(--bg); color:var(--text);
transition:border-color .2s;
}
input:focus, select:focus, textarea:focus {
outline:none; border-color:var(--accent);
box-shadow:0 0 0 2px rgba(67,206,162,.10);
}
input[type="checkbox"], input[type="radio"] {
width:15px; height:15px; cursor:pointer; accent-color:var(--accent);
}
select option { background:var(--panel); color:var(--text); }
/* Panels */
.section, .settings-section, .send-panel, .stats,
.control-panel, .auto-trigger-section {
background:var(--panel); border:1px solid var(--border);
border-radius:var(--r); padding:13px; margin-bottom:10px;
}
.form-group { margin-bottom:12px; }
.form-group:last-child { margin-bottom:0; }
.help-text { font-size:.76em; color:var(--muted); margin-top:4px; line-height:1.4; }
.form-row {
display:grid;
grid-template-columns:repeat(auto-fit,minmax(min(100%,190px),1fr));
gap:10px; margin-bottom:10px;
}
.form-row label { text-transform:none; font-size:.78em; }
.checkbox-group, .checkbox-row {
display:flex; align-items:center; gap:8px;
margin-bottom:8px; padding:7px 10px;
background:var(--card); border-radius:6px; border:1px solid var(--border);
}
.checkbox-group label, .checkbox-row label {
text-transform:none; cursor:pointer; margin-bottom:0;
color:var(--text); font-size:.85em; letter-spacing:0;
}
/* Alerts */
.alert {
padding:9px 13px; border-radius:var(--r);
margin-bottom:10px; display:none;
align-items:center; gap:9px;
font-size:.83em; font-weight:600;
border:1px solid var(--border); background:var(--panel); color:var(--text);
}
.alert.show { display:flex; }
.alert-success { border-color:rgba(67,206,162,.4); color:var(--accent); background:rgba(67,206,162,.07); }
.alert-info { border-color:rgba(88,166,255,.4); color:var(--blue); background:rgba(88,166,255,.07); }
.alert-warning { border-color:rgba(227,179,65,.4); color:var(--yellow); background:rgba(227,179,65,.07); }
.alert-icon { font-size:1.15em; }
.alert-text { flex:1; }
@keyframes slideDown { from{opacity:0;transform:translateY(-6px);}to{opacity:1;transform:translateY(0);} }
/* Connection status */
.connection-status {
position:fixed; top:10px; right:10px;
padding:4px 11px; border-radius:20px;
font-size:.75em; font-weight:600; z-index:1000;
border:1px solid var(--border); background:var(--panel); color:var(--muted);
}
.connection-status.connected { border-color:rgba(67,206,162,.5); color:var(--accent); background:rgba(67,206,162,.08); }
.connection-status.disconnected { border-color:rgba(248,81,73,.5); color:var(--red); background:rgba(248,81,73,.08); }
@media (max-width:480px) { .content{padding:8px;} h2{font-size:.78em;} }
/* graph.h specific */
.dbc-upload {
background:var(--panel); border:1px solid var(--border);
border-radius:var(--r); padding:13px; margin-bottom:10px;
}
.upload-area {
border:2px dashed var(--border); border-radius:var(--r);
padding:26px 18px; text-align:center; cursor:pointer;
transition:all .2s; color:var(--muted); font-size:.85em;
}
.upload-area:hover { border-color:var(--accent); color:var(--accent); background:rgba(67,206,162,.04); }
.upload-area input { display:none; }
.upload-area p { margin-top:8px; }
.signal-selector {
background:var(--panel); border:1px solid var(--border);
border-radius:var(--r); padding:13px; margin-bottom:10px;
}
.search-container { margin-bottom:10px; position:relative; }
.search-box {
width:100%; padding:8px 10px 8px 34px;
border:1px solid var(--border); border-radius:var(--r);
font-size:.85em; background:var(--bg); color:var(--text);
}
.search-box:focus { outline:none; border-color:var(--accent); box-shadow:0 0 0 2px rgba(67,206,162,.10); }
.search-icon { position:absolute; left:10px; top:50%; transform:translateY(-50%); color:var(--accent); font-size:1.05em; }
.search-info {
margin-top:5px; padding:5px 10px;
background:rgba(88,166,255,.08); border-radius:5px;
font-size:.78em; color:var(--blue); font-weight:600;
border:1px solid rgba(88,166,255,.2);
}
.no-results { text-align:center; padding:28px 18px; color:var(--muted); font-size:.85em; }
.no-results-icon { font-size:2.2em; margin-bottom:8px; opacity:.4; }
.sort-controls { display:flex; gap:5px; margin-bottom:10px; align-items:center; flex-wrap:wrap; }
.sort-label { font-weight:600; color:var(--muted); font-size:.77em; }
.sort-btn {
padding:4px 10px; border:1px solid var(--border);
background:var(--bg); border-radius:6px; font-size:.76em;
cursor:pointer; font-weight:600; color:var(--muted); font-family:inherit;
transition:all .15s;
}
.sort-btn:hover { border-color:var(--accent); color:var(--accent); }
.sort-btn.active { border-color:var(--accent); color:var(--accent); background:rgba(67,206,162,.10); }
.signal-grid {
display:grid;
grid-template-columns:repeat(auto-fill,minmax(min(100%,170px),1fr));
gap:7px; margin-top:10px;
}
.signal-item {
background:var(--card); padding:9px;
border-radius:var(--r); border:1px solid var(--border); cursor:pointer; transition:all .15s;
}
.signal-item:hover { border-color:var(--accent); background:rgba(67,206,162,.04); }
.signal-item.selected { border-color:var(--accent); background:rgba(67,206,162,.10); }
.signal-name { font-weight:600; color:var(--text); margin-bottom:3px; font-size:.83em; }
.signal-info { font-size:.74em; color:var(--muted); line-height:1.4; }
.highlight { background:rgba(227,179,65,.22); color:var(--yellow); padding:1px 0; font-weight:600; }
.controls { display:flex; gap:5px; margin-bottom:10px; flex-wrap:wrap; }
.selection-info {
background:rgba(88,166,255,.07); padding:9px 12px;
border-radius:var(--r); margin-bottom:10px;
border:1px solid rgba(88,166,255,.2); border-left:3px solid var(--blue);
font-size:.83em; color:var(--text);
}
.selection-info strong { color:var(--blue); }
.status { padding:9px 12px; background:rgba(227,179,65,.07); border-radius:var(--r); margin-bottom:10px; font-size:.83em; border:1px solid rgba(227,179,65,.2); color:var(--yellow); }
.status.success { background:rgba(67,206,162,.07); border-color:rgba(67,206,162,.2); color:var(--accent); }
.status.error { background:rgba(248,81,73,.07); border-color:rgba(248,81,73,.2); color:var(--red); }
</style>
</head>
<body>
<div class="header">
<h1>CAN Signal Graph</h1>
<p>Real-time Signal Visualization (Offline Mode)</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">📟 Serial1</a>
<a href="/serial2">📟 Serial2</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)">
<p style="font-size: 1.1em; margin-bottom: 8px;">Click to upload DBC</p>
<p style="color: black; 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">
<strong>Selected: <span id="selected-count">0</span> / 20</strong>
</div>
<div class="controls">
<button class="btn btn-success" onclick="startGraphing()">Start</button>
<button class="btn btn-danger" onclick="stopGraphing()">Stop</button>
<button class="btn btn-primary" onclick="clearSelection()">Clear</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, 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 (A→Z)</button>
<button class="sort-btn" id="sort-name-desc" onclick="setSortMode('name-desc')">Name (Z→A)</button>
</div>
<div id="signal-list" class="signal-grid"></div>
</div>
<div style="background:rgba(88,166,255,.07);padding:10px 13px;border-radius:8px;margin-top:10px;border:1px solid rgba(88,166,255,.2);border-left:3px solid var(--blue);font-size:.83em;">
<p style="color: #333; font-size: 0.9em; margin: 0;">
<strong> Info:</strong> Click "Start" to open a new window with real-time graphs.
Your selected signals will be saved automatically.
</p>
</div>
</div>
</div>
</div>
<script>
let ws;
let dbcData = {};
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');
showStatus('Connected', 'success');
};
ws.onclose = function() {
console.log('WebSocket disconnected');
showStatus('Disconnected - Reconnecting...', 'error');
setTimeout(initWebSocket, 3000);
};
ws.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
} catch(e) {
console.error('Error parsing WebSocket data:', e);
}
};
}
function loadDBCFile(event) {
const file = event.target.files[0];
if (!file) return;
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 to localStorage:', e);
}
}
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 from localStorage:', filename);
return true;
}
} catch(e) {
console.error('Failed to load DBC from localStorage:', e);
}
return false;
}
function parseDBCContent(content) {
dbcData = {messages: {}, valueTables: {}};
allSignals = [];
const lines = content.split('\n');
let currentMessage = null;
// 먼저 VAL_ 정의를 파싱
for (let line of lines) {
line = line.trim();
if (line.startsWith('VAL_ ')) {
// VAL_ 형식: VAL_ <MessageID> <SignalName> <value> "<text>" ...;
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];
// Extended CAN ID 처리
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('Parsed Value Table: ' + key, dbcData.valueTables[key]);
}
}
}
// 메시지와 시그널 파싱
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('Parsed 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
};
// Value Table 연결
const vtKey = currentMessage.id + '_' + signalName;
if (dbcData.valueTables[vtKey]) {
signal.valueTable = dbcData.valueTables[vtKey];
console.log(' Signal with value table: ' + signalName, signal.valueTable);
}
currentMessage.signals.push(signal);
allSignals.push(signal);
console.log(' Signal: ' + signal.name + ' [' + signal.startBit + '|' + signal.bitLength + '] Unit: ' + signal.unit);
}
}
}
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 changed to:', 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 {
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 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">' +
'CAN ID: ' + highlightedId + ' (' + highlightedMsgName + ')<br>' +
'Bit: ' + signal.startBit + '|' + signal.bitLength + ' | ' + unitText +
'</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('Max ' + MAX_SIGNALS + ' signals!', '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();
}
}
function saveSelectedSignals() {
try {
localStorage.setItem('selected_signals', JSON.stringify(selectedSignals));
localStorage.setItem('sort_mode', sortMode);
console.log('Saved', selectedSignals.length, 'signals with sort mode:', sortMode);
} catch(e) {
console.error('Failed to save selected 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'));
document.getElementById('sort-' + sortMode).classList.add('active');
}
if (saved) {
const signals = JSON.parse(saved);
signals.forEach(savedSignal => {
let found = false;
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);
found = true;
break;
}
}
});
displaySignals();
if (selectedSignals.length > 0) {
showStatus('Restored ' + selectedSignals.length + ' selected signals', 'success');
}
}
} catch(e) {
console.error('Failed to load selected signals:', e);
}
}
function startGraphing() {
if (selectedSignals.length === 0) {
showStatus('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', 'error');
}
function debugDBCInfo() {
console.log('=== DBC Debug Info ===');
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);
console.log('\n=== Value Tables ===');
for (let key in dbcData.valueTables) {
console.log(key + ':', dbcData.valueTables[key]);
}
console.log('\n=== Messages ===');
Object.values(dbcData.messages).forEach(msg => {
console.log('\nMessage: ' + msg.name);
console.log(' ID: ' + msg.id + ' (0x' + msg.id.toString(16).toUpperCase() + ')');
console.log(' DLC: ' + msg.dlc);
console.log(' Signals: ' + msg.signals.length);
msg.signals.forEach(sig => {
console.log(' - ' + sig.name + ': ' + sig.startBit + '|' + sig.bitLength + ' [' + sig.unit + ']');
if (sig.valueTable) {
console.log(' Value Table:', sig.valueTable);
}
});
});
showStatus('Debug info printed to console (F12)', 'success');
}
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);
}
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 = '';
});
});
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);
}
});
window.addEventListener('load', function() {
if (loadDBCFromLocalStorage()) {
setTimeout(() => loadSelectedSignals(), 100);
}
});
initWebSocket();
</script>
</body>
</html>
)rawliteral";
#endif