Files
esp32s3_canlogger_mcp2515/aa-serial_terminal.h
2025-12-03 08:52:27 +00:00

785 lines
26 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 SERIAL_TERMINAL_H
#define SERIAL_TERMINAL_H
const char serial_terminal_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>Serial Terminal - Byun CAN Logger</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; }
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
margin-bottom: 15px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px;
border-radius: 10px;
text-align: center;
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.3);
}
.stat-label {
font-size: 0.8em;
opacity: 0.9;
margin-bottom: 5px;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 1.4em;
font-weight: 700;
font-family: 'Courier New', monospace;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
margin-bottom: 15px;
background: #f8f9fa;
padding: 15px;
border-radius: 10px;
border: 2px solid #e0e0e0;
}
.control-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.control-group label {
font-weight: 600;
color: #2c3e50;
font-size: 0.85em;
}
.control-group select,
.control-group input {
padding: 8px 10px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 0.9em;
transition: all 0.3s;
}
.control-group select:focus,
.control-group input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.format-selector {
display: flex;
align-items: center;
gap: 12px;
background: white;
padding: 10px 15px;
border-radius: 8px;
border: 2px solid #667eea;
margin-bottom: 12px;
}
.format-selector label {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
font-weight: 600;
color: #2c3e50;
font-size: 0.9em;
transition: all 0.3s;
}
.format-selector label:hover {
color: #667eea;
}
.format-selector input[type="radio"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #667eea;
}
.format-info {
font-size: 0.75em;
color: #7f8c8d;
margin-left: 3px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 5px;
font-size: 0.9em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
text-align: center;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-success {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
color: white;
}
.btn-success:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(17, 153, 142, 0.4);
}
.btn-danger {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: white;
}
.btn-danger:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.4);
}
.btn-warning {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
.btn-secondary {
background: #95a5a6;
color: white;
}
.btn-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.terminal-container {
background: #1e1e1e;
border-radius: 10px;
padding: 15px;
margin-bottom: 15px;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
}
.terminal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
}
.terminal-output {
height: 450px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 0.85em;
color: #0f0;
background: #000;
padding: 10px;
border-radius: 5px;
white-space: pre-wrap;
word-wrap: break-word;
}
.terminal-output::-webkit-scrollbar {
width: 10px;
}
.terminal-output::-webkit-scrollbar-track {
background: #2c3e50;
border-radius: 5px;
}
.terminal-output::-webkit-scrollbar-thumb {
background: #667eea;
border-radius: 5px;
}
.terminal-output::-webkit-scrollbar-thumb:hover {
background: #764ba2;
}
.terminal-line {
margin-bottom: 2px;
line-height: 1.5;
padding: 2px 0;
}
.terminal-line.tx {
color: #ff6b6b;
}
.terminal-line.rx {
color: #38ef7d;
}
.terminal-timestamp {
color: #95a5a6;
margin-right: 8px;
font-size: 0.9em;
}
.terminal-direction {
font-weight: 700;
margin-right: 8px;
}
.input-area {
display: flex;
gap: 10px;
margin-top: 12px;
}
.input-area input {
flex: 1;
padding: 12px;
border: 2px solid #667eea;
border-radius: 5px;
font-family: 'Courier New', monospace;
font-size: 0.95em;
background: #2c3e50;
color: #fff;
}
.input-area input:focus {
outline: none;
border-color: #764ba2;
box-shadow: 0 0 0 3px rgba(118, 75, 162, 0.2);
}
.input-area input::placeholder {
color: #95a5a6;
}
.status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}
.status-indicator.on {
background: #38ef7d;
box-shadow: 0 0 8px #38ef7d;
animation: pulse 2s ease-in-out infinite;
}
.status-indicator.off {
background: #95a5a6;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.status-text {
display: flex;
align-items: center;
font-weight: 600;
color: white;
font-size: 0.95em;
}
.terminal-info {
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.85em;
color: #333;
}
@media (max-width: 768px) {
.controls {
grid-template-columns: 1fr;
}
.terminal-output {
height: 300px;
font-size: 0.75em;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📟 Serial Terminal</h1>
<p>RS232/UART Communication Interface with Real-time Logging</p>
</div>
<div class="nav">
<a href="/">📊 Monitor</a>
<a href="/transmit">📤 Transmit</a>
<a href="/graph">📈 Graph</a>
<a href="/graph-view">📊 Graph View</a>
<a href="/settings"> Settings</a>
<a href="/serial" class="active">📟 Serial</a>
</div>
<div class="content">
<!-- -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">📥 RECEIVED</div>
<div class="stat-value" id="rx-count">0</div>
</div>
<div class="stat-card" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
<div class="stat-label">📤 TRANSMITTED</div>
<div class="stat-value" id="tx-count">0</div>
</div>
<div class="stat-card" style="background: linear-gradient(135deg, #43cea2 0%, #185a9d 100%);">
<div class="stat-label">💾 LOG SIZE</div>
<div class="stat-value" id="log-size">0 KB</div>
</div>
<div class="stat-card" style="background: linear-gradient(135deg, #f2994a 0%, #f2c94c 100%);">
<div class="stat-label">📦 QUEUE</div>
<div class="stat-value" id="queue-usage">0%</div>
</div>
</div>
<div class="terminal-info">
<strong> Info:</strong> Configure serial port settings below. Data will appear in real-time in the terminal window.
All received (RX) and transmitted (TX) data is timestamped.
</div>
<!-- Serial -->
<div class="controls">
<div class="control-group">
<label> Baud Rate</label>
<select id="baudrate">
<option value="9600">9600</option>
<option value="19200">19200</option>
<option value="38400">38400</option>
<option value="57600">57600</option>
<option value="115200" selected>115200</option>
<option value="230400">230400</option>
<option value="460800">460800</option>
<option value="921600">921600</option>
</select>
</div>
<div class="control-group">
<label>📊 Data Bits</label>
<select id="databits">
<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="control-group">
<label>🔀 Parity</label>
<select id="parity">
<option value="0" selected>None</option>
<option value="1">Even</option>
<option value="2">Odd</option>
</select>
</div>
<div class="control-group">
<label> Stop Bits</label>
<select id="stopbits">
<option value="1" selected>1</option>
<option value="2">2</option>
</select>
</div>
</div>
<!-- -->
<div class="format-selector">
<label style="font-weight: 700; color: #2c3e50;">📁 Log Format:</label>
<label>
<input type="radio" name="serial-format" value="csv" checked>
<span>📄 CSV</span>
<span class="format-info">(Text - Easy to Read)</span>
</label>
<label>
<input type="radio" name="serial-format" value="bin">
<span>📦 BIN</span>
<span class="format-info">(Binary - Compact)</span>
</label>
</div>
<!-- -->
<div class="btn-group" style="margin-bottom: 15px;">
<button class="btn btn-primary" onclick="applySerialConfig()">✓ Apply Settings</button>
<button class="btn btn-success" id="log-btn" onclick="toggleSerialLogging()">
<span class="status-indicator off"></span>
Start Logging
</button>
<button class="btn btn-warning" onclick="clearTerminal()">🗑️ Clear Terminal</button>
<button class="btn btn-secondary" onclick="toggleAutoScroll()">
<span id="autoscroll-text">🔽 Auto-scroll: ON</span>
</button>
</div>
<!-- -->
<div class="terminal-container">
<div class="terminal-header">
<div class="status-text">
<span class="status-indicator" id="logging-status"></span>
<span id="logging-text">Serial Terminal Ready</span>
</div>
<div style="color: #95a5a6; font-size: 0.85em;">
Lines: <strong id="line-count" style="color: white;">0</strong>
</div>
</div>
<div class="terminal-output" id="terminal">
<div style="color: #95a5a6; text-align: center; padding: 20px;">
Waiting for data...
</div>
</div>
<div class="input-area">
<input type="text" id="serial-input" placeholder="Type command and press Enter..."
onkeypress="handleKeyPress(event)">
<button class="btn btn-primary" onclick="sendSerialData()">📤 Send</button>
</div>
</div>
</div>
</div>
<script>
let ws;
let autoScroll = true;
let serialLogging = false;
let totalRx = 0;
let totalTx = 0;
let logSize = 0;
let queueUsed = 0;
let queueSize = 2000;
let lineCount = 0;
function initWebSocket() {
ws = new WebSocket('ws://' + window.location.hostname + ':81');
ws.onopen = function() {
console.log(' WebSocket connected');
requestSerialConfig();
};
ws.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'update') {
updateStats(data);
if (data.serialMessages && data.serialMessages.length > 0) {
data.serialMessages.forEach(msg => {
addTerminalLine(msg);
});
}
} else if (data.type === 'serialConfig') {
loadSerialConfig(data);
}
} catch (e) {
console.error('Parse error:', e);
}
};
ws.onerror = function(error) {
console.error('WebSocket error:', error);
};
ws.onclose = function() {
console.log(' WebSocket disconnected');
setTimeout(initWebSocket, 2000);
};
}
function requestSerialConfig() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({cmd: 'getSerialConfig'}));
}
}
function loadSerialConfig(config) {
document.getElementById('baudrate').value = config.baudRate || 115200;
document.getElementById('databits').value = config.dataBits || 8;
document.getElementById('parity').value = config.parity || 0;
document.getElementById('stopbits').value = config.stopBits || 1;
console.log(' Serial config loaded:', config);
}
function applySerialConfig() {
const config = {
cmd: 'setSerialConfig',
baudRate: parseInt(document.getElementById('baudrate').value),
dataBits: parseInt(document.getElementById('databits').value),
parity: parseInt(document.getElementById('parity').value),
stopBits: parseInt(document.getElementById('stopbits').value)
};
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(config));
console.log(' Serial config applied:', config);
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = ' Applied!';
btn.style.background = 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)';
setTimeout(() => {
btn.textContent = originalText;
btn.style.background = '';
}, 2000);
}
}
function toggleSerialLogging() {
if (ws && ws.readyState === WebSocket.OPEN) {
if (serialLogging) {
ws.send(JSON.stringify({cmd: 'stopSerialLogging'}));
} else {
let serialFormat = 'csv';
const formatRadios = document.getElementsByName('serial-format');
for (const radio of formatRadios) {
if (radio.checked) {
serialFormat = radio.value;
break;
}
}
ws.send(JSON.stringify({
cmd: 'startSerialLogging',
format: serialFormat
}));
console.log(' Start serial logging: ' + serialFormat);
}
serialLogging = !serialLogging;
updateLoggingUI();
}
}
function updateLoggingUI() {
const btn = document.getElementById('log-btn');
const status = document.getElementById('logging-status');
const text = document.getElementById('logging-text');
if (serialLogging) {
btn.innerHTML = '<span class="status-indicator on"></span>⏹️ Stop Logging';
btn.className = 'btn btn-danger';
status.className = 'status-indicator on';
text.textContent = '🟢 Logging Active';
} else {
btn.innerHTML = '<span class="status-indicator off"></span>▶️ Start Logging';
btn.className = 'btn btn-success';
status.className = 'status-indicator off';
text.textContent = 'Serial Terminal Ready';
}
}
function sendSerialData() {
const input = document.getElementById('serial-input');
const data = input.value.trim();
if (data.length === 0) return;
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
cmd: 'sendSerial',
data: data
}));
input.value = '';
console.log(' Sent:', data);
}
}
function handleKeyPress(event) {
if (event.key === 'Enter') {
sendSerialData();
}
}
function addTerminalLine(msg) {
const terminal = document.getElementById('terminal');
// 첫 번째 라인이면 안내 메시지 제거
if (lineCount === 0) {
terminal.innerHTML = '';
}
const line = document.createElement('div');
line.className = 'terminal-line ' + (msg.isTx ? 'tx' : 'rx');
// 타임스탬프 포맷
const date = new Date(msg.timestamp / 1000);
const timestamp = date.toLocaleTimeString('ko-KR', {hour12: false}) + '.' +
String(date.getMilliseconds()).padStart(3, '0');
const direction = msg.isTx ? '[TX]' : '[RX]';
line.innerHTML =
'<span class="terminal-timestamp">' + timestamp + '</span>' +
'<span class="terminal-direction">' + direction + '</span>' +
escapeHtml(msg.data);
terminal.appendChild(line);
lineCount++;
// 자동 스크롤
if (autoScroll) {
terminal.scrollTop = terminal.scrollHeight;
}
// 최대 1000줄로 제한
while (terminal.children.length > 1000) {
terminal.removeChild(terminal.firstChild);
lineCount--;
}
document.getElementById('line-count').textContent = lineCount;
}
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
function clearTerminal() {
if (lineCount > 0) {
if (!confirm('Clear ' + lineCount + ' lines?')) return;
}
document.getElementById('terminal').innerHTML =
'<div style="color: #95a5a6; text-align: center; padding: 20px;">Waiting for data...</div>';
lineCount = 0;
document.getElementById('line-count').textContent = '0';
console.log(' Terminal cleared');
}
function toggleAutoScroll() {
autoScroll = !autoScroll;
const btn = event.target;
btn.querySelector('#autoscroll-text').textContent =
(autoScroll ? '🔽' : '') + ' Auto-scroll: ' + (autoScroll ? 'ON' : 'OFF');
if (!autoScroll) {
btn.style.opacity = '0.7';
} else {
btn.style.opacity = '1';
}
}
function updateStats(data) {
if (data.totalSerialRx !== undefined) {
totalRx = data.totalSerialRx;
document.getElementById('rx-count').textContent = totalRx.toLocaleString();
}
if (data.totalSerialTx !== undefined) {
totalTx = data.totalSerialTx;
document.getElementById('tx-count').textContent = totalTx.toLocaleString();
}
if (data.serialFileSize !== undefined) {
logSize = data.serialFileSize;
const sizeKB = (logSize / 1024).toFixed(1);
const sizeMB = (logSize / 1024 / 1024).toFixed(2);
document.getElementById('log-size').textContent =
logSize > 1024 * 1024 ? sizeMB + ' MB' : sizeKB + ' KB';
}
if (data.serialQueueUsed !== undefined && data.serialQueueSize !== undefined) {
queueUsed = data.serialQueueUsed;
queueSize = data.serialQueueSize;
const percentage = ((queueUsed / queueSize) * 100).toFixed(0);
document.getElementById('queue-usage').textContent = percentage + '%';
}
if (data.serialLogging !== undefined && data.serialLogging !== serialLogging) {
serialLogging = data.serialLogging;
updateLoggingUI();
}
}
// 초기화
window.addEventListener('load', function() {
initWebSocket();
});
</script>
</body>
</html>
)rawliteral";
#endif