Files
esp32s3_canlogger_mcp2515/aa-transmit.h
2025-12-03 08:49:17 +00:00

802 lines
29 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.

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 TRANSMIT_H
#define TRANSMIT_H
const char transmit_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 Transmit & Sequence Editor</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, 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: 1.8em; margin-bottom: 5px; }
.header p { opacity: 0.9; font-size: 0.9em; }
.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; }
.section {
background: #f8f9fa;
border-radius: 10px;
padding: 20px;
margin-bottom: 15px;
border: 2px solid #e0e0e0;
}
h2 {
color: #333;
margin-bottom: 15px;
font-size: 1.3em;
border-bottom: 3px solid #667eea;
padding-bottom: 8px;
}
h3 {
color: #555;
margin-top: 20px;
margin-bottom: 10px;
font-size: 1.1em;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-bottom: 12px;
}
.form-group {
display: flex;
flex-direction: column;
}
label {
font-weight: 600;
margin-bottom: 5px;
color: #333;
font-size: 0.9em;
}
input, select {
padding: 10px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 0.95em;
transition: all 0.3s;
}
input:focus, select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.button-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
button {
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, #667eea 0%, #764ba2 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-warning {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
.btn-info {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.btn-small {
padding: 6px 12px;
font-size: 0.85em;
}
.steps-list {
background: white;
border-radius: 8px;
padding: 15px;
max-height: 400px;
overflow-y: auto;
}
.step-item {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
padding: 12px;
margin-bottom: 10px;
border-radius: 5px;
border-left: 4px solid #667eea;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.3s;
}
.step-item:hover {
transform: translateX(5px);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.step-info {
flex: 1;
font-family: 'Courier New', monospace;
font-size: 0.85em;
color: #333;
}
.step-actions {
display: flex;
gap: 5px;
}
.sequence-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 15px;
}
.sequence-card {
background: white;
padding: 15px;
border-radius: 8px;
border: 2px solid #ddd;
transition: all 0.3s;
}
.sequence-card:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
transform: translateY(-3px);
}
.sequence-card h3 {
color: #667eea;
margin: 0 0 10px 0;
font-size: 1.1em;
}
.sequence-info {
font-size: 0.85em;
color: #666;
margin-bottom: 12px;
}
.status-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
font-size: 0.8em;
font-weight: 600;
margin-right: 5px;
margin-bottom: 5px;
}
.badge-once { background: #e3f2fd; color: #1976d2; }
.badge-count { background: #fff3e0; color: #f57c00; }
.badge-infinite { background: #f3e5f5; color: #7b1fa2; }
.badge-running { background: #c8e6c9; color: #388e3c; animation: pulse 2s ease-in-out infinite; }
.badge-steps { background: #e0e0e0; color: #555; }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.connection-status {
position: fixed;
top: 10px;
right: 10px;
padding: 8px 15px;
border-radius: 20px;
font-size: 0.85em;
font-weight: 600;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.connection-status.connected {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
color: white;
}
.connection-status.disconnected {
background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);
color: white;
animation: pulse 2s ease-in-out infinite;
}
.info-box {
background: linear-gradient(135deg, #e3f2fd 0%, #f0f9ff 100%);
padding: 12px 15px;
border-radius: 8px;
margin-bottom: 15px;
border-left: 4px solid #667eea;
font-size: 0.9em;
color: #333;
}
@media (max-width: 768px) {
.form-row { grid-template-columns: 1fr; }
.sequence-list { grid-template-columns: 1fr; }
.content { padding: 10px; }
.section { padding: 15px; }
}
</style>
</head>
<body>
<div id="connection-status" class="connection-status disconnected">🔴 Disconnected</div>
<div class="container">
<div class="header">
<h1>🚀 CAN Transmit & Sequence Editor</h1>
<p>Create and Execute Complex CAN Message Sequences</p>
</div>
<div class="nav">
<a href="/">📊 Monitor</a>
<a href="/transmit" class="active">📤 Transmit</a>
<a href="/graph">📈 Graph</a>
<a href="/graph-view">📊 Graph View</a>
<a href="/settings"> Settings</a>
<a href="/serial">📟 Serial</a>
</div>
<div class="content">
<!-- 퀀 -->
<div class="section">
<h2>📝 Create New Sequence</h2>
<div class="info-box">
<strong> Info:</strong> A sequence is a series of CAN messages sent with specific timing.
You can repeat sequences once, a fixed number of times, or infinitely.
</div>
<div class="form-row">
<div class="form-group">
<label>Sequence Name</label>
<input type="text" id="seq-name" placeholder="My Sequence" maxlength="31">
</div>
<div class="form-group">
<label>Repeat Mode</label>
<select id="repeat-mode">
<option value="0">Once</option>
<option value="1">Repeat Count</option>
<option value="2">Infinite Loop</option>
</select>
</div>
<div class="form-group">
<label>Repeat Count</label>
<input type="number" id="repeat-count" value="1" min="1" max="1000" disabled>
</div>
</div>
<h3> Add Step to Sequence</h3>
<div class="form-row">
<div class="form-group">
<label>CAN ID (Hex)</label>
<input type="text" id="step-id" placeholder="0x123 or 123" maxlength="8">
</div>
<div class="form-group">
<label>ID Type</label>
<select id="step-extended">
<option value="0">Standard (11-bit)</option>
<option value="1">Extended (29-bit)</option>
</select>
</div>
<div class="form-group">
<label>DLC (Data Length)</label>
<select id="step-dlc">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8" selected>8</option>
</select>
</div>
<div class="form-group">
<label>Delay After (ms)</label>
<input type="number" id="step-delay" value="100" min="0" max="60000">
</div>
</div>
<div class="form-group" style="margin-bottom: 15px;">
<label>Data (8 bytes, Hex)</label>
<input type="text" id="step-data" placeholder="00 11 22 33 44 55 66 77" maxlength="23">
</div>
<div class="button-group">
<button class="btn-primary" onclick="addStep()"> Add Step</button>
<button class="btn-success" onclick="saveSequence()">💾 Save Sequence</button>
<button class="btn-danger" onclick="clearSteps()">🗑️ Clear All Steps</button>
</div>
<h3>📋 Steps in Current Sequence (<span id="step-count">0</span>)</h3>
<div class="steps-list" id="steps-list">
<p style="text-align: center; color: #999; padding: 20px;">No steps added yet. Add your first step above!</p>
</div>
</div>
<!-- 퀀 -->
<div class="section">
<h2>📚 Saved Sequences</h2>
<div class="button-group" style="margin-bottom: 15px;">
<button class="btn-info" onclick="loadSequences()">🔄 Refresh List</button>
<button class="btn-warning" onclick="stopAllSequences()">⏹️ Stop All Running</button>
</div>
<div class="sequence-list" id="sequence-list">
<p style="text-align: center; color: #999; padding: 40px;">Loading sequences...</p>
</div>
</div>
</div>
</div>
<script>
let ws;
let currentSteps = [];
let sequences = [];
let runningSequenceIndex = -1;
function updateConnectionStatus(connected) {
const status = document.getElementById('connection-status');
if (connected) {
status.className = 'connection-status connected';
status.innerHTML = '🟢 Connected';
} else {
status.className = 'connection-status disconnected';
status.innerHTML = '🔴 Reconnecting...';
}
}
function initWebSocket() {
ws = new WebSocket('ws://' + window.location.hostname + ':81/');
ws.onopen = function() {
console.log(' WebSocket connected');
updateConnectionStatus(true);
setTimeout(loadSequences, 500);
};
ws.onclose = function() {
console.log(' WebSocket disconnected');
updateConnectionStatus(false);
setTimeout(initWebSocket, 3000);
};
ws.onerror = function(error) {
console.error('WebSocket error:', error);
};
ws.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
console.log('Received:', data.type);
if (data.type === 'sequences') {
handleSequencesResponse(data);
} else if (data.type === 'sequenceDetail') {
handleSequenceDetail(data);
} else if (data.type === 'sequenceSaved') {
handleSequenceSaved(data);
} else if (data.type === 'sequenceDeleted') {
handleSequenceDeleted(data);
} else if (data.type === 'sequenceStarted') {
alert(' Sequence started!');
loadSequences();
} else if (data.type === 'sequenceStopped') {
alert(' Sequence stopped!');
loadSequences();
}
} catch (e) {
console.error('Parse error:', e);
}
};
}
function handleSequencesResponse(data) {
sequences = data.sequences || data.list || [];
sequences = sequences.map((seq, idx) => ({
...seq,
index: seq.index !== undefined ? seq.index : idx,
mode: seq.mode !== undefined ? seq.mode : seq.repeatMode,
count: seq.count !== undefined ? seq.count : seq.repeatCount
}));
console.log('Loaded ' + sequences.length + ' sequences');
updateSequenceList();
}
function handleSequenceSaved(data) {
if (data.success) {
alert(' Sequence saved successfully!');
clearSteps();
loadSequences();
} else {
alert(' Failed to save sequence\n(Max 10 sequences allowed)');
}
}
function handleSequenceDeleted(data) {
if (data.success) {
alert(' Sequence deleted!');
loadSequences();
} else {
alert(' Failed to delete sequence');
}
}
function loadSequences() {
if (ws && ws.readyState === WebSocket.OPEN) {
console.log('Requesting sequences...');
ws.send(JSON.stringify({cmd: 'getSequences'}));
} else {
console.log('WebSocket not ready, retrying...');
setTimeout(loadSequences, 1000);
}
}
function addStep() {
const id = document.getElementById('step-id').value.trim();
const extended = parseInt(document.getElementById('step-extended').value);
const dlc = parseInt(document.getElementById('step-dlc').value);
const dataStr = document.getElementById('step-data').value.trim();
const delay = parseInt(document.getElementById('step-delay').value);
if (id === '') {
alert(' Please enter CAN ID');
return;
}
let canId = 0;
if (id.startsWith('0x') || id.startsWith('0X')) {
canId = parseInt(id.substring(2), 16);
} else {
canId = parseInt(id, 16);
}
if (isNaN(canId)) {
alert(' Invalid CAN ID (use hex format like 0x123 or 123)');
return;
}
let dataBytes = [];
if (dataStr) {
const parts = dataStr.split(/[\s,]+/);
dataBytes = parts.map(b => {
const val = parseInt(b, 16);
return isNaN(val) ? 0 : val;
});
}
while (dataBytes.length < 8) {
dataBytes.push(0);
}
const step = {
canId: canId,
extended: extended === 1,
dlc: dlc,
data: dataBytes.slice(0, 8),
delayMs: delay
};
currentSteps.push(step);
updateStepsList();
// 입력 필드 초기화
document.getElementById('step-id').value = '';
document.getElementById('step-data').value = '';
console.log(' Step added:', step);
}
function removeStep(index) {
currentSteps.splice(index, 1);
updateStepsList();
}
function clearSteps() {
if (currentSteps.length > 0) {
if (!confirm('Clear all ' + currentSteps.length + ' steps?')) return;
}
currentSteps = [];
updateStepsList();
document.getElementById('seq-name').value = '';
}
function updateStepsList() {
const list = document.getElementById('steps-list');
document.getElementById('step-count').textContent = currentSteps.length;
if (currentSteps.length === 0) {
list.innerHTML = '<p style="text-align: center; color: #999; padding: 20px;">No steps added yet. Add your first step above!</p>';
return;
}
list.innerHTML = '';
currentSteps.forEach((step, index) => {
const item = document.createElement('div');
item.className = 'step-item';
const dataStr = step.data.map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ');
item.innerHTML =
'<div class="step-info">' +
'<strong>Step ' + (index + 1) + ':</strong> ' +
'ID: <strong>0x' + step.canId.toString(16).toUpperCase() + '</strong> ' +
'<span style="color: #666;">(' + (step.extended ? 'Ext' : 'Std') + ')</span> | ' +
'DLC: <strong>' + step.dlc + '</strong> | ' +
'Data: <strong>[' + dataStr + ']</strong> | ' +
'Delay: <strong>' + step.delayMs + 'ms</strong>' +
'</div>' +
'<div class="step-actions">' +
'<button class="btn-danger btn-small" onclick="removeStep(' + index + ')">🗑️ Delete</button>' +
'</div>';
list.appendChild(item);
});
}
function saveSequence() {
const name = document.getElementById('seq-name').value.trim();
const mode = parseInt(document.getElementById('repeat-mode').value);
const count = parseInt(document.getElementById('repeat-count').value);
if (name === '') {
alert(' Please enter sequence name');
return;
}
if (currentSteps.length === 0) {
alert(' Please add at least one step');
return;
}
const sequence = {
cmd: 'addSequence',
name: name,
repeatMode: mode,
repeatCount: count,
steps: currentSteps.map(step => ({
id: '0x' + step.canId.toString(16).toUpperCase(),
ext: step.extended,
dlc: step.dlc,
data: step.data,
delay: step.delayMs
}))
};
console.log('Saving sequence:', sequence);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(sequence));
} else {
alert(' Not connected to device');
}
}
function updateSequenceList() {
const list = document.getElementById('sequence-list');
if (!sequences || sequences.length === 0) {
list.innerHTML = '<p style="text-align: center; color: #999; padding: 40px;">No saved sequences. Create your first sequence above!</p>';
return;
}
list.innerHTML = '';
sequences.forEach((seq, idx) => {
const card = document.createElement('div');
card.className = 'sequence-card';
const mode = seq.mode !== undefined ? seq.mode : seq.repeatMode;
const count = seq.count !== undefined ? seq.count : seq.repeatCount;
const index = seq.index !== undefined ? seq.index : idx;
const isRunning = seq.running || false;
let modeBadge = '';
if (mode === 0) {
modeBadge = '<span class="status-badge badge-once">▶️ Once</span>';
} else if (mode === 1) {
modeBadge = '<span class="status-badge badge-count">🔁 Count: ' + count + '</span>';
} else if (mode === 2) {
modeBadge = '<span class="status-badge badge-infinite">♾️ Infinite</span>';
}
if (isRunning) {
modeBadge += '<span class="status-badge badge-running">🟢 RUNNING</span>';
}
const stepCount = typeof seq.steps === 'number' ? seq.steps :
(Array.isArray(seq.steps) ? seq.steps.length : seq.stepCount || 0);
card.innerHTML =
'<h3>' + seq.name + '</h3>' +
'<div class="sequence-info">' +
modeBadge +
'<span class="status-badge badge-steps">📋 ' + stepCount + ' steps</span>' +
'</div>' +
'<div class="button-group">' +
'<button class="btn-primary btn-small" onclick="loadSequenceDetail(' + index + ')">📝 Edit</button>' +
'<button class="btn-success btn-small" onclick="startSequence(' + index + ')">▶️ Run</button>' +
'<button class="btn-warning btn-small" onclick="stopSequence()">⏹️ Stop</button>' +
'<button class="btn-danger btn-small" onclick="deleteSequence(' + index + ')">🗑️ Delete</button>' +
'</div>';
list.appendChild(card);
});
}
function loadSequenceDetail(index) {
if (ws && ws.readyState === WebSocket.OPEN) {
console.log('Loading sequence detail:', index);
ws.send(JSON.stringify({cmd: 'getSequence', index: index}));
}
}
function handleSequenceDetail(data) {
const seq = data.sequence;
document.getElementById('seq-name').value = seq.name;
document.getElementById('repeat-mode').value = seq.mode !== undefined ? seq.mode : seq.repeatMode;
document.getElementById('repeat-count').value = seq.count !== undefined ? seq.count : seq.repeatCount;
// Repeat count 입력 활성화 상태 업데이트
updateRepeatCountState();
currentSteps = [];
if (seq.steps && Array.isArray(seq.steps)) {
seq.steps.forEach(step => {
let dataBytes = [];
if (typeof step.data === 'string') {
dataBytes = step.data.split(/[\s,]+/).map(b => parseInt(b, 16));
} else if (Array.isArray(step.data)) {
dataBytes = step.data;
}
currentSteps.push({
canId: typeof step.id === 'string' ? parseInt(step.id.replace('0x', ''), 16) : step.id,
extended: step.ext || step.extended || false,
dlc: step.dlc,
data: dataBytes,
delayMs: step.delay || step.delayMs || 0
});
});
}
updateStepsList();
document.querySelector('.section').scrollIntoView({ behavior: 'smooth' });
alert(' Sequence loaded! You can now edit and save it.');
}
function startSequence(index) {
if (ws && ws.readyState === WebSocket.OPEN) {
console.log('Starting sequence:', index);
ws.send(JSON.stringify({cmd: 'startSequence', index: index}));
} else {
alert(' Not connected to device');
}
}
function stopSequence() {
if (ws && ws.readyState === WebSocket.OPEN) {
console.log('Stopping sequence');
ws.send(JSON.stringify({cmd: 'stopSequence'}));
}
}
function stopAllSequences() {
stopSequence();
}
function deleteSequence(index) {
if (!confirm(' Delete this sequence?\nThis action cannot be undone.')) {
return;
}
if (ws && ws.readyState === WebSocket.OPEN) {
// Arduino는 'removeSequence' 명령어 사용
ws.send(JSON.stringify({cmd: 'removeSequence', index: index}));
console.log('Delete sequence:', index);
setTimeout(loadSequences, 500);
} else {
alert(' Not connected to device');
}
}
function updateRepeatCountState() {
const mode = parseInt(document.getElementById('repeat-mode').value);
const countInput = document.getElementById('repeat-count');
countInput.disabled = (mode !== 1);
}
// Event Listeners
document.getElementById('repeat-mode').addEventListener('change', updateRepeatCountState);
// Enter 키로 Step 추가
['step-id', 'step-data', 'step-delay'].forEach(id => {
document.getElementById(id).addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
addStep();
}
});
});
// 페이지 로드 시 WebSocket 초기화
window.addEventListener('load', function() {
initWebSocket();
updateRepeatCountState();
});
</script>
</body>
</html>
)rawliteral";
#endif