adding web flasher tool
This commit is contained in:
456
flash_tool/templates/index.html
Normal file
456
flash_tool/templates/index.html
Normal file
@@ -0,0 +1,456 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ESP32 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, #667eea 0%, #764ba2 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;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #667eea;
|
||||
margin-bottom: 10px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
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: #667eea;
|
||||
}
|
||||
|
||||
.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: #667eea;
|
||||
background: #f8f9ff;
|
||||
}
|
||||
|
||||
.file-selected {
|
||||
border-color: #667eea;
|
||||
border-style: solid;
|
||||
background: #f8f9ff;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.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, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.output-section {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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, #667eea, #764ba2);
|
||||
width: 0%;
|
||||
animation: progress 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes progress {
|
||||
0% { width: 0%; }
|
||||
50% { width: 100%; }
|
||||
100% { width: 0%; }
|
||||
}
|
||||
|
||||
.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">
|
||||
<h1>ESP32 Firmware Flasher</h1>
|
||||
<p class="subtitle">Flash firmware packages to ESP32 devices</p>
|
||||
|
||||
<form id="flashForm">
|
||||
<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"></div>
|
||||
</div>
|
||||
|
||||
<div class="status-message" id="statusMessage"></div>
|
||||
</form>
|
||||
|
||||
<div class="output-section">
|
||||
<label>Output Log</label>
|
||||
<div class="output-box" id="output"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let statusCheckInterval = null;
|
||||
let socket = null;
|
||||
let heartbeatInterval = null;
|
||||
|
||||
// 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();
|
||||
|
||||
const flashBtn = document.getElementById('flashBtn');
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const output = document.getElementById('output');
|
||||
const statusMessage = document.getElementById('statusMessage');
|
||||
|
||||
// Disable form
|
||||
flashBtn.disabled = true;
|
||||
flashBtn.textContent = '⚡ Flashing...';
|
||||
progressBar.classList.add('active');
|
||||
output.textContent = '';
|
||||
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;
|
||||
|
||||
// Check if complete
|
||||
if (!data.running && statusCheckInterval) {
|
||||
clearInterval(statusCheckInterval);
|
||||
statusCheckInterval = null;
|
||||
|
||||
const statusMessage = document.getElementById('statusMessage');
|
||||
if (data.success === true) {
|
||||
statusMessage.textContent = '✓ Firmware flashed successfully!';
|
||||
statusMessage.className = 'status-message success';
|
||||
} else if (data.success === false) {
|
||||
statusMessage.textContent = '✗ Flash operation failed. Check the output log for details.';
|
||||
statusMessage.className = 'status-message error';
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
const flashBtn = document.getElementById('flashBtn');
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
|
||||
flashBtn.disabled = false;
|
||||
flashBtn.textContent = '⚡ Flash Firmware';
|
||||
progressBar.classList.remove('active');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user