Files
250928_esp32_spi_sdcard_ads…/transmit.h

1301 lines
50 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

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 Transmitter</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 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, #f093fb 0%, #f5576c 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; }
.mode-tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
border-bottom: 3px solid #f093fb;
}
.mode-tab {
padding: 12px 25px;
background: #f8f9fa;
border: none;
border-radius: 10px 10px 0 0;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
color: #666;
}
.mode-tab:hover {
background: #e9ecef;
}
.mode-tab.active {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
transform: translateY(-2px);
}
.mode-content {
display: none;
}
.mode-content.active {
display: block;
}
.message-form {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
.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;
}
.form-group label {
font-weight: 600;
margin-bottom: 5px;
color: #333;
font-size: 0.9em;
}
.form-group input, .form-group select {
padding: 10px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 0.95em;
font-family: 'Courier New', monospace;
}
.form-group input:focus, .form-group select:focus {
outline: none;
border-color: #f093fb;
}
.data-bytes {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(45px, 1fr));
gap: 8px;
max-width: 100%;
}
.data-bytes input {
text-align: center;
text-transform: uppercase;
width: 100%;
min-width: 45px;
padding: 10px 5px;
}
.btn {
padding: 12px 20px;
border: none;
border-radius: 5px;
font-size: 0.95em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 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, #f2994a 0%, #f2c94c 100%);
color: white;
}
.btn-info {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,0,0,0.3); }
.message-list {
background: #f8f9fa;
border-radius: 10px;
padding: 15px;
}
.message-item {
background: white;
padding: 12px;
margin-bottom: 10px;
border-radius: 8px;
display: grid;
grid-template-columns: 90px 70px 1fr auto;
gap: 10px;
align-items: center;
border-left: 4px solid #f093fb;
}
.message-item.active {
border-left-color: #38ef7d;
background: #f0fff4;
}
.message-id {
font-weight: 700;
color: #f5576c;
font-family: 'Courier New', monospace;
font-size: 0.9em;
}
.message-type {
padding: 4px 8px;
border-radius: 5px;
font-size: 0.75em;
font-weight: 600;
text-align: center;
}
.type-std { background: #3498db; color: white; }
.type-ext { background: #9b59b6; color: white; }
.message-data {
font-family: 'Courier New', monospace;
color: #666;
font-size: 0.85em;
word-break: break-all;
}
.message-controls {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.btn-small {
padding: 6px 12px;
font-size: 0.8em;
}
/* Sequence Mode Styles */
.sequence-builder {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
.sequence-step {
background: white;
padding: 15px;
margin-bottom: 10px;
border-radius: 8px;
border-left: 4px solid #667eea;
display: flex;
align-items: center;
gap: 15px;
}
.step-number {
background: #667eea;
color: white;
width: 35px;
height: 35px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 1.1em;
flex-shrink: 0;
}
.step-type {
padding: 6px 12px;
border-radius: 5px;
font-size: 0.8em;
font-weight: 600;
flex-shrink: 0;
}
.step-message {
background: #3498db;
color: white;
}
.step-delay {
background: #f39c12;
color: white;
}
.step-content {
flex: 1;
font-family: 'Courier New', monospace;
font-size: 0.9em;
color: #333;
}
.step-controls {
display: flex;
gap: 5px;
}
.sequence-controls {
background: #e3f2fd;
padding: 15px;
border-radius: 8px;
margin-bottom: 15px;
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.repeat-option {
display: flex;
align-items: center;
gap: 10px;
}
.repeat-option label {
font-weight: 600;
color: #333;
}
.repeat-option input[type="number"] {
width: 80px;
padding: 8px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 0.9em;
}
h2 {
color: #333;
margin: 20px 0 15px 0;
padding-bottom: 8px;
border-bottom: 3px solid #f093fb;
font-size: 1.3em;
}
.status-bar {
background: #2c3e50;
color: white;
padding: 10px 15px;
border-radius: 5px;
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9em;
}
.status-bar span { font-weight: 600; }
.preset-manager {
background: #fff3cd;
padding: 15px;
border-radius: 10px;
margin-bottom: 15px;
border-left: 4px solid #f2994a;
}
.preset-controls {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 10px;
}
.preset-controls input {
flex: 1;
min-width: 200px;
padding: 8px;
border: 2px solid #f2994a;
border-radius: 5px;
font-size: 0.9em;
}
.preset-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 10px;
margin-top: 10px;
}
.preset-item {
background: white;
padding: 10px;
border-radius: 5px;
border: 2px solid #f2c94c;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.3s;
}
.preset-item:hover {
transform: translateX(5px);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.preset-name {
font-weight: 600;
color: #333;
font-size: 0.9em;
}
.preset-info {
font-size: 0.75em;
color: #666;
margin-top: 3px;
}
.preset-buttons {
display: flex;
gap: 5px;
}
@media (max-width: 768px) {
body { padding: 5px; }
.header h1 { font-size: 1.5em; }
.content { padding: 10px; }
.form-row { grid-template-columns: 1fr; }
.data-bytes {
grid-template-columns: repeat(4, 1fr);
gap: 6px;
}
.data-bytes input {
min-width: 40px;
font-size: 0.9em;
padding: 8px 4px;
}
.message-item {
grid-template-columns: 1fr;
gap: 8px;
}
.message-controls {
justify-content: flex-start;
}
.nav a {
padding: 8px 10px;
font-size: 0.85em;
}
h2 {
font-size: 1.1em;
}
.status-bar {
flex-direction: column;
gap: 8px;
text-align: center;
}
.preset-list {
grid-template-columns: 1fr;
}
.sequence-step {
flex-direction: column;
align-items: flex-start;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>CAN Transmitter</h1>
<p>Send CAN Messages - Periodic & Sequence Mode</p>
</div>
<div class="nav">
<a href="/">Monitor</a>
<a href="/transmit" class="active">Transmit</a>
<a href="/graph">Graph</a>
</div>
<div class="content">
<div class="status-bar">
<span id="connection-status">Disconnected</span>
<span id="tx-count">Sent: 0</span>
</div>
<!-- Mode Tabs -->
<div class="mode-tabs">
<button class="mode-tab active" onclick="switchMode('periodic')">⏱️ Periodic Mode</button>
<button class="mode-tab" onclick="switchMode('sequence')">📋 Sequence Mode</button>
</div>
<!-- Periodic Mode -->
<div id="periodic-mode" class="mode-content active">
<h2>Message List Presets</h2>
<div class="preset-manager">
<div class="preset-controls">
<input type="text" id="preset-name" placeholder="Enter preset name...">
<button class="btn btn-warning" onclick="savePreset()">Save Current List</button>
</div>
<div class="preset-list" id="preset-list">
<p style="text-align: center; color: #666; font-size: 0.9em;">No saved presets</p>
</div>
</div>
<h2>Add CAN Message</h2>
<div class="message-form">
<div class="form-row">
<div class="form-group">
<label>CAN ID (Hex)</label>
<input type="text" id="can-id" placeholder="123" maxlength="8">
</div>
<div class="form-group">
<label>Type</label>
<select id="msg-type">
<option value="std">Standard</option>
<option value="ext">Extended</option>
</select>
</div>
<div class="form-group">
<label>DLC</label>
<select id="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>Interval (ms)</label>
<input type="number" id="interval" value="100" min="10" max="10000">
</div>
</div>
<div class="form-group" style="margin-bottom: 15px;">
<label>Send All Once - Waiting Time (ms)</label>
<input type="number" id="send-all-delay" value="10" min="0" max="1000"
style="max-width: 200px; padding: 10px; border: 2px solid #ddd; border-radius: 5px;">
<small style="color: #666; margin-top: 5px; display: block;">
Delay between messages when using "Send All Once" button
</small>
</div>
<div class="form-group" style="margin-bottom: 15px;">
<label>Data Bytes (Hex)</label>
<div class="data-bytes">
<input type="text" id="d0" placeholder="00" maxlength="2" value="00">
<input type="text" id="d1" placeholder="00" maxlength="2" value="00">
<input type="text" id="d2" placeholder="00" maxlength="2" value="00">
<input type="text" id="d3" placeholder="00" maxlength="2" value="00">
<input type="text" id="d4" placeholder="00" maxlength="2" value="00">
<input type="text" id="d5" placeholder="00" maxlength="2" value="00">
<input type="text" id="d6" placeholder="00" maxlength="2" value="00">
<input type="text" id="d7" placeholder="00" maxlength="2" value="00">
</div>
</div>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<button class="btn btn-primary" onclick="addMessage()">Add to List</button>
<button class="btn btn-success" onclick="sendOnce()">Send Once</button>
</div>
</div>
<h2>Message List</h2>
<div style="margin-bottom: 15px; display: flex; gap: 10px; flex-wrap: wrap;">
<button class="btn btn-success" onclick="startAll()">Start All</button>
<button class="btn btn-danger" onclick="stopAll()">Stop All</button>
<button class="btn btn-info" onclick="sendAllOnce()">📤 Send All Once</button>
<button class="btn btn-danger" onclick="clearAll()">Clear All</button>
</div>
<div class="message-list" id="message-list">
<p style="text-align: center; color: #666; font-size: 0.9em;">No messages added yet</p>
</div>
</div>
<!-- Sequence Mode -->
<div id="sequence-mode" class="mode-content">
<h2>Sequence Builder</h2>
<div class="sequence-controls">
<div class="repeat-option">
<label>Repeat Mode:</label>
<select id="repeat-mode" style="padding: 8px; border: 2px solid #ddd; border-radius: 5px;">
<option value="once">Once</option>
<option value="count">Count</option>
<option value="infinite">Infinite</option>
</select>
</div>
<div class="repeat-option" id="repeat-count-group" style="display:none;">
<label>Count:</label>
<input type="number" id="repeat-count" value="10" min="1" max="10000">
</div>
<button class="btn btn-success" onclick="startSequence()" id="start-seq-btn">▶️ Start Sequence</button>
<button class="btn btn-danger" onclick="stopSequence()" id="stop-seq-btn" style="display:none;">⏹️ Stop Sequence</button>
<span id="seq-status" style="font-weight: 600; color: #667eea;"></span>
</div>
<div class="message-form">
<h3 style="margin-bottom: 15px; color: #667eea;">Add Step to Sequence</h3>
<div class="form-row">
<div class="form-group">
<label>Step Type</label>
<select id="seq-step-type" onchange="updateStepForm()">
<option value="message">CAN Message</option>
<option value="delay">Delay</option>
</select>
</div>
</div>
<div id="message-step-form">
<div class="form-row">
<div class="form-group">
<label>CAN ID (Hex)</label>
<input type="text" id="seq-can-id" placeholder="123" maxlength="8">
</div>
<div class="form-group">
<label>Type</label>
<select id="seq-msg-type">
<option value="std">Standard</option>
<option value="ext">Extended</option>
</select>
</div>
<div class="form-group">
<label>DLC</label>
<select id="seq-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>
<div class="form-group" style="margin-bottom: 15px;">
<label>Data Bytes (Hex)</label>
<div class="data-bytes">
<input type="text" id="seq-d0" placeholder="00" maxlength="2" value="00">
<input type="text" id="seq-d1" placeholder="00" maxlength="2" value="00">
<input type="text" id="seq-d2" placeholder="00" maxlength="2" value="00">
<input type="text" id="seq-d3" placeholder="00" maxlength="2" value="00">
<input type="text" id="seq-d4" placeholder="00" maxlength="2" value="00">
<input type="text" id="seq-d5" placeholder="00" maxlength="2" value="00">
<input type="text" id="seq-d6" placeholder="00" maxlength="2" value="00">
<input type="text" id="seq-d7" placeholder="00" maxlength="2" value="00">
</div>
</div>
</div>
<div id="delay-step-form" style="display:none;">
<div class="form-group" style="max-width: 300px;">
<label>Delay Time (ms)</label>
<input type="number" id="seq-delay" value="100" min="1" max="60000"
style="padding: 10px; border: 2px solid #ddd; border-radius: 5px;">
</div>
</div>
<div style="margin-top: 15px;">
<button class="btn btn-primary" onclick="addSequenceStep()"> Add Step</button>
</div>
</div>
<h2>Sequence Steps</h2>
<div style="margin-bottom: 15px; display: flex; gap: 10px; flex-wrap: wrap;">
<button class="btn btn-danger" onclick="clearSequence()">Clear All Steps</button>
<button class="btn btn-warning" onclick="saveSequencePreset()">💾 Save Sequence</button>
<button class="btn btn-info" onclick="loadSequencePreset()">📂 Load Sequence</button>
</div>
<div class="sequence-builder" id="sequence-list">
<p style="text-align: center; color: #666; font-size: 0.9em;">No steps added yet</p>
</div>
</div>
</div>
</div>
<script>
let ws;
let messages = [];
let txCount = 0;
let currentMode = 'periodic';
// Sequence Mode Variables
let sequenceSteps = [];
let sequenceRunning = false;
let sequenceAbort = false;
function initWebSocket() {
ws = new WebSocket('ws://' + window.location.hostname + ':81');
ws.onopen = function() {
console.log('WebSocket connected');
document.getElementById('connection-status').innerHTML = 'Connected';
};
ws.onclose = function() {
console.log('WebSocket disconnected');
document.getElementById('connection-status').innerHTML = 'Disconnected';
setTimeout(initWebSocket, 3000);
};
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.type === 'txStatus') {
txCount = data.count;
document.getElementById('tx-count').textContent = 'Sent: ' + txCount;
}
};
}
// Mode Switching
function switchMode(mode) {
currentMode = mode;
document.querySelectorAll('.mode-tab').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.mode-content').forEach(content => content.classList.remove('active'));
if (mode === 'periodic') {
document.querySelectorAll('.mode-tab')[0].classList.add('active');
document.getElementById('periodic-mode').classList.add('active');
} else {
document.querySelectorAll('.mode-tab')[1].classList.add('active');
document.getElementById('sequence-mode').classList.add('active');
}
}
// Sequence Mode Functions
function updateStepForm() {
const stepType = document.getElementById('seq-step-type').value;
if (stepType === 'message') {
document.getElementById('message-step-form').style.display = 'block';
document.getElementById('delay-step-form').style.display = 'none';
} else {
document.getElementById('message-step-form').style.display = 'none';
document.getElementById('delay-step-form').style.display = 'block';
}
}
document.getElementById('repeat-mode').addEventListener('change', function() {
const mode = this.value;
const countGroup = document.getElementById('repeat-count-group');
if (mode === 'count') {
countGroup.style.display = 'flex';
} else {
countGroup.style.display = 'none';
}
});
function addSequenceStep() {
const stepType = document.getElementById('seq-step-type').value;
if (stepType === 'message') {
const id = document.getElementById('seq-can-id').value.toUpperCase();
const type = document.getElementById('seq-msg-type').value;
const dlc = parseInt(document.getElementById('seq-dlc').value);
if (!id || !/^[0-9A-F]+$/.test(id)) {
alert('Invalid CAN ID!');
return;
}
const data = [];
for (let i = 0; i < 8; i++) {
const val = document.getElementById('seq-d' + i).value.toUpperCase();
if (!/^[0-9A-F]{0,2}$/.test(val)) {
alert('Invalid data byte D' + i + '!');
return;
}
data.push(val.padStart(2, '0'));
}
sequenceSteps.push({
type: 'message',
id: id,
msgType: type,
dlc: dlc,
data: data
});
} else {
const delay = parseInt(document.getElementById('seq-delay').value);
if (delay < 1) {
alert('Delay must be at least 1ms!');
return;
}
sequenceSteps.push({
type: 'delay',
delay: delay
});
}
updateSequenceList();
}
function updateSequenceList() {
const list = document.getElementById('sequence-list');
if (sequenceSteps.length === 0) {
list.innerHTML = '<p style="text-align: center; color: #666; font-size: 0.9em;">No steps added yet</p>';
return;
}
list.innerHTML = '';
sequenceSteps.forEach((step, index) => {
const stepDiv = document.createElement('div');
stepDiv.className = 'sequence-step';
let content = '';
let stepTypeClass = '';
let stepTypeName = '';
if (step.type === 'message') {
stepTypeClass = 'step-message';
stepTypeName = 'MSG';
content = 'ID: 0x' + step.id + ' | ' + step.msgType.toUpperCase() + ' | DLC: ' + step.dlc + ' | Data: ' + step.data.join(' ');
} else {
stepTypeClass = 'step-delay';
stepTypeName = 'DELAY';
content = 'Wait ' + step.delay + ' ms';
}
stepDiv.innerHTML =
'<div class="step-number">' + (index + 1) + '</div>' +
'<div class="step-type ' + stepTypeClass + '">' + stepTypeName + '</div>' +
'<div class="step-content">' + content + '</div>' +
'<div class="step-controls">' +
'<button class="btn btn-warning btn-small" onclick="moveStepUp(' + index + ')">↑</button>' +
'<button class="btn btn-warning btn-small" onclick="moveStepDown(' + index + ')">↓</button>' +
'<button class="btn btn-danger btn-small" onclick="deleteStep(' + index + ')">Delete</button>' +
'</div>';
list.appendChild(stepDiv);
});
}
function moveStepUp(index) {
if (index > 0) {
[sequenceSteps[index], sequenceSteps[index - 1]] = [sequenceSteps[index - 1], sequenceSteps[index]];
updateSequenceList();
}
}
function moveStepDown(index) {
if (index < sequenceSteps.length - 1) {
[sequenceSteps[index], sequenceSteps[index + 1]] = [sequenceSteps[index + 1], sequenceSteps[index]];
updateSequenceList();
}
}
function deleteStep(index) {
sequenceSteps.splice(index, 1);
updateSequenceList();
}
function clearSequence() {
if (confirm('Clear all sequence steps?')) {
sequenceSteps = [];
updateSequenceList();
}
}
async function startSequence() {
if (sequenceSteps.length === 0) {
alert('No sequence steps added!');
return;
}
if (!ws || ws.readyState !== WebSocket.OPEN) {
alert('WebSocket not connected!');
return;
}
sequenceRunning = true;
sequenceAbort = false;
document.getElementById('start-seq-btn').style.display = 'none';
document.getElementById('stop-seq-btn').style.display = 'inline-block';
const repeatMode = document.getElementById('repeat-mode').value;
const repeatCount = parseInt(document.getElementById('repeat-count').value) || 1;
let totalIterations = 0;
if (repeatMode === 'once') {
totalIterations = 1;
} else if (repeatMode === 'count') {
totalIterations = repeatCount;
} else {
totalIterations = -1; // Infinite
}
let currentIteration = 0;
while (sequenceRunning && (totalIterations === -1 || currentIteration < totalIterations)) {
if (sequenceAbort) break;
currentIteration++;
if (totalIterations === -1) {
document.getElementById('seq-status').textContent = 'Running... (Iteration: ' + currentIteration + ')';
} else {
document.getElementById('seq-status').textContent = 'Running... (' + currentIteration + ' / ' + totalIterations + ')';
}
for (let i = 0; i < sequenceSteps.length; i++) {
if (sequenceAbort || !sequenceRunning) break;
const step = sequenceSteps[i];
if (step.type === 'message') {
sendCanMessage(step.id, step.msgType, step.dlc, step.data);
} else if (step.type === 'delay') {
await sleep(step.delay);
}
}
if (sequenceAbort || !sequenceRunning) break;
}
stopSequence();
}
function stopSequence() {
sequenceRunning = false;
sequenceAbort = true;
document.getElementById('start-seq-btn').style.display = 'inline-block';
document.getElementById('stop-seq-btn').style.display = 'none';
document.getElementById('seq-status').textContent = 'Stopped';
setTimeout(() => {
document.getElementById('seq-status').textContent = '';
}, 3000);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function saveSequencePreset() {
if (sequenceSteps.length === 0) {
alert('No sequence steps to save!');
return;
}
const name = prompt('Enter sequence preset name:');
if (!name) return;
try {
let presets = JSON.parse(localStorage.getItem('sequence_presets') || '{}');
presets[name] = {
steps: JSON.parse(JSON.stringify(sequenceSteps)),
savedAt: new Date().toISOString()
};
localStorage.setItem('sequence_presets', JSON.stringify(presets));
alert('Sequence preset "' + name + '" saved!');
} catch(e) {
console.error('Failed to save sequence preset:', e);
alert('Failed to save sequence preset!');
}
}
function loadSequencePreset() {
try {
const presets = JSON.parse(localStorage.getItem('sequence_presets') || '{}');
const names = Object.keys(presets);
if (names.length === 0) {
alert('No saved sequence presets!');
return;
}
let options = 'Select a sequence preset:\n\n';
names.forEach((name, index) => {
options += (index + 1) + '. ' + name + '\n';
});
const selection = prompt(options + '\nEnter number:');
if (!selection) return;
const index = parseInt(selection) - 1;
if (index >= 0 && index < names.length) {
const name = names[index];
if (sequenceSteps.length > 0) {
if (!confirm('Current sequence will be replaced. Continue?')) {
return;
}
}
sequenceSteps = JSON.parse(JSON.stringify(presets[name].steps));
updateSequenceList();
alert('Loaded sequence preset "' + name + '"');
}
} catch(e) {
console.error('Failed to load sequence preset:', e);
alert('Failed to load sequence preset!');
}
}
// Periodic Mode Functions (기존 함수들)
function savePreset() {
const presetName = document.getElementById('preset-name').value.trim();
if (!presetName) {
alert('Please enter a preset name!');
return;
}
if (messages.length === 0) {
alert('No messages to save!');
return;
}
try {
let presets = JSON.parse(localStorage.getItem('tx_presets') || '{}');
if (presets[presetName] && !confirm('Preset "' + presetName + '" already exists. Overwrite?')) {
return;
}
presets[presetName] = {
messages: JSON.parse(JSON.stringify(messages)),
savedAt: new Date().toISOString(),
count: messages.length
};
localStorage.setItem('tx_presets', JSON.stringify(presets));
document.getElementById('preset-name').value = '';
loadPresetList();
alert('Preset "' + presetName + '" saved successfully!');
} catch(e) {
console.error('Failed to save preset:', e);
alert('Failed to save preset!');
}
}
function loadPreset(presetName) {
try {
const presets = JSON.parse(localStorage.getItem('tx_presets') || '{}');
const preset = presets[presetName];
if (!preset) {
alert('Preset not found!');
return;
}
if (messages.length > 0 && !confirm('Current message list will be replaced. Continue?')) {
return;
}
stopAll();
messages = JSON.parse(JSON.stringify(preset.messages));
messages.forEach(msg => msg.active = false);
updateMessageList();
alert('Loaded preset "' + presetName + '" with ' + preset.count + ' messages');
} catch(e) {
console.error('Failed to load preset:', e);
alert('Failed to load preset!');
}
}
function deletePreset(presetName) {
if (!confirm('Delete preset "' + presetName + '"?')) {
return;
}
try {
let presets = JSON.parse(localStorage.getItem('tx_presets') || '{}');
delete presets[presetName];
localStorage.setItem('tx_presets', JSON.stringify(presets));
loadPresetList();
} catch(e) {
console.error('Failed to delete preset:', e);
alert('Failed to delete preset!');
}
}
function loadPresetList() {
const presetListDiv = document.getElementById('preset-list');
try {
const presets = JSON.parse(localStorage.getItem('tx_presets') || '{}');
const presetNames = Object.keys(presets);
if (presetNames.length === 0) {
presetListDiv.innerHTML = '<p style="text-align: center; color: #666; font-size: 0.9em;">No saved presets</p>';
return;
}
presetListDiv.innerHTML = '';
presetNames.sort().forEach(name => {
const preset = presets[name];
const item = document.createElement('div');
item.className = 'preset-item';
const savedDate = new Date(preset.savedAt);
const dateStr = savedDate.toLocaleDateString() + ' ' + savedDate.toLocaleTimeString();
item.innerHTML =
'<div>' +
'<div class="preset-name">' + name + '</div>' +
'<div class="preset-info">' + preset.count + ' messages | ' + dateStr + '</div>' +
'</div>' +
'<div class="preset-buttons">' +
'<button class="btn btn-success btn-small" onclick="loadPreset(\'' + name + '\')">Load</button>' +
'<button class="btn btn-danger btn-small" onclick="deletePreset(\'' + name + '\')">Delete</button>' +
'</div>';
presetListDiv.appendChild(item);
});
} catch(e) {
console.error('Failed to load preset list:', e);
presetListDiv.innerHTML = '<p style="text-align: center; color: #e74c3c; font-size: 0.9em;">Error loading presets</p>';
}
}
function addMessage() {
const id = document.getElementById('can-id').value.toUpperCase();
const type = document.getElementById('msg-type').value;
const dlc = parseInt(document.getElementById('dlc').value);
const interval = parseInt(document.getElementById('interval').value);
if (!id || !/^[0-9A-F]+$/.test(id)) {
alert('Invalid CAN ID!');
return;
}
const data = [];
for (let i = 0; i < 8; i++) {
const val = document.getElementById('d' + i).value.toUpperCase();
if (!/^[0-9A-F]{0,2}$/.test(val)) {
alert('Invalid data byte D' + i + '!');
return;
}
data.push(val.padStart(2, '0'));
}
const msg = {
id: id,
type: type,
dlc: dlc,
data: data,
interval: interval,
active: false
};
messages.push(msg);
updateMessageList();
}
function sendOnce() {
const id = document.getElementById('can-id').value.toUpperCase();
const type = document.getElementById('msg-type').value;
const dlc = parseInt(document.getElementById('dlc').value);
const data = [];
for (let i = 0; i < 8; i++) {
data.push(document.getElementById('d' + i).value.toUpperCase().padStart(2, '0'));
}
sendCanMessage(id, type, dlc, data);
}
function sendCanMessage(id, type, dlc, data) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
alert('WebSocket not connected!');
return;
}
const cmd = {
cmd: 'sendCan',
id: id,
type: type,
dlc: dlc,
data: data.join('')
};
ws.send(JSON.stringify(cmd));
console.log('Sent CAN message: ID=' + id + ', DLC=' + dlc + ', Data=' + data.join(''));
}
function updateMessageList() {
const list = document.getElementById('message-list');
if (messages.length === 0) {
list.innerHTML = '<p style="text-align: center; color: #666; font-size: 0.9em;">No messages</p>';
return;
}
list.innerHTML = '';
messages.forEach((msg, index) => {
const item = document.createElement('div');
item.className = 'message-item' + (msg.active ? ' active' : '');
item.innerHTML =
'<div class="message-id">0x' + msg.id + '</div>' +
'<div class="message-type type-' + msg.type + '">' + msg.type.toUpperCase() + '</div>' +
'<div class="message-data">' + msg.data.slice(0, msg.dlc).join(' ') + '</div>' +
'<div class="message-controls">' +
'<button class="btn ' + (msg.active ? 'btn-danger' : 'btn-success') + ' btn-small" onclick="toggleMessage(' + index + ')">' +
(msg.active ? 'Stop' : 'Start') +
'</button>' +
'<button class="btn btn-danger btn-small" onclick="deleteMessage(' + index + ')">Delete</button>' +
'</div>';
list.appendChild(item);
});
}
function toggleMessage(index) {
messages[index].active = !messages[index].active;
const cmd = {
cmd: messages[index].active ? 'startMsg' : 'stopMsg',
index: index,
id: messages[index].id,
type: messages[index].type,
dlc: messages[index].dlc,
data: messages[index].data.join(''),
interval: messages[index].interval
};
ws.send(JSON.stringify(cmd));
updateMessageList();
}
function deleteMessage(index) {
if (messages[index].active) {
ws.send(JSON.stringify({cmd: 'stopMsg', index: index}));
}
messages.splice(index, 1);
updateMessageList();
}
function startAll() {
messages.forEach((msg, index) => {
if (!msg.active) {
toggleMessage(index);
}
});
}
function stopAll() {
ws.send(JSON.stringify({cmd: 'stopAll'}));
messages.forEach(msg => msg.active = false);
updateMessageList();
}
function sendAllOnce() {
if (messages.length === 0) {
alert('No messages in the list!');
return;
}
if (!ws || ws.readyState !== WebSocket.OPEN) {
alert('WebSocket not connected!');
return;
}
const delayMs = parseInt(document.getElementById('send-all-delay').value) || 10;
const btn = event.target;
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = ' Sending...';
let sentCount = 0;
function sendNext(index) {
if (index >= messages.length) {
console.log('Sent all messages once: ' + sentCount + ' messages');
btn.innerHTML = ' Sent ' + sentCount + ' msgs';
btn.style.background = 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)';
setTimeout(() => {
btn.innerHTML = originalText;
btn.style.background = '';
btn.disabled = false;
}, 2000);
return;
}
const msg = messages[index];
sendCanMessage(msg.id, msg.type, msg.dlc, msg.data);
sentCount++;
btn.innerHTML = ' Sending ' + (index + 1) + '/' + messages.length;
setTimeout(() => {
sendNext(index + 1);
}, delayMs);
}
sendNext(0);
}
function clearAll() {
if (confirm('Clear all messages?')) {
stopAll();
messages = [];
updateMessageList();
}
}
// Input validation
document.querySelectorAll('input[id^="d"], input[id^="seq-d"]').forEach(input => {
input.addEventListener('input', function() {
this.value = this.value.toUpperCase().replace(/[^0-9A-F]/g, '');
});
});
document.getElementById('can-id').addEventListener('input', function() {
this.value = this.value.toUpperCase().replace(/[^0-9A-F]/g, '');
});
document.getElementById('seq-can-id').addEventListener('input', function() {
this.value = this.value.toUpperCase().replace(/[^0-9A-F]/g, '');
});
window.addEventListener('load', function() {
loadPresetList();
});
initWebSocket();
</script>
</body>
</html>
)rawliteral";
#endif