Files
esp32-bmc/main/html/index.html
Valentin Haudiquet 064e8812a4 Initial commit
ESP32-S3 firmware acting as BMC for another board, controlling an ATX power supply and providing access to UART.

Co-authored with GLM-5
2026-03-11 17:39:43 +01:00

826 lines
25 KiB
HTML
Raw 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.
<!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>