Files
2026-02-20 17:50:40 +00:00

338 lines
12 KiB
C

#ifndef WEB_CAN_H
#define WEB_CAN_H
const char HTML_CAN[] = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CAN Transmit - ESP32 Logger</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a2e;
color: #eee;
line-height: 1.6;
}
.header {
background: #16213e;
padding: 1rem;
text-align: center;
border-bottom: 2px solid #e94560;
}
.header h1 { color: #e94560; font-size: 1.5rem; }
.nav {
background: #0f3460;
padding: 0.5rem;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 0.5rem;
}
.nav a {
color: #fff;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background 0.3s;
}
.nav a:hover, .nav a.active { background: #e94560; }
.container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.form-card {
background: #16213e;
padding: 2rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.form-card h2 {
color: #e94560;
margin-bottom: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #00d9ff;
}
.form-group input, .form-group select {
width: 100%;
padding: 0.75rem;
background: #0f3460;
border: 1px solid #e94560;
color: #fff;
border-radius: 4px;
font-size: 1rem;
}
.form-group input:focus {
outline: none;
border-color: #00d9ff;
}
.data-bytes {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 0.5rem;
}
.data-bytes input {
text-align: center;
padding: 0.5rem;
}
.btn {
background: #e94560;
color: #fff;
border: none;
padding: 1rem 2rem;
font-size: 1rem;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
width: 100%;
}
.btn:hover { background: #ff6b6b; }
.btn-green { background: #00d9ff; }
.btn-green:hover { background: #00b8d4; }
.row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.info-text {
color: #666;
font-size: 0.875rem;
margin-top: 0.25rem;
}
</style>
</head>
<body>
<div class="header">
<h1>CAN Transmit</h1>
</div>
<nav class="nav">
<a href="/">Dashboard</a>
<a href="/graph">Graph</a>
<a href="/files">Files</a>
<a href="/can" class="active">CAN Transmit</a>
<a href="/settings">Settings</a>
</nav>
<div class="container">
<div class="form-card">
<h2>Transmit CAN Frame</h2>
<form id="canForm" onsubmit="sendFrame(event)">
<div class="form-group">
<label>CAN ID (Hex)</label>
<input type="text" id="canId" placeholder="0x100" value="0x100">
<div class="info-text">Example: 0x100, 0x7DF, 0x18FF1234</div>
</div>
<div class="row">
<div class="form-group">
<label>Frame Type</label>
<select id="frameType" onchange="updateDataLengthOptions()">
<option value="standard">Classic CAN (11-bit, 8 bytes max)</option>
<option value="extended">Classic CAN Extended (29-bit, 8 bytes max)</option>
<option value="fd">CAN FD (up to 64 bytes)</option>
</select>
</div>
<div class="form-group">
<label>Data Length</label>
<select id="dataLength" onchange="updateDataFields()">
<option value="0">0 bytes</option>
<option value="1">1 byte</option>
<option value="2">2 bytes</option>
<option value="4">4 bytes</option>
<option value="8" selected>8 bytes</option>
</select>
</div>
</div>
<div class="form-group">
<label>Data Bytes (Hex)</label>
<div class="data-bytes" id="dataBytes">
<input type="text" maxlength="2" value="00">
<input type="text" maxlength="2" value="00">
<input type="text" maxlength="2" value="00">
<input type="text" maxlength="2" value="00">
<input type="text" maxlength="2" value="00">
<input type="text" maxlength="2" value="00">
<input type="text" maxlength="2" value="00">
<input type="text" maxlength="2" value="00">
</div>
</div>
<div class="row">
<div class="form-group">
<label>Repeat Count</label>
<input type="number" id="repeatCount" value="1" min="1" max="10000">
<div class="info-text">1-10000 (1 = single)</div>
</div>
<div class="form-group">
<label>Delay (ms)</label>
<input type="number" id="delayMs" value="100" min="0" max="10000">
<div class="info-text">Delay between repeats</div>
</div>
</div>
<button type="submit" class="btn">Send Frame</button>
</form>
</div>
<div class="form-card">
<h2>Quick Send</h2>
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
<button class="btn btn-green" onclick="quickSend('0x100', [0x01, 0x02, 0x03, 0x04])">
Test Frame 1
</button>
<button class="btn btn-green" onclick="quickSend('0x200', [0xAA, 0xBB, 0xCC, 0xDD])">
Test Frame 2
</button>
<button class="btn btn-green" onclick="quickSend('0x7DF', [0x02, 0x01, 0x0C, 0x00])">
OBD2 RPM
</button>
</div>
</div>
</div>
<script>
function updateDataLengthOptions() {
const frameType = document.getElementById('frameType').value;
const dataLengthSelect = document.getElementById('dataLength');
const currentValue = dataLengthSelect.value;
dataLengthSelect.innerHTML = '';
const classicOptions = [
{value: '0', text: '0 bytes'},
{value: '1', text: '1 byte'},
{value: '2', text: '2 bytes'},
{value: '3', text: '3 bytes'},
{value: '4', text: '4 bytes'},
{value: '5', text: '5 bytes'},
{value: '6', text: '6 bytes'},
{value: '7', text: '7 bytes'},
{value: '8', text: '8 bytes'}
];
const fdOptions = [
{value: '0', text: '0 bytes'},
{value: '8', text: '8 bytes'},
{value: '12', text: '12 bytes'},
{value: '16', text: '16 bytes'},
{value: '20', text: '20 bytes'},
{value: '24', text: '24 bytes'},
{value: '32', text: '32 bytes'},
{value: '48', text: '48 bytes'},
{value: '64', text: '64 bytes'}
];
const options = frameType === 'fd' ? fdOptions : classicOptions;
options.forEach(opt => {
const option = document.createElement('option');
option.value = opt.value;
option.textContent = opt.text;
if (opt.value === currentValue || (opt.value === '8' && !options.find(o => o.value === currentValue))) {
option.selected = true;
}
dataLengthSelect.appendChild(option);
});
updateDataFields();
}
function updateDataFields() {
const len = parseInt(document.getElementById('dataLength').value);
const container = document.getElementById('dataBytes');
container.innerHTML = '';
const displayCount = len <= 8 ? len : 8;
for (let i = 0; i < displayCount; i++) {
const input = document.createElement('input');
input.type = 'text';
input.maxLength = 2;
input.value = '00';
container.appendChild(input);
}
if (len > 8) {
const info = document.createElement('div');
info.style.gridColumn = '1 / -1';
info.style.fontSize = '0.75rem';
info.style.color = '#aaa';
info.textContent = `Showing first 8 of ${len} bytes`;
container.appendChild(info);
}
}
function sendFrame(event) {
event.preventDefault();
const canId = document.getElementById('canId').value;
const frameType = document.getElementById('frameType').value;
const dataLength = parseInt(document.getElementById('dataLength').value);
const repeatCount = parseInt(document.getElementById('repeatCount').value);
const delayMs = parseInt(document.getElementById('delayMs').value);
const dataBytes = [];
const inputs = document.querySelectorAll('#dataBytes input');
inputs.forEach(input => {
if (input.type === 'text') {
dataBytes.push(parseInt(input.value, 16) || 0);
}
});
while (dataBytes.length < dataLength) {
dataBytes.push(0);
}
const payload = {
id: canId,
type: frameType,
length: dataLength,
data: dataBytes.slice(0, dataLength),
repeat: repeatCount,
delay: delayMs,
isFD: frameType === 'fd'
};
fetch('/api/can/send', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
})
.then(r => r.json())
.then(data => {
alert('Frame sent successfully!');
})
.catch(err => {
alert('Error: ' + err.message);
});
}
function quickSend(id, data) {
document.getElementById('canId').value = id;
document.getElementById('frameType').value = 'standard';
updateDataLengthOptions();
document.getElementById('dataLength').value = data.length;
updateDataFields();
const inputs = document.querySelectorAll('#dataBytes input');
data.forEach((byte, i) => {
if (inputs[i]) inputs[i].value = byte.toString(16).padStart(2, '0');
});
}
updateDataLengthOptions();
</script>
</body>
</html>
)rawliteral";
#endif // WEB_CAN_H