Files
soundshot/flash_tool/templates/index.html
2025-10-25 11:29:53 -05:00

571 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SoundShot Firmware Flasher</title>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #d94125 0%, #a33318 100%);
min-height: 100vh;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
}
.container {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 700px;
width: 100%;
padding: 40px;
}
.header {
text-align: center;
margin-bottom: 30px;
background: linear-gradient(135deg, #2c2c2c 0%, #1a1a1a 100%);
padding: 40px 20px 30px 20px;
margin: -40px -40px 30px -40px;
border-radius: 12px 12px 0 0;
}
.logo {
max-width: 450px;
width: 100%;
height: auto;
margin-bottom: 10px;
}
h1 {
color: #c04026;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: #d0d0d0;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
font-size: 14px;
}
input[type="text"],
select {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
input[type="text"]:focus,
select:focus {
outline: none;
border-color: #c04026;
}
.file-input-wrapper {
position: relative;
overflow: hidden;
display: inline-block;
width: 100%;
}
.file-input-wrapper input[type="file"] {
position: absolute;
left: -9999px;
}
.file-input-label {
display: block;
padding: 12px;
border: 2px dashed #e0e0e0;
border-radius: 8px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
color: #666;
}
.file-input-label:hover {
border-color: #c04026;
background: #fef6f4;
}
.file-selected {
border-color: #c04026;
border-style: solid;
background: #fef6f4;
color: #c04026;
}
.btn {
width: 100%;
padding: 14px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #d94125 0%, #a33318 100%);
color: white;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(217, 65, 37, 0.3);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.output-section {
margin-top: 30px;
}
.output-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.output-toggle {
background: #f0f0f0;
border: none;
border-radius: 6px;
padding: 8px 16px;
cursor: pointer;
font-size: 13px;
color: #333;
transition: background 0.3s;
}
.output-toggle:hover {
background: #e0e0e0;
}
.output-box {
background: #1e1e1e;
color: #d4d4d4;
padding: 20px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 13px;
max-height: 400px;
overflow-y: auto;
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
display: none;
}
.output-box.expanded {
display: block;
}
.output-box:empty::before {
content: 'Output will appear here...';
color: #666;
}
.progress-bar {
width: 100%;
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
margin: 20px 0;
display: none;
}
.progress-bar.active {
display: block;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, #d94125, #a33318);
width: 0%;
transition: width 0.3s ease-out;
}
.progress-bar-fill.indeterminate {
animation: progress 2s ease-in-out infinite;
}
@keyframes progress {
0% { width: 0%; }
50% { width: 100%; }
100% { width: 0%; }
}
.progress-text {
text-align: center;
font-size: 13px;
color: #c04026;
font-weight: 600;
margin-top: 5px;
display: none;
}
.progress-text.active {
display: block;
}
.status-message {
padding: 12px;
border-radius: 8px;
margin: 15px 0;
font-weight: 500;
display: none;
}
.status-message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
display: block;
}
.status-message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
display: block;
}
.refresh-btn {
padding: 8px 16px;
background: #f0f0f0;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
color: #333;
margin-left: 10px;
transition: background 0.3s;
}
.refresh-btn:hover {
background: #e0e0e0;
}
.port-section {
display: flex;
align-items: flex-end;
gap: 10px;
}
.port-section .form-group {
flex: 1;
margin-bottom: 0;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<img src="/static/SoundShot.png" alt="SoundShot" class="logo">
<p class="subtitle">Firmware Flasher</p>
</div>
<form id="flashForm" onsubmit="return false;">
<div class="port-section">
<div class="form-group">
<label for="port">Serial Port</label>
<select id="port" name="port" required>
<option value="">Select a port...</option>
</select>
</div>
<button type="button" class="refresh-btn" onclick="refreshPorts()">🔄 Refresh</button>
</div>
<div class="form-group">
<label for="firmware">Firmware Package (.zip)</label>
<div class="file-input-wrapper">
<input type="file" id="firmware" name="firmware" accept=".zip" required>
<label for="firmware" class="file-input-label" id="fileLabel">
📦 Click to select firmware package
</label>
</div>
</div>
<!-- Hidden fields with default values -->
<input type="hidden" id="chip" name="chip" value="esp32">
<input type="hidden" id="baud" name="baud" value="460800">
<input type="hidden" id="flash_mode" name="flash_mode" value="dio">
<input type="hidden" id="flash_freq" name="flash_freq" value="40m">
<input type="hidden" id="flash_size" name="flash_size" value="2MB">
<button type="submit" class="btn btn-primary" id="flashBtn">
⚡ Flash Firmware
</button>
<div class="progress-bar" id="progressBar">
<div class="progress-bar-fill" id="progressBarFill"></div>
</div>
<div class="progress-text" id="progressText">0%</div>
<div class="status-message" id="statusMessage"></div>
</form>
<div class="output-section">
<div class="output-header">
<label>Output Log</label>
<button type="button" class="output-toggle" id="outputToggle" onclick="toggleOutput()">▼ Show Details</button>
</div>
<div class="output-box" id="output"></div>
</div>
</div>
<script>
let statusCheckInterval = null;
let socket = null;
let heartbeatInterval = null;
// Toggle output visibility
function toggleOutput() {
const output = document.getElementById('output');
const toggle = document.getElementById('outputToggle');
if (output.classList.contains('expanded')) {
output.classList.remove('expanded');
toggle.textContent = '▼ Show Details';
} else {
output.classList.add('expanded');
toggle.textContent = '▲ Hide Details';
}
}
// Initialize Socket.IO connection
function initWebSocket() {
socket = io();
socket.on('connect', () => {
console.log('WebSocket connected');
// Start sending heartbeats every 2 seconds
heartbeatInterval = setInterval(() => {
socket.emit('heartbeat');
}, 2000);
});
socket.on('disconnect', () => {
console.log('WebSocket disconnected');
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
});
socket.on('heartbeat_ack', (data) => {
// Heartbeat acknowledged
});
}
// Load ports on page load
document.addEventListener('DOMContentLoaded', () => {
initWebSocket();
refreshPorts();
});
// Clean up on page unload
window.addEventListener('beforeunload', () => {
if (socket) {
socket.disconnect();
}
});
// Handle file selection
document.getElementById('firmware').addEventListener('change', function(e) {
const fileLabel = document.getElementById('fileLabel');
if (e.target.files.length > 0) {
fileLabel.textContent = '✓ ' + e.target.files[0].name;
fileLabel.classList.add('file-selected');
} else {
fileLabel.textContent = '📦 Click to select firmware package';
fileLabel.classList.remove('file-selected');
}
});
async function refreshPorts() {
const portSelect = document.getElementById('port');
portSelect.innerHTML = '<option value="">Loading ports...</option>';
try {
const response = await fetch('/api/ports');
const data = await response.json();
if (data.success) {
portSelect.innerHTML = '<option value="">Select a port...</option>';
data.ports.forEach(port => {
const option = document.createElement('option');
option.value = port.device;
option.textContent = `${port.device} - ${port.description}`;
portSelect.appendChild(option);
});
} else {
portSelect.innerHTML = '<option value="">Error loading ports</option>';
}
} catch (error) {
portSelect.innerHTML = '<option value="">Error loading ports</option>';
console.error('Error:', error);
}
}
document.getElementById('flashForm').addEventListener('submit', async (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const flashBtn = document.getElementById('flashBtn');
const progressBar = document.getElementById('progressBar');
const progressBarFill = document.getElementById('progressBarFill');
const progressText = document.getElementById('progressText');
const output = document.getElementById('output');
const outputToggle = document.getElementById('outputToggle');
const statusMessage = document.getElementById('statusMessage');
// Disable form
flashBtn.disabled = true;
flashBtn.textContent = '⚡ Flashing...';
progressBar.classList.add('active');
progressBarFill.classList.add('indeterminate');
progressBarFill.style.width = '0%';
progressText.classList.add('active');
progressText.textContent = '0%';
output.textContent = '';
output.classList.remove('expanded');
outputToggle.textContent = '▼ Show Details';
statusMessage.style.display = 'none';
statusMessage.className = 'status-message';
// Create FormData
const formData = new FormData(e.target);
try {
const response = await fetch('/api/flash', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
// Start checking status
statusCheckInterval = setInterval(checkStatus, 500);
} else {
showError(data.error);
resetForm();
}
} catch (error) {
showError('Failed to start flash: ' + error.message);
resetForm();
}
});
async function checkStatus() {
try {
const response = await fetch('/api/status');
const data = await response.json();
// Update output
const output = document.getElementById('output');
output.textContent = data.output.join('\n');
output.scrollTop = output.scrollHeight;
// Update progress bar
const progressBarFill = document.getElementById('progressBarFill');
const progressText = document.getElementById('progressText');
if (data.progress !== undefined && data.progress > 0) {
// Switch from indeterminate to determinate
progressBarFill.classList.remove('indeterminate');
progressBarFill.style.width = data.progress + '%';
progressText.textContent = data.progress.toFixed(1) + '%';
}
// Check if complete
if (!data.running && statusCheckInterval) {
clearInterval(statusCheckInterval);
statusCheckInterval = null;
const statusMessage = document.getElementById('statusMessage');
if (data.success === true) {
progressBarFill.style.width = '100%';
progressText.textContent = '100%';
statusMessage.textContent = '✓ Flash completed successfully!';
statusMessage.className = 'status-message success';
statusMessage.style.display = 'block';
} else if (data.success === false) {
statusMessage.textContent = '✗ Flash operation failed. Check the output log for details.';
statusMessage.className = 'status-message error';
statusMessage.style.display = 'block';
}
resetForm();
}
} catch (error) {
console.error('Error checking status:', error);
clearInterval(statusCheckInterval);
statusCheckInterval = null;
resetForm();
}
}
function showError(message) {
const statusMessage = document.getElementById('statusMessage');
statusMessage.textContent = '✗ ' + message;
statusMessage.className = 'status-message error';
statusMessage.style.display = 'block';
}
function resetForm() {
const flashBtn = document.getElementById('flashBtn');
const progressBar = document.getElementById('progressBar');
const progressBarFill = document.getElementById('progressBarFill');
const progressText = document.getElementById('progressText');
flashBtn.disabled = false;
flashBtn.textContent = '⚡ Flash Firmware';
// Delay hiding progress to let user see completion
setTimeout(() => {
progressBar.classList.remove('active');
progressText.classList.remove('active');
progressBarFill.classList.remove('indeterminate');
progressBarFill.style.width = '0%';
}, 2000);
}
</script>
</body>
</html>