ESP32-S3 firmware acting as BMC for another board, controlling an ATX power supply and providing access to UART. Co-authored with GLM-5
826 lines
25 KiB
HTML
826 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>VisionFive2 BMC Dashboard</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||
color: #eee;
|
||
min-height: 100vh;
|
||
padding: 20px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
header {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
padding: 20px;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border-radius: 15px;
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
h1 {
|
||
font-size: 2.5em;
|
||
margin-bottom: 10px;
|
||
background: linear-gradient(90deg, #00d4ff, #00ff88);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
}
|
||
|
||
.status-bar {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 30px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.status-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.status-dot {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
background: #ff4444;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
.status-dot.connected {
|
||
background: #00ff88;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.5; }
|
||
}
|
||
|
||
.grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||
gap: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.card {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border-radius: 15px;
|
||
padding: 20px;
|
||
backdrop-filter: blur(10px);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.card h2 {
|
||
font-size: 1.3em;
|
||
margin-bottom: 15px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.card h2 .icon {
|
||
font-size: 1.5em;
|
||
}
|
||
|
||
/* Power Control */
|
||
.power-buttons {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.btn {
|
||
padding: 12px 24px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 1em;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.btn:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.btn-power-on {
|
||
background: linear-gradient(135deg, #00c853, #00e676);
|
||
color: #000;
|
||
}
|
||
|
||
.btn-power-off {
|
||
background: linear-gradient(135deg, #ff1744, #ff5252);
|
||
color: #fff;
|
||
}
|
||
|
||
.btn-reset {
|
||
background: linear-gradient(135deg, #ff9100, #ffab40);
|
||
color: #000;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
color: #fff;
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.power-status {
|
||
margin-top: 15px;
|
||
padding: 15px;
|
||
background: rgba(0, 0, 0, 0.2);
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.power-indicator {
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 50%;
|
||
background: #ff4444;
|
||
}
|
||
|
||
.power-indicator.on {
|
||
background: #00ff88;
|
||
box-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
|
||
}
|
||
|
||
/* GPIO Monitor */
|
||
.gpio-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||
gap: 10px;
|
||
}
|
||
|
||
.gpio-item {
|
||
background: rgba(0, 0, 0, 0.2);
|
||
padding: 10px;
|
||
border-radius: 8px;
|
||
text-align: center;
|
||
}
|
||
|
||
.gpio-name {
|
||
font-size: 0.8em;
|
||
color: #888;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.gpio-pin {
|
||
font-size: 0.9em;
|
||
color: #00d4ff;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.gpio-value {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.gpio-led {
|
||
width: 16px;
|
||
height: 16px;
|
||
border-radius: 50%;
|
||
background: #333;
|
||
border: 2px solid #555;
|
||
}
|
||
|
||
.gpio-led.high {
|
||
background: #00ff88;
|
||
border-color: #00ff88;
|
||
box-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
|
||
}
|
||
|
||
.gpio-toggle {
|
||
padding: 4px 8px;
|
||
font-size: 0.8em;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
color: #fff;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.gpio-toggle:hover {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
/* Serial Console */
|
||
.console-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 400px;
|
||
}
|
||
|
||
.console-output {
|
||
flex: 1;
|
||
background: #0a0a0a;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 0.9em;
|
||
overflow-y: auto;
|
||
margin-bottom: 10px;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
cursor: text;
|
||
}
|
||
|
||
.console-output:focus {
|
||
outline: none;
|
||
border-color: #00d4ff;
|
||
}
|
||
|
||
.console-output pre {
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
}
|
||
|
||
.console-output .input-line {
|
||
color: #00ff88;
|
||
}
|
||
|
||
.console-output .cursor {
|
||
display: inline-block;
|
||
width: 8px;
|
||
height: 1em;
|
||
background: #00ff88;
|
||
animation: blink 1s step-end infinite;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
@keyframes blink {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0; }
|
||
}
|
||
|
||
.console-controls {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
|
||
.console-controls select {
|
||
padding: 8px 12px;
|
||
border-radius: 6px;
|
||
background: rgba(0, 0, 0, 0.3);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
color: #fff;
|
||
}
|
||
|
||
/* System Info */
|
||
.info-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 10px;
|
||
}
|
||
|
||
.info-item {
|
||
padding: 10px;
|
||
background: rgba(0, 0, 0, 0.2);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.info-label {
|
||
font-size: 0.8em;
|
||
color: #888;
|
||
margin-bottom: 3px;
|
||
}
|
||
|
||
.info-value {
|
||
font-size: 1em;
|
||
color: #00d4ff;
|
||
font-family: 'Courier New', monospace;
|
||
}
|
||
|
||
/* Toast Notifications */
|
||
.toast-container {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.toast {
|
||
padding: 15px 20px;
|
||
border-radius: 8px;
|
||
margin-bottom: 10px;
|
||
animation: slideIn 0.3s ease;
|
||
max-width: 300px;
|
||
}
|
||
|
||
.toast.success {
|
||
background: rgba(0, 255, 136, 0.2);
|
||
border: 1px solid #00ff88;
|
||
}
|
||
|
||
.toast.error {
|
||
background: rgba(255, 68, 68, 0.2);
|
||
border: 1px solid #ff4444;
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from {
|
||
transform: translateX(100%);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translateX(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
/* Responsive */
|
||
@media (max-width: 600px) {
|
||
h1 {
|
||
font-size: 1.8em;
|
||
}
|
||
|
||
.status-bar {
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.power-buttons {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.btn {
|
||
width: 100%;
|
||
}
|
||
|
||
.info-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<header>
|
||
<h1>🖥️ VisionFive2 BMC Dashboard</h1>
|
||
<div class="status-bar">
|
||
<div class="status-item">
|
||
<div class="status-dot" id="eth-status"></div>
|
||
<span>Ethernet: <span id="eth-text">Disconnected</span></span>
|
||
</div>
|
||
<div class="status-item">
|
||
<span>IP: <span id="ip-address">-</span></span>
|
||
</div>
|
||
<div class="status-item">
|
||
<span>UART: <span id="uart-baud">115200</span> baud</span>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="grid">
|
||
<!-- Power Control Card -->
|
||
<div class="card">
|
||
<h2><span class="icon">⚡</span> Power Control</h2>
|
||
<div class="power-buttons">
|
||
<button class="btn btn-power-on" onclick="powerOn()">Power On</button>
|
||
<button class="btn btn-power-off" onclick="powerOff()">Power Off</button>
|
||
<button class="btn btn-reset" onclick="reset()">Reset</button>
|
||
</div>
|
||
<div class="power-status">
|
||
<div class="power-indicator" id="power-indicator"></div>
|
||
<span>Power Status: <strong id="power-status">Unknown</strong></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- System Info Card -->
|
||
<div class="card">
|
||
<h2><span class="icon">ℹ️</span> System Info</h2>
|
||
<div class="info-grid">
|
||
<div class="info-item">
|
||
<div class="info-label">MAC Address</div>
|
||
<div class="info-value" id="mac-address">-</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">Netmask</div>
|
||
<div class="info-value" id="netmask">-</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">Gateway</div>
|
||
<div class="info-value" id="gateway">-</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">Link Speed</div>
|
||
<div class="info-value" id="link-speed">-</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid">
|
||
<!-- GPIO Monitor Card -->
|
||
<div class="card">
|
||
<h2><span class="icon">🔌</span> GPIO Monitor</h2>
|
||
<div class="gpio-grid" id="gpio-grid">
|
||
<!-- GPIO items will be populated by JavaScript -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Serial Console Card -->
|
||
<div class="card">
|
||
<h2><span class="icon">💻</span> Serial Console</h2>
|
||
<div class="console-container">
|
||
<div class="console-output" id="console-output" tabindex="0">
|
||
<pre>Connecting to serial console...</pre>
|
||
</div>
|
||
<div class="console-controls">
|
||
<label>Baud Rate:</label>
|
||
<select id="baud-select" onchange="setBaudRate()">
|
||
<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>
|
||
<button class="btn btn-secondary" onclick="clearConsole()">Clear</button>
|
||
<button class="btn btn-secondary" onclick="connectWebSocket()">Reconnect</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toast-container" id="toast-container"></div>
|
||
|
||
<script>
|
||
// WebSocket connection
|
||
let ws = null;
|
||
let wsConnected = false;
|
||
|
||
// API base URL
|
||
const API_BASE = '';
|
||
|
||
// Toast notification
|
||
function showToast(message, type = 'success') {
|
||
const container = document.getElementById('toast-container');
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast ${type}`;
|
||
toast.textContent = message;
|
||
container.appendChild(toast);
|
||
|
||
setTimeout(() => {
|
||
toast.remove();
|
||
}, 3000);
|
||
}
|
||
|
||
// API helper
|
||
async function apiCall(endpoint, method = 'GET', data = null) {
|
||
const options = {
|
||
method: method,
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
};
|
||
|
||
if (data) {
|
||
options.body = JSON.stringify(data);
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE}${endpoint}`, options);
|
||
const result = await response.json();
|
||
|
||
if (!result.success) {
|
||
throw new Error(result.error || 'Unknown error');
|
||
}
|
||
|
||
return result;
|
||
} catch (error) {
|
||
showToast(error.message, 'error');
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Power control functions
|
||
async function powerOn() {
|
||
try {
|
||
await apiCall('/api/power/on', 'POST');
|
||
showToast('Power on sequence initiated');
|
||
updatePowerStatus();
|
||
} catch (error) {
|
||
console.error('Power on failed:', error);
|
||
}
|
||
}
|
||
|
||
async function powerOff() {
|
||
try {
|
||
await apiCall('/api/power/off', 'POST');
|
||
showToast('Power off sequence initiated');
|
||
updatePowerStatus();
|
||
} catch (error) {
|
||
console.error('Power off failed:', error);
|
||
}
|
||
}
|
||
|
||
async function reset() {
|
||
try {
|
||
await apiCall('/api/power/reset', 'POST');
|
||
showToast('Reset sequence initiated');
|
||
} catch (error) {
|
||
console.error('Reset failed:', error);
|
||
}
|
||
}
|
||
|
||
async function updatePowerStatus() {
|
||
try {
|
||
const result = await apiCall('/api/power/status');
|
||
const indicator = document.getElementById('power-indicator');
|
||
const status = document.getElementById('power-status');
|
||
|
||
if (result.data.power_good) {
|
||
indicator.classList.add('on');
|
||
status.textContent = 'ON';
|
||
} else {
|
||
indicator.classList.remove('on');
|
||
status.textContent = 'OFF';
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to get power status:', error);
|
||
}
|
||
}
|
||
|
||
// GPIO functions
|
||
async function updateGPIOStates() {
|
||
try {
|
||
const result = await apiCall('/api/gpio');
|
||
const grid = document.getElementById('gpio-grid');
|
||
grid.innerHTML = '';
|
||
|
||
result.gpios.forEach(gpio => {
|
||
const item = document.createElement('div');
|
||
item.className = 'gpio-item';
|
||
|
||
const isOutput = gpio.mode === 'output';
|
||
|
||
item.innerHTML = `
|
||
<div class="gpio-name">${gpio.name}</div>
|
||
<div class="gpio-pin">GPIO ${gpio.pin}</div>
|
||
<div class="gpio-value">
|
||
<div class="gpio-led ${gpio.value ? 'high' : ''}"></div>
|
||
<span>${gpio.value}</span>
|
||
${isOutput ? `<button class="gpio-toggle" onclick="toggleGPIO(${gpio.pin})">Toggle</button>` : ''}
|
||
</div>
|
||
`;
|
||
|
||
grid.appendChild(item);
|
||
});
|
||
} catch (error) {
|
||
console.error('Failed to get GPIO states:', error);
|
||
}
|
||
}
|
||
|
||
async function toggleGPIO(pin) {
|
||
try {
|
||
// Get current state
|
||
const result = await apiCall('/api/gpio');
|
||
const gpio = result.gpios.find(g => g.pin === pin);
|
||
|
||
if (gpio) {
|
||
const newValue = gpio.value ? 0 : 1;
|
||
await apiCall(`/api/gpio/set?pin=${pin}`, 'POST', { value: newValue });
|
||
updateGPIOStates();
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to toggle GPIO:', error);
|
||
}
|
||
}
|
||
|
||
// Serial console functions
|
||
let inputBuffer = '';
|
||
let cursorVisible = true;
|
||
|
||
function connectWebSocket() {
|
||
const protocol = window.location.protocol === 'https' ? 'wss' : 'ws';
|
||
const wsUrl = `${protocol}://${window.location.host}/api/serial/ws`;
|
||
|
||
ws = new WebSocket(wsUrl);
|
||
|
||
ws.onopen = () => {
|
||
wsConnected = true;
|
||
clearConsole();
|
||
appendConsole('Connected to serial console\n');
|
||
updateCursor();
|
||
};
|
||
|
||
ws.onmessage = (event) => {
|
||
appendConsole(event.data);
|
||
};
|
||
|
||
ws.onclose = () => {
|
||
wsConnected = false;
|
||
appendConsole('\nDisconnected from serial console\n');
|
||
|
||
// Try to reconnect after 3 seconds
|
||
setTimeout(connectWebSocket, 3000);
|
||
};
|
||
|
||
ws.onerror = (error) => {
|
||
console.error('WebSocket error:', error);
|
||
appendConsole('WebSocket error\n');
|
||
};
|
||
}
|
||
|
||
// Strip ANSI escape sequences from text
|
||
function stripAnsi(text) {
|
||
// Remove ANSI escape sequences (cursor movement, colors, etc.)
|
||
return text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
|
||
.replace(/\x1b\][^\x07]*\x07/g, '')
|
||
.replace(/\x1b[()][AB012]/g, '')
|
||
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '');
|
||
}
|
||
|
||
function appendConsole(data) {
|
||
const output = document.getElementById('console-output');
|
||
const pre = output.querySelector('pre') || document.createElement('pre');
|
||
|
||
if (!output.contains(pre)) {
|
||
output.innerHTML = '';
|
||
output.appendChild(pre);
|
||
}
|
||
|
||
// Remove cursor before appending, then re-add
|
||
const cursor = pre.querySelector('.cursor');
|
||
if (cursor) cursor.remove();
|
||
|
||
// Strip ANSI escape sequences for cleaner display
|
||
const cleanData = stripAnsi(data);
|
||
pre.textContent += cleanData;
|
||
updateCursor();
|
||
output.scrollTop = output.scrollHeight;
|
||
}
|
||
|
||
function updateCursor() {
|
||
const output = document.getElementById('console-output');
|
||
const pre = output.querySelector('pre');
|
||
if (!pre) return;
|
||
|
||
// Remove existing cursor
|
||
const existingCursor = pre.querySelector('.cursor');
|
||
if (existingCursor) existingCursor.remove();
|
||
|
||
// Add cursor at end
|
||
if (wsConnected) {
|
||
const cursor = document.createElement('span');
|
||
cursor.className = 'cursor';
|
||
pre.appendChild(cursor);
|
||
}
|
||
}
|
||
|
||
function sendChar(char) {
|
||
if (wsConnected) {
|
||
ws.send(char);
|
||
}
|
||
}
|
||
|
||
function sendBuffer() {
|
||
if (inputBuffer && wsConnected) {
|
||
ws.send(inputBuffer + '\n');
|
||
inputBuffer = '';
|
||
updateCursor();
|
||
}
|
||
}
|
||
|
||
function handleConsoleKeyDown(event) {
|
||
if (!wsConnected) return;
|
||
|
||
const output = document.getElementById('console-output');
|
||
|
||
// Handle special keys
|
||
if (event.key === 'Enter') {
|
||
event.preventDefault();
|
||
sendChar('\n');
|
||
inputBuffer = '';
|
||
} else if (event.key === 'Backspace') {
|
||
event.preventDefault();
|
||
if (inputBuffer.length > 0) {
|
||
inputBuffer = inputBuffer.slice(0, -1);
|
||
sendChar('\b'); // Send backspace to remote
|
||
}
|
||
} else if (event.key === 'Tab') {
|
||
event.preventDefault();
|
||
sendChar('\t');
|
||
inputBuffer += '\t';
|
||
} else if (event.key === 'Escape') {
|
||
event.preventDefault();
|
||
sendChar('\x1b');
|
||
} else if (event.ctrlKey && event.key.length === 1) {
|
||
// Handle Ctrl+C, Ctrl+D, etc.
|
||
event.preventDefault();
|
||
const ctrlChar = String.fromCharCode(event.key.charCodeAt(0) - 96);
|
||
sendChar(ctrlChar);
|
||
} else if (event.key.length === 1 && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
||
// Regular character
|
||
event.preventDefault();
|
||
sendChar(event.key);
|
||
inputBuffer += event.key;
|
||
}
|
||
}
|
||
|
||
function clearConsole() {
|
||
const output = document.getElementById('console-output');
|
||
output.innerHTML = '<pre></pre>';
|
||
inputBuffer = '';
|
||
updateCursor();
|
||
}
|
||
|
||
// Initialize console keyboard handling
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const output = document.getElementById('console-output');
|
||
output.addEventListener('keydown', handleConsoleKeyDown);
|
||
|
||
// Focus console on click
|
||
output.addEventListener('click', () => {
|
||
output.focus();
|
||
});
|
||
});
|
||
|
||
async function setBaudRate() {
|
||
const baud = document.getElementById('baud-select').value;
|
||
try {
|
||
await apiCall('/api/serial/config', 'PUT', { baud_rate: parseInt(baud) });
|
||
document.getElementById('uart-baud').textContent = baud;
|
||
showToast(`Baud rate set to ${baud}`);
|
||
} catch (error) {
|
||
console.error('Failed to set baud rate:', error);
|
||
}
|
||
}
|
||
|
||
// System info functions
|
||
async function updateSystemInfo() {
|
||
try {
|
||
const result = await apiCall('/api/system/info');
|
||
|
||
document.getElementById('ip-address').textContent = result.data.ip || '-';
|
||
document.getElementById('mac-address').textContent = result.data.mac || '-';
|
||
document.getElementById('netmask').textContent = result.data.netmask || '-';
|
||
document.getElementById('gateway').textContent = result.data.gateway || '-';
|
||
document.getElementById('link-speed').textContent = result.data.link_speed ? `${result.data.link_speed} Mbps` : '-';
|
||
|
||
// Update Ethernet status
|
||
const ethStatus = document.getElementById('eth-status');
|
||
const ethText = document.getElementById('eth-text');
|
||
|
||
if (result.data.ethernet_connected) {
|
||
ethStatus.classList.add('connected');
|
||
ethText.textContent = 'Connected';
|
||
} else {
|
||
ethStatus.classList.remove('connected');
|
||
ethText.textContent = 'Disconnected';
|
||
}
|
||
|
||
document.getElementById('uart-baud').textContent = result.data.uart_baud_rate;
|
||
} catch (error) {
|
||
console.error('Failed to get system info:', error);
|
||
}
|
||
}
|
||
|
||
// Initialize
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
updateSystemInfo();
|
||
updateGPIOStates();
|
||
updatePowerStatus();
|
||
connectWebSocket();
|
||
|
||
// Periodic updates
|
||
setInterval(updateSystemInfo, 5000);
|
||
setInterval(updateGPIOStates, 2000);
|
||
setInterval(updatePowerStatus, 2000);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|