✨ 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:
+124
-35
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user