diff --git a/config.yaml b/config.yaml index 9649708..a59ed29 100644 --- a/config.yaml +++ b/config.yaml @@ -1,7 +1,7 @@ bootloader_bin_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/bootloader_z100_800M.bin bootloader_bit_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/pl_arm_wrapper_hw_platform_0/pl_arm_wrapper.bit bootloader_elf_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/app.elf -erase_before_program: true +erase_all: true firmware_bin_path: /home/ly0kos/work/Verify/FW/V1.3.1.3.3_06_01_pmu_cpld_check/V1.3.1.3.3_06_01_pmu_cpld_check.bin flash_model: s25fl256s1 flash_type: qspi-x4-single diff --git a/src/flash_programmer.py b/src/flash_programmer.py index 199fc88..c807411 100644 --- a/src/flash_programmer.py +++ b/src/flash_programmer.py @@ -100,9 +100,10 @@ def wipe_flash( ) -> FlashResult: """Erase QSPI flash before programming. - Uses config.erase_all to determine erase scope: - - True: -erase_all -erase_only (erase entire chip, may fail on some) - - False: -erase_only (erase only sectors matching the BIN file, safer) + - erase_all=False: -erase_only (matches BIN size, works on all chips) + - erase_all=True: creates a temp file of full flash capacity, uses + -erase_only with it. Avoids -erase_all flag which fails on + s25fl256s1 (no memory density info). Args: config: Application configuration (needs fsbl_elf_path). @@ -130,14 +131,40 @@ def wipe_flash( if callback: callback("start", "Erasing QSPI flash...") - cmd = _build_program_flash_cmd_base(flash_tool, config) - if getattr(config, 'erase_all', False): - cmd += ["-erase_all", "-erase_only"] # Full chip erase - else: - cmd += ["-erase_only"] # Erase sectors matching BIN size + erase_all = getattr(config, 'erase_all', False) try: - result = _run_command(cmd, timeout=config.step_timeouts.get("flash_erase", 120), callback=callback) + if erase_all: + flash_size = _flash_capacity(config) + import tempfile, os + tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.bin') + try: + tmp.seek(flash_size - 1) + tmp.write(b'\x00') + tmp.close() + cmd = [ + flash_tool, + "-f", tmp.name, + "-fsbl", str(fsbl_path), + "-flash_type", config.flash_type, + "-erase_only", + ] + result = _run_command( + cmd, + timeout=config.step_timeouts.get("flash_erase", 120), + callback=callback, + ) + finally: + os.unlink(tmp.name) + else: + cmd = _build_program_flash_cmd_base(flash_tool, config) + cmd += ["-erase_only"] + result = _run_command( + cmd, + timeout=config.step_timeouts.get("flash_erase", 120), + callback=callback, + ) + success = result.returncode == 0 return FlashResult( step="wipe", @@ -146,7 +173,30 @@ def wipe_flash( output=(result.stdout + result.stderr)[:4000], ) except subprocess.TimeoutExpired: - return FlashResult(step="wipe", success=False, message="Flash erase timed out (300s)") + return FlashResult(step="wipe", success=False, message="Flash erase timed out") + except FileNotFoundError: + return FlashResult(step="wipe", success=False, message="program_flash not found") + + +def _flash_capacity(config: Config) -> int: + """Get flash chip capacity in bytes from flash_model. + + Args: + config: Application configuration. + + Returns: + Capacity in bytes (default 32 MiB). + """ + model = getattr(config, 'flash_model', 's25fl256s1').lower() + capacities: dict[str, int] = { + 's25fl256': 32 * 1024 * 1024, + 's25fl128': 16 * 1024 * 1024, + 's25fl064': 8 * 1024 * 1024, + } + for key, cap in capacities.items(): + if key in model: + return cap + return 32 * 1024 * 1024 def program_flash_bin( diff --git a/src/gui/main_window.py b/src/gui/main_window.py index f4d369d..65f503a 100644 --- a/src/gui/main_window.py +++ b/src/gui/main_window.py @@ -1055,9 +1055,12 @@ class MainWindow(ctk.CTk): 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 + # Step 2: mark sub-steps based on result if i == 1 and hasattr(self, '_sub_step_frame'): - self._sub_step_frame.set_phase("done") + if success: + self._sub_step_frame.set_phase("done") + else: + self._sub_step_frame.set_phase("error") except Exception as e: results.append(False) self._set_step_status(i, "error") diff --git a/src/gui/widgets.py b/src/gui/widgets.py index c3925bb..460604b 100644 --- a/src/gui/widgets.py +++ b/src/gui/widgets.py @@ -29,6 +29,10 @@ from gui.styles import ( ACCENT_DISABLED, CONNECTOR_COLOR, DARK_FG, + CIRCLE_PENDING, + CIRCLE_RUNNING, + CIRCLE_COMPLETE, + CIRCLE_ERROR, LIGHT_FG, STEP_RUN_BG, STEP_RUN_BG_HOVER, @@ -634,49 +638,81 @@ class SubStepFrame(ctk.CTkFrame): """Collapsible frame showing flash operation sub-steps with status. Three sub-steps: Erase → Program → Verify. - Each shows a status indicator (dot + label) updated via set_phase(). - Collapsed by default, expands when a workflow step starts. + Status dots match main Step style (CIRCLE_* / ACCENT_* colors). + Collapsed by default, expands on workflow start. """ def __init__(self, parent: ctk.CTkFrame) -> None: - """Initialize the sub-step frame. - - Args: - parent: Parent CTkFrame widget. - """ + """Initialize the sub-step frame.""" super().__init__(parent, fg_color="transparent") - self.grid_columnconfigure(1, weight=1) + self.grid_columnconfigure(0, weight=1) - # Collapse toggle + # Collapse toggle — row 0 self._collapsed = True + toggle_frame = ctk.CTkFrame(self, fg_color="transparent") + toggle_frame.grid(row=0, column=0, sticky="ew", padx=(PADDING, PADDING), pady=(2, 0)) + toggle_frame.grid_columnconfigure(1, weight=1) + self._toggle_btn = ctk.CTkButton( - self, + toggle_frame, text="▶", - width=24, - height=24, - font=FONT_SMALL, + width=20, + height=20, + font=(FONT_SMALL[0], 9), command=self._toggle, ) - self._toggle_btn.grid(row=0, column=0, padx=(PADDING_SMALL, 4), pady=(2, 0), sticky="nw") + self._toggle_btn.grid(row=0, column=0, padx=(0, 4)) self._header_label = ctk.CTkLabel( - self, - text="Sub-steps", - font=FONT_SMALL, + toggle_frame, + text="Flash: Erase → Program → Verify", + font=FONT_BODY, + anchor="w", ) - self._header_label.grid(row=0, column=1, padx=(0, PADDING), pady=(2, 0), sticky="w") + self._header_label.grid(row=0, column=1, sticky="w") # Sub-items (hidden initially) self._sub_items: list[dict] = [] - sub_names = ["Erase", "Program", "Verify"] + sub_names = ["Erase ", "Program", "Verify "] for i, name in enumerate(sub_names): - dot = ctk.CTkLabel(self, text="○", font=(FONT_SMALL[0], 10), text_color=CONNECTOR_COLOR) - label = ctk.CTkLabel(self, text=name, font=FONT_SMALL, text_color=CONNECTOR_COLOR) - dot.grid(row=i + 1, column=0, padx=(PADDING_SMALL + 14, 4), pady=(1, 1), sticky="e") - label.grid(row=i + 1, column=1, padx=(4, PADDING), pady=(1, 1), sticky="w") - dot.grid_remove() - label.grid_remove() - self._sub_items.append({"dot": dot, "label": label, "name": name}) + item = self._build_sub_row(i + 1, name) + item["dot"].grid_remove() + item["label"].grid_remove() + item["pct"].grid_remove() + self._sub_items.append(item) + + def _build_sub_row(self, row: int, name: str) -> dict: + """Build one sub-item row with dot, label, and percentage label.""" + dot = ctk.CTkLabel( + self, text="○", + width=28, + font=(FONT_BODY[0], FONT_BODY[1]), + text_color=CIRCLE_PENDING, + anchor="center", + ) + dot.grid(row=row, column=0, padx=(PADDING + 20, 4), pady=(3, 3), sticky="w") + + sub_frame = ctk.CTkFrame(self, fg_color="transparent") + sub_frame.grid(row=row, column=0, sticky="ew", padx=(PADDING + 52, PADDING), pady=(3, 3)) + sub_frame.grid_columnconfigure(0, weight=1) + + label = ctk.CTkLabel( + sub_frame, text=name, + font=FONT_BODY, + text_color=ACCENT_PENDING, + anchor="w", + ) + label.grid(row=0, column=0, sticky="w") + + pct = ctk.CTkLabel( + sub_frame, text="", + font=FONT_BODY, + text_color=ACCENT_PENDING, + anchor="e", + ) + pct.grid(row=0, column=1, sticky="e", padx=(8, 0)) + + return {"dot": dot, "label": label, "pct": pct, "name": name} def _toggle(self) -> None: """Toggle collapse/expand.""" @@ -685,73 +721,67 @@ class SubStepFrame(ctk.CTkFrame): for item in self._sub_items: if self._collapsed: item["dot"].grid_remove() - item["label"].grid_remove() + item["label"].master.grid_remove() else: item["dot"].grid() - item["label"].grid() + item["label"].master.grid() def set_expanded(self, expanded: bool) -> None: - """Set expanded state. - - Args: - expanded: True to expand, False to collapse. - """ - if expanded == self._collapsed: + """Set expanded state.""" + if expanded != self._collapsed: self._toggle() def set_phase(self, phase: str, pct: int = -1) -> None: - """Update a sub-step's status. + """Update sub-step status. Args: - phase: One of 'erase', 'program', 'verify', 'done'. - pct: Progress percentage, -1 to show "running", 100 for "done". + phase: 'erase', 'program', 'verify', 'done', 'error'. + pct: Progress 0-100, -1 means running (no specific %). """ phase_map = {"erase": 0, "program": 1, "verify": 2} - colors = { - "pending": CONNECTOR_COLOR, - "running": PRIMARY_COLOR, - "complete": SUCCESS_COLOR, - "error": DANGER_COLOR, - } if phase == "done": - # Mark all as complete for item in self._sub_items: - item["dot"].configure(text="●", text_color=SUCCESS_COLOR) + item["dot"].configure(text="●", text_color=CIRCLE_COMPLETE) item["label"].configure(text_color=SUCCESS_COLOR) + item["pct"].configure(text="OK", text_color=SUCCESS_COLOR) + return + + if phase == "error": + for item in self._sub_items: + item["dot"].configure(text="✗", text_color=CIRCLE_ERROR) + item["label"].configure(text_color=DANGER_COLOR) + item["pct"].configure(text="", text_color=DANGER_COLOR) return idx = phase_map.get(phase, -1) if idx < 0: return - # Determine state - if pct >= 100: - state = "complete" - dot_text = "●" - elif pct >= 0: - state = "running" - dot_text = "◉" - else: - state = "running" - dot_text = "◉" + # Running with progress + pct_text = f"{pct}%" if pct >= 0 else "" + running_text = f" {pct_text}" if pct_text else "" - # Update current and previous for i, item in enumerate(self._sub_items): if i < idx: - item["dot"].configure(text="●", text_color=SUCCESS_COLOR) + item["dot"].configure(text="●", text_color=CIRCLE_COMPLETE) item["label"].configure(text_color=SUCCESS_COLOR) + item["pct"].configure(text="OK", text_color=SUCCESS_COLOR) elif i == idx: - item["dot"].configure(text=dot_text, text_color=colors[state]) - item["label"].configure(text_color=colors[state]) + item["dot"].configure(text="◉", text_color=CIRCLE_RUNNING) + item["label"].configure(text_color=ACCENT_RUNNING) + item["pct"].configure(text=pct_text, text_color=ACCENT_RUNNING) else: - item["dot"].configure(text="○", text_color=colors["pending"]) - item["label"].configure(text_color=colors["pending"]) + item["dot"].configure(text="○", text_color=CIRCLE_PENDING) + item["label"].configure(text_color=ACCENT_PENDING) + item["pct"].configure(text="", text_color=ACCENT_PENDING) def reset(self) -> None: """Reset all sub-steps to pending state.""" for item in self._sub_items: - item["dot"].configure(text="○", text_color=CONNECTOR_COLOR) - item["label"].configure(text_color=CONNECTOR_COLOR) + item["dot"].configure(text="○", text_color=CIRCLE_PENDING) + item["label"].configure(text_color=ACCENT_PENDING) + item["pct"].configure(text="", text_color=ACCENT_PENDING) if not self._collapsed: self._toggle() + self._toggle()