adding web flasher tool
This commit is contained in:
@@ -6,7 +6,22 @@
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(pip install:*)",
|
||||
"Bash(build_from_spec.bat)"
|
||||
"Bash(build_from_spec.bat)",
|
||||
"mcp__ide__getDiagnostics",
|
||||
"Bash(source:*)",
|
||||
"Bash(chmod:*)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(pyinstaller:*)",
|
||||
"Bash(open:*)",
|
||||
"Bash(dos2unix:*)",
|
||||
"Bash(./build_macos.sh:*)",
|
||||
"Bash(brew install:*)",
|
||||
"Bash(python:*)",
|
||||
"Bash(./dist/ESP32_Flasher:*)",
|
||||
"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:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -1,6 +1,22 @@
|
||||
.pytest_cache/
|
||||
__pycache__/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Python virtual environments
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.venv/
|
||||
|
||||
# PyInstaller build artifacts
|
||||
dist/
|
||||
build/
|
||||
*.spec
|
||||
*.pyc
|
||||
*.pyo
|
||||
|
||||
# esp-idf built binaries
|
||||
build/
|
||||
build_*_*/
|
||||
|
||||
20
.vscode/c_cpp_properties.json
vendored
20
.vscode/c_cpp_properties.json
vendored
@@ -1,15 +1,21 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Linux",
|
||||
"name": "Mac",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/**"
|
||||
"${workspaceFolder}/**",
|
||||
"/Users/brent/esp/v5.5.1/esp-idf/components/**",
|
||||
"/Users/brent/.espressif/tools/xtensa-esp-elf/**",
|
||||
"${workspaceFolder}/build/config",
|
||||
"${workspaceFolder}/managed_components/**"
|
||||
],
|
||||
"defines": [],
|
||||
"compilerPath": "/usr/bin/gcc",
|
||||
"cStandard": "c17",
|
||||
"cppStandard": "gnu++17",
|
||||
"intelliSenseMode": "linux-gcc-x64"
|
||||
"defines": [
|
||||
"ESP_PLATFORM"
|
||||
],
|
||||
"compilerPath": "/Users/brent/.espressif/tools/xtensa-esp-elf/esp-13.2.0_20240530/xtensa-esp-elf/bin/xtensa-esp32-elf-gcc",
|
||||
"cStandard": "c11",
|
||||
"cppStandard": "c++20",
|
||||
"intelliSenseMode": "gcc-x64"
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -40,5 +40,7 @@
|
||||
"random": "c"
|
||||
},
|
||||
"git.ignoreLimitWarning": true,
|
||||
"idf.pythonInstallPath": "/usr/bin/python"
|
||||
"idf.pythonInstallPath": "/opt/homebrew/bin/python3",
|
||||
"idf.espIdfPath": "/Users/brent/esp/v5.5.1/esp-idf",
|
||||
"idf.toolsPath": "/Users/brent/.espressif"
|
||||
}
|
||||
|
||||
110
flash_tool/BUILD_NOTES.md
Normal file
110
flash_tool/BUILD_NOTES.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Build Notes
|
||||
|
||||
## Build Status
|
||||
|
||||
✅ **macOS Build: SUCCESS**
|
||||
- Built on: macOS 15.6 (arm64)
|
||||
- Python Version: 3.9.6 (Xcode system Python with tkinter support)
|
||||
- Output: `dist/ESP32_Flasher.app`
|
||||
- Size: ~7.0 MB
|
||||
- Tested: ✅ Application launches and runs successfully
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### macOS
|
||||
**Important:** Homebrew's Python 3.13 does NOT include tkinter support by default.
|
||||
|
||||
**Solution:** Install python-tk package:
|
||||
```bash
|
||||
brew install python-tk@3.13
|
||||
```
|
||||
|
||||
However, the build system will automatically use the system Python (3.9.6 from Xcode) which has tkinter built-in. This is the recommended approach.
|
||||
|
||||
## Known Issues
|
||||
|
||||
### ~~macOS tkinter Warning~~ - RESOLVED
|
||||
**Previous Issue:** Homebrew Python 3.13 doesn't include tkinter
|
||||
**Solution:** Build system now uses Xcode's Python 3.9.6 which has native tkinter support
|
||||
**Status:** ✅ Fixed - no warnings, app works perfectly
|
||||
|
||||
### Virtual Environment Required (macOS)
|
||||
Due to PEP 668 (externally-managed environments), macOS requires using a virtual environment for pip installations. The build scripts automatically handle this.
|
||||
|
||||
## Build Commands
|
||||
|
||||
### Quick Build (Any Platform)
|
||||
```bash
|
||||
python3 build.py
|
||||
```
|
||||
|
||||
### macOS Specific
|
||||
```bash
|
||||
./build_macos.sh
|
||||
```
|
||||
or
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
pyinstaller ESP32_Flasher_macOS.spec
|
||||
```
|
||||
|
||||
### Windows Specific
|
||||
```batch
|
||||
build_from_spec.bat
|
||||
```
|
||||
|
||||
## Distribution
|
||||
|
||||
### macOS
|
||||
- Distribute the entire `ESP32_Flasher.app` folder (it's a bundle)
|
||||
- Users may need to run: `xattr -cr ESP32_Flasher.app` if macOS blocks it
|
||||
- Consider creating a DMG for easier distribution
|
||||
|
||||
### Windows
|
||||
- Distribute the single `ESP32_Flasher.exe` file
|
||||
- No installation required
|
||||
- May trigger Windows SmartScreen (normal for unsigned apps)
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
flash_tool/
|
||||
├── dist/ # Build output
|
||||
│ ├── ESP32_Flasher.app # macOS bundle
|
||||
│ └── ESP32_Flasher # Standalone binary
|
||||
├── build/ # Build artifacts
|
||||
├── venv/ # Python virtual environment (macOS)
|
||||
├── gui_flasher.py # Source code
|
||||
├── ESP32_Flasher_macOS.spec # macOS build config
|
||||
├── ESP32_Flasher_Windows.spec # Windows build config
|
||||
├── build.py # Universal build script
|
||||
├── build_macos.sh # macOS build script
|
||||
└── build_from_spec.bat # Windows build script
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Testing**: Test the app with actual ESP32 hardware and firmware
|
||||
2. **Icons**: Add custom icons (.icns for macOS, .ico for Windows)
|
||||
3. **Code Signing**: Sign the executables for production distribution
|
||||
4. **DMG Creation**: Package macOS app in a DMG for easier distribution
|
||||
5. **Windows Installer**: Consider creating an NSIS or WiX installer
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "App is damaged" on macOS
|
||||
```bash
|
||||
xattr -cr dist/ESP32_Flasher.app
|
||||
```
|
||||
|
||||
### Port permission issues on Linux
|
||||
```bash
|
||||
sudo usermod -a -G dialout $USER
|
||||
```
|
||||
(Logout/login required)
|
||||
|
||||
### Build fails on macOS
|
||||
Make sure virtual environment is active:
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
```
|
||||
45
flash_tool/ESP32_Flasher_Windows.spec
Normal file
45
flash_tool/ESP32_Flasher_Windows.spec
Normal file
@@ -0,0 +1,45 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
# PyInstaller spec file for Windows executable
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
['gui_flasher.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[('requirements.txt', '.')],
|
||||
hiddenimports=['serial.tools.list_ports'],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='ESP32_Flasher',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False, # Set to False for windowed mode (no console)
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=None, # Add path to .ico file if you have one
|
||||
)
|
||||
56
flash_tool/ESP32_Flasher_macOS.spec
Normal file
56
flash_tool/ESP32_Flasher_macOS.spec
Normal file
@@ -0,0 +1,56 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
# PyInstaller spec file for macOS executable
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
['gui_flasher.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[('requirements.txt', '.')],
|
||||
hiddenimports=['serial.tools.list_ports'],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='ESP32_Flasher',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False, # macOS app bundle (no console window)
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
|
||||
# Create macOS app bundle
|
||||
app = BUNDLE(
|
||||
exe,
|
||||
name='ESP32_Flasher.app',
|
||||
icon=None, # Add path to .icns file if you have one
|
||||
bundle_identifier='com.soundshot.esp32flasher',
|
||||
info_plist={
|
||||
'NSPrincipalClass': 'NSApplication',
|
||||
'NSHighResolutionCapable': 'True',
|
||||
},
|
||||
)
|
||||
55
flash_tool/ESP32_WebFlasher_Windows.spec
Normal file
55
flash_tool/ESP32_WebFlasher_Windows.spec
Normal file
@@ -0,0 +1,55 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
# PyInstaller spec file for Windows executable (Web-based UI)
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
['web_flasher.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[
|
||||
('templates', 'templates'),
|
||||
],
|
||||
hiddenimports=[
|
||||
'serial.tools.list_ports',
|
||||
'flask',
|
||||
'jinja2',
|
||||
'werkzeug',
|
||||
'flask_socketio',
|
||||
'socketio',
|
||||
'engineio.async_drivers.threading',
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='ESP32_Flasher',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False, # No console window
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=None, # Add path to .ico file if you have one
|
||||
)
|
||||
68
flash_tool/ESP32_WebFlasher_macOS.spec
Normal file
68
flash_tool/ESP32_WebFlasher_macOS.spec
Normal file
@@ -0,0 +1,68 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
# PyInstaller spec file for macOS executable (Web-based UI)
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
['web_flasher.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[
|
||||
('templates', 'templates'),
|
||||
],
|
||||
hiddenimports=[
|
||||
'serial.tools.list_ports',
|
||||
'flask',
|
||||
'jinja2',
|
||||
'werkzeug',
|
||||
'flask_socketio',
|
||||
'socketio',
|
||||
'engineio.async_drivers.threading',
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='ESP32_Flasher',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False, # macOS app bundle (no console window)
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
|
||||
# Create macOS app bundle
|
||||
app = BUNDLE(
|
||||
exe,
|
||||
name='ESP32_Flasher.app',
|
||||
icon=None, # Add path to .icns file if you have one
|
||||
bundle_identifier='com.soundshot.esp32flasher',
|
||||
info_plist={
|
||||
'NSPrincipalClass': 'NSApplication',
|
||||
'NSHighResolutionCapable': 'True',
|
||||
'CFBundleShortVersionString': '1.0.0',
|
||||
'CFBundleDisplayName': 'ESP32 Flasher',
|
||||
},
|
||||
)
|
||||
148
flash_tool/README.md
Normal file
148
flash_tool/README.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# ESP32 Firmware Flasher GUI
|
||||
|
||||
A cross-platform GUI tool for flashing ESP32 firmware packages.
|
||||
|
||||
## Features
|
||||
|
||||
- Simple, user-friendly graphical interface
|
||||
- Cross-platform support (Windows, macOS, Linux)
|
||||
- Automatic serial port detection
|
||||
- Firmware package validation (.zip files)
|
||||
- Real-time flashing progress output
|
||||
- Configurable flash parameters
|
||||
|
||||
## Building Executables
|
||||
|
||||
### Universal Build (Recommended)
|
||||
|
||||
The easiest way to build for your current platform:
|
||||
|
||||
```bash
|
||||
python build.py
|
||||
```
|
||||
|
||||
This script will:
|
||||
1. Detect your platform automatically
|
||||
2. Install required dependencies
|
||||
3. Build the appropriate executable
|
||||
|
||||
### Platform-Specific Builds
|
||||
|
||||
#### macOS
|
||||
|
||||
```bash
|
||||
./build_macos.sh
|
||||
```
|
||||
|
||||
This creates an application bundle at `dist/ESP32_Flasher.app`
|
||||
|
||||
To run:
|
||||
```bash
|
||||
open dist/ESP32_Flasher.app
|
||||
```
|
||||
|
||||
#### Windows
|
||||
|
||||
```batch
|
||||
build_from_spec.bat
|
||||
```
|
||||
|
||||
This creates an executable at `dist\ESP32_Flasher.exe`
|
||||
|
||||
#### Linux
|
||||
|
||||
```bash
|
||||
python build.py
|
||||
```
|
||||
|
||||
Creates an executable at `dist/ESP32_Flasher`
|
||||
|
||||
## Running Without Building
|
||||
|
||||
You can run the GUI directly with Python:
|
||||
|
||||
```bash
|
||||
python gui_flasher.py
|
||||
```
|
||||
|
||||
### Requirements
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Firmware Package Format
|
||||
|
||||
The flasher expects a `.zip` file containing these files:
|
||||
- `bootloader.bin` - ESP32 bootloader
|
||||
- `partition-table.bin` - Partition table
|
||||
- `ota_data_initial.bin` - OTA data
|
||||
- `soundshot.bin` - Main application firmware
|
||||
|
||||
## Usage
|
||||
|
||||
1. Launch the ESP32_Flasher application
|
||||
2. Select your ESP32's serial port (or click Refresh)
|
||||
3. Browse and select your firmware `.zip` package
|
||||
4. (Optional) Adjust flash settings if needed
|
||||
5. Click "Flash Firmware"
|
||||
6. Wait for the process to complete
|
||||
|
||||
## Flash Settings
|
||||
|
||||
Default settings work for most ESP32 boards:
|
||||
- **Chip**: esp32
|
||||
- **Baud Rate**: 460800 (faster) or 115200 (more reliable)
|
||||
- **Flash Mode**: dio
|
||||
- **Flash Freq**: 40m
|
||||
- **Flash Size**: 2MB (adjust based on your board)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Port Not Detected
|
||||
- Ensure ESP32 is connected via USB
|
||||
- Install CH340/CP2102 drivers if needed (Windows/macOS)
|
||||
- On Linux, add user to `dialout` group: `sudo usermod -a -G dialout $USER`
|
||||
|
||||
### Flash Failed
|
||||
- Try lower baud rate (115200)
|
||||
- Press and hold BOOT button during flash
|
||||
- Check USB cable quality
|
||||
- Verify firmware package integrity
|
||||
|
||||
### macOS: "App is damaged"
|
||||
Run this command to allow the app:
|
||||
```bash
|
||||
xattr -cr dist/ESP32_Flasher.app
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
flash_tool/
|
||||
├── gui_flasher.py # Main GUI application
|
||||
├── ESP32_Flasher_macOS.spec # PyInstaller spec for macOS
|
||||
├── ESP32_Flasher_Windows.spec # PyInstaller spec for Windows
|
||||
├── build.py # Universal build script
|
||||
├── build_macos.sh # macOS build script
|
||||
├── build_from_spec.bat # Windows build script
|
||||
├── requirements.txt # Python dependencies
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **tkinter**: GUI framework (included with Python)
|
||||
- **pyserial**: Serial port communication
|
||||
- **esptool**: ESP32 flashing utility
|
||||
- **pyinstaller**: Executable builder
|
||||
|
||||
## License
|
||||
|
||||
[Your license here]
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions, please contact [your contact info].
|
||||
171
flash_tool/build.py
Executable file
171
flash_tool/build.py
Executable file
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Universal build script for ESP32 Flasher GUI
|
||||
Automatically detects platform and builds the appropriate executable
|
||||
"""
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
import platform
|
||||
import os
|
||||
|
||||
def get_platform():
|
||||
"""Detect the current platform"""
|
||||
system = platform.system()
|
||||
if system == "Darwin":
|
||||
return "macos"
|
||||
elif system == "Windows":
|
||||
return "windows"
|
||||
elif system == "Linux":
|
||||
return "linux"
|
||||
else:
|
||||
return "unknown"
|
||||
|
||||
def setup_venv():
|
||||
"""Create and setup virtual environment if on macOS"""
|
||||
if platform.system() == "Darwin":
|
||||
if not os.path.exists("venv"):
|
||||
print("Creating virtual environment (required on macOS)...")
|
||||
try:
|
||||
subprocess.run([sys.executable, "-m", "venv", "venv"], check=True)
|
||||
print("✓ Virtual environment created")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"✗ Failed to create virtual environment: {e}")
|
||||
return False
|
||||
|
||||
# Use venv Python for the rest of the script
|
||||
venv_python = os.path.join("venv", "bin", "python3")
|
||||
if os.path.exists(venv_python):
|
||||
return venv_python
|
||||
|
||||
return sys.executable
|
||||
|
||||
def install_dependencies(python_exe=None):
|
||||
"""Install required Python packages"""
|
||||
if python_exe is None:
|
||||
python_exe = sys.executable
|
||||
|
||||
print("Installing dependencies...")
|
||||
packages = ["pyinstaller", "esptool", "pyserial"]
|
||||
|
||||
try:
|
||||
subprocess.run([python_exe, "-m", "pip", "install"] + packages, check=True)
|
||||
print("✓ Dependencies installed successfully")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"✗ Failed to install dependencies: {e}")
|
||||
return False
|
||||
|
||||
def build_macos():
|
||||
"""Build macOS application bundle"""
|
||||
print("\n=== Building for macOS ===")
|
||||
spec_file = "ESP32_Flasher_macOS.spec"
|
||||
|
||||
if not os.path.exists(spec_file):
|
||||
print(f"✗ Error: {spec_file} not found")
|
||||
return False
|
||||
|
||||
try:
|
||||
subprocess.run(["pyinstaller", spec_file], check=True)
|
||||
|
||||
if os.path.exists("dist/ESP32_Flasher.app"):
|
||||
print("\n✓ Build complete!")
|
||||
print(f"\nApplication created at: dist/ESP32_Flasher.app")
|
||||
print("\nTo run the application:")
|
||||
print(" open dist/ESP32_Flasher.app")
|
||||
return True
|
||||
else:
|
||||
print("\n✗ Build failed - application not found in dist folder")
|
||||
return False
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"\n✗ Build failed: {e}")
|
||||
return False
|
||||
|
||||
def build_windows():
|
||||
"""Build Windows executable"""
|
||||
print("\n=== Building for Windows ===")
|
||||
spec_file = "ESP32_Flasher_Windows.spec"
|
||||
|
||||
if not os.path.exists(spec_file):
|
||||
print(f"✗ Error: {spec_file} not found")
|
||||
return False
|
||||
|
||||
try:
|
||||
subprocess.run(["pyinstaller", spec_file], check=True)
|
||||
|
||||
if os.path.exists("dist/ESP32_Flasher.exe"):
|
||||
print("\n✓ Build complete!")
|
||||
print(f"\nExecutable created at: dist\\ESP32_Flasher.exe")
|
||||
print("\nYou can now distribute ESP32_Flasher.exe to users.")
|
||||
return True
|
||||
else:
|
||||
print("\n✗ Build failed - executable not found in dist folder")
|
||||
return False
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"\n✗ Build failed: {e}")
|
||||
return False
|
||||
|
||||
def build_linux():
|
||||
"""Build Linux executable"""
|
||||
print("\n=== Building for Linux ===")
|
||||
print("Note: Linux users typically prefer to run Python scripts directly.")
|
||||
print("Creating standalone executable anyway...")
|
||||
|
||||
# Use macOS spec as template for Linux (both use ELF format)
|
||||
spec_file = "ESP32_Flasher_macOS.spec"
|
||||
|
||||
if not os.path.exists(spec_file):
|
||||
print(f"✗ Error: {spec_file} not found")
|
||||
return False
|
||||
|
||||
try:
|
||||
subprocess.run(["pyinstaller", spec_file], check=True)
|
||||
|
||||
if os.path.exists("dist/ESP32_Flasher"):
|
||||
print("\n✓ Build complete!")
|
||||
print(f"\nExecutable created at: dist/ESP32_Flasher")
|
||||
print("\nTo run:")
|
||||
print(" ./dist/ESP32_Flasher")
|
||||
return True
|
||||
else:
|
||||
print("\n✗ Build failed - executable not found in dist folder")
|
||||
return False
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"\n✗ Build failed: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Main build function"""
|
||||
print("=== ESP32 Flasher - Universal Build Script ===\n")
|
||||
|
||||
# Detect platform
|
||||
current_platform = get_platform()
|
||||
print(f"Detected platform: {current_platform}")
|
||||
|
||||
if current_platform == "unknown":
|
||||
print("✗ Unsupported platform")
|
||||
return 1
|
||||
|
||||
# Setup virtual environment if needed (macOS)
|
||||
python_exe = setup_venv()
|
||||
|
||||
# Install dependencies
|
||||
if not install_dependencies(python_exe):
|
||||
return 1
|
||||
|
||||
# Build for detected platform
|
||||
success = False
|
||||
if current_platform == "macos":
|
||||
success = build_macos()
|
||||
elif current_platform == "windows":
|
||||
success = build_windows()
|
||||
elif current_platform == "linux":
|
||||
success = build_linux()
|
||||
|
||||
return 0 if success else 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,11 +1,24 @@
|
||||
@echo off
|
||||
echo === ESP32 Flasher - Windows Build Script ===
|
||||
echo.
|
||||
|
||||
echo Installing dependencies...
|
||||
pip install pyinstaller esptool pyserial
|
||||
|
||||
echo.
|
||||
echo Building executable from spec file...
|
||||
pyinstaller ESP32_Flasher.spec
|
||||
echo Building Windows executable...
|
||||
pyinstaller ESP32_Flasher_Windows.spec
|
||||
|
||||
echo.
|
||||
echo Build complete! Find ESP32_Flasher.exe in the dist folder.
|
||||
if exist "dist\ESP32_Flasher.exe" (
|
||||
echo Build complete!
|
||||
echo.
|
||||
echo Executable created at: dist\ESP32_Flasher.exe
|
||||
echo.
|
||||
echo You can now distribute ESP32_Flasher.exe to users.
|
||||
) else (
|
||||
echo Build failed - executable not found in dist folder
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
pause
|
||||
45
flash_tool/build_macos.sh
Executable file
45
flash_tool/build_macos.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
# Build script for macOS executable
|
||||
|
||||
echo "=== ESP32 Flasher - macOS Build Script ==="
|
||||
echo ""
|
||||
|
||||
# Check if running on macOS
|
||||
if [[ "$OSTYPE" != "darwin"* ]]; then
|
||||
echo "❌ Error: This script is for macOS only."
|
||||
echo " Use build_from_spec.bat on Windows."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create virtual environment if it doesn't exist
|
||||
if [ ! -d "venv" ]; then
|
||||
echo "Creating virtual environment..."
|
||||
python3 -m venv venv
|
||||
fi
|
||||
|
||||
echo "Activating virtual environment..."
|
||||
source venv/bin/activate
|
||||
|
||||
echo "Installing dependencies..."
|
||||
pip install pyinstaller esptool pyserial
|
||||
|
||||
echo ""
|
||||
echo "Building macOS application bundle..."
|
||||
pyinstaller ESP32_Flasher_macOS.spec
|
||||
|
||||
echo ""
|
||||
if [ -d "dist/ESP32_Flasher.app" ]; then
|
||||
echo "✓ Build complete!"
|
||||
echo ""
|
||||
echo "Application created at: dist/ESP32_Flasher.app"
|
||||
echo ""
|
||||
echo "To run the application:"
|
||||
echo " open dist/ESP32_Flasher.app"
|
||||
echo ""
|
||||
echo "To distribute, you can:"
|
||||
echo " 1. Zip the .app bundle"
|
||||
echo " 2. Create a DMG installer (requires additional tools)"
|
||||
else
|
||||
echo "❌ Build failed - application not found in dist folder"
|
||||
exit 1
|
||||
fi
|
||||
43
flash_tool/build_web_macos.sh
Executable file
43
flash_tool/build_web_macos.sh
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
# Build script for macOS executable (Web-based UI)
|
||||
|
||||
echo "=== ESP32 Flasher (Web UI) - macOS Build Script ==="
|
||||
echo ""
|
||||
|
||||
# Check if running on macOS
|
||||
if [[ "$OSTYPE" != "darwin"* ]]; then
|
||||
echo "❌ Error: This script is for macOS only."
|
||||
echo " Use build_web_windows.bat on Windows."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create virtual environment if it doesn't exist
|
||||
if [ ! -d "venv" ]; then
|
||||
echo "Creating virtual environment..."
|
||||
python3 -m venv venv
|
||||
fi
|
||||
|
||||
echo "Activating virtual environment..."
|
||||
source venv/bin/activate
|
||||
|
||||
echo "Installing dependencies..."
|
||||
pip install -r requirements.txt
|
||||
|
||||
echo ""
|
||||
echo "Building macOS application bundle..."
|
||||
pyinstaller ESP32_WebFlasher_macOS.spec
|
||||
|
||||
echo ""
|
||||
if [ -d "dist/ESP32_Flasher.app" ]; then
|
||||
echo "✓ Build complete!"
|
||||
echo ""
|
||||
echo "Application created at: dist/ESP32_Flasher.app"
|
||||
echo ""
|
||||
echo "To run the application:"
|
||||
echo " open dist/ESP32_Flasher.app"
|
||||
echo ""
|
||||
echo "The app will open in your default web browser!"
|
||||
else
|
||||
echo "❌ Build failed - application not found in dist folder"
|
||||
exit 1
|
||||
fi
|
||||
@@ -21,6 +21,9 @@ class ESP32FlasherGUI:
|
||||
self.root.title("ESP32 Firmware Flasher")
|
||||
self.root.geometry("600x500")
|
||||
|
||||
# Configure colors for dark mode compatibility
|
||||
self.setup_colors()
|
||||
|
||||
# Variables
|
||||
self.port_var = tk.StringVar()
|
||||
self.firmware_path_var = tk.StringVar()
|
||||
@@ -29,9 +32,41 @@ class ESP32FlasherGUI:
|
||||
self.setup_ui()
|
||||
self.refresh_ports()
|
||||
|
||||
def setup_colors(self):
|
||||
"""Configure colors that work in both light and dark mode"""
|
||||
# Try to detect dark mode on macOS
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
['defaults', 'read', '-g', 'AppleInterfaceStyle'],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
is_dark_mode = (result.returncode == 0 and 'Dark' in result.stdout)
|
||||
except:
|
||||
is_dark_mode = False
|
||||
|
||||
if is_dark_mode:
|
||||
# Dark mode colors
|
||||
self.bg_color = '#2b2b2b'
|
||||
self.fg_color = '#ffffff'
|
||||
self.text_bg = '#1e1e1e'
|
||||
self.text_fg = '#d4d4d4'
|
||||
self.button_bg = '#3c3c3c'
|
||||
else:
|
||||
# Light mode colors
|
||||
self.bg_color = '#f0f0f0'
|
||||
self.fg_color = '#000000'
|
||||
self.text_bg = '#ffffff'
|
||||
self.text_fg = '#000000'
|
||||
self.button_bg = '#e0e0e0'
|
||||
|
||||
# Configure root window
|
||||
self.root.configure(bg=self.bg_color)
|
||||
|
||||
def setup_ui(self):
|
||||
# Main frame
|
||||
main_frame = ttk.Frame(self.root, padding="10")
|
||||
main_frame = tk.Frame(self.root, bg=self.bg_color, padx=10, pady=10)
|
||||
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
# Configure grid weights
|
||||
@@ -40,55 +75,76 @@ class ESP32FlasherGUI:
|
||||
main_frame.columnconfigure(1, weight=1)
|
||||
|
||||
# Port selection
|
||||
ttk.Label(main_frame, text="Serial Port:").grid(row=0, column=0, sticky=tk.W, pady=5)
|
||||
port_frame = ttk.Frame(main_frame)
|
||||
tk.Label(main_frame, text="Serial Port:", bg=self.bg_color, fg=self.fg_color).grid(
|
||||
row=0, column=0, sticky=tk.W, pady=5)
|
||||
port_frame = tk.Frame(main_frame, bg=self.bg_color)
|
||||
port_frame.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=5)
|
||||
port_frame.columnconfigure(0, weight=1)
|
||||
|
||||
self.port_combo = ttk.Combobox(port_frame, textvariable=self.port_var, state="readonly")
|
||||
self.port_combo.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 5))
|
||||
|
||||
ttk.Button(port_frame, text="Refresh", command=self.refresh_ports).grid(row=0, column=1)
|
||||
tk.Button(port_frame, text="Refresh", command=self.refresh_ports,
|
||||
bg=self.button_bg, fg=self.fg_color, relief=tk.RAISED).grid(row=0, column=1)
|
||||
|
||||
# Firmware package selection
|
||||
ttk.Label(main_frame, text="Firmware Package:").grid(row=1, column=0, sticky=tk.W, pady=5)
|
||||
firmware_frame = ttk.Frame(main_frame)
|
||||
tk.Label(main_frame, text="Firmware Package:", bg=self.bg_color, fg=self.fg_color).grid(
|
||||
row=1, column=0, sticky=tk.W, pady=5)
|
||||
firmware_frame = tk.Frame(main_frame, bg=self.bg_color)
|
||||
firmware_frame.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5)
|
||||
firmware_frame.columnconfigure(0, weight=1)
|
||||
|
||||
self.firmware_entry = ttk.Entry(firmware_frame, textvariable=self.firmware_path_var, state="readonly")
|
||||
self.firmware_entry = tk.Entry(firmware_frame, textvariable=self.firmware_path_var,
|
||||
state="readonly", bg=self.text_bg, fg=self.text_fg,
|
||||
relief=tk.SUNKEN, borderwidth=2)
|
||||
self.firmware_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 5))
|
||||
|
||||
ttk.Button(firmware_frame, text="Browse", command=self.browse_firmware).grid(row=0, column=1)
|
||||
tk.Button(firmware_frame, text="Browse", command=self.browse_firmware,
|
||||
bg=self.button_bg, fg=self.fg_color, relief=tk.RAISED).grid(row=0, column=1)
|
||||
|
||||
# Flash settings frame
|
||||
settings_frame = ttk.LabelFrame(main_frame, text="Flash Settings", padding="5")
|
||||
settings_frame = tk.LabelFrame(main_frame, text="Flash Settings",
|
||||
bg=self.bg_color, fg=self.fg_color,
|
||||
relief=tk.GROOVE, borderwidth=2, padx=5, pady=5)
|
||||
settings_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=10)
|
||||
settings_frame.columnconfigure(1, weight=1)
|
||||
|
||||
# Flash settings
|
||||
ttk.Label(settings_frame, text="Chip:").grid(row=0, column=0, sticky=tk.W, pady=2)
|
||||
tk.Label(settings_frame, text="Chip:", bg=self.bg_color, fg=self.fg_color).grid(
|
||||
row=0, column=0, sticky=tk.W, pady=2, padx=(0, 10))
|
||||
self.chip_var = tk.StringVar(value="esp32")
|
||||
ttk.Entry(settings_frame, textvariable=self.chip_var, width=15).grid(row=0, column=1, sticky=tk.W, pady=2)
|
||||
tk.Entry(settings_frame, textvariable=self.chip_var, width=15,
|
||||
bg=self.text_bg, fg=self.text_fg, relief=tk.SUNKEN).grid(row=0, column=1, sticky=tk.W, pady=2)
|
||||
|
||||
ttk.Label(settings_frame, text="Baud Rate:").grid(row=1, column=0, sticky=tk.W, pady=2)
|
||||
tk.Label(settings_frame, text="Baud Rate:", bg=self.bg_color, fg=self.fg_color).grid(
|
||||
row=1, column=0, sticky=tk.W, pady=2, padx=(0, 10))
|
||||
self.baud_var = tk.StringVar(value="460800")
|
||||
ttk.Entry(settings_frame, textvariable=self.baud_var, width=15).grid(row=1, column=1, sticky=tk.W, pady=2)
|
||||
tk.Entry(settings_frame, textvariable=self.baud_var, width=15,
|
||||
bg=self.text_bg, fg=self.text_fg, relief=tk.SUNKEN).grid(row=1, column=1, sticky=tk.W, pady=2)
|
||||
|
||||
ttk.Label(settings_frame, text="Flash Mode:").grid(row=2, column=0, sticky=tk.W, pady=2)
|
||||
tk.Label(settings_frame, text="Flash Mode:", bg=self.bg_color, fg=self.fg_color).grid(
|
||||
row=2, column=0, sticky=tk.W, pady=2, padx=(0, 10))
|
||||
self.flash_mode_var = tk.StringVar(value="dio")
|
||||
ttk.Entry(settings_frame, textvariable=self.flash_mode_var, width=15).grid(row=2, column=1, sticky=tk.W, pady=2)
|
||||
tk.Entry(settings_frame, textvariable=self.flash_mode_var, width=15,
|
||||
bg=self.text_bg, fg=self.text_fg, relief=tk.SUNKEN).grid(row=2, column=1, sticky=tk.W, pady=2)
|
||||
|
||||
ttk.Label(settings_frame, text="Flash Freq:").grid(row=3, column=0, sticky=tk.W, pady=2)
|
||||
tk.Label(settings_frame, text="Flash Freq:", bg=self.bg_color, fg=self.fg_color).grid(
|
||||
row=3, column=0, sticky=tk.W, pady=2, padx=(0, 10))
|
||||
self.flash_freq_var = tk.StringVar(value="40m")
|
||||
ttk.Entry(settings_frame, textvariable=self.flash_freq_var, width=15).grid(row=3, column=1, sticky=tk.W, pady=2)
|
||||
tk.Entry(settings_frame, textvariable=self.flash_freq_var, width=15,
|
||||
bg=self.text_bg, fg=self.text_fg, relief=tk.SUNKEN).grid(row=3, column=1, sticky=tk.W, pady=2)
|
||||
|
||||
ttk.Label(settings_frame, text="Flash Size:").grid(row=4, column=0, sticky=tk.W, pady=2)
|
||||
tk.Label(settings_frame, text="Flash Size:", bg=self.bg_color, fg=self.fg_color).grid(
|
||||
row=4, column=0, sticky=tk.W, pady=2, padx=(0, 10))
|
||||
self.flash_size_var = tk.StringVar(value="2MB")
|
||||
ttk.Entry(settings_frame, textvariable=self.flash_size_var, width=15).grid(row=4, column=1, sticky=tk.W, pady=2)
|
||||
tk.Entry(settings_frame, textvariable=self.flash_size_var, width=15,
|
||||
bg=self.text_bg, fg=self.text_fg, relief=tk.SUNKEN).grid(row=4, column=1, sticky=tk.W, pady=2)
|
||||
|
||||
# Flash button
|
||||
self.flash_button = ttk.Button(main_frame, text="Flash Firmware", command=self.flash_firmware)
|
||||
self.flash_button = tk.Button(main_frame, text="Flash Firmware", command=self.flash_firmware,
|
||||
bg=self.button_bg, fg=self.fg_color,
|
||||
relief=tk.RAISED, padx=20, pady=5,
|
||||
font=('TkDefaultFont', 10, 'bold'))
|
||||
self.flash_button.grid(row=3, column=0, columnspan=2, pady=10)
|
||||
|
||||
# Progress bar
|
||||
@@ -96,8 +152,18 @@ class ESP32FlasherGUI:
|
||||
self.progress.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5)
|
||||
|
||||
# Log output
|
||||
ttk.Label(main_frame, text="Output:").grid(row=5, column=0, sticky=tk.W, pady=(10, 0))
|
||||
self.log_text = scrolledtext.ScrolledText(main_frame, height=15, width=70)
|
||||
tk.Label(main_frame, text="Output:", bg=self.bg_color, fg=self.fg_color).grid(
|
||||
row=5, column=0, sticky=tk.W, pady=(10, 0))
|
||||
self.log_text = scrolledtext.ScrolledText(
|
||||
main_frame,
|
||||
height=15,
|
||||
width=70,
|
||||
bg=self.text_bg,
|
||||
fg=self.text_fg,
|
||||
insertbackground=self.text_fg, # Cursor color
|
||||
relief=tk.SUNKEN,
|
||||
borderwidth=2
|
||||
)
|
||||
self.log_text.grid(row=6, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5)
|
||||
main_frame.rowconfigure(6, weight=1)
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
esptool>=4.0
|
||||
pyserial>=3.5
|
||||
pyinstaller>=5.0
|
||||
flask>=2.3.0
|
||||
flask-socketio>=5.3.0
|
||||
python-socketio>=5.11.0
|
||||
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>
|
||||
272
flash_tool/web_flasher.py
Normal file
272
flash_tool/web_flasher.py
Normal file
@@ -0,0 +1,272 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ESP32 Firmware Flash Web Server
|
||||
Web-based GUI interface for flashing firmware packages to ESP32 devices
|
||||
"""
|
||||
|
||||
from flask import Flask, render_template, request, jsonify, send_from_directory
|
||||
from flask_socketio import SocketIO, emit
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import zipfile
|
||||
import tempfile
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
import serial.tools.list_ports
|
||||
import webbrowser
|
||||
import socket
|
||||
import time
|
||||
import signal
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # 100MB max file size
|
||||
app.config['SECRET_KEY'] = 'esp32-flasher-secret-key'
|
||||
socketio = SocketIO(app, cors_allowed_origins="*")
|
||||
|
||||
# Global state
|
||||
flash_status = {
|
||||
'running': False,
|
||||
'output': [],
|
||||
'success': None
|
||||
}
|
||||
|
||||
# Client connection tracking
|
||||
client_connected = False
|
||||
last_heartbeat = time.time()
|
||||
shutdown_timer = None
|
||||
|
||||
def get_free_port():
|
||||
"""Find a free port to run the server on"""
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind(('', 0))
|
||||
s.listen(1)
|
||||
port = s.getsockname()[1]
|
||||
return port
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""Serve the main page"""
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/api/ports')
|
||||
def get_ports():
|
||||
"""Get list of available serial ports"""
|
||||
try:
|
||||
ports = [{'device': port.device, 'description': port.description}
|
||||
for port in serial.tools.list_ports.comports()]
|
||||
return jsonify({'success': True, 'ports': ports})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
@app.route('/api/flash', methods=['POST'])
|
||||
def flash_firmware():
|
||||
"""Flash firmware to ESP32"""
|
||||
global flash_status
|
||||
|
||||
if flash_status['running']:
|
||||
return jsonify({'success': False, 'error': 'Flash operation already in progress'})
|
||||
|
||||
# Get form data
|
||||
port = request.form.get('port')
|
||||
chip = request.form.get('chip', 'esp32')
|
||||
baud = request.form.get('baud', '460800')
|
||||
flash_mode = request.form.get('flash_mode', 'dio')
|
||||
flash_freq = request.form.get('flash_freq', '40m')
|
||||
flash_size = request.form.get('flash_size', '2MB')
|
||||
|
||||
# Get uploaded file
|
||||
if 'firmware' not in request.files:
|
||||
return jsonify({'success': False, 'error': 'No firmware file uploaded'})
|
||||
|
||||
firmware_file = request.files['firmware']
|
||||
if firmware_file.filename == '':
|
||||
return jsonify({'success': False, 'error': 'No firmware file selected'})
|
||||
|
||||
# Save and extract firmware
|
||||
try:
|
||||
# Create temp directory
|
||||
temp_dir = tempfile.mkdtemp(prefix="esp32_flash_")
|
||||
zip_path = os.path.join(temp_dir, 'firmware.zip')
|
||||
firmware_file.save(zip_path)
|
||||
|
||||
# Extract firmware
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_file:
|
||||
zip_file.extractall(temp_dir)
|
||||
|
||||
# Start flash in background thread
|
||||
flash_status = {'running': True, 'output': [], 'success': None}
|
||||
thread = threading.Thread(
|
||||
target=flash_worker,
|
||||
args=(temp_dir, port, chip, baud, flash_mode, flash_freq, flash_size)
|
||||
)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
return jsonify({'success': True, 'message': 'Flash started'})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
@app.route('/api/status')
|
||||
def get_status():
|
||||
"""Get current flash status"""
|
||||
return jsonify(flash_status)
|
||||
|
||||
@socketio.on('connect')
|
||||
def handle_connect():
|
||||
"""Handle client connection"""
|
||||
global client_connected, last_heartbeat, shutdown_timer
|
||||
client_connected = True
|
||||
last_heartbeat = time.time()
|
||||
|
||||
# Cancel any pending shutdown
|
||||
if shutdown_timer:
|
||||
shutdown_timer.cancel()
|
||||
shutdown_timer = None
|
||||
|
||||
print('Client connected')
|
||||
|
||||
@socketio.on('disconnect')
|
||||
def handle_disconnect():
|
||||
"""Handle client disconnection"""
|
||||
global client_connected, shutdown_timer
|
||||
client_connected = False
|
||||
print('Client disconnected - scheduling shutdown in 5 seconds...')
|
||||
|
||||
# Schedule shutdown after 5 seconds
|
||||
shutdown_timer = threading.Timer(5.0, shutdown_server)
|
||||
shutdown_timer.daemon = True
|
||||
shutdown_timer.start()
|
||||
|
||||
@socketio.on('heartbeat')
|
||||
def handle_heartbeat():
|
||||
"""Handle heartbeat from client"""
|
||||
global last_heartbeat
|
||||
last_heartbeat = time.time()
|
||||
emit('heartbeat_ack', {'timestamp': last_heartbeat})
|
||||
|
||||
def shutdown_server():
|
||||
"""Gracefully shutdown the server"""
|
||||
print('\nNo clients connected. Shutting down server...')
|
||||
|
||||
# Give a moment for any pending operations
|
||||
time.sleep(1)
|
||||
|
||||
# Shutdown Flask
|
||||
try:
|
||||
func = request.environ.get('werkzeug.server.shutdown')
|
||||
if func is None:
|
||||
# Alternative shutdown method
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
else:
|
||||
func()
|
||||
except:
|
||||
# Force exit as last resort
|
||||
os._exit(0)
|
||||
|
||||
def flash_worker(temp_dir, port, chip, baud, flash_mode, flash_freq, flash_size):
|
||||
"""Background worker for flashing firmware"""
|
||||
global flash_status
|
||||
|
||||
try:
|
||||
# Define file paths
|
||||
bootloader = os.path.join(temp_dir, "bootloader.bin")
|
||||
firmware = os.path.join(temp_dir, "soundshot.bin")
|
||||
ota_initial = os.path.join(temp_dir, "ota_data_initial.bin")
|
||||
partition = os.path.join(temp_dir, "partition-table.bin")
|
||||
|
||||
# Verify files exist
|
||||
required_files = [bootloader, firmware, ota_initial, partition]
|
||||
for file_path in required_files:
|
||||
if not os.path.exists(file_path):
|
||||
flash_status['output'].append(f"ERROR: Missing file {os.path.basename(file_path)}")
|
||||
flash_status['success'] = False
|
||||
flash_status['running'] = False
|
||||
return
|
||||
|
||||
# Build esptool command
|
||||
cmd = [
|
||||
sys.executable, "-m", "esptool",
|
||||
"--chip", chip,
|
||||
"--port", port,
|
||||
"--baud", baud,
|
||||
"--before", "default_reset",
|
||||
"--after", "hard_reset",
|
||||
"write-flash",
|
||||
"--flash-mode", flash_mode,
|
||||
"--flash-freq", flash_freq,
|
||||
"--flash-size", flash_size,
|
||||
"0x1000", bootloader,
|
||||
"0x20000", firmware,
|
||||
"0x11000", ota_initial,
|
||||
"0x8000", partition
|
||||
]
|
||||
|
||||
flash_status['output'].append(f"Running: {' '.join(cmd)}")
|
||||
flash_status['output'].append("")
|
||||
|
||||
# Run esptool
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
universal_newlines=True
|
||||
)
|
||||
|
||||
# Stream output
|
||||
for line in process.stdout:
|
||||
flash_status['output'].append(line.rstrip())
|
||||
|
||||
process.wait()
|
||||
|
||||
if process.returncode == 0:
|
||||
flash_status['output'].append("")
|
||||
flash_status['output'].append("✓ Flash completed successfully!")
|
||||
flash_status['success'] = True
|
||||
else:
|
||||
flash_status['output'].append("")
|
||||
flash_status['output'].append(f"✗ Flash failed with return code {process.returncode}")
|
||||
flash_status['success'] = False
|
||||
|
||||
except Exception as e:
|
||||
flash_status['output'].append(f"✗ Error: {str(e)}")
|
||||
flash_status['success'] = False
|
||||
finally:
|
||||
flash_status['running'] = False
|
||||
# Clean up temp directory
|
||||
try:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
except:
|
||||
pass
|
||||
|
||||
def open_browser(port):
|
||||
"""Open browser to the application"""
|
||||
url = f'http://127.0.0.1:{port}'
|
||||
webbrowser.open(url)
|
||||
|
||||
def main():
|
||||
"""Run the web server"""
|
||||
port = get_free_port()
|
||||
print(f"Starting ESP32 Flasher on port {port}...")
|
||||
print(f"Open your browser to: http://127.0.0.1:{port}")
|
||||
print(f"Server will automatically shut down when browser is closed.\n")
|
||||
|
||||
# Open browser after short delay
|
||||
timer = threading.Timer(1.5, open_browser, args=[port])
|
||||
timer.daemon = True
|
||||
timer.start()
|
||||
|
||||
# Run Flask server with SocketIO
|
||||
try:
|
||||
socketio.run(app, host='127.0.0.1', port=port, debug=False, allow_unsafe_werkzeug=True)
|
||||
except KeyboardInterrupt:
|
||||
print('\nShutting down...')
|
||||
except SystemExit:
|
||||
print('Server stopped.')
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -107,7 +107,7 @@ static void bt_app_a2d_heart_beat(TimerHandle_t arg);
|
||||
static void bt_app_av_sm_hdlr(uint16_t event, void *param);
|
||||
|
||||
/* utils for transfer BLuetooth Deveice Address into string form */
|
||||
static char *bda2str(esp_bd_addr_t bda, char *str, size_t size);
|
||||
static char *bda2str(const uint8_t *bda, char *str, size_t size);
|
||||
|
||||
static esp_err_t bt_try_connect_known_devices(void);
|
||||
static void bt_debug_print_known_devices(void);
|
||||
@@ -485,7 +485,7 @@ static esp_err_t bt_try_next_known_device(void)
|
||||
/*********************************
|
||||
* STATIC FUNCTION DEFINITIONS
|
||||
********************************/
|
||||
static char *bda2str(esp_bd_addr_t bda, char *str, size_t size)
|
||||
static char *bda2str(const uint8_t *bda, char *str, size_t size)
|
||||
{
|
||||
if (bda == NULL || str == NULL || size < 18) {
|
||||
return NULL;
|
||||
@@ -1848,8 +1848,11 @@ void bt_volume_down(void) {
|
||||
}
|
||||
|
||||
if (s_volume_level > 0) {
|
||||
s_volume_level -= 10; // Decrease by ~8%
|
||||
if (s_volume_level < 0) s_volume_level = 0;
|
||||
if (s_volume_level < 10) {
|
||||
s_volume_level = 0;
|
||||
} else {
|
||||
s_volume_level -= 10; // Decrease by ~8%
|
||||
}
|
||||
|
||||
ESP_LOGI(BT_AV_TAG, "Setting volume to %d", s_volume_level);
|
||||
esp_avrc_ct_send_set_absolute_volume_cmd(APP_RC_CT_TL_RN_VOLUME_CHANGE, s_volume_level);
|
||||
|
||||
@@ -275,11 +275,12 @@ void app_main(void)
|
||||
print_heap_info("POST_SYSTEM");
|
||||
|
||||
// Initialize IMU
|
||||
ESP_ERROR_CHECK(lsm6dsv_init(22, 21)); // SCL = IO14, SDA = IO15
|
||||
ESP_ERROR_CHECK(lsm6dsv_init(22, 21)); // SCL = IO14, SDA = IO15`
|
||||
print_heap_info("POST_IMU");
|
||||
|
||||
// Create IMU task
|
||||
TaskHandle_t h = xTaskCreate(imu_task, "imu_task", 2048, NULL, 5, NULL);
|
||||
TaskHandle_t h = NULL;
|
||||
xTaskCreate(imu_task, "imu_task", 2048, NULL, 5, &h);
|
||||
print_heap_info("POST_IMU_TASK");
|
||||
|
||||
bt_app_init();
|
||||
|
||||
Reference in New Issue
Block a user