From 5a893a034c4e6e9460120cd80c80449e0b15ccca Mon Sep 17 00:00:00 2001 From: Brent Perteet Date: Mon, 13 Oct 2025 20:51:52 -0500 Subject: [PATCH] update flasher --- .claude/settings.local.json | 5 +- .vscode/c_cpp_properties.json | 17 +++++ .vscode/launch.json | 50 +++++++++++++++ .vscode/settings.json | 6 +- .vscode/tasks.json | 26 +++++++- flash_tool/build_web_windows.bat | 32 ++++++++++ flash_tool/templates/index.html | 103 +++++++++++++++++++++++++++++-- flash_tool/web_flasher.py | 24 ++++++- 8 files changed, 250 insertions(+), 13 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 flash_tool/build_web_windows.bat diff --git a/.claude/settings.local.json b/.claude/settings.local.json index eae2782..b71e5da 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -21,8 +21,9 @@ "Bash(pkill:*)", "Bash(curl -s http://127.0.0.1:5000/)", "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": [] } -} +} \ No newline at end of file diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 090c786..ad25342 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -1,5 +1,22 @@ { "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", "includePath": [ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..c55fde4 --- /dev/null +++ b/.vscode/launch.json @@ -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"] + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 7b64351..537ae04 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,11 @@ { "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": [ "board/esp32-wrover-kit-3.3v.cfg" ], "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", "files.associations": { "esp_system.h": "c", @@ -40,7 +40,7 @@ "random": "c" }, "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.toolsPath": "/Users/brent/.espressif" } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ad02cc3..e364f01 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,11 +1,35 @@ { "version": "2.0.0", "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", "type": "shell", "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" } ] diff --git a/flash_tool/build_web_windows.bat b/flash_tool/build_web_windows.bat new file mode 100644 index 0000000..3e15b43 --- /dev/null +++ b/flash_tool/build_web_windows.bat @@ -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 diff --git a/flash_tool/templates/index.html b/flash_tool/templates/index.html index 652ec3f..e5a617b 100644 --- a/flash_tool/templates/index.html +++ b/flash_tool/templates/index.html @@ -136,6 +136,28 @@ 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; @@ -148,6 +170,11 @@ line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; + display: none; + } + + .output-box.expanded { + display: block; } .output-box:empty::before { @@ -173,6 +200,10 @@ height: 100%; background: linear-gradient(90deg, #667eea, #764ba2); width: 0%; + transition: width 0.3s ease-out; + } + + .progress-bar-fill.indeterminate { animation: progress 2s ease-in-out infinite; } @@ -182,6 +213,19 @@ 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 { padding: 12px; border-radius: 8px; @@ -270,14 +314,18 @@
-
+
+
0%
- +
+ + +
@@ -287,6 +335,20 @@ 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(); @@ -368,14 +430,23 @@ 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'; @@ -413,6 +484,16 @@ 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); @@ -420,11 +501,15 @@ const statusMessage = document.getElementById('statusMessage'); 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.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(); @@ -441,15 +526,25 @@ 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'; - progressBar.classList.remove('active'); + + // 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); } diff --git a/flash_tool/web_flasher.py b/flash_tool/web_flasher.py index acbca9c..bdeb68e 100644 --- a/flash_tool/web_flasher.py +++ b/flash_tool/web_flasher.py @@ -29,7 +29,8 @@ socketio = SocketIO(app, cors_allowed_origins="*") flash_status = { 'running': False, 'output': [], - 'success': None + 'success': None, + 'progress': 0 } # Client connection tracking @@ -96,7 +97,7 @@ def flash_firmware(): zip_file.extractall(temp_dir) # Start flash in background thread - flash_status = {'running': True, 'output': [], 'success': None} + flash_status = {'running': True, 'output': [], 'success': None, 'progress': 0} thread = threading.Thread( target=flash_worker, 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): """Background worker for flashing firmware""" global flash_status + import re try: # Define file paths @@ -217,9 +219,24 @@ def flash_worker(temp_dir, port, chip, baud, flash_mode, flash_freq, flash_size) 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 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() @@ -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 completed successfully!") flash_status['success'] = True + flash_status['progress'] = 100 else: flash_status['output'].append("") flash_status['output'].append(f"✗ Flash failed with return code {process.returncode}")