Files
250928_esp32_spi_sdcard_ads…/graph.h

587 lines
22 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>
* { 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; }
.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;
}
.upload-area {
border: 3px dashed #43cea2;
border-radius: 10px;
padding: 30px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.upload-area:hover { background: #f0f9ff; border-color: #185a9d; }
.upload-area input { display: none; }
.signal-selector {
background: #f8f9fa;
padding: 15px;
border-radius: 10px;
margin-bottom: 15px;
}
.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(200px, 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;
}
.signal-item:hover { border-color: #43cea2; transform: translateY(-2px); }
.signal-item.selected { border-color: #185a9d; background: #e3f2fd; }
.signal-name { font-weight: 600; color: #333; margin-bottom: 5px; font-size: 0.9em; }
.signal-info { font-size: 0.8em; color: #666; }
.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: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; background: #fff3cd; border-radius: 5px; margin-bottom: 15px; font-size: 0.9em; }
.status.success { background: #d4edda; color: #155724; }
.status.error { background: #f8d7da; color: #721c24; }
.selection-info {
background: #e3f2fd;
padding: 12px 15px;
border-radius: 8px;
margin-bottom: 15px;
border-left: 4px solid #185a9d;
font-size: 0.9em;
color: #333;
}
.selection-info 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; }
}
</style>
</head>
<body>
<div class="container">
<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>
</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: #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">
<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>
</div>
<div class="signal-selector">
<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: #e3f2fd; padding: 15px; border-radius: 8px; margin-top: 15px; border-left: 4px solid #185a9d;">
<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 sortMode = 'selection';
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: {}};
allSignals = [];
const lines = content.split('\n');
let currentMessage = null;
for (let line of lines) {
line = line.trim();
if (line.startsWith('BO_ ')) {
const match = line.match(/BO_\s+(\d+)\s+(\w+)\s*:/);
if (match) {
const id = parseInt(match[1]);
const name = match[2];
currentMessage = {id: id, name: name, signals: []};
dbcData.messages[id] = currentMessage;
}
}
else if (line.startsWith('SG_ ') && currentMessage) {
const match = line.match(/SG_\s+(\w+)\s*:\s*(\d+)\|(\d+)@([01])([+-])\s*\(([^,]+),([^)]+)\)\s*\[([^\]]+)\]\s*"([^"]*)"/);
if (match) {
const signal = {
name: match[1],
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]),
unit: match[9],
messageId: currentMessage.id,
messageName: currentMessage.name
};
currentMessage.signals.push(signal);
allSignals.push(signal);
}
}
}
displaySignals();
showStatus('DBC loaded: ' + Object.keys(dbcData.messages).length + ' messages', 'success');
}
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([...allSignals]);
sortedSignals.forEach(signal => {
const item = document.createElement('div');
item.className = 'signal-item';
if (isSignalSelected(signal)) {
item.classList.add('selected');
}
item.onclick = () => toggleSignal(signal, item);
item.innerHTML =
'<div class="signal-name">' + signal.name + '</div>' +
'<div class="signal-info">' +
'ID: 0x' + signal.messageId.toString(16).toUpperCase() + ' | ' +
signal.bitLength + 'bit' +
(signal.unit ? ' | ' + signal.unit : '') +
'</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 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