Files

616 lines
22 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">
<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: 20px; }
.section {
background: #f8f9fa;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
}
h2 {
color: #333;
margin-bottom: 15px;
font-size: 1.3em;
border-bottom: 3px solid #667eea;
padding-bottom: 8px;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 15px;
}
.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: 1em;
transition: all 0.3s;
}
input:focus, select:focus {
outline: none;
border-color: #667eea;
}
.button-group {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
padding: 12px 24px;
border: none;
border-radius: 5px;
font-size: 1em;
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;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.steps-list {
background: white;
border-radius: 8px;
padding: 15px;
max-height: 400px;
overflow-y: auto;
}
.step-item {
background: #f8f9fa;
padding: 12px;
margin-bottom: 10px;
border-radius: 5px;
border-left: 4px solid #667eea;
display: flex;
justify-content: space-between;
align-items: center;
}
.step-info {
flex: 1;
font-family: 'Courier New', monospace;
font-size: 0.9em;
}
.step-actions {
display: flex;
gap: 5px;
}
.btn-small {
padding: 5px 10px;
font-size: 0.85em;
}
.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(0,0,0,0.1);
}
.sequence-card h3 {
color: #667eea;
margin-bottom: 10px;
font-size: 1.1em;
}
.sequence-info {
font-size: 0.9em;
color: #666;
margin-bottom: 10px;
}
.status-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8em;
font-weight: 600;
margin-right: 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; }
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;
}
.sequence-list {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚀 CAN Sequence Transmitter</h1>
<p>Create and Execute 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>
</div>
<div class="content">
<!-- 퀀 -->
<div class="section">
<h2>Create New Sequence</h2>
<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">Count</option>
<option value="2">Infinite</option>
</select>
</div>
<div class="form-group">
<label>Repeat Count</label>
<input type="number" id="repeat-count" value="1" min="1" max="1000">
</div>
</div>
<h3 style="margin-top: 20px; margin-bottom: 10px; color: #555;">Add Step</h3>
<div class="form-row">
<div class="form-group">
<label>CAN ID (Hex)</label>
<input type="text" id="step-id" placeholder="0x123" maxlength="8">
</div>
<div class="form-group">
<label>ID Type</label>
<select id="step-extended">
<option value="0">Standard</option>
<option value="1">Extended</option>
</select>
</div>
<div class="form-group">
<label>DLC</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 (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</button>
</div>
<h3 style="margin-top: 20px; margin-bottom: 10px; color: #555;">Steps (<span id="step-count">0</span>)</h3>
<div class="steps-list" id="steps-list">
<p style="text-align: center; color: #999;">No steps added yet</p>
</div>
</div>
<!-- 퀀 -->
<div class="section">
<h2>Saved Sequences</h2>
<div class="sequence-list" id="sequence-list">
<p style="text-align: center; color: #999;">Loading...</p>
</div>
</div>
</div>
</div>
<script>
let ws;
let currentSteps = [];
let sequences = [];
function initWebSocket() {
ws = new WebSocket('ws://' + window.location.hostname + ':81/');
ws.onopen = function() {
console.log('WebSocket connected');
loadSequences();
};
ws.onclose = function() {
console.log('WebSocket disconnected');
setTimeout(initWebSocket, 3000);
};
ws.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'sequences') {
sequences = data.sequences;
updateSequenceList();
} else if (data.type === 'sequenceDetail') {
handleSequenceDetail(data);
} else if (data.type === 'sequenceSaved') {
if (data.success) {
alert('Sequence saved successfully!');
clearSteps();
loadSequences();
} else {
alert('Failed to save sequence (max 10 sequences)');
}
} else if (data.type === 'sequenceDeleted') {
if (data.success) {
loadSequences();
}
} else if (data.type === 'sequenceStarted') {
alert('Sequence started!');
} else if (data.type === 'sequenceStopped') {
alert('Sequence stopped!');
}
} catch (e) {
console.error('Parse error:', e);
}
};
}
function loadSequences() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({cmd: 'getSequences'}));
}
}
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);
// ID 검증
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');
return;
}
// Data 파싱
const dataBytes = dataStr.split(' ').map(b => parseInt(b, 16));
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 = '';
}
function removeStep(index) {
currentSteps.splice(index, 1);
updateStepsList();
}
function clearSteps() {
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;">No steps added yet</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=0x' + step.canId.toString(16).toUpperCase() + ' ' +
(step.extended ? '(Ext)' : '(Std)') + ' | ' +
'DLC=' + step.dlc + ' | ' +
'Data=[' + dataStr + '] | ' +
'Delay=' + step.delayMs + 'ms' +
'</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: 'saveSequence',
name: name,
mode: mode,
repeatCount: count,
steps: currentSteps
};
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(sequence));
}
}
function updateSequenceList() {
const list = document.getElementById('sequence-list');
if (sequences.length === 0) {
list.innerHTML = '<p style="text-align: center; color: #999;">No saved sequences</p>';
return;
}
list.innerHTML = '';
sequences.forEach(seq => {
const card = document.createElement('div');
card.className = 'sequence-card';
let modeBadge = '';
if (seq.mode === 0) {
modeBadge = '<span class="status-badge badge-once">Once</span>';
} else if (seq.mode === 1) {
modeBadge = '<span class="status-badge badge-count">Count: ' + seq.count + '</span>';
} else if (seq.mode === 2) {
modeBadge = '<span class="status-badge badge-infinite">Infinite</span>';
}
card.innerHTML =
'<h3>' + seq.name + '</h3>' +
'<div class="sequence-info">' +
modeBadge +
'<span class="status-badge" style="background: #e0e0e0; color: #555;">Steps: ' + seq.steps + '</span>' +
'</div>' +
'<div class="button-group">' +
'<button class="btn-primary btn-small" onclick="loadSequence(' + seq.index + ')">📝 Edit</button>' +
'<button class="btn-success btn-small" onclick="startSequence(' + seq.index + ')">▶️ Run</button>' +
'<button class="btn-warning btn-small" onclick="stopSequence()">⏹️ Stop</button>' +
'<button class="btn-danger btn-small" onclick="deleteSequence(' + seq.index + ')">🗑️ Delete</button>' +
'</div>';
list.appendChild(card);
});
}
function loadSequence(index) {
if (ws && ws.readyState === WebSocket.OPEN) {
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;
document.getElementById('repeat-count').value = seq.count;
// 스텝 로드
currentSteps = [];
seq.steps.forEach(step => {
const dataBytes = step.data.split(' ').map(b => parseInt(b, 16));
currentSteps.push({
canId: step.id,
extended: step.ext,
dlc: step.dlc,
data: dataBytes,
delayMs: step.delay
});
});
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) {
ws.send(JSON.stringify({cmd: 'startSequence', index: index}));
}
}
function stopSequence() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({cmd: 'stopSequence'}));
}
}
function deleteSequence(index) {
if (!confirm('Are you sure you want to delete this sequence?')) {
return;
}
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({cmd: 'deleteSequence', index: index}));
}
}
// Repeat mode 변경 시 count 필드 활성화/비활성화
document.getElementById('repeat-mode').addEventListener('change', function() {
const countInput = document.getElementById('repeat-count');
if (this.value === '1') {
countInput.disabled = false;
} else {
countInput.disabled = true;
}
});
window.addEventListener('load', function() {
initWebSocket();
});
</script>
</body>
</html>
)rawliteral";
#endif