adding flasher
This commit is contained in:
271
flash_tool/gui_flasher.py
Normal file
271
flash_tool/gui_flasher.py
Normal file
@@ -0,0 +1,271 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ESP32 Firmware Flash GUI Tool
|
||||
Simple GUI interface for flashing firmware packages to ESP32 devices
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import zipfile
|
||||
import tempfile
|
||||
import os
|
||||
from pathlib import Path
|
||||
import serial.tools.list_ports
|
||||
|
||||
class ESP32FlasherGUI:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("ESP32 Firmware Flasher")
|
||||
self.root.geometry("600x500")
|
||||
|
||||
# Variables
|
||||
self.port_var = tk.StringVar()
|
||||
self.firmware_path_var = tk.StringVar()
|
||||
self.temp_dir = None
|
||||
|
||||
self.setup_ui()
|
||||
self.refresh_ports()
|
||||
|
||||
def setup_ui(self):
|
||||
# Main frame
|
||||
main_frame = ttk.Frame(self.root, padding="10")
|
||||
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
# Configure grid weights
|
||||
self.root.columnconfigure(0, weight=1)
|
||||
self.root.rowconfigure(0, weight=1)
|
||||
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)
|
||||
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)
|
||||
|
||||
# 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)
|
||||
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.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)
|
||||
|
||||
# Flash settings frame
|
||||
settings_frame = ttk.LabelFrame(main_frame, text="Flash Settings", padding="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)
|
||||
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)
|
||||
|
||||
ttk.Label(settings_frame, text="Baud Rate:").grid(row=1, column=0, sticky=tk.W, pady=2)
|
||||
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)
|
||||
|
||||
ttk.Label(settings_frame, text="Flash Mode:").grid(row=2, column=0, sticky=tk.W, pady=2)
|
||||
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)
|
||||
|
||||
ttk.Label(settings_frame, text="Flash Freq:").grid(row=3, column=0, sticky=tk.W, pady=2)
|
||||
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)
|
||||
|
||||
ttk.Label(settings_frame, text="Flash Size:").grid(row=4, column=0, sticky=tk.W, pady=2)
|
||||
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)
|
||||
|
||||
# Flash button
|
||||
self.flash_button = ttk.Button(main_frame, text="Flash Firmware", command=self.flash_firmware)
|
||||
self.flash_button.grid(row=3, column=0, columnspan=2, pady=10)
|
||||
|
||||
# Progress bar
|
||||
self.progress = ttk.Progressbar(main_frame, mode='indeterminate')
|
||||
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)
|
||||
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)
|
||||
|
||||
def refresh_ports(self):
|
||||
"""Refresh the list of available serial ports"""
|
||||
ports = [port.device for port in serial.tools.list_ports.comports()]
|
||||
self.port_combo['values'] = ports
|
||||
if ports and not self.port_var.get():
|
||||
self.port_var.set(ports[0])
|
||||
|
||||
def browse_firmware(self):
|
||||
"""Browse for firmware package (.zip file)"""
|
||||
filename = filedialog.askopenfilename(
|
||||
title="Select Firmware Package",
|
||||
filetypes=[("ZIP files", "*.zip"), ("All files", "*.*")]
|
||||
)
|
||||
if filename:
|
||||
self.firmware_path_var.set(filename)
|
||||
self.validate_firmware_package(filename)
|
||||
|
||||
def validate_firmware_package(self, zip_path):
|
||||
"""Validate that the ZIP contains required firmware files"""
|
||||
required_files = [
|
||||
"bootloader.bin",
|
||||
"soundshot.bin",
|
||||
"ota_data_initial.bin",
|
||||
"partition-table.bin"
|
||||
]
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_file:
|
||||
zip_contents = zip_file.namelist()
|
||||
missing_files = []
|
||||
|
||||
for required_file in required_files:
|
||||
if required_file not in zip_contents:
|
||||
missing_files.append(required_file)
|
||||
|
||||
if missing_files:
|
||||
messagebox.showwarning(
|
||||
"Invalid Package",
|
||||
f"Missing required files:\n{', '.join(missing_files)}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
self.log_message(f"✓ Valid firmware package: {Path(zip_path).name}")
|
||||
return True
|
||||
|
||||
except zipfile.BadZipFile:
|
||||
messagebox.showerror("Error", "Invalid ZIP file")
|
||||
return False
|
||||
|
||||
def log_message(self, message):
|
||||
"""Add message to log output"""
|
||||
self.log_text.insert(tk.END, message + "\n")
|
||||
self.log_text.see(tk.END)
|
||||
self.root.update()
|
||||
|
||||
def extract_firmware_package(self, zip_path):
|
||||
"""Extract firmware package to temporary directory"""
|
||||
if self.temp_dir:
|
||||
# Clean up previous temp dir
|
||||
import shutil
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
self.temp_dir = tempfile.mkdtemp(prefix="esp32_flash_")
|
||||
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_file:
|
||||
zip_file.extractall(self.temp_dir)
|
||||
|
||||
return self.temp_dir
|
||||
|
||||
def flash_firmware(self):
|
||||
"""Flash the firmware to ESP32"""
|
||||
# Validate inputs
|
||||
if not self.port_var.get():
|
||||
messagebox.showerror("Error", "Please select a serial port")
|
||||
return
|
||||
|
||||
if not self.firmware_path_var.get():
|
||||
messagebox.showerror("Error", "Please select a firmware package")
|
||||
return
|
||||
|
||||
# Disable flash button and start progress
|
||||
self.flash_button.config(state="disabled")
|
||||
self.progress.start()
|
||||
self.log_text.delete(1.0, tk.END)
|
||||
|
||||
# Run flash in separate thread to prevent UI freezing
|
||||
thread = threading.Thread(target=self._flash_worker)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def _flash_worker(self):
|
||||
"""Worker thread for flashing firmware"""
|
||||
try:
|
||||
# Extract firmware package
|
||||
self.log_message("Extracting firmware package...")
|
||||
temp_dir = self.extract_firmware_package(self.firmware_path_var.get())
|
||||
|
||||
# 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")
|
||||
|
||||
# Build esptool command
|
||||
cmd = [
|
||||
sys.executable, "-m", "esptool",
|
||||
"--chip", self.chip_var.get(),
|
||||
"--port", self.port_var.get(),
|
||||
"--baud", self.baud_var.get(),
|
||||
"--before", "default_reset",
|
||||
"--after", "hard_reset",
|
||||
"write-flash",
|
||||
"--flash-mode", self.flash_mode_var.get(),
|
||||
"--flash-freq", self.flash_freq_var.get(),
|
||||
"--flash-size", self.flash_size_var.get(),
|
||||
"0x1000", bootloader,
|
||||
"0x20000", firmware,
|
||||
"0x11000", ota_initial,
|
||||
"0x8000", partition
|
||||
]
|
||||
|
||||
self.log_message(f"Running: {' '.join(cmd)}\n")
|
||||
|
||||
# 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:
|
||||
self.root.after(0, self.log_message, line.rstrip())
|
||||
|
||||
process.wait()
|
||||
|
||||
if process.returncode == 0:
|
||||
self.root.after(0, self.log_message, "\n✓ Flash completed successfully!")
|
||||
self.root.after(0, messagebox.showinfo, "Success", "Firmware flashed successfully!")
|
||||
else:
|
||||
self.root.after(0, self.log_message, f"\n✗ Flash failed with return code {process.returncode}")
|
||||
self.root.after(0, messagebox.showerror, "Error", "Flash operation failed!")
|
||||
|
||||
except Exception as e:
|
||||
self.root.after(0, self.log_message, f"\n✗ Error: {str(e)}")
|
||||
self.root.after(0, messagebox.showerror, "Error", f"Flash operation failed: {str(e)}")
|
||||
|
||||
finally:
|
||||
# Re-enable UI
|
||||
self.root.after(0, self.progress.stop)
|
||||
self.root.after(0, lambda: self.flash_button.config(state="normal"))
|
||||
|
||||
# Clean up temp directory
|
||||
if self.temp_dir:
|
||||
import shutil
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
self.temp_dir = None
|
||||
|
||||
def main():
|
||||
root = tk.Tk()
|
||||
app = ESP32FlasherGUI(root)
|
||||
root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user