✨ feat: flash phase tracking, erase checkbox, flash_model config
Step 2 UI now shows real-time flash operation phases parsed from program_flash output: - Erase: "Performing Erase Operation..." → sf erase → Erased: OK - Program: "Performing Program Operation..." → write blocks 0%→100% - Verify: "Performing Verify Operation..." → read+cmp 0%→100% Configuration additions: - erase_before_program checkbox: controls whether to pre-erase before programming - flash_model: chip type for capacity reference (s25fl256s1 = 32 MiB) subprocess_utils.py now emits 'phase' callbacks (erase/program/verify/done) detected from program_flash output transitions.
This commit is contained in:
@@ -25,6 +25,8 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
||||
"bootloader_bin_path": "",
|
||||
"fsbl_elf_path": "",
|
||||
"flash_type": "qspi-x4-single",
|
||||
"flash_model": "s25fl256s1",
|
||||
"erase_before_program": True,
|
||||
"firmware_bin_path": "",
|
||||
"reboot_timeout": 30,
|
||||
"ping_timeout": 5,
|
||||
@@ -60,6 +62,8 @@ class Config:
|
||||
bootloader_bin_path: str = ""
|
||||
fsbl_elf_path: str = ""
|
||||
flash_type: str = "qspi-x4-single"
|
||||
flash_model: str = "s25fl256s1"
|
||||
erase_before_program: bool = True
|
||||
firmware_bin_path: str = ""
|
||||
reboot_timeout: int = 30
|
||||
ping_timeout: int = 5
|
||||
@@ -166,6 +170,8 @@ class Config:
|
||||
"bootloader_bin_path": self._relative_path(self.bootloader_bin_path),
|
||||
"fsbl_elf_path": self._relative_path(self.fsbl_elf_path),
|
||||
"flash_type": self.flash_type,
|
||||
"flash_model": self.flash_model,
|
||||
"erase_before_program": self.erase_before_program,
|
||||
"firmware_bin_path": self._relative_path(self.firmware_bin_path),
|
||||
"reboot_timeout": self.reboot_timeout,
|
||||
"ping_timeout": self.ping_timeout,
|
||||
|
||||
+16
-5
@@ -279,7 +279,11 @@ def full_flash_program(
|
||||
bin_path: str | Path,
|
||||
callback: ProgressCallback = None,
|
||||
) -> list[FlashResult]:
|
||||
"""Execute full flash workflow: wipe → program → verify.
|
||||
"""Execute full flash workflow: (optional wipe) → program → verify.
|
||||
|
||||
If config.erase_before_program is True, erases flash sectors before
|
||||
programming. Otherwise, program_flash handles sector erase during
|
||||
programming (per-sector, not full chip).
|
||||
|
||||
Args:
|
||||
config: Application configuration.
|
||||
@@ -291,10 +295,17 @@ def full_flash_program(
|
||||
"""
|
||||
results: list[FlashResult] = []
|
||||
|
||||
# Step 1: Erase the entire flash
|
||||
results.append(wipe_flash(config, callback))
|
||||
if not results[-1].success:
|
||||
return results
|
||||
# Step 1: Erase (optional — controlled by config.erase_before_program)
|
||||
if getattr(config, 'erase_before_program', True):
|
||||
results.append(wipe_flash(config, callback))
|
||||
if not results[-1].success:
|
||||
return results
|
||||
else:
|
||||
results.append(FlashResult(
|
||||
step="wipe",
|
||||
success=True,
|
||||
message="Erase skipped (handled by program_flash)",
|
||||
))
|
||||
|
||||
# Step 2: Program and verify (program_flash -verify)
|
||||
results.append(program_flash_bin(config, bin_path, callback))
|
||||
|
||||
+41
-4
@@ -80,6 +80,7 @@ class MainWindow(ctk.CTk):
|
||||
self._zynq_jtag_present: bool = False
|
||||
self._zynq_jtag_info = None
|
||||
self._uart_monitor: UartMonitor | None = None
|
||||
self._flash_phase: str = ""
|
||||
|
||||
# StringVar references for config sync
|
||||
self._ip_string_var: ctk.StringVar | None = None
|
||||
@@ -186,6 +187,7 @@ class MainWindow(ctk.CTk):
|
||||
self._config.bootloader_elf_path = files["bootloader_elf_path"]
|
||||
self._config.bootloader_bin_path = files["bootloader_bin_path"]
|
||||
self._config.fsbl_elf_path = files["fsbl_elf_path"]
|
||||
self._config.erase_before_program = self._erase_cb_var.get()
|
||||
self._config.firmware_bin_path = files["firmware_bin_path"]
|
||||
|
||||
def _save_config(self) -> None:
|
||||
@@ -398,7 +400,7 @@ class MainWindow(ctk.CTk):
|
||||
row=0, column=0, sticky="nsew",
|
||||
padx=PADDING, pady=PADDING,
|
||||
)
|
||||
panel.grid_rowconfigure(6, weight=1)
|
||||
panel.grid_rowconfigure(7, weight=1)
|
||||
|
||||
title = ctk.CTkLabel(
|
||||
panel,
|
||||
@@ -461,6 +463,27 @@ class MainWindow(ctk.CTk):
|
||||
self._fsbl_selector.set_path(str(resolved))
|
||||
self._fsbl_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||
|
||||
# Flash options
|
||||
flash_frame = ctk.CTkFrame(panel)
|
||||
flash_frame.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||
flash_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self._erase_cb_var = ctk.BooleanVar(value=self._config.erase_before_program)
|
||||
self._erase_cb = ctk.CTkCheckBox(
|
||||
flash_frame,
|
||||
text="Erase flash before programming",
|
||||
variable=self._erase_cb_var,
|
||||
font=FONT_SMALL,
|
||||
)
|
||||
self._erase_cb.grid(row=0, column=0, sticky="w", padx=PADDING_SMALL)
|
||||
|
||||
flash_model_label = ctk.CTkLabel(
|
||||
flash_frame,
|
||||
text=f"Flash: {self._config.flash_model} (32 MiB)",
|
||||
font=FONT_SMALL,
|
||||
)
|
||||
flash_model_label.grid(row=1, column=0, sticky="w", padx=PADDING_SMALL, pady=(2, 0))
|
||||
|
||||
# Firmware BIN path (TFTP upload)
|
||||
self._firmware_bin_selector = FileSelector(
|
||||
panel,
|
||||
@@ -470,7 +493,7 @@ class MainWindow(ctk.CTk):
|
||||
if self._config.firmware_bin_path:
|
||||
resolved = self._config.resolve_path(self._config.firmware_bin_path)
|
||||
self._firmware_bin_selector.set_path(str(resolved))
|
||||
self._firmware_bin_selector.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||
self._firmware_bin_selector.grid(row=7, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||
|
||||
# Save button
|
||||
save_btn = ctk.CTkButton(
|
||||
@@ -479,7 +502,7 @@ class MainWindow(ctk.CTk):
|
||||
font=FONT_SMALL,
|
||||
command=self._save_config,
|
||||
)
|
||||
save_btn.grid(row=7, column=0, padx=PADDING, pady=PADDING)
|
||||
save_btn.grid(row=8, column=0, padx=PADDING, pady=PADDING)
|
||||
|
||||
def _build_serial_panel(self, parent: ctk.CTkFrame) -> None:
|
||||
"""Build the serial monitor panel."""
|
||||
@@ -725,7 +748,21 @@ class MainWindow(ctk.CTk):
|
||||
return False
|
||||
|
||||
def _flash_callback(self, status: str, message: str) -> None:
|
||||
"""Callback for flash programming progress."""
|
||||
"""Callback for flash programming progress — tracks phases."""
|
||||
if status == "phase":
|
||||
# Track current flash phase for UI
|
||||
self._flash_phase = message # 'erase', 'program', 'verify', 'done'
|
||||
phase_names = {"erase": "Erasing", "program": "Programming", "verify": "Verifying", "done": "Done"}
|
||||
label = phase_names.get(message, message)
|
||||
self._log_message(f" ▸ {label}")
|
||||
return
|
||||
if status == "progress":
|
||||
pct = message.rstrip('%')
|
||||
# Show progress within current phase
|
||||
phase_label = {"erase": "Erase", "program": "Program", "verify": "Verify"}.get(
|
||||
self._flash_phase, "Flash")
|
||||
self._log_message(f" [{phase_label}] {pct}%")
|
||||
return
|
||||
self._log_message(f" [{status}] {message}")
|
||||
|
||||
def _jtag_callback(self, status: str, message: str) -> None:
|
||||
|
||||
+27
-2
@@ -1,7 +1,8 @@
|
||||
"""Non-blocking subprocess execution with real-time output parsing.
|
||||
|
||||
Provides run_streaming() which streams stdout/stderr to callbacks
|
||||
in real time, parsing for warnings, errors, and progress indicators.
|
||||
in real time, parsing for warnings, errors, progress indicators,
|
||||
and flash operation phase transitions (erase/program/verify).
|
||||
Used by flash_programmer and bitstream_programmer.
|
||||
"""
|
||||
|
||||
@@ -15,7 +16,7 @@ from typing import Callable
|
||||
|
||||
StreamCallback = Callable[[str, str], None] | None
|
||||
"""Callback type: (category, message) where category is one of:
|
||||
'out', 'err', 'warn', 'error', 'progress', 'done'"""
|
||||
'out', 'err', 'warn', 'error', 'progress', 'phase', 'done'"""
|
||||
|
||||
# Regex patterns for parsing tool output
|
||||
_WARNING_RE = re.compile(r"WARNING\s*[:\[].*", re.IGNORECASE)
|
||||
@@ -24,6 +25,12 @@ _PROGRESS_RE = re.compile(r"(\d{1,3})\s*%")
|
||||
_INFO_KEYWORDS = ["connected", "downloading", "running", "initialization",
|
||||
"verif", "write", "erase", "flash operation"]
|
||||
|
||||
# Phase transition patterns for program_flash output
|
||||
_PHASE_ERASE_RE = re.compile(r"Performing Erase Operation", re.IGNORECASE)
|
||||
_PHASE_PROGRAM_RE = re.compile(r"Performing Program Operation", re.IGNORECASE)
|
||||
_PHASE_VERIFY_RE = re.compile(r"Performing Verify Operation", re.IGNORECASE)
|
||||
_PHASE_DONE_RE = re.compile(r"(Erase|Program|Verify) Operation successful", re.IGNORECASE)
|
||||
|
||||
|
||||
def run_streaming(
|
||||
cmd: list[str],
|
||||
@@ -133,6 +140,24 @@ def _parse_and_callback(
|
||||
callback("warn", line)
|
||||
return
|
||||
|
||||
# Check for phase transitions (program_flash output)
|
||||
if _PHASE_ERASE_RE.search(line):
|
||||
callback("phase", "erase")
|
||||
callback("out", line)
|
||||
return
|
||||
if _PHASE_PROGRAM_RE.search(line):
|
||||
callback("phase", "program")
|
||||
callback("out", line)
|
||||
return
|
||||
if _PHASE_VERIFY_RE.search(line):
|
||||
callback("phase", "verify")
|
||||
callback("out", line)
|
||||
return
|
||||
if _PHASE_DONE_RE.search(line):
|
||||
callback("phase", "done")
|
||||
callback("out", line)
|
||||
return
|
||||
|
||||
# Check for progress percentage
|
||||
m = _PROGRESS_RE.search(line)
|
||||
if m:
|
||||
|
||||
Reference in New Issue
Block a user