feat: add TFTP download verify, Step 4 sub-steps, and fix UI layout

- Implement _tftp_get() for downloading files from Zynq via TFTP
- Add TftpSubStepFrame widget for Step 4 progress display
- Refactor Step 4 into 5 sub-steps: Upload, Download Verify, CRC Check, Reboot, Boot Verify
- Fix SubStepFrame.reset() bug that caused collapse after execution
- Fix layout overlap: Step 4 sub-step and progress bar now use separate rows
- Improve ProgressIndicator appearance with better styling and spacing
- Fix UART monitor to start regardless of _uart_available flag
- Add CRC32 verification between uploaded and downloaded files
This commit is contained in:
2026-06-11 11:47:35 +08:00
parent 5ce544cd3c
commit f04fbe2130
3 changed files with 417 additions and 64 deletions
+124 -35
View File
@@ -21,7 +21,7 @@ from zynq_checker import check_zynq_jtag, get_zynq_status_dict
from flash_programmer import full_flash_program, FlashResult
from bitstream_programmer import full_bitstream_program, BitstreamResult
from ip_verifier import ping_ip
from tftp_manager import tftp_upload_verify, TftpResult
from tftp_manager import tftp_upload, tftp_download, tftp_upload_verify, TftpResult, _compute_crc32
from reboot_manager import reboot_zynq, RebootResult
from boot_verifier import verify_boot, verify_zynq_status, BootVerificationResult
from serial_monitor import detect_serial_ports, parse_boot_output, check_uart_available, UartMonitor
@@ -53,6 +53,7 @@ from gui.widgets import (
FileSelector,
LogDisplay,
SubStepFrame,
TftpSubStepFrame,
UartMonitorWindow,
)
@@ -331,7 +332,6 @@ class MainWindow(ctk.CTk):
row=0, column=0, sticky="nsew",
padx=PADDING, pady=(PADDING, 0),
)
panel.grid_rowconfigure(10, weight=1)
panel.grid_columnconfigure(0, weight=1)
# ── Title ──
@@ -364,23 +364,29 @@ class MainWindow(ctk.CTk):
self._steps.append(step)
self._step_statuses[i] = "pending"
# Divider line between steps
# Divider line between steps (skip rows occupied by sub-steps)
if i > 0:
# Dividers go between main steps only, not between step and sub-step
# Row calculation: account for sub-steps from previous steps
prev_steps_with_substeps = sum(1 for j in range(i) if j in (1, 3))
divider_row = i * 2 + 1 + prev_steps_with_substeps
divider = ctk.CTkFrame(
panel,
height=1,
fg_color=CONNECTOR_COLOR,
)
divider.grid(
row=i * 2, column=0,
row=divider_row, column=0,
sticky="ew",
padx=PADDING,
pady=(2, 0),
)
# Position step
# Position step (account for sub-steps from previous steps)
prev_steps_with_substeps = sum(1 for j in range(i) if j in (1, 3))
step_row = i * 2 + 1 + prev_steps_with_substeps
step.grid(
row=i * 2 + 1, column=0,
row=step_row, column=0,
sticky="nsew",
padx=PADDING_SMALL,
pady=(0, 2),
@@ -390,20 +396,31 @@ class MainWindow(ctk.CTk):
if i == 1:
self._sub_step_frame = SubStepFrame(panel)
self._sub_step_frame.grid(
row=i * 2 + 2, column=0,
row=step_row + 1, column=0,
sticky="nsew",
padx=PADDING_SMALL + 16,
pady=(0, 2),
)
# ── Progress indicator ──
# Step 4: add collapsible sub-step frame
if i == 3:
self._tftp_sub_step_frame = TftpSubStepFrame(panel)
self._tftp_sub_step_frame.grid(
row=step_row + 1, column=0,
sticky="nsew",
padx=PADDING_SMALL + 16,
pady=(0, 2),
)
# ── Progress indicator ──
self._progress = ProgressIndicator(panel)
self._progress.grid(
row=8, column=0,
sticky="nsew",
row=11, column=0,
sticky="ew",
padx=PADDING,
pady=(PADDING, PADDING_SMALL),
)
panel.grid_rowconfigure(11, minsize=40)
# ── Run All button ──
self._run_btn = ctk.CTkButton(
@@ -415,10 +432,10 @@ class MainWindow(ctk.CTk):
corner_radius=CORNER_RADIUS,
)
self._run_btn.grid(
row=9, column=0,
row=12, column=0,
sticky="nsew",
padx=PADDING,
pady=PADDING_SMALL,
pady=(0, PADDING_SMALL),
)
def _build_log_panel(self, parent: ctk.CTkFrame) -> None:
@@ -898,7 +915,7 @@ class MainWindow(ctk.CTk):
self._log_message(f" [{status}] {message}")
def _run_step_4_load_main_program(self) -> bool:
"""Step 4: Load main program (TFTP upload + reboot + boot verify)."""
"""Step 4: Load main program (TFTP upload + download verify + CRC + reboot + boot verify)."""
if not self._config:
return False
@@ -907,42 +924,86 @@ class MainWindow(ctk.CTk):
self._log_message(" No Firmware BIN file selected")
return False
bin_path = Path(files["firmware_bin_path"])
remote_name = self._config.tftp_upload_name
all_results: list[bool] = []
# 4a: TFTP upload
self._log_message("Step 4a: TFTP upload & verify...")
# 4a: TFTP Upload
self._log_message("Step 4a: TFTP upload...")
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_phase("Upload")
try:
bin_path = Path(files["firmware_bin_path"])
tftp_result = tftp_upload_verify(
self._config, bin_path,
callback=self._tftp_callback,
tftp_result = tftp_upload(
self._config, bin_path, remote_name,
callback=self._tftp_sub_callback,
)
status = "" if tftp_result.success else ""
self._log_message(f" {status} {tftp_result.message}")
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_step_complete("Upload") if tftp_result.success else self._tftp_sub_step_frame.set_step_error("Upload")
all_results.append(tftp_result.success)
except Exception as e:
self._log_message(f" ✗ TFTP error: {e}")
self._log_message(f" ✗ TFTP upload error: {e}")
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_step_error("Upload")
all_results.append(False)
if not all_results[-1]:
self._log_message(" Skipping reboot & boot verify (TFTP failed)")
self._log_message(" Skipping download verify (TFTP upload failed)")
return False
# 4a2: Post-download Zynq status check
self._log_message("Step 4b: Checking Zynq status after download...")
# 4b: TFTP Download Verify
self._log_message("Step 4b: TFTP download verify...")
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_phase("Download Verify")
try:
zynq_status = verify_zynq_status(
self._config,
uart_available=self._uart_available,
callback=self._zynq_status_callback,
download_result = tftp_download(
self._config, remote_name,
callback=self._tftp_sub_callback,
)
status = "" if zynq_status.success else ""
self._log_message(f" {status} {zynq_status.message}")
status = "" if download_result.success else ""
self._log_message(f" {status} {download_result.message}")
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_step_complete("Download Verify") if download_result.success else self._tftp_sub_step_frame.set_step_error("Download Verify")
all_results.append(download_result.success)
except Exception as e:
self._log_message(f"Zynq status check error: {e}")
self._log_message(f"TFTP download error: {e}")
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_step_error("Download Verify")
all_results.append(False)
# 4c: Reboot
self._log_message("Step 4c: Rebooting Zynq...")
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 reboot (CRC mismatch)")
return False
# 4d: Reboot
self._log_message("Step 4d: Rebooting Zynq...")
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_phase("Reboot")
try:
reboot_result = reboot_zynq(
self._config,
@@ -951,17 +1012,23 @@ class MainWindow(ctk.CTk):
)
status = "" if reboot_result.success else ""
self._log_message(f" {status} {reboot_result.message}")
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_step_complete("Reboot") if reboot_result.success else self._tftp_sub_step_frame.set_step_error("Reboot")
all_results.append(reboot_result.success)
except Exception as e:
self._log_message(f" ✗ Reboot error: {e}")
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_step_error("Reboot")
all_results.append(False)
if not all_results[-1]:
self._log_message(" Skipping boot verify (reboot failed)")
return False
# 4d: Verify boot
self._log_message("Step 4d: Verifying boot...")
# 4e: Verify boot
self._log_message("Step 4e: Verifying boot...")
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_phase("Boot Verify")
try:
boot_result = verify_boot(
self._config,
@@ -975,9 +1042,13 @@ class MainWindow(ctk.CTk):
if boot_result.serial_found:
self._log_message(f" Boot info: {boot_result.boot_info}")
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_step_complete("Boot Verify") if boot_result.success else self._tftp_sub_step_frame.set_step_error("Boot Verify")
all_results.append(boot_result.success)
except Exception as e:
self._log_message(f" ✗ Boot verify error: {e}")
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_step_error("Boot Verify")
all_results.append(False)
return all(all_results)
@@ -986,6 +1057,10 @@ class MainWindow(ctk.CTk):
"""Callback for TFTP progress."""
self._log_message(f" [{status}] {message}")
def _tftp_sub_callback(self, status: str, message: str) -> None:
"""Callback for TFTP sub-step progress."""
self._log_message(f" [{status}] {message}")
def _reboot_callback(self, status: str, message: str) -> None:
"""Callback for reboot progress."""
self._log_message(f" [{status}] {message}")
@@ -1121,6 +1196,11 @@ class MainWindow(ctk.CTk):
self._sub_step_frame.set_expanded(True)
self._flash_phase = ""
# Step 4: expand sub-step frame and reset
if i == 3 and hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.reset()
self._tftp_sub_step_frame.set_expanded(True)
# 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
@@ -1142,6 +1222,13 @@ class MainWindow(ctk.CTk):
self._sub_step_frame.set_phase("done")
else:
self._sub_step_frame.set_phase("error")
# Step 4: mark sub-steps based on result
if i == 3 and hasattr(self, '_tftp_sub_step_frame'):
if success:
self._tftp_sub_step_frame.set_phase("Boot Verify")
else:
self._tftp_sub_step_frame.set_step_error("Boot Verify")
except Exception as e:
results.append(False)
self._set_step_status(i, "error")
@@ -1210,7 +1297,7 @@ class MainWindow(ctk.CTk):
and self._uart_window.is_monitoring):
self._log_message(" [UART] Already monitoring via UART window")
return
if not self._uart_available or not self._config:
if not self._config:
return
port = self._config.serial_port
if not port:
@@ -1226,6 +1313,8 @@ class MainWindow(ctk.CTk):
self._uart_monitor.on_error = lambda e: self._log_message(f" [UART] Error: {e}")
if self._uart_monitor.start():
self._log_message(" [UART] Serial monitor started")
else:
self._log_message(" [UART] Serial monitor failed to start (port may be busy)")
def _stop_uart_monitor(self) -> None:
"""Stop background serial monitoring."""