feat: collapsible flash sub-steps UI + erase_all checkbox

Rename erase_before_program → erase_all (semantics fix):
- erase_all: true  → -erase_all -erase_only (entire flash chip)
- erase_all: false → -erase_only (sectors matching BIN size)
Both options always erase; the checkbox controls scope, not yes/no.

UI additions:
- SubStepFrame widget: collapsible Erase/Program/Verify sub-steps
  with status dots (○/◉/●) and colored labels
- Automatically expands on Step 2 start, collapses on completion
- Phase and progress driven by real-time program_flash output parsing
- Checkbox label: '整片Flash擦除 (Erase entire flash)'

subprocess_utils.py phase detection:
- Parses 'Performing Erase/Program/Verify Operation' for phase transitions
- Emits 'phase' callbacks (erase/program/verify/done)
- Progress % tracked within current phase
This commit is contained in:
Jeremy Shen
2026-06-10 13:45:57 +08:00
parent ea3e3b8ac7
commit 95c04d94ae
6 changed files with 190 additions and 37 deletions
+34 -8
View File
@@ -52,6 +52,7 @@ from gui.widgets import (
StatusDisplay,
FileSelector,
LogDisplay,
SubStepFrame,
)
@@ -187,7 +188,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.erase_all = self._erase_cb_var.get()
self._config.firmware_bin_path = files["firmware_bin_path"]
def _save_config(self) -> None:
@@ -338,6 +339,16 @@ class MainWindow(ctk.CTk):
pady=(0, 2),
)
# Step 2: add collapsible sub-step frame
if i == 1:
self._sub_step_frame = SubStepFrame(panel)
self._sub_step_frame.grid(
row=i * 2 + 2, column=0,
sticky="nsew",
padx=PADDING_SMALL + 16,
pady=(0, 2),
)
# ── Progress indicator ──
self._progress = ProgressIndicator(panel)
self._progress.grid(
@@ -468,10 +479,10 @@ class MainWindow(ctk.CTk):
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_var = ctk.BooleanVar(value=self._config.erase_all)
self._erase_cb = ctk.CTkCheckBox(
flash_frame,
text="Erase flash before programming",
text="整片Flash擦除 (Erase entire flash)",
variable=self._erase_cb_var,
font=FONT_SMALL,
)
@@ -748,20 +759,25 @@ class MainWindow(ctk.CTk):
return False
def _flash_callback(self, status: str, message: str) -> None:
"""Callback for flash programming progress — tracks phases."""
"""Callback for flash programming — drives sub-step UI."""
if status == "phase":
# Track current flash phase for UI
self._flash_phase = message # 'erase', 'program', 'verify', 'done'
self._flash_phase = message
phase_names = {"erase": "Erasing", "program": "Programming", "verify": "Verifying", "done": "Done"}
label = phase_names.get(message, message)
self._log_message(f"{label}")
if hasattr(self, '_sub_step_frame'):
self._sub_step_frame.set_phase(message)
return
if status == "progress":
pct = message.rstrip('%')
# Show progress within current phase
try:
pct = int(message.rstrip('%'))
except ValueError:
pct = -1
phase_label = {"erase": "Erase", "program": "Program", "verify": "Verify"}.get(
self._flash_phase, "Flash")
self._log_message(f" [{phase_label}] {pct}%")
if hasattr(self, '_sub_step_frame'):
self._sub_step_frame.set_phase(self._flash_phase, pct)
return
self._log_message(f" [{status}] {message}")
@@ -1018,6 +1034,12 @@ class MainWindow(ctk.CTk):
step_idx = i - start_index
self._set_step_status(i, "running")
# Step 2: expand sub-step frame and reset
if i == 1 and hasattr(self, '_sub_step_frame'):
self._sub_step_frame.reset()
self._sub_step_frame.set_expanded(True)
self._flash_phase = ""
# Log step timeout estimate
timeout_key, default_timeout = step_timeout_keys[i] if i < len(step_timeout_keys) else ("", 30)
step_timeout = self._config.step_timeouts.get(timeout_key, default_timeout) if self._config else default_timeout
@@ -1032,6 +1054,10 @@ class MainWindow(ctk.CTk):
self._set_step_status(i, "complete" if success else "error")
elapsed = time.time() - t_step_start
self._log_message(f" ⏱ Step {i+1} completed in {elapsed:.0f}s")
# Step 2: mark all sub-steps done
if i == 1 and hasattr(self, '_sub_step_frame'):
self._sub_step_frame.set_phase("done")
except Exception as e:
results.append(False)
self._set_step_status(i, "error")