update flasher

This commit is contained in:
Brent Perteet
2025-10-13 20:51:52 -05:00
parent 2513a9e7fb
commit 5a893a034c
8 changed files with 250 additions and 13 deletions

View File

@@ -21,7 +21,8 @@
"Bash(pkill:*)", "Bash(pkill:*)",
"Bash(curl -s http://127.0.0.1:5000/)", "Bash(curl -s http://127.0.0.1:5000/)",
"Bash(./build_web_macos.sh:*)", "Bash(./build_web_macos.sh:*)",
"Bash(./dist/ESP32_Flasher.app/Contents/MacOS/ESP32_Flasher:*)" "Bash(./dist/ESP32_Flasher.app/Contents/MacOS/ESP32_Flasher:*)",
"Bash(where:*)"
], ],
"deny": [] "deny": []
} }

View File

@@ -1,5 +1,22 @@
{ {
"configurations": [ "configurations": [
{
"name": "Win32",
"includePath": [
"${workspaceFolder}/**",
"${env:USERPROFILE}/esp/v5.4.1/esp-idf/components/**",
"${env:USERPROFILE}/.espressif/tools/xtensa-esp-elf/**",
"${workspaceFolder}/build/config",
"${workspaceFolder}/managed_components/**"
],
"defines": [
"ESP_PLATFORM"
],
"compilerPath": "${env:USERPROFILE}/.espressif/tools/xtensa-esp-elf/esp-13.2.0_20241113/xtensa-esp-elf/bin/xtensa-esp32-elf-gcc.exe",
"cStandard": "c11",
"cppStandard": "c++20",
"intelliSenseMode": "gcc-x64"
},
{ {
"name": "Mac", "name": "Mac",
"includePath": [ "includePath": [

50
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,50 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Build ESP32 Firmware (Windows)",
"type": "process",
"request": "launch",
"command": "idf.py",
"args": ["build"],
"cwd": "${workspaceFolder}",
"windows": {
"command": "idf.py"
},
"problemMatcher": ["$gcc"]
},
{
"name": "Build ESP32 Firmware (macOS)",
"type": "process",
"request": "launch",
"command": "bash",
"args": ["-lc", "source ${IDF_PATH:-/opt/esp/idf}/export.sh && idf.py build"],
"cwd": "${workspaceFolder}",
"osx": {
"command": "bash"
},
"problemMatcher": ["$gcc"]
},
{
"name": "Build Windows Flasher",
"type": "process",
"request": "launch",
"command": "${workspaceFolder}/flash_tool/build_from_spec.bat",
"cwd": "${workspaceFolder}/flash_tool",
"windows": {
"command": "${workspaceFolder}/flash_tool/build_from_spec.bat"
}
},
{
"name": "Build macOS Flasher",
"type": "process",
"request": "launch",
"command": "${workspaceFolder}/flash_tool/build_macos.sh",
"cwd": "${workspaceFolder}/flash_tool",
"osx": {
"command": "bash",
"args": ["${workspaceFolder}/flash_tool/build_macos.sh"]
}
}
]
}

View File

@@ -1,11 +1,11 @@
{ {
"C_Cpp.intelliSenseEngine": "default", "C_Cpp.intelliSenseEngine": "default",
"idf.espIdfPathWin": "C:\\esp\\frameworks\\esp-idf-v5.3.3", "idf.espIdfPathWin": "C:\\Users\\Brent.Perteet\\esp\\v5.4.1\\esp-idf",
"idf.openOcdConfigs": [ "idf.openOcdConfigs": [
"board/esp32-wrover-kit-3.3v.cfg" "board/esp32-wrover-kit-3.3v.cfg"
], ],
"idf.portWin": "COM4", "idf.portWin": "COM4",
"idf.toolsPathWin": "C:\\esp\\frameworks\\esp-idf-v5.3.3\\tools\\tools", "idf.toolsPathWin": "C:\\Users\\Brent.Perteet\\.espressif",
"idf.flashType": "UART", "idf.flashType": "UART",
"files.associations": { "files.associations": {
"esp_system.h": "c", "esp_system.h": "c",
@@ -40,7 +40,7 @@
"random": "c" "random": "c"
}, },
"git.ignoreLimitWarning": true, "git.ignoreLimitWarning": true,
"idf.pythonInstallPath": "/opt/homebrew/bin/python3", "idf.pythonInstallPath": "C:\\Users\\Brent.Perteet\\.espressif\\tools\\idf-python\\3.11.2\\python.exe",
"idf.espIdfPath": "/Users/brent/esp/v5.5.1/esp-idf", "idf.espIdfPath": "/Users/brent/esp/v5.5.1/esp-idf",
"idf.toolsPath": "/Users/brent/.espressif" "idf.toolsPath": "/Users/brent/.espressif"
} }

26
.vscode/tasks.json vendored
View File

@@ -1,11 +1,35 @@
{ {
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{
"label": "adapter",
"type": "shell",
"command": "idf.py",
"args": ["build"],
"windows": {
"command": "cmd.exe",
"args": ["/c", "${env:USERPROFILE}\\esp\\v5.4.1\\esp-idf\\export.bat", "&&", "idf.py", "build"]
},
"osx": {
"command": "bash",
"args": ["-lc", "source ${IDF_PATH:-/opt/esp/idf}/export.sh && idf.py build"]
},
"linux": {
"command": "bash",
"args": ["-lc", "source ${IDF_PATH:-/opt/esp/idf}/export.sh && idf.py build"]
},
"group": { "kind": "build", "isDefault": true },
"problemMatcher": ["${config:idf.cmakeCompilerArgs}"],
"presentation": {
"reveal": "always",
"panel": "dedicated"
}
},
{ {
"label": "ESP-IDF: Build (RAM) + Sync to Host", "label": "ESP-IDF: Build (RAM) + Sync to Host",
"type": "shell", "type": "shell",
"command": "bash -lc 'source ${IDF_PATH:-/opt/esp/idf}/export.sh && idf.py build && rsync -a --delete \"$PWD/build/\" /host_build/'", "command": "bash -lc 'source ${IDF_PATH:-/opt/esp/idf}/export.sh && idf.py build && rsync -a --delete \"$PWD/build/\" /host_build/'",
"group": { "kind": "build", "isDefault": true }, "group": "build",
"problemMatcher": "$gcc" "problemMatcher": "$gcc"
} }
] ]

View File

@@ -0,0 +1,32 @@
@echo off
echo === ESP32 Flasher (Web UI) - Windows Build Script ===
echo.
REM Check if running on Windows
if not "%OS%"=="Windows_NT" (
echo Error: This script is for Windows only.
echo Use build_web_macos.sh on macOS.
exit /b 1
)
echo Installing dependencies...
pip install -r requirements.txt
echo.
echo Building Windows executable...
pyinstaller ESP32_WebFlasher_Windows.spec
echo.
if exist "dist\ESP32_Flasher.exe" (
echo Build complete!
echo.
echo Executable created at: dist\ESP32_Flasher.exe
echo.
echo The app will open in your default web browser!
echo You can now distribute ESP32_Flasher.exe to users.
) else (
echo Build failed - executable not found in dist folder
exit /b 1
)
pause

View File

@@ -136,6 +136,28 @@
margin-top: 30px; 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 { .output-box {
background: #1e1e1e; background: #1e1e1e;
color: #d4d4d4; color: #d4d4d4;
@@ -148,6 +170,11 @@
line-height: 1.6; line-height: 1.6;
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
display: none;
}
.output-box.expanded {
display: block;
} }
.output-box:empty::before { .output-box:empty::before {
@@ -173,6 +200,10 @@
height: 100%; height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2); background: linear-gradient(90deg, #667eea, #764ba2);
width: 0%; width: 0%;
transition: width 0.3s ease-out;
}
.progress-bar-fill.indeterminate {
animation: progress 2s ease-in-out infinite; animation: progress 2s ease-in-out infinite;
} }
@@ -182,6 +213,19 @@
100% { width: 0%; } 100% { width: 0%; }
} }
.progress-text {
text-align: center;
font-size: 13px;
color: #667eea;
font-weight: 600;
margin-top: 5px;
display: none;
}
.progress-text.active {
display: block;
}
.status-message { .status-message {
padding: 12px; padding: 12px;
border-radius: 8px; border-radius: 8px;
@@ -270,14 +314,18 @@
</button> </button>
<div class="progress-bar" id="progressBar"> <div class="progress-bar" id="progressBar">
<div class="progress-bar-fill"></div> <div class="progress-bar-fill" id="progressBarFill"></div>
</div> </div>
<div class="progress-text" id="progressText">0%</div>
<div class="status-message" id="statusMessage"></div> <div class="status-message" id="statusMessage"></div>
</form> </form>
<div class="output-section"> <div class="output-section">
<div class="output-header">
<label>Output Log</label> <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 class="output-box" id="output"></div>
</div> </div>
</div> </div>
@@ -287,6 +335,20 @@
let socket = null; let socket = null;
let heartbeatInterval = 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 // Initialize Socket.IO connection
function initWebSocket() { function initWebSocket() {
socket = io(); socket = io();
@@ -368,14 +430,23 @@
const flashBtn = document.getElementById('flashBtn'); const flashBtn = document.getElementById('flashBtn');
const progressBar = document.getElementById('progressBar'); const progressBar = document.getElementById('progressBar');
const progressBarFill = document.getElementById('progressBarFill');
const progressText = document.getElementById('progressText');
const output = document.getElementById('output'); const output = document.getElementById('output');
const outputToggle = document.getElementById('outputToggle');
const statusMessage = document.getElementById('statusMessage'); const statusMessage = document.getElementById('statusMessage');
// Disable form // Disable form
flashBtn.disabled = true; flashBtn.disabled = true;
flashBtn.textContent = '⚡ Flashing...'; flashBtn.textContent = '⚡ Flashing...';
progressBar.classList.add('active'); progressBar.classList.add('active');
progressBarFill.classList.add('indeterminate');
progressBarFill.style.width = '0%';
progressText.classList.add('active');
progressText.textContent = '0%';
output.textContent = ''; output.textContent = '';
output.classList.remove('expanded');
outputToggle.textContent = '▼ Show Details';
statusMessage.style.display = 'none'; statusMessage.style.display = 'none';
statusMessage.className = 'status-message'; statusMessage.className = 'status-message';
@@ -413,6 +484,16 @@
output.textContent = data.output.join('\n'); output.textContent = data.output.join('\n');
output.scrollTop = output.scrollHeight; 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 // Check if complete
if (!data.running && statusCheckInterval) { if (!data.running && statusCheckInterval) {
clearInterval(statusCheckInterval); clearInterval(statusCheckInterval);
@@ -420,11 +501,15 @@
const statusMessage = document.getElementById('statusMessage'); const statusMessage = document.getElementById('statusMessage');
if (data.success === true) { if (data.success === true) {
statusMessage.textContent = '✓ Firmware flashed successfully!'; progressBarFill.style.width = '100%';
progressText.textContent = '100%';
statusMessage.textContent = '✓ Flash completed successfully!';
statusMessage.className = 'status-message success'; statusMessage.className = 'status-message success';
statusMessage.style.display = 'block';
} else if (data.success === false) { } else if (data.success === false) {
statusMessage.textContent = '✗ Flash operation failed. Check the output log for details.'; statusMessage.textContent = '✗ Flash operation failed. Check the output log for details.';
statusMessage.className = 'status-message error'; statusMessage.className = 'status-message error';
statusMessage.style.display = 'block';
} }
resetForm(); resetForm();
@@ -441,15 +526,25 @@
const statusMessage = document.getElementById('statusMessage'); const statusMessage = document.getElementById('statusMessage');
statusMessage.textContent = '✗ ' + message; statusMessage.textContent = '✗ ' + message;
statusMessage.className = 'status-message error'; statusMessage.className = 'status-message error';
statusMessage.style.display = 'block';
} }
function resetForm() { function resetForm() {
const flashBtn = document.getElementById('flashBtn'); const flashBtn = document.getElementById('flashBtn');
const progressBar = document.getElementById('progressBar'); const progressBar = document.getElementById('progressBar');
const progressBarFill = document.getElementById('progressBarFill');
const progressText = document.getElementById('progressText');
flashBtn.disabled = false; flashBtn.disabled = false;
flashBtn.textContent = '⚡ Flash Firmware'; flashBtn.textContent = '⚡ Flash Firmware';
// Delay hiding progress to let user see completion
setTimeout(() => {
progressBar.classList.remove('active'); progressBar.classList.remove('active');
progressText.classList.remove('active');
progressBarFill.classList.remove('indeterminate');
progressBarFill.style.width = '0%';
}, 2000);
} }
</script> </script>
</body> </body>

View File

@@ -29,7 +29,8 @@ socketio = SocketIO(app, cors_allowed_origins="*")
flash_status = { flash_status = {
'running': False, 'running': False,
'output': [], 'output': [],
'success': None 'success': None,
'progress': 0
} }
# Client connection tracking # Client connection tracking
@@ -96,7 +97,7 @@ def flash_firmware():
zip_file.extractall(temp_dir) zip_file.extractall(temp_dir)
# Start flash in background thread # Start flash in background thread
flash_status = {'running': True, 'output': [], 'success': None} flash_status = {'running': True, 'output': [], 'success': None, 'progress': 0}
thread = threading.Thread( thread = threading.Thread(
target=flash_worker, target=flash_worker,
args=(temp_dir, port, chip, baud, flash_mode, flash_freq, flash_size) args=(temp_dir, port, chip, baud, flash_mode, flash_freq, flash_size)
@@ -169,6 +170,7 @@ def shutdown_server():
def flash_worker(temp_dir, port, chip, baud, flash_mode, flash_freq, flash_size): def flash_worker(temp_dir, port, chip, baud, flash_mode, flash_freq, flash_size):
"""Background worker for flashing firmware""" """Background worker for flashing firmware"""
global flash_status global flash_status
import re
try: try:
# Define file paths # Define file paths
@@ -217,9 +219,24 @@ def flash_worker(temp_dir, port, chip, baud, flash_mode, flash_freq, flash_size)
universal_newlines=True universal_newlines=True
) )
# Regex to parse progress lines
# Example: "Writing at 0x0011325c... (85 %)"
# or "Writing at 0x0011325c [========================> ] 85.3% 622592/730050 bytes..."
progress_pattern = re.compile(r'(\d+(?:\.\d+)?)\s*%')
# Stream output # Stream output
for line in process.stdout: for line in process.stdout:
flash_status['output'].append(line.rstrip()) line_stripped = line.rstrip()
flash_status['output'].append(line_stripped)
# Try to extract progress percentage
match = progress_pattern.search(line_stripped)
if match:
try:
percent = float(match.group(1))
flash_status['progress'] = percent
except ValueError:
pass
process.wait() process.wait()
@@ -227,6 +244,7 @@ def flash_worker(temp_dir, port, chip, baud, flash_mode, flash_freq, flash_size)
flash_status['output'].append("") flash_status['output'].append("")
flash_status['output'].append("✓ Flash completed successfully!") flash_status['output'].append("✓ Flash completed successfully!")
flash_status['success'] = True flash_status['success'] = True
flash_status['progress'] = 100
else: else:
flash_status['output'].append("") flash_status['output'].append("")
flash_status['output'].append(f"✗ Flash failed with return code {process.returncode}") flash_status['output'].append(f"✗ Flash failed with return code {process.returncode}")