Files
esp32s3-mcp2518FD-logger/canfd_index.h
2026-02-27 10:02:27 +00:00

582 lines
20 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.

#ifndef CANFD_INDEX_H
#define CANFD_INDEX_H
const char canfd_index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32-S3 CAN FD Logger</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, 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: 28px; margin-bottom: 5px; }
.header p { opacity: 0.9; font-size: 14px; }
/* Navigation */
.nav {
background: #f8f9fa;
padding: 0;
border-bottom: 2px solid #e0e0e0;
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.nav a {
padding: 15px 20px;
text-decoration: none;
color: #666;
font-weight: 500;
font-size: 14px;
border-bottom: 3px solid transparent;
transition: all 0.3s;
}
.nav a:hover {
background: #e9ecef;
color: #667eea;
}
.nav a.active {
color: #764ba2;
border-bottom-color: #764ba2;
background: white;
font-weight: 600;
}
.content { padding: 20px; }
/* Status Cards */
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.status-card {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.status-card h3 {
color: #666;
font-size: 13px;
margin-bottom: 10px;
text-transform: uppercase;
}
.status-card .value {
color: #333;
font-size: 24px;
font-weight: 600;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.status-ok { background: #10b981; color: white; }
.status-error { background: #ef4444; color: white; }
.status-warn { background: #f59e0b; color: white; }
/* Controls */
.controls {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
.controls h2 {
color: #333;
font-size: 18px;
margin-bottom: 15px;
}
.btn-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
}
.btn {
padding: 12px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-start { background: #10b981; color: white; }
.btn-start:hover { background: #059669; }
.btn-stop { background: #ef4444; color: white; }
.btn-stop:hover { background: #dc2626; }
.btn-refresh { background: #667eea; color: white; }
.btn-refresh:hover { background: #5568d3; }
/* Settings */
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.setting-group {
background: #f8f9fa;
padding: 15px;
border-radius: 10px;
}
.setting-label {
color: #666;
font-size: 13px;
font-weight: 600;
margin-bottom: 8px;
display: block;
}
select, input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
}
/* File List */
.file-section {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin-top: 20px;
}
.file-section h2 {
color: #333;
font-size: 18px;
margin-bottom: 15px;
}
.file-list {
max-height: 400px;
overflow-y: auto;
}
.file-item {
background: white;
padding: 12px 15px;
margin-bottom: 8px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 10px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.file-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.file-checkbox {
width: 18px;
height: 18px;
}
.file-info {
flex: 1;
}
.file-name {
font-weight: 600;
color: #333;
font-size: 14px;
}
.file-size {
color: #999;
font-size: 12px;
margin-left: 10px;
}
.file-actions {
display: flex;
gap: 5px;
}
.file-btn {
padding: 6px 12px;
border: none;
border-radius: 5px;
font-size: 12px;
cursor: pointer;
transition: all 0.3s;
}
.btn-download { background: #10b981; color: white; }
.btn-download:hover { background: #059669; }
.btn-delete { background: #ef4444; color: white; }
.btn-delete:hover { background: #dc2626; }
/* Real-time Monitor */
.monitor {
background: #1a1a1a;
color: #00ff00;
padding: 15px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 12px;
max-height: 300px;
overflow-y: auto;
margin-top: 20px;
}
.monitor-line {
margin-bottom: 2px;
white-space: nowrap;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚗 ESP32-S3 CAN FD Logger</h1>
<p>MCP2517FD High-Speed Data Logger with Web Interface</p>
</div>
<div class="nav">
<a href="/" class="active">📊 Dashboard</a>
<a href="/settings"> Settings</a>
<a href="/graph">📈 Graph</a>
</div>
<div class="content">
<!-- System Status -->
<div class="status-grid">
<div class="status-card">
<h3>CAN Controller</h3>
<div class="value">
<span class="status-badge" id="canStatus">Loading...</span>
</div>
</div>
<div class="status-card">
<h3>SD Card</h3>
<div class="value">
<span class="status-badge" id="sdStatus">Loading...</span>
</div>
</div>
<div class="status-card">
<h3>Total Frames</h3>
<div class="value" id="canFrames">0</div>
</div>
<div class="status-card">
<h3>Queue Usage</h3>
<div class="value" id="queueUsage">0 / 10000</div>
</div>
<div class="status-card">
<h3>Errors</h3>
<div class="value" id="canErrors">0</div>
</div>
<div class="status-card">
<h3>Free PSRAM</h3>
<div class="value" id="psram">- KB</div>
</div>
</div>
<!-- CAN Settings -->
<div class="settings-grid">
<div class="setting-group">
<label class="setting-label">CAN Mode</label>
<select id="canMode">
<option value="fd">CAN FD Mode</option>
<option value="classic">Classic CAN Mode</option>
</select>
</div>
<div class="setting-group">
<label class="setting-label">Bit Rate</label>
<select id="bitRate">
<option value="125000">125 kbps</option>
<option value="250000">250 kbps</option>
<option value="500000" selected>500 kbps</option>
<option value="1000000">1 Mbps</option>
</select>
</div>
<div class="setting-group" id="dataRateGroup">
<label class="setting-label">Data Rate (CAN FD)</label>
<select id="dataRate">
<option value="1">x1</option>
<option value="2">x2</option>
<option value="4" selected>x4 (2 Mbps)</option>
<option value="8">x8</option>
</select>
</div>
<div class="setting-group">
<label class="setting-label">Controller Mode</label>
<select id="controllerMode">
<option value="0" selected>Normal</option>
<option value="1">Listen Only</option>
<option value="2">Loopback</option>
</select>
</div>
</div>
<!-- Logging Controls -->
<div class="controls">
<h2>🎬 Logging Control</h2>
<div class="btn-group">
<button class="btn btn-start" onclick="startLogging()"> Start Logging</button>
<button class="btn btn-stop" onclick="stopLogging()"> Stop Logging</button>
<button class="btn btn-refresh" onclick="applySettings()">💾 Apply Settings</button>
<button class="btn btn-refresh" onclick="loadFileList()">🔄 Refresh Files</button>
</div>
</div>
<!-- File List -->
<div class="file-section">
<h2>📁 Log Files on SD Card</h2>
<div style="margin-bottom: 10px;">
<button class="btn btn-start" onclick="downloadSelected()" style="padding: 8px 16px;"> Download Selected</button>
<button class="btn btn-delete" onclick="deleteSelected()" style="padding: 8px 16px;">🗑 Delete Selected</button>
</div>
<div class="file-list" id="fileList">
<p style="text-align: center; color: #999; padding: 20px;">Loading files...</p>
</div>
</div>
<!-- Real-time Monitor -->
<div class="controls">
<h2>📺 Real-time CAN Monitor</h2>
<div class="monitor" id="monitor">
<div class="monitor-line">Waiting for CAN messages...</div>
</div>
</div>
</div>
</div>
<script>
let ws;
let monitorLines = [];
const MAX_MONITOR_LINES = 50;
// WebSocket connection
function initWebSocket() {
ws = new WebSocket('ws://' + window.location.hostname + ':81');
ws.onopen = function() {
console.log('WebSocket connected');
updateStatus();
loadFileList();
};
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.type === 'status') {
updateStatusDisplay(data);
} else if (data.type === 'canMessage') {
addMonitorLine(data);
} else if (data.type === 'files') {
displayFiles(data.files);
}
};
ws.onclose = function() {
console.log('WebSocket disconnected');
setTimeout(initWebSocket, 3000);
};
}
function updateStatus() {
fetch('/status')
.then(r => r.json())
.then(data => updateStatusDisplay(data))
.catch(err => console.error('Status update failed:', err));
}
function updateStatusDisplay(data) {
// CAN Status
const canStatus = document.getElementById('canStatus');
if (data.canReady) {
canStatus.className = 'status-badge status-ok';
canStatus.textContent = 'Ready';
} else {
canStatus.className = 'status-badge status-error';
canStatus.textContent = 'Offline';
}
// SD Status
const sdStatus = document.getElementById('sdStatus');
if (data.sdReady) {
sdStatus.className = 'status-badge status-ok';
sdStatus.textContent = 'Ready';
} else {
sdStatus.className = 'status-badge status-error';
sdStatus.textContent = 'Error';
}
// Statistics
document.getElementById('canFrames').textContent = data.canFrames.toLocaleString();
document.getElementById('queueUsage').textContent = data.queueUsage + ' / 10000';
document.getElementById('canErrors').textContent = data.canErrors;
document.getElementById('psram').textContent = data.freePsram + ' KB';
}
function addMonitorLine(msg) {
const line = `[${msg.timestamp}] ID: 0x${msg.id} DLC: ${msg.len} ${msg.fd ? 'FD' : 'CAN'} ${msg.brs ? 'BRS' : ''} Data: ${msg.data}`;
monitorLines.push(line);
if (monitorLines.length > MAX_MONITOR_LINES) {
monitorLines.shift();
}
const monitor = document.getElementById('monitor');
monitor.innerHTML = monitorLines.map(l => `<div class="monitor-line">${l}</div>`).join('');
monitor.scrollTop = monitor.scrollHeight;
}
function startLogging() {
fetch('/logging/start', {method: 'POST'})
.then(r => r.json())
.then(data => {
alert(data.message);
updateStatus();
loadFileList();
});
}
function stopLogging() {
fetch('/logging/stop', {method: 'POST'})
.then(r => r.json())
.then(data => {
alert(data.message);
updateStatus();
loadFileList();
});
}
function applySettings() {
const settings = {
mode: document.getElementById('canMode').value,
bitRate: parseInt(document.getElementById('bitRate').value),
dataRate: parseInt(document.getElementById('dataRate').value),
controllerMode: parseInt(document.getElementById('controllerMode').value)
};
fetch('/settings/apply', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(settings)
})
.then(r => r.json())
.then(data => {
alert(data.message + '\n\nPlease restart ESP32 to apply changes.');
});
}
function loadFileList() {
fetch('/files/list')
.then(r => r.json())
.then(data => displayFiles(data.files))
.catch(err => console.error('File list failed:', err));
}
function displayFiles(files) {
const fileList = document.getElementById('fileList');
if (!files || files.length === 0) {
fileList.innerHTML = '<p style="text-align: center; color: #999; padding: 20px;">No files found</p>';
return;
}
fileList.innerHTML = files.map(file => `
<div class="file-item">
<input type="checkbox" class="file-checkbox" value="${file.name}">
<div class="file-info">
<span class="file-name">${file.name}</span>
<span class="file-size">${formatBytes(file.size)}</span>
</div>
<div class="file-actions">
<button class="file-btn btn-download" onclick="downloadFile('${file.name}')">Download</button>
<button class="file-btn btn-delete" onclick="deleteFile('${file.name}')">Delete</button>
</div>
</div>
`).join('');
}
function downloadFile(filename) {
window.location.href = '/download?file=' + encodeURIComponent(filename);
}
function deleteFile(filename) {
if (!confirm(`Delete "${filename}"?\n\nThis cannot be undone.`)) return;
fetch('/files/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({filename: filename})
})
.then(r => r.json())
.then(data => {
alert(data.message);
loadFileList();
});
}
function downloadSelected() {
const selected = Array.from(document.querySelectorAll('.file-checkbox:checked'))
.map(cb => cb.value);
if (selected.length === 0) {
alert('Please select files to download');
return;
}
selected.forEach(filename => {
setTimeout(() => downloadFile(filename), 100);
});
}
function deleteSelected() {
const selected = Array.from(document.querySelectorAll('.file-checkbox:checked'))
.map(cb => cb.value);
if (selected.length === 0) {
alert('Please select files to delete');
return;
}
if (!confirm(`Delete ${selected.length} file(s)?\n\nThis cannot be undone.`)) return;
selected.forEach(filename => deleteFile(filename));
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// CAN Mode change handler
document.getElementById('canMode').addEventListener('change', function() {
const dataRateGroup = document.getElementById('dataRateGroup');
if (this.value === 'classic') {
dataRateGroup.style.display = 'none';
} else {
dataRateGroup.style.display = 'block';
}
});
// Initialize
initWebSocket();
setInterval(updateStatus, 2000);
</script>
</body>
</html>
)rawliteral";
#endif