338 lines
12 KiB
C
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
|