fix: skip marking, fixed panel ratio, dialog hints, 30s boot, ping+UDP fallback
- TftpSubStepFrame.set_step_skipped(): ◌ Skip in WARNING color - _reload_config(): suppress callbacks to avoid duplicate log/status wipe - _log_all_file_info(): removed clear_info(), updates in-place per-key - Removed duplicate _log_file_info method - main_frame grid: uniform col group for strict 7:3 ratio - FileSelector browse title shows expected file type hint - boot_wait_delay: default 10→30s - Step 4 IP fallback: ping first, then UDP scan, extra boot_delay retry
This commit is contained in:
@@ -75,7 +75,7 @@ class Config:
|
||||
ping_count: int = 3
|
||||
uart_delay: int = 3
|
||||
inter_step_delay: int = 2
|
||||
boot_wait_delay: int = 10
|
||||
boot_wait_delay: int = 30
|
||||
step_timeouts: dict[str, int] = field(default_factory=lambda: {})
|
||||
|
||||
# Internal: path to the config file on disk
|
||||
|
||||
+84
-57
@@ -222,8 +222,15 @@ class MainWindow(ctk.CTk):
|
||||
try:
|
||||
fresh = Config.from_file(user_path)
|
||||
self._config = fresh
|
||||
# Update UI selectors with resolved (absolute) paths — selectors
|
||||
# display only the filename internally.
|
||||
# Update UI selectors — suppress callbacks to avoid
|
||||
# redundant file-info logging and status-panel rewrites.
|
||||
_sels = [
|
||||
self._bit_selector, self._elf_selector,
|
||||
self._bootloader_bin_selector, self._fsbl_selector,
|
||||
self._firmware_bin_selector,
|
||||
]
|
||||
for s in _sels:
|
||||
s._suppress_callback = True
|
||||
for attr, selector in [
|
||||
("bootloader_bit_path", self._bit_selector),
|
||||
("bootloader_elf_path", self._elf_selector),
|
||||
@@ -236,6 +243,8 @@ class MainWindow(ctk.CTk):
|
||||
resolved = fresh.resolve_path(val)
|
||||
selector.set_relative_path(val)
|
||||
selector.set_path(str(resolved))
|
||||
for s in _sels:
|
||||
s._suppress_callback = False
|
||||
# Update IP
|
||||
if self._ip_string_var:
|
||||
self._ip_string_var.set(fresh.zynq_ip)
|
||||
@@ -245,8 +254,6 @@ class MainWindow(ctk.CTk):
|
||||
except Exception:
|
||||
pass # Keep existing config on error
|
||||
|
||||
self._log_all_file_info()
|
||||
|
||||
def _sync_config_from_ui(self) -> None:
|
||||
"""Read current UI values and update the Config object.
|
||||
|
||||
@@ -387,26 +394,14 @@ class MainWindow(ctk.CTk):
|
||||
f"{info.size_human} | CRC {info.crc32_hex} | {info.mtime_iso}"
|
||||
)
|
||||
|
||||
def _log_file_info(self, info: FileInfo) -> None:
|
||||
"""Log file metadata in a human-readable format.
|
||||
|
||||
Args:
|
||||
info: FileInfo dataclass with computed metadata.
|
||||
"""
|
||||
filename = os.path.basename(info.path) if info.path else "(unknown)"
|
||||
if not info.exists:
|
||||
self._log_message(f" ⚠ {filename}: file not found")
|
||||
return
|
||||
self._log_message(
|
||||
f" ✓ {filename}: "
|
||||
f"{info.size_human} | CRC {info.crc32_hex} | {info.mtime_iso}"
|
||||
)
|
||||
|
||||
def _log_all_file_info(self) -> None:
|
||||
"""Display file metadata in the status info panel."""
|
||||
"""Update file metadata in the status info panel (preserves other info).
|
||||
|
||||
Each file line is updated in-place via ``set_info()`` — existing
|
||||
Zynq info, tool versions, etc. are left untouched.
|
||||
"""
|
||||
if not self._config:
|
||||
return
|
||||
self._status.clear_info()
|
||||
paths = [
|
||||
("FSBL ELF", self._config.fsbl_elf_path),
|
||||
("Bitstream", self._config.bootloader_bit_path),
|
||||
@@ -416,6 +411,7 @@ class MainWindow(ctk.CTk):
|
||||
]
|
||||
for label, path in paths:
|
||||
if not path:
|
||||
self._status.set_info(label, "(not set)")
|
||||
continue
|
||||
resolved = self._config.resolve_path(path)
|
||||
info = compute_file_info(resolved)
|
||||
@@ -435,8 +431,8 @@ class MainWindow(ctk.CTk):
|
||||
self.main_frame = ctk.CTkScrollableFrame(self)
|
||||
self.main_frame.pack(fill="both", expand=True, padx=PADDING_LARGE, pady=PADDING_LARGE)
|
||||
self.main_frame.grid_rowconfigure(1, weight=1)
|
||||
self.main_frame.grid_columnconfigure(0, weight=7)
|
||||
self.main_frame.grid_columnconfigure(1, weight=3)
|
||||
self.main_frame.grid_columnconfigure(0, weight=7, uniform="main_col")
|
||||
self.main_frame.grid_columnconfigure(1, weight=3, uniform="main_col")
|
||||
|
||||
# ── Header ──
|
||||
self._build_header(self.main_frame)
|
||||
@@ -771,7 +767,7 @@ class MainWindow(ctk.CTk):
|
||||
self._download_verify_var = ctk.BooleanVar(value=self._config.download_verify)
|
||||
self._download_verify_cb = ctk.CTkCheckBox(
|
||||
flash_frame,
|
||||
text="下载校验 (Step 4.2 Download Verify)",
|
||||
text="下载校验 + CRC (Step 4.2)",
|
||||
variable=self._download_verify_var,
|
||||
font=FONT_SMALL,
|
||||
command=self._on_download_verify_toggle,
|
||||
@@ -1417,11 +1413,16 @@ class MainWindow(ctk.CTk):
|
||||
self._log_message(" ⏳ Waiting 2s before download...")
|
||||
time.sleep(2)
|
||||
|
||||
# 4b: TFTP Download Verify (skipped if disabled in config)
|
||||
# 4b+4c: TFTP Download Verify + CRC Check
|
||||
if not self._config.download_verify:
|
||||
self._log_message("Step 4b: Download verify — skipped (disabled in config)")
|
||||
all_results.append(True)
|
||||
self._log_message("Step 4b+4c: Download verify & CRC — skipped (disabled in config)")
|
||||
all_results.append(True) # 4b
|
||||
all_results.append(True) # 4c
|
||||
if hasattr(self, '_tftp_sub_step_frame'):
|
||||
self._tftp_sub_step_frame.set_step_skipped("Download Verify")
|
||||
self._tftp_sub_step_frame.set_step_skipped("CRC Check")
|
||||
else:
|
||||
# 4b: Download Verify
|
||||
self._log_message("Step 4b: TFTP download verify...")
|
||||
self._tftp_phase = "Download Verify"
|
||||
if hasattr(self, '_tftp_sub_step_frame'):
|
||||
@@ -1443,32 +1444,31 @@ class MainWindow(ctk.CTk):
|
||||
self._tftp_sub_step_frame.set_step_error("Download Verify")
|
||||
all_results.append(False)
|
||||
|
||||
if not all_results[-1]:
|
||||
self._log_message(" Skipping CRC check (download failed)")
|
||||
return False
|
||||
|
||||
# 4c: CRC Check
|
||||
self._log_message("Step 4c: CRC check...")
|
||||
if hasattr(self, '_tftp_sub_step_frame'):
|
||||
self._tftp_sub_step_frame.set_phase("CRC Check")
|
||||
original_crc = _compute_crc32(bin_path)
|
||||
downloaded_crc = download_result.crc_local
|
||||
crc_match = original_crc == downloaded_crc
|
||||
self._log_message(f" Original CRC: {original_crc:#010x}")
|
||||
self._log_message(f" Downloaded CRC: {downloaded_crc:#010x}")
|
||||
if crc_match:
|
||||
self._log_message(" ✓ CRC match")
|
||||
if hasattr(self, '_tftp_sub_step_frame'):
|
||||
self._tftp_sub_step_frame.set_step_complete("CRC Check")
|
||||
all_results.append(True)
|
||||
else:
|
||||
self._log_message(" ✗ CRC mismatch")
|
||||
if hasattr(self, '_tftp_sub_step_frame'):
|
||||
self._tftp_sub_step_frame.set_step_error("CRC Check")
|
||||
all_results.append(False)
|
||||
if not all_results[-1]:
|
||||
self._log_message(" Skipping CRC check (download failed)")
|
||||
else:
|
||||
# 4c: CRC Check
|
||||
self._log_message("Step 4c: CRC check...")
|
||||
if hasattr(self, '_tftp_sub_step_frame'):
|
||||
self._tftp_sub_step_frame.set_phase("CRC Check")
|
||||
original_crc = _compute_crc32(bin_path)
|
||||
downloaded_crc = download_result.crc_local
|
||||
crc_match = original_crc == downloaded_crc
|
||||
self._log_message(f" Original CRC: {original_crc:#010x}")
|
||||
self._log_message(f" Downloaded CRC: {downloaded_crc:#010x}")
|
||||
if crc_match:
|
||||
self._log_message(" ✓ CRC match")
|
||||
if hasattr(self, '_tftp_sub_step_frame'):
|
||||
self._tftp_sub_step_frame.set_step_complete("CRC Check")
|
||||
all_results.append(True)
|
||||
else:
|
||||
self._log_message(" ✗ CRC mismatch")
|
||||
if hasattr(self, '_tftp_sub_step_frame'):
|
||||
self._tftp_sub_step_frame.set_step_error("CRC Check")
|
||||
all_results.append(False)
|
||||
|
||||
if not all_results[-1]:
|
||||
self._log_message(" Skipping reboot (CRC mismatch)")
|
||||
self._log_message(" Skipping reboot (download/CRC failed)")
|
||||
return False
|
||||
|
||||
# 4d: Reboot
|
||||
@@ -1565,16 +1565,38 @@ class MainWindow(ctk.CTk):
|
||||
discovered_ip = parsed.ip_address
|
||||
self._log_message(f" IP from UART buffer: {discovered_ip}")
|
||||
|
||||
# Discover actual IP after reboot (may have changed via DHCP)
|
||||
# Fallback: if no IP from UART, try ping → UDP scan, then extra delay
|
||||
if not discovered_ip:
|
||||
self._log_message(" ⏳ Network stack — waiting 3s...")
|
||||
time.sleep(3)
|
||||
self._log_message(" Scanning for Zynq IP (192.168.100.11–30)...")
|
||||
discovered_ip = self._discover_zynq_ip()
|
||||
if discovered_ip:
|
||||
self._log_message(f" ✓ Found Zynq at {discovered_ip}")
|
||||
|
||||
# Try ping configured IP first (fast, reliable)
|
||||
self._log_message(f" Pinging {self._config.zynq_ip}...")
|
||||
ok, _ = ping_ip(self._config.zynq_ip, timeout=2)
|
||||
if ok:
|
||||
discovered_ip = self._config.zynq_ip
|
||||
self._log_message(f" ✓ Zynq responds to ping at {discovered_ip}")
|
||||
else:
|
||||
self._log_message(" ⚠ Zynq not found on any scanned IP")
|
||||
# Fallback: UDP broadcast scan
|
||||
self._log_message(" Scanning for Zynq IP (192.168.100.11–30)...")
|
||||
discovered_ip = self._discover_zynq_ip()
|
||||
if discovered_ip:
|
||||
self._log_message(f" ✓ Found Zynq at {discovered_ip}")
|
||||
else:
|
||||
self._log_message(" ⚠ Zynq not found — extra boot delay...")
|
||||
self._log_message(f" ⏳ Waiting {boot_delay}s for late boot...")
|
||||
time.sleep(boot_delay)
|
||||
# Retry ping + scan after extra delay
|
||||
ok2, _ = ping_ip(self._config.zynq_ip, timeout=2)
|
||||
if ok2:
|
||||
discovered_ip = self._config.zynq_ip
|
||||
self._log_message(f" ✓ Zynq responds to ping at {discovered_ip}")
|
||||
else:
|
||||
discovered_ip = self._discover_zynq_ip()
|
||||
if discovered_ip:
|
||||
self._log_message(f" ✓ Found Zynq at {discovered_ip}")
|
||||
else:
|
||||
self._log_message(" ⚠ Zynq not found on any scanned IP")
|
||||
|
||||
if discovered_ip and discovered_ip != self._config.zynq_ip:
|
||||
self._log_message(
|
||||
@@ -1799,6 +1821,11 @@ class MainWindow(ctk.CTk):
|
||||
results.append(False)
|
||||
self._set_step_status(i, "error")
|
||||
self._log_message(f" Exception in step {i+1}: {e}")
|
||||
# Mark sub-step frames as error too
|
||||
if i == 1 and hasattr(self, '_sub_step_frame'):
|
||||
self._sub_step_frame.set_phase("error")
|
||||
if i == 3 and hasattr(self, '_tftp_sub_step_frame'):
|
||||
self._tftp_sub_step_frame.set_step_error("Boot Verify")
|
||||
|
||||
self._progress.set_value((step_idx + 1) / total)
|
||||
|
||||
|
||||
+13
-1
@@ -544,6 +544,7 @@ class FileSelector(ctk.CTkFrame):
|
||||
|
||||
self._callback = callback
|
||||
self._file_types = file_types or [("All files", "*")]
|
||||
self._hint_text = label_text.rstrip(":") # e.g. "FSBL ELF (.elf)"
|
||||
self._full_path: str = "" # absolute path (internal)
|
||||
self._relative_path: str = "" # relative path (synced to Config)
|
||||
self._display_text = ctk.StringVar(value="")
|
||||
@@ -593,7 +594,7 @@ class FileSelector(ctk.CTkFrame):
|
||||
from tkinter import filedialog
|
||||
|
||||
file_path = filedialog.askopenfilename(
|
||||
title="Select file",
|
||||
title=f"Select {self._hint_text}",
|
||||
filetypes=self._file_types,
|
||||
)
|
||||
if file_path:
|
||||
@@ -974,6 +975,17 @@ class TftpSubStepFrame(ctk.CTkFrame):
|
||||
item["label"].configure(text_color=_C_ERROR)
|
||||
self._stop_pulse()
|
||||
|
||||
def set_step_skipped(self, phase: str) -> None:
|
||||
"""Mark a specific step as skipped (warning color, Skip text)."""
|
||||
idx = self._substep_names.index(phase) if phase in self._substep_names else -1
|
||||
if idx < 0:
|
||||
return
|
||||
item = self._sub_items[idx]
|
||||
item["dot"].configure(text="◌", text_color=WARNING_COLOR)
|
||||
item["label"].configure(text_color=WARNING_COLOR)
|
||||
item["pct"].configure(text="Skip", text_color=WARNING_COLOR)
|
||||
self._stop_pulse()
|
||||
|
||||
def _start_pulse(self, idx: int) -> None:
|
||||
"""Start pulsing the running sub-step."""
|
||||
_LIGHT = RUNNING_FADE_LIGHT
|
||||
|
||||
Reference in New Issue
Block a user