671 lines
25 KiB
HTML
671 lines
25 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>ESP32 Debug Dongle</title>
|
|
|
|
<!-- xterm.js from CDN -->
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css">
|
|
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.min.js"></script>
|
|
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
background: #1a1a2e;
|
|
color: #eee;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.header {
|
|
background: #16213e;
|
|
padding: 10px 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
border-bottom: 1px solid #0f3460;
|
|
}
|
|
|
|
.header h1 {
|
|
margin: 0;
|
|
font-size: 1.2em;
|
|
color: #e94560;
|
|
}
|
|
|
|
.controls {
|
|
display: flex;
|
|
gap: 15px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.control-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.control-group label {
|
|
font-size: 0.85em;
|
|
color: #aaa;
|
|
}
|
|
|
|
select, button {
|
|
background: #0f3460;
|
|
color: #fff;
|
|
border: 1px solid #1a1a2e;
|
|
padding: 6px 12px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
select:hover, button:hover {
|
|
background: #1a4a7a;
|
|
}
|
|
|
|
button.danger {
|
|
background: #e94560;
|
|
}
|
|
|
|
button.danger:hover {
|
|
background: #ff6b6b;
|
|
}
|
|
|
|
.status {
|
|
display: flex;
|
|
gap: 15px;
|
|
font-size: 0.8em;
|
|
}
|
|
|
|
.status-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: #666;
|
|
}
|
|
|
|
.status-dot.connected {
|
|
background: #4ade80;
|
|
}
|
|
|
|
.status-dot.disconnected {
|
|
background: #ef4444;
|
|
}
|
|
|
|
.main-row {
|
|
flex: 1;
|
|
display: flex;
|
|
min-height: 0;
|
|
}
|
|
|
|
.terminal-container {
|
|
flex: 1;
|
|
padding: 10px;
|
|
overflow: hidden;
|
|
min-width: 0;
|
|
}
|
|
|
|
#terminal {
|
|
height: 100%;
|
|
}
|
|
|
|
/* Right-side comms panel (MeshCore today, more later) */
|
|
.comms-panel {
|
|
flex: 0 0 340px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: #16213e;
|
|
border-left: 1px solid #0f3460;
|
|
padding: 10px 12px;
|
|
gap: 10px;
|
|
min-height: 0;
|
|
}
|
|
.comms-panel h2 {
|
|
margin: 0;
|
|
font-size: 1em;
|
|
color: #e94560;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
.comms-cfg {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
font-size: 0.85em;
|
|
}
|
|
.comms-cfg input {
|
|
background: #0f3460;
|
|
color: #fff;
|
|
border: 1px solid #1a4a7a;
|
|
padding: 6px 8px;
|
|
border-radius: 4px;
|
|
font-size: 0.9em;
|
|
width: 100%;
|
|
}
|
|
.comms-cfg .row { display: flex; gap: 6px; }
|
|
.comms-cfg .row input { flex: 1; }
|
|
#meshMessages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
background: #0f1a30;
|
|
border: 1px solid #0f3460;
|
|
border-radius: 4px;
|
|
padding: 6px 8px;
|
|
font-family: Menlo, Monaco, "Courier New", monospace;
|
|
font-size: 0.8em;
|
|
line-height: 1.4;
|
|
min-height: 60px;
|
|
}
|
|
.mc-msg { margin-bottom: 4px; word-break: break-word; }
|
|
.mc-msg .who { color: #60a5fa; }
|
|
.mc-msg.tx .who { color: #4ade80; }
|
|
.mc-msg .meta { color: #666; font-size: 0.85em; }
|
|
.mc-empty { color: #666; }
|
|
.comms-send { display: flex; gap: 6px; }
|
|
.comms-send input { flex: 1; }
|
|
.mesh-dot {
|
|
width: 8px; height: 8px; border-radius: 50%;
|
|
background: #666; display: inline-block;
|
|
}
|
|
.mesh-dot.up { background: #4ade80; }
|
|
.mesh-dot.down { background: #ef4444; }
|
|
|
|
.footer {
|
|
background: #16213e;
|
|
padding: 8px 20px;
|
|
font-size: 0.75em;
|
|
color: #666;
|
|
border-top: 1px solid #0f3460;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.controls {
|
|
gap: 10px;
|
|
}
|
|
.control-group label {
|
|
display: none;
|
|
}
|
|
}
|
|
@media (max-width: 900px) {
|
|
.main-row { flex-direction: column; }
|
|
.comms-panel {
|
|
flex: 0 0 45vh;
|
|
border-left: none;
|
|
border-top: 1px solid #0f3460;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>🔌 ESP32 Debug Dongle</h1>
|
|
<div class="controls">
|
|
<div class="control-group">
|
|
<label>Port:</label>
|
|
<select id="portSelect">
|
|
<option value="0">Internal (Debug)</option>
|
|
<option value="1">USB Serial</option>
|
|
<option value="2">External (Serial1)</option>
|
|
</select>
|
|
</div>
|
|
<div class="control-group">
|
|
<label>Baud:</label>
|
|
<select id="baudSelect">
|
|
<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>
|
|
<button class="danger" onclick="doReset()" title="Pulse the target reset line">Reset</button>
|
|
<button onclick="doButton()" title="Pulse the target button / wake line">Button</button>
|
|
<button onclick="clearTerminal()">Clear</button>
|
|
<button onclick="reconnect()">Reconnect</button>
|
|
<button id="logBtn" onclick="toggleLog()">Log: --</button>
|
|
<button onclick="toggleFiles()">Files</button>
|
|
</div>
|
|
<div class="status">
|
|
<div class="status-item">
|
|
<span class="status-dot" id="wsStatus"></span>
|
|
<span>WebSocket</span>
|
|
</div>
|
|
<div class="status-item">
|
|
<span class="status-dot" id="btStatus"></span>
|
|
<span>Bluetooth</span>
|
|
</div>
|
|
<div class="status-item" id="wifiInfo" style="display:none;">
|
|
<span id="wifiIcon">📶</span>
|
|
<span id="wifiText">WiFi</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="filesPanel" style="display:none; background:#16213e; border-bottom:1px solid #0f3460; padding:10px 20px; max-height:40vh; overflow:auto;">
|
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
|
|
<strong>SD log files</strong>
|
|
<span><span id="logState" style="color:#aaa; margin-right:10px;"></span><button onclick="refreshLogs()">Refresh</button></span>
|
|
</div>
|
|
<table id="filesTable" style="width:100%; border-collapse:collapse; font-size:0.85em;"></table>
|
|
</div>
|
|
|
|
<div class="main-row">
|
|
<div class="terminal-container">
|
|
<div id="terminal"></div>
|
|
</div>
|
|
|
|
<div class="comms-panel" id="commsPanel">
|
|
<h2>
|
|
<span>📡 MeshCore</span>
|
|
<span><span class="mesh-dot" id="meshDot"></span> <span id="meshState" style="font-size:0.75em;color:#aaa;">…</span></span>
|
|
</h2>
|
|
<div class="comms-cfg">
|
|
<input type="text" id="meshChan" placeholder="Channel name (e.g. SensorsTest)">
|
|
<div class="row">
|
|
<input type="text" id="meshPsk" placeholder="PSK (base64, 16 or 32-byte key)">
|
|
<button onclick="programChannel()">Program</button>
|
|
</div>
|
|
</div>
|
|
<div id="meshMessages"><div class="mc-empty">No messages yet.</div></div>
|
|
<div class="comms-send">
|
|
<input type="text" id="meshInput" placeholder="Message…" onkeydown="if(event.key==='Enter')sendMesh()">
|
|
<button onclick="sendMesh()">Send</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="footer">
|
|
<span id="rxCount">RX: 0 bytes</span>
|
|
<span id="txCount">TX: 0 bytes</span>
|
|
<span id="heap">Heap: --</span>
|
|
</div>
|
|
|
|
<script>
|
|
// Terminal setup
|
|
const term = new Terminal({
|
|
cursorBlink: true,
|
|
fontSize: 14,
|
|
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
|
theme: {
|
|
background: '#1a1a2e',
|
|
foreground: '#eee',
|
|
cursor: '#e94560',
|
|
cursorAccent: '#1a1a2e',
|
|
selection: 'rgba(233, 69, 96, 0.3)',
|
|
black: '#1a1a2e',
|
|
red: '#e94560',
|
|
green: '#4ade80',
|
|
yellow: '#fbbf24',
|
|
blue: '#60a5fa',
|
|
magenta: '#c084fc',
|
|
cyan: '#22d3d3',
|
|
white: '#eee',
|
|
brightBlack: '#666',
|
|
brightRed: '#ff6b6b',
|
|
brightGreen: '#86efac',
|
|
brightYellow: '#fcd34d',
|
|
brightBlue: '#93c5fd',
|
|
brightMagenta: '#d8b4fe',
|
|
brightCyan: '#67e8f9',
|
|
brightWhite: '#fff'
|
|
}
|
|
});
|
|
|
|
// Addons
|
|
const fitAddon = new FitAddon.FitAddon();
|
|
const webLinksAddon = new WebLinksAddon.WebLinksAddon();
|
|
|
|
term.loadAddon(fitAddon);
|
|
term.loadAddon(webLinksAddon);
|
|
|
|
// Open terminal
|
|
term.open(document.getElementById('terminal'));
|
|
fitAddon.fit();
|
|
|
|
// Resize handler
|
|
window.addEventListener('resize', () => {
|
|
fitAddon.fit();
|
|
});
|
|
|
|
// Stats
|
|
let rxBytes = 0;
|
|
let txBytes = 0;
|
|
|
|
// WebSocket connection
|
|
let ws = null;
|
|
let reconnectTimer = null;
|
|
|
|
function connect() {
|
|
const wsUrl = `ws://${window.location.host}/ws`;
|
|
term.writeln(`\x1b[33mConnecting to ${wsUrl}...\x1b[0m`);
|
|
|
|
ws = new WebSocket(wsUrl);
|
|
ws.binaryType = 'arraybuffer';
|
|
|
|
ws.onopen = () => {
|
|
term.writeln('\x1b[32mConnected!\x1b[0m\r\n');
|
|
updateStatus('wsStatus', true);
|
|
|
|
// Request initial status
|
|
sendCommand({ cmd: 'getStatus' });
|
|
sendCommand({ cmd: 'meshGet' });
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
term.writeln('\r\n\x1b[31mDisconnected\x1b[0m');
|
|
updateStatus('wsStatus', false);
|
|
|
|
// Auto-reconnect after 3 seconds
|
|
if (!reconnectTimer) {
|
|
reconnectTimer = setTimeout(() => {
|
|
reconnectTimer = null;
|
|
connect();
|
|
}, 3000);
|
|
}
|
|
};
|
|
|
|
ws.onerror = (err) => {
|
|
term.writeln('\r\n\x1b[31mWebSocket error\x1b[0m');
|
|
console.error('WebSocket error:', err);
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
// Control/mesh events arrive as text frames (0x00-prefixed JSON).
|
|
if (typeof event.data === 'string') {
|
|
let s = event.data;
|
|
if (s.charCodeAt(0) === 0) s = s.slice(1);
|
|
try { handleResponse(JSON.parse(s)); } catch (e) {}
|
|
return;
|
|
}
|
|
if (event.data instanceof ArrayBuffer) {
|
|
const data = new Uint8Array(event.data);
|
|
|
|
// Check for command response (starts with 0x00)
|
|
if (data.length > 0 && data[0] === 0x00) {
|
|
const jsonStr = new TextDecoder().decode(data.slice(1));
|
|
try {
|
|
const msg = JSON.parse(jsonStr);
|
|
handleResponse(msg);
|
|
} catch (e) {
|
|
console.error('JSON parse error:', e);
|
|
}
|
|
} else {
|
|
// Regular serial data
|
|
rxBytes += data.length;
|
|
updateStats();
|
|
|
|
// Write to terminal
|
|
const text = new TextDecoder().decode(data);
|
|
term.write(text);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
function handleResponse(msg) {
|
|
switch (msg.type) {
|
|
case 'status':
|
|
document.getElementById('portSelect').value = msg.currentPort;
|
|
document.getElementById('baudSelect').value = msg.baudSerial1;
|
|
updateStatus('btStatus', msg.btConnected);
|
|
document.getElementById('heap').textContent = `Heap: ${msg.freeHeap}`;
|
|
updateLogUi(msg);
|
|
|
|
// Show WiFi info
|
|
const wifiInfo = document.getElementById('wifiInfo');
|
|
const wifiText = document.getElementById('wifiText');
|
|
wifiInfo.style.display = 'flex';
|
|
if (msg.wifiMode === 'station' && msg.rssi) {
|
|
const signal = msg.rssi > -50 ? '📶' : msg.rssi > -70 ? '📶' : '📶';
|
|
wifiText.textContent = `${msg.rssi}dBm`;
|
|
} else {
|
|
wifiText.textContent = 'AP Mode';
|
|
}
|
|
break;
|
|
|
|
case 'portChanged':
|
|
document.getElementById('portSelect').value = msg.port;
|
|
const portNames = ['Internal', 'USB Serial', 'External'];
|
|
term.writeln(`\r\n\x1b[33m[Switched to ${portNames[msg.port]}]\x1b[0m\r\n`);
|
|
break;
|
|
|
|
case 'mesh':
|
|
appendMeshMsg(msg);
|
|
break;
|
|
|
|
case 'meshcfg':
|
|
updateMeshCfg(msg);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ---- MeshCore comms panel ----
|
|
let meshEnabled = false;
|
|
function updateMeshCfg(c) {
|
|
meshEnabled = !!c.enabled;
|
|
const dot = document.getElementById('meshDot');
|
|
const state = document.getElementById('meshState');
|
|
const chan = document.getElementById('meshChan');
|
|
const psk = document.getElementById('meshPsk');
|
|
if (!meshEnabled) {
|
|
dot.className = 'mesh-dot';
|
|
state.textContent = 'not in build';
|
|
} else if (c.up) {
|
|
dot.className = 'mesh-dot up';
|
|
state.textContent = `up · rx ${c.rx} tx ${c.tx}`;
|
|
} else {
|
|
dot.className = 'mesh-dot down';
|
|
state.textContent = 'radio down';
|
|
}
|
|
// Don't clobber a field the user is editing.
|
|
if (document.activeElement !== chan && typeof c.channel !== 'undefined') chan.value = c.channel;
|
|
if (document.activeElement !== psk && typeof c.psk !== 'undefined') psk.value = c.psk;
|
|
const disabled = !meshEnabled;
|
|
document.getElementById('meshInput').disabled = disabled;
|
|
}
|
|
function appendMeshMsg(m) {
|
|
const box = document.getElementById('meshMessages');
|
|
const empty = box.querySelector('.mc-empty');
|
|
if (empty) empty.remove();
|
|
const div = document.createElement('div');
|
|
div.className = 'mc-msg' + (m.dir === 'tx' ? ' tx' : '');
|
|
const t = new Date().toLocaleTimeString();
|
|
let meta = m.dir === 'tx' ? '↑' : '↓';
|
|
if (m.channel) meta += ' ' + m.channel;
|
|
if (typeof m.rssi !== 'undefined' && m.rssi) meta += ` ${m.rssi}dBm`;
|
|
div.innerHTML = `<span class="meta">[${t}] ${escapeHtml(meta)}</span> ` +
|
|
`<span class="who">${escapeHtml(m.sender || '?')}:</span> ` +
|
|
escapeHtml(m.text || '');
|
|
box.appendChild(div);
|
|
box.scrollTop = box.scrollHeight;
|
|
}
|
|
function escapeHtml(s) {
|
|
return String(s).replace(/[&<>"']/g, c =>
|
|
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
}
|
|
function programChannel() {
|
|
const name = document.getElementById('meshChan').value.trim();
|
|
const psk = document.getElementById('meshPsk').value.trim();
|
|
if (!psk) { alert('Enter a PSK (base64 16- or 32-byte key)'); return; }
|
|
sendCommand({ cmd: 'meshPsk', name: name, psk: psk });
|
|
setTimeout(() => sendCommand({ cmd: 'meshGet' }), 300);
|
|
}
|
|
function sendMesh() {
|
|
const inp = document.getElementById('meshInput');
|
|
const text = inp.value;
|
|
if (!text) return;
|
|
sendCommand({ cmd: 'meshSend', text: text });
|
|
inp.value = '';
|
|
}
|
|
|
|
function sendCommand(cmd) {
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
const json = JSON.stringify(cmd);
|
|
const data = new Uint8Array(json.length + 1);
|
|
data[0] = 0x00; // Command prefix
|
|
for (let i = 0; i < json.length; i++) {
|
|
data[i + 1] = json.charCodeAt(i);
|
|
}
|
|
ws.send(data);
|
|
}
|
|
}
|
|
|
|
function sendData(data) {
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
const bytes = new TextEncoder().encode(data);
|
|
txBytes += bytes.length;
|
|
updateStats();
|
|
ws.send(bytes);
|
|
}
|
|
}
|
|
|
|
// Terminal input handler
|
|
term.onData(data => {
|
|
sendData(data);
|
|
});
|
|
|
|
// UI handlers
|
|
document.getElementById('portSelect').addEventListener('change', (e) => {
|
|
sendCommand({ cmd: 'setPort', port: parseInt(e.target.value) });
|
|
});
|
|
|
|
document.getElementById('baudSelect').addEventListener('change', (e) => {
|
|
const port = parseInt(document.getElementById('portSelect').value);
|
|
sendCommand({ cmd: 'setBaud', port: port, baud: parseInt(e.target.value) });
|
|
});
|
|
|
|
function clearTerminal() {
|
|
term.clear();
|
|
rxBytes = 0;
|
|
txBytes = 0;
|
|
updateStats();
|
|
}
|
|
|
|
function reconnect() {
|
|
if (ws) {
|
|
ws.close();
|
|
}
|
|
if (reconnectTimer) {
|
|
clearTimeout(reconnectTimer);
|
|
reconnectTimer = null;
|
|
}
|
|
setTimeout(connect, 100);
|
|
}
|
|
|
|
function updateStatus(elementId, connected) {
|
|
const el = document.getElementById(elementId);
|
|
el.classList.toggle('connected', connected);
|
|
el.classList.toggle('disconnected', !connected);
|
|
}
|
|
|
|
function updateStats() {
|
|
document.getElementById('rxCount').textContent = `RX: ${formatBytes(rxBytes)}`;
|
|
document.getElementById('txCount').textContent = `TX: ${formatBytes(txBytes)}`;
|
|
}
|
|
|
|
function formatBytes(bytes) {
|
|
if (bytes < 1024) return bytes + ' B';
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
return (bytes / 1024 / 1024).toFixed(1) + ' MB';
|
|
}
|
|
|
|
// ---- Target control lines (reset / button) ----
|
|
function pulseLine(path, label) {
|
|
fetch(path + '?ms=200').then(r => r.text())
|
|
.then(t => term.writeln(`\r\n\x1b[33m[${label}] ${t.trim()}\x1b[0m`))
|
|
.catch(() => term.writeln(`\r\n\x1b[31m[${label} failed]\x1b[0m`));
|
|
}
|
|
function doReset() { pulseLine('/api/reset', 'reset'); }
|
|
function doButton() { pulseLine('/api/button', 'button'); }
|
|
|
|
// ---- SD logging controls ----
|
|
let logOn = false;
|
|
function toggleLog() {
|
|
fetch('/api/log?on=' + (logOn ? 0 : 1))
|
|
.then(r => r.json()).then(updateLogUi)
|
|
.catch(() => term.writeln('\r\n\x1b[31m[log toggle failed]\x1b[0m'));
|
|
}
|
|
function updateLogUi(s) {
|
|
if (typeof s.log === 'undefined') return;
|
|
logOn = !!s.log;
|
|
const b = document.getElementById('logBtn');
|
|
b.textContent = 'Log: ' + (logOn ? 'ON' : 'off');
|
|
b.classList.toggle('danger', logOn);
|
|
const st = document.getElementById('logState');
|
|
if (st) {
|
|
st.textContent = !s.sd ? 'no SD card'
|
|
: s.logfile ? ('file: ' + String(s.logfile).split('/').pop())
|
|
: (s.ntp ? 'idle' : 'waiting for NTP date');
|
|
}
|
|
}
|
|
function toggleFiles() {
|
|
const p = document.getElementById('filesPanel');
|
|
const show = p.style.display === 'none';
|
|
p.style.display = show ? 'block' : 'none';
|
|
if (show) refreshLogs();
|
|
}
|
|
function refreshLogs() {
|
|
fetch('/api/logs').then(r => r.json()).then(list => {
|
|
const t = document.getElementById('filesTable');
|
|
if (!list.length) { t.innerHTML = '<tr><td style="color:#666;">(no log files yet)</td></tr>'; return; }
|
|
t.innerHTML = list.map(f =>
|
|
`<tr style="border-bottom:1px solid #0f3460;">
|
|
<td style="padding:4px 0;">${f.name}${f.active ? ' <span style="color:#4ade80;">(active)</span>' : ''}</td>
|
|
<td style="text-align:right; color:#aaa;">${formatBytes(f.size)}</td>
|
|
<td style="text-align:right; padding-left:12px;"><a href="/api/logfile?name=${encodeURIComponent(f.name)}" style="color:#60a5fa;">download</a></td>
|
|
<td style="text-align:right; padding-left:12px;">${f.active ? '' : `<a href="#" onclick="deleteLog('${f.name}');return false;" style="color:#e94560;">delete</a>`}</td>
|
|
</tr>`).join('');
|
|
}).catch(() => {
|
|
document.getElementById('filesTable').innerHTML = '<tr><td style="color:#e94560;">(failed -- SD only on T3-S3 build)</td></tr>';
|
|
});
|
|
}
|
|
function deleteLog(name) {
|
|
if (!confirm('Delete ' + name + '?')) return;
|
|
fetch('/api/logdelete?name=' + encodeURIComponent(name)).then(() => refreshLogs());
|
|
}
|
|
|
|
// Initial connection
|
|
term.writeln('\x1b[1;36m╔═══════════════════════════════════════╗\x1b[0m');
|
|
term.writeln('\x1b[1;36m║ ESP32 Debug Dongle Terminal ║\x1b[0m');
|
|
term.writeln('\x1b[1;36m╚═══════════════════════════════════════╝\x1b[0m');
|
|
term.writeln('');
|
|
term.writeln('Ports:');
|
|
term.writeln(' • Internal: ESP32 debug output (virtual serial)');
|
|
term.writeln(' • USB Serial: Main UART (shared with USB)');
|
|
term.writeln(' • External: Serial1 (GPIO16=RX, GPIO17=TX)');
|
|
term.writeln('');
|
|
|
|
connect();
|
|
|
|
// Periodic status update
|
|
setInterval(() => {
|
|
sendCommand({ cmd: 'getStatus' });
|
|
}, 5000);
|
|
</script>
|
|
</body>
|
|
</html>
|