adding web flasher tool
This commit is contained in:
@@ -6,8 +6,23 @@
|
|||||||
"Bash(git add:*)",
|
"Bash(git add:*)",
|
||||||
"Bash(git commit:*)",
|
"Bash(git commit:*)",
|
||||||
"Bash(pip install:*)",
|
"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": []
|
"deny": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -1,6 +1,22 @@
|
|||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Python virtual environments
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# PyInstaller build artifacts
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.spec
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
|
||||||
# esp-idf built binaries
|
# esp-idf built binaries
|
||||||
build/
|
build/
|
||||||
build_*_*/
|
build_*_*/
|
||||||
|
|||||||
20
.vscode/c_cpp_properties.json
vendored
20
.vscode/c_cpp_properties.json
vendored
@@ -1,15 +1,21 @@
|
|||||||
{
|
{
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Linux",
|
"name": "Mac",
|
||||||
"includePath": [
|
"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": [],
|
"defines": [
|
||||||
"compilerPath": "/usr/bin/gcc",
|
"ESP_PLATFORM"
|
||||||
"cStandard": "c17",
|
],
|
||||||
"cppStandard": "gnu++17",
|
"compilerPath": "/Users/brent/.espressif/tools/xtensa-esp-elf/esp-13.2.0_20240530/xtensa-esp-elf/bin/xtensa-esp32-elf-gcc",
|
||||||
"intelliSenseMode": "linux-gcc-x64"
|
"cStandard": "c11",
|
||||||
|
"cppStandard": "c++20",
|
||||||
|
"intelliSenseMode": "gcc-x64"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version": 4
|
"version": 4
|
||||||
|
|||||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -40,5 +40,7 @@
|
|||||||
"random": "c"
|
"random": "c"
|
||||||
},
|
},
|
||||||
"git.ignoreLimitWarning": true,
|
"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 off
|
||||||
|
echo === ESP32 Flasher - Windows Build Script ===
|
||||||
|
echo.
|
||||||
|
|
||||||
echo Installing dependencies...
|
echo Installing dependencies...
|
||||||
pip install pyinstaller esptool pyserial
|
pip install pyinstaller esptool pyserial
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo Building executable from spec file...
|
echo Building Windows executable...
|
||||||
pyinstaller ESP32_Flasher.spec
|
pyinstaller ESP32_Flasher_Windows.spec
|
||||||
|
|
||||||
echo.
|
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
|
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
|
||||||
@@ -20,84 +20,150 @@ class ESP32FlasherGUI:
|
|||||||
self.root = root
|
self.root = root
|
||||||
self.root.title("ESP32 Firmware Flasher")
|
self.root.title("ESP32 Firmware Flasher")
|
||||||
self.root.geometry("600x500")
|
self.root.geometry("600x500")
|
||||||
|
|
||||||
|
# Configure colors for dark mode compatibility
|
||||||
|
self.setup_colors()
|
||||||
|
|
||||||
# Variables
|
# Variables
|
||||||
self.port_var = tk.StringVar()
|
self.port_var = tk.StringVar()
|
||||||
self.firmware_path_var = tk.StringVar()
|
self.firmware_path_var = tk.StringVar()
|
||||||
self.temp_dir = None
|
self.temp_dir = None
|
||||||
|
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
self.refresh_ports()
|
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):
|
def setup_ui(self):
|
||||||
# Main frame
|
# 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))
|
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||||
|
|
||||||
# Configure grid weights
|
# Configure grid weights
|
||||||
self.root.columnconfigure(0, weight=1)
|
self.root.columnconfigure(0, weight=1)
|
||||||
self.root.rowconfigure(0, weight=1)
|
self.root.rowconfigure(0, weight=1)
|
||||||
main_frame.columnconfigure(1, weight=1)
|
main_frame.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
# Port selection
|
# Port selection
|
||||||
ttk.Label(main_frame, text="Serial Port:").grid(row=0, column=0, sticky=tk.W, pady=5)
|
tk.Label(main_frame, text="Serial Port:", bg=self.bg_color, fg=self.fg_color).grid(
|
||||||
port_frame = ttk.Frame(main_frame)
|
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.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=5)
|
||||||
port_frame.columnconfigure(0, weight=1)
|
port_frame.columnconfigure(0, weight=1)
|
||||||
|
|
||||||
self.port_combo = ttk.Combobox(port_frame, textvariable=self.port_var, state="readonly")
|
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))
|
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
|
# Firmware package selection
|
||||||
ttk.Label(main_frame, text="Firmware Package:").grid(row=1, column=0, sticky=tk.W, pady=5)
|
tk.Label(main_frame, text="Firmware Package:", bg=self.bg_color, fg=self.fg_color).grid(
|
||||||
firmware_frame = ttk.Frame(main_frame)
|
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.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5)
|
||||||
firmware_frame.columnconfigure(0, weight=1)
|
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))
|
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
|
# 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.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=10)
|
||||||
settings_frame.columnconfigure(1, weight=1)
|
settings_frame.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
# Flash settings
|
# 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")
|
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")
|
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")
|
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")
|
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")
|
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
|
# 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)
|
self.flash_button.grid(row=3, column=0, columnspan=2, pady=10)
|
||||||
|
|
||||||
# Progress bar
|
# Progress bar
|
||||||
self.progress = ttk.Progressbar(main_frame, mode='indeterminate')
|
self.progress = ttk.Progressbar(main_frame, mode='indeterminate')
|
||||||
self.progress.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5)
|
self.progress.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5)
|
||||||
|
|
||||||
# Log output
|
# Log output
|
||||||
ttk.Label(main_frame, text="Output:").grid(row=5, column=0, sticky=tk.W, pady=(10, 0))
|
tk.Label(main_frame, text="Output:", bg=self.bg_color, fg=self.fg_color).grid(
|
||||||
self.log_text = scrolledtext.ScrolledText(main_frame, height=15, width=70)
|
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)
|
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)
|
main_frame.rowconfigure(6, weight=1)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
esptool>=4.0
|
esptool>=4.0
|
||||||
pyserial>=3.5
|
pyserial>=3.5
|
||||||
pyinstaller>=5.0
|
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);
|
static void bt_app_av_sm_hdlr(uint16_t event, void *param);
|
||||||
|
|
||||||
/* utils for transfer BLuetooth Deveice Address into string form */
|
/* 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 esp_err_t bt_try_connect_known_devices(void);
|
||||||
static void bt_debug_print_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 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) {
|
if (bda == NULL || str == NULL || size < 18) {
|
||||||
return NULL;
|
return NULL;
|
||||||
@@ -1848,8 +1848,11 @@ void bt_volume_down(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (s_volume_level > 0) {
|
if (s_volume_level > 0) {
|
||||||
s_volume_level -= 10; // Decrease by ~8%
|
if (s_volume_level < 10) {
|
||||||
if (s_volume_level < 0) s_volume_level = 0;
|
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_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);
|
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");
|
print_heap_info("POST_SYSTEM");
|
||||||
|
|
||||||
// Initialize IMU
|
// 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");
|
print_heap_info("POST_IMU");
|
||||||
|
|
||||||
// Create IMU task
|
// 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");
|
print_heap_info("POST_IMU_TASK");
|
||||||
|
|
||||||
bt_app_init();
|
bt_app_init();
|
||||||
|
|||||||
Reference in New Issue
Block a user