"""GUI to exercise firmware flasher with target selection. The GUI provides: - Target selection (combobox) to choose FPGA target - Configuration management dialog (global config, models, targets) - Address input for flash operations - Buttons to call erase, write, verify, and full sequence - Log output window Reference: qggrifobeamupform.cpp for UI structure. """ from __future__ import annotations import os import threading import tkinter as tk from tkinter import filedialog, messagebox, scrolledtext, ttk, simpledialog from typing import Optional from pathlib import Path from pydownloadfwviasrio.core.core import FirmwareFlasher from pydownloadfwviasrio.profiles import ( GlobalConfig, FlashTarget, FlashModel, ProfileManager, initialize_from_targets_ini ) from pydownloadfwviasrio.tftp_client import SRIOTFTPClient class FlasherGUI: def __init__(self, root: tk.Tk) -> None: self.root = root root.title("Firmware Flasher - SRIO over TFTP") # Will be initialized after log widget is created self.profile_manager: Optional[ProfileManager] = None self.current_target: Optional[FlashTarget] = None self.current_model: Optional[FlashModel] = None self.flasher: Optional[FirmwareFlasher] = None # Top frame: target selection target_frame = tk.Frame(root) target_frame.pack(padx=8, pady=8, fill=tk.X) tk.Label(target_frame, text="Target FPGA:").pack(side=tk.LEFT, padx=4) self.target_combo = ttk.Combobox( target_frame, values=[], # Will be populated after initialization state="readonly", width=30, ) self.target_combo.pack(side=tk.LEFT, padx=4) self.target_combo.bind("<>", self._on_target_selected) self.config_btn = tk.Button(target_frame, text="βš™οΈ Manage Configuration", command=self._manage_config) self.config_btn.pack(side=tk.LEFT, padx=4) # Target info panel info_frame = tk.LabelFrame(root, text="Target Information", padx=10, pady=10) info_frame.pack(padx=8, pady=(0, 8), fill=tk.X) self.info_label = tk.Label( info_frame, text="No target selected", justify=tk.LEFT, anchor=tk.W, font=("Courier", 9), ) self.info_label.pack(fill=tk.X) # Control buttons control_frame = tk.Frame(root) control_frame.pack(padx=8, pady=8, fill=tk.X) self.erase_btn = tk.Button(control_frame, text="πŸ—‘οΈ Erase", command=self._erase, width=12) self.erase_btn.pack(side=tk.LEFT, padx=4) self.write_btn = tk.Button(control_frame, text="✍️ Write", command=self._write, width=12) self.write_btn.pack(side=tk.LEFT, padx=4) self.verify_btn = tk.Button(control_frame, text="πŸ” Verify", command=self._verify, width=12) self.verify_btn.pack(side=tk.LEFT, padx=4) self.full_btn = tk.Button(control_frame, text="πŸ”„ Full Sequence", command=self._full_sequence, width=18) self.full_btn.pack(side=tk.LEFT, padx=4) # Abort button (initially disabled, enabled during operations) self.abort_btn = tk.Button( control_frame, text="πŸ›‘ ABORT", command=self._abort_operation, width=12, bg="#FF6B6B", fg="white", font=('TkDefaultFont', 9, 'bold'), state=tk.DISABLED ) self.abort_btn.pack(side=tk.LEFT, padx=12) # Verify after write checkbox (ben visibile con colore) self.verify_after_write_var = tk.BooleanVar(value=False) verify_check = tk.Checkbutton( control_frame, text="βœ“ Verify after each write", variable=self.verify_after_write_var, command=self._on_verify_checkbox_changed, font=("TkDefaultFont", 9, "bold"), fg="#0066CC", # Blu per visibilitΓ  ) verify_check.pack(side=tk.LEFT, padx=12) # Progress bar progress_frame = tk.Frame(root) progress_frame.pack(padx=8, pady=(0, 8), fill=tk.X) tk.Label(progress_frame, text="Progress:").pack(side=tk.LEFT, padx=4) self.progress = ttk.Progressbar(progress_frame, length=400, mode='determinate') self.progress.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4) self.progress_label = tk.Label(progress_frame, text="0%", width=6) self.progress_label.pack(side=tk.LEFT, padx=4) # Log windows frame (stacked horizontally) logs_frame = tk.Frame(root) logs_frame.pack(padx=8, pady=(0, 8), fill=tk.BOTH, expand=True) # General log (top) general_log_frame = tk.LabelFrame(logs_frame, text="General Log", padx=5, pady=5) general_log_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, pady=(0, 4)) self.log_widget = scrolledtext.ScrolledText(general_log_frame, width=80, height=12) self.log_widget.pack(fill=tk.BOTH, expand=True) # TFTP commands log (bottom) tftp_log_frame = tk.LabelFrame(logs_frame, text="TFTP Commands", padx=5, pady=5) tftp_log_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, pady=(4, 0)) self.tftp_log_widget = scrolledtext.ScrolledText(tftp_log_frame, width=80, height=12, font=("Courier", 8)) self.tftp_log_widget.pack(fill=tk.BOTH, expand=True) # Initialize configuration after log widget exists self._initialize_config() def _initialize_config(self) -> None: """Initialize configuration manager - called after log widget is created.""" self.profile_manager = ProfileManager() # If empty, try to initialize from targets.ini if not self.profile_manager.targets: targets_ini = Path("_OLD/Vecchia_app/FpgaBeamMeUp/targets.ini") if targets_ini.exists(): self._append_log(f"Loading configuration from {targets_ini}...") self.profile_manager.load_from_ini(targets_ini) self.profile_manager.save() self._append_log(f"Loaded {len(self.profile_manager.targets)} targets, {len(self.profile_manager.models)} models") else: self._append_log("WARNING: No configuration found!") # Update combobox with target names self.target_combo['values'] = self.profile_manager.list_targets() # Load verify_after_write setting from config self.verify_after_write_var.set(self.profile_manager.global_config.verify_after_write) # Select default target if available if self.profile_manager.global_config.default_target in self.profile_manager.targets: self.target_combo.set(self.profile_manager.global_config.default_target) self._on_target_selected(None) elif self.profile_manager.targets: self.target_combo.current(0) self._on_target_selected(None) def _append_log(self, message: str) -> None: """Append message to general log.""" self.log_widget.insert(tk.END, message + "\n") self.log_widget.see(tk.END) def _append_tftp_log(self, message: str) -> None: """Append message to TFTP commands log.""" self.tftp_log_widget.insert(tk.END, message + "\n") self.tftp_log_widget.see(tk.END) def _on_verify_checkbox_changed(self) -> None: """Save verify_after_write setting when checkbox changes.""" new_value = self.verify_after_write_var.get() self.profile_manager.global_config.verify_after_write = new_value self.profile_manager.save() status = "enabled" if new_value else "disabled" self._append_log(f"Chunk verification after write: {status}") def _disable_controls(self) -> None: """Disable all operation buttons and enable abort button.""" self.erase_btn.config(state=tk.DISABLED) self.write_btn.config(state=tk.DISABLED) self.verify_btn.config(state=tk.DISABLED) self.full_btn.config(state=tk.DISABLED) self.config_btn.config(state=tk.DISABLED) self.target_combo.config(state=tk.DISABLED) self.abort_btn.config(state=tk.NORMAL) self.operation_running = True self.abort_requested = False def _enable_controls(self) -> None: """Enable all operation buttons and disable abort button.""" self.erase_btn.config(state=tk.NORMAL) self.write_btn.config(state=tk.NORMAL) self.verify_btn.config(state=tk.NORMAL) self.full_btn.config(state=tk.NORMAL) self.config_btn.config(state=tk.NORMAL) self.target_combo.config(state='readonly') self.abort_btn.config(state=tk.DISABLED) self.operation_running = False self.abort_requested = False def _abort_operation(self) -> None: """Request abort of current operation.""" self.abort_requested = True self._append_log("⚠️ ABORT requested - stopping operation...") self.abort_btn.config(state=tk.DISABLED, text="πŸ›‘ Aborting...") def _check_abort(self) -> bool: """Check if abort was requested. Returns: True if abort was requested, False otherwise. """ if self.abort_requested: self._append_log("❌ Operation aborted by user") return True return False def _update_progress(self, current: int, total: int) -> None: """Update progress bar and percentage label.""" if total > 0: percentage = int((current / total) * 100) self.progress['value'] = percentage self.progress_label.config(text=f"{percentage}%") self.root.update_idletasks() def _on_target_selected(self, event) -> None: """Handle target selection change.""" selected_name = self.target_combo.get() result = self.profile_manager.get_target_with_model(selected_name) if result: self.current_target, self.current_model = result # Update info panel golden_path = self.current_target.golden_binary_path or "(not configured)" user_path = self.current_target.user_binary_path or "(not configured)" info_text = f"""Target: {self.current_target.id_target} ({self.current_target.description}) IP:Port : {self.profile_manager.global_config.ip}:{self.profile_manager.global_config.port} SRIO Slot : 0x{self.current_target.slot_address:02X} Architecture : {self.current_target.architecture} Model : {self.current_model.model} ({self.current_model.description}) Flash Type : {self.current_model.flash_type} Addressing : {'4-byte' if self.current_model.is_4byte_addressing else '3-byte'} Sectors : {self.current_model.num_sectors} Golden Area : 0x{self.current_model.golden_start:08X} - 0x{self.current_model.golden_stop:08X} User Area : 0x{self.current_model.user_start:08X} - 0x{self.current_model.user_stop:08X} Golden Binary : {golden_path} User Binary : {user_path}""" self.info_label.config(text=info_text) # Log selection self._append_log(f"Target selected: {self.current_target.id_target}") self._append_log(f" Model: {self.current_model.model}, Slot: 0x{self.current_target.slot_address:02X}") self._append_log(f" User area: 0x{self.current_model.user_start:08X} - 0x{self.current_model.user_stop:08X}") if self.current_target.golden_binary_path: self._append_log(f" Golden binary: {self.current_target.golden_binary_path}") if self.current_target.user_binary_path: self._append_log(f" User binary: {self.current_target.user_binary_path}") # Create client and flasher (using a dummy FlashProfile for compatibility) # TODO: Update FirmwareFlasher to work directly with target+model from pydownloadfwviasrio.profiles import FlashProfile temp_profile = FlashProfile( name=self.current_target.id_target, slot_address=f"0x{self.current_target.slot_address:02X}", ip=self.profile_manager.global_config.ip, port=self.profile_manager.global_config.port, base_address=self.current_model.user_start, size=(self.current_model.user_stop - self.current_model.user_start + 1), binary_path=None, # Will be selected during operation ) client = SRIOTFTPClient( self.profile_manager.global_config.ip, self.profile_manager.global_config.port, max_retries=self.profile_manager.global_config.max_tftp_retries, ) self.flasher = FirmwareFlasher( profile=temp_profile, client=client, max_register_retries=self.profile_manager.global_config.max_register_retries, ) def _ask_memory_area(self) -> Optional[tuple[str, int, Optional[str]]]: """Ask user to select memory area (golden or user). Returns: Tuple of (area_name, address, binary_path) or None if cancelled. """ if not self.current_model: return None # Create dialog dialog = tk.Toplevel(self.root) dialog.title("Select Memory Area") dialog.geometry("400x200") dialog.transient(self.root) dialog.grab_set() result = {'area': None} # Info frame info_frame = tk.LabelFrame(dialog, text="Available Memory Areas", padx=10, pady=10) info_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) golden_text = f"Golden Area: 0x{self.current_model.golden_start:08X} - 0x{self.current_model.golden_stop:08X}" user_text = f"User Area: 0x{self.current_model.user_start:08X} - 0x{self.current_model.user_stop:08X}" tk.Label(info_frame, text=golden_text, font=("Courier", 9)).pack(anchor=tk.W) tk.Label(info_frame, text=user_text, font=("Courier", 9)).pack(anchor=tk.W) # Buttons btn_frame = tk.Frame(dialog) btn_frame.pack(pady=10) def select_golden(): result['area'] = 'golden' dialog.destroy() def select_user(): result['area'] = 'user' dialog.destroy() tk.Button(btn_frame, text="Golden Area", command=select_golden, width=15).pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="User Area", command=select_user, width=15).pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="Cancel", command=dialog.destroy, width=15).pack(side=tk.LEFT, padx=5) # Center dialog dialog.wait_window() if result['area'] == 'golden': binary_path = self.current_target.golden_binary_path if self.current_target else None return ('golden', self.current_model.golden_start, binary_path) elif result['area'] == 'user': binary_path = self.current_target.user_binary_path if self.current_target else None return ('user', self.current_model.user_start, binary_path) else: return None def _run_in_thread(self, func) -> None: """Run a function in a background thread to keep GUI responsive.""" import threading def wrapper(): try: self._disable_controls() func() except Exception as e: self._append_log(f"❌ Exception: {e}") finally: # Always re-enable controls self.root.after(0, self._enable_controls) threading.Thread(target=wrapper, daemon=True).start() def _erase(self) -> None: def job() -> None: if not self.flasher: self._append_log("ERROR: No profile selected") return # Ask user which area to erase area_info = self._ask_memory_area() if not area_info: self._append_log("Operation cancelled") return if self._check_abort(): return area_name, address, _ = area_info self._update_progress(0, 100) self._append_log(f"Erasing {area_name} area at 0x{address:08X}") self.flasher.erase(address, 65536, log=self._append_log, tftp_log=self._append_tftp_log, progress_callback=self._update_progress, abort_check=self._check_abort) if self._check_abort(): return self._update_progress(100, 100) self._append_log(f"βœ… Erase completed") self._run_in_thread(job) def _write(self) -> None: def job() -> None: if not self.flasher: self._append_log("ERROR: No target selected") return # Ask user which area to write area_info = self._ask_memory_area() if not area_info: self._append_log("Operation cancelled") return if self._check_abort(): return area_name, address, configured_binary = area_info # Use configured binary or ask user if configured_binary and os.path.exists(configured_binary): path = configured_binary self._append_log(f"Using configured {area_name} binary: {path}") else: if configured_binary: self._append_log(f"WARNING: Configured binary not found: {configured_binary}") path = filedialog.askopenfilename(title=f"Select {area_name} binary to write") if not path: self._append_log("Operation cancelled") return if self._check_abort(): return with open(path, "rb") as f: data = f.read() self._update_progress(0, 100) self._append_log(f"Writing {len(data)} bytes to {area_name} area at 0x{address:08X}") ok = self.flasher.write( address, data, log=self._append_log, tftp_log=self._append_tftp_log, progress_callback=self._update_progress, verify_after_write=self.verify_after_write_var.get(), max_chunk_write_retries=self.profile_manager.global_config.max_chunk_write_retries, abort_check=self._check_abort, ) if self._check_abort(): return self._update_progress(100, 100) if ok: self._append_log(f"βœ… Write completed") else: self._append_log(f"❌ Write failed") self._run_in_thread(job) def _verify(self) -> None: def job() -> None: if not self.flasher: self._append_log("ERROR: No target selected") return # Ask user which area to verify area_info = self._ask_memory_area() if not area_info: self._append_log("Operation cancelled") return if self._check_abort(): return area_name, address, configured_binary = area_info # Use configured binary or ask user if configured_binary and os.path.exists(configured_binary): path = configured_binary self._append_log(f"Using configured {area_name} binary: {path}") else: if configured_binary: self._append_log(f"WARNING: Configured binary not found: {configured_binary}") path = filedialog.askopenfilename(title=f"Select {area_name} binary to verify") if not path: self._append_log("Operation cancelled") return if self._check_abort(): return with open(path, "rb") as f: data = f.read() self._update_progress(0, 100) self._append_log(f"Verifying {len(data)} bytes in {area_name} area at 0x{address:08X}") ok = self.flasher.verify(address, data, log=self._append_log, tftp_log=self._append_tftp_log, progress_callback=self._update_progress, abort_check=self._check_abort) if self._check_abort(): return self._update_progress(100, 100) if ok: self._append_log("βœ… Verification successful!") else: self._append_log("❌ Verification FAILED!") self._run_in_thread(job) def _full_sequence(self) -> None: def job() -> None: if not self.flasher: self._append_log("ERROR: No target selected") return # Use target's binary_path if configured, otherwise ask user if self.current_target and self.current_target.binary_path: path = self.current_target.binary_path if not os.path.exists(path): self._append_log(f"ERROR: Configured binary not found: {path}") return self._append_log(f"Using configured binary: {path}") else: path = filedialog.askopenfilename(title="Select binary for full sequence") if not path: return if self._check_abort(): return with open(path, "rb") as f: data = f.read() address = self._get_address() # Erase self._append_log("=== πŸ—‘οΈ Starting Erase ===") self._update_progress(0, 100) ok = self.flasher.erase(address, len(data), log=self._append_log, progress_callback=self._update_progress, abort_check=self._check_abort) if not ok: self._append_log("❌ ABORT: Erase failed") return if self._check_abort(): return # Write self._append_log("=== ✍️ Starting Write ===") self._update_progress(0, 100) ok = self.flasher.write( address, data, log=self._append_log, tftp_log=self._append_tftp_log, progress_callback=self._update_progress, verify_after_write=self.verify_after_write_var.get(), max_chunk_write_retries=self.profile_manager.global_config.max_chunk_write_retries, abort_check=self._check_abort, ) if not ok: self._append_log("❌ ABORT: Write failed") return if self._check_abort(): return # Verify self._append_log("=== πŸ” Starting Verify ===") self._update_progress(0, 100) ok = self.flasher.verify(address, data, log=self._append_log, progress_callback=self._update_progress, abort_check=self._check_abort) if self._check_abort(): return self._update_progress(100, 100) if ok: self._append_log("πŸŽ‰ SUCCESS: Full sequence completed") else: self._append_log("❌ FAILED: Verify mismatch") self._run_in_thread(job) def _manage_config(self) -> None: """Open configuration management dialog.""" ConfigManagerDialog(self.root, self.profile_manager, self._refresh_targets) def _refresh_targets(self) -> None: """Refresh target list in combobox after changes.""" current = self.target_combo.get() self.target_combo['values'] = self.profile_manager.list_targets() # Try to keep current selection if current in self.profile_manager.list_targets(): self.target_combo.set(current) elif self.profile_manager.targets: self.target_combo.current(0) self._on_target_selected(None) class ConfigManagerDialog: """Dialog for managing global config, models, and targets.""" def __init__(self, parent: tk.Tk, manager: ProfileManager, refresh_callback) -> None: self.manager = manager self.refresh_callback = refresh_callback self.dialog = tk.Toplevel(parent) self.dialog.title("Configuration Manager") self.dialog.geometry("900x600") # Create notebook (tabbed interface) notebook = ttk.Notebook(self.dialog) notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # Tab 1: Global Configuration global_tab = tk.Frame(notebook) notebook.add(global_tab, text="Global Config") self._create_global_tab(global_tab) # Tab 2: Flash Models models_tab = tk.Frame(notebook) notebook.add(models_tab, text="Flash Models") self._create_models_tab(models_tab) # Tab 3: Targets targets_tab = tk.Frame(notebook) notebook.add(targets_tab, text="Targets") self._create_targets_tab(targets_tab) # Bottom buttons btn_frame = tk.Frame(self.dialog) btn_frame.pack(fill=tk.X, padx=10, pady=(0, 10)) tk.Button(btn_frame, text="Save & Close", command=self._save_and_close, width=15).pack(side=tk.RIGHT, padx=5) tk.Button(btn_frame, text="Close", command=self.dialog.destroy, width=15).pack(side=tk.RIGHT, padx=5) def _create_global_tab(self, parent: tk.Frame) -> None: """Create global configuration tab.""" form_frame = tk.LabelFrame(parent, text="Connection Settings", padx=20, pady=20) form_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) row = 0 # IP Address tk.Label(form_frame, text="IP Address:").grid(row=row, column=0, sticky=tk.W, pady=5) self.global_ip_entry = tk.Entry(form_frame, width=40) self.global_ip_entry.grid(row=row, column=1, pady=5) self.global_ip_entry.insert(0, self.manager.global_config.ip) row += 1 # Port tk.Label(form_frame, text="Port:").grid(row=row, column=0, sticky=tk.W, pady=5) self.global_port_entry = tk.Entry(form_frame, width=40) self.global_port_entry.grid(row=row, column=1, pady=5) self.global_port_entry.insert(0, str(self.manager.global_config.port)) row += 1 # SRIO Base tk.Label(form_frame, text="SRIO Base (hex):").grid(row=row, column=0, sticky=tk.W, pady=5) self.global_srio_entry = tk.Entry(form_frame, width=40) self.global_srio_entry.grid(row=row, column=1, pady=5) self.global_srio_entry.insert(0, f"0x{self.manager.global_config.srio_base:X}") row += 1 # FPGA Base tk.Label(form_frame, text="FPGA Base (hex):").grid(row=row, column=0, sticky=tk.W, pady=5) self.global_fpga_entry = tk.Entry(form_frame, width=40) self.global_fpga_entry.grid(row=row, column=1, pady=5) self.global_fpga_entry.insert(0, f"0x{self.manager.global_config.fpga_base:X}") row += 1 # Sector Size tk.Label(form_frame, text="Sector Size (hex):").grid(row=row, column=0, sticky=tk.W, pady=5) self.global_sector_entry = tk.Entry(form_frame, width=40) self.global_sector_entry.grid(row=row, column=1, pady=5) self.global_sector_entry.insert(0, f"0x{self.manager.global_config.fpga_sector:X}") row += 1 # Default Target tk.Label(form_frame, text="Default Target:").grid(row=row, column=0, sticky=tk.W, pady=5) self.global_default_combo = ttk.Combobox(form_frame, width=37, state="readonly") self.global_default_combo.grid(row=row, column=1, pady=5) self.global_default_combo['values'] = list(self.manager.targets.keys()) if self.manager.global_config.default_target in self.manager.targets: self.global_default_combo.set(self.manager.global_config.default_target) row += 1 # === Retry Configuration Section === tk.Label(form_frame, text="━━━━━ Retry Configuration ━━━━━", font=("Arial", 9, "bold")).grid(row=row, column=0, columnspan=2, pady=(15, 5)) row += 1 # TFTP Retries tk.Label(form_frame, text="TFTP Retries:").grid(row=row, column=0, sticky=tk.W, pady=5) self.global_tftp_retries_entry = tk.Entry(form_frame, width=40) self.global_tftp_retries_entry.grid(row=row, column=1, pady=5) self.global_tftp_retries_entry.insert(0, str(self.manager.global_config.max_tftp_retries)) row += 1 # Register Retries tk.Label(form_frame, text="Register Retries:").grid(row=row, column=0, sticky=tk.W, pady=5) self.global_register_retries_entry = tk.Entry(form_frame, width=40) self.global_register_retries_entry.grid(row=row, column=1, pady=5) self.global_register_retries_entry.insert(0, str(self.manager.global_config.max_register_retries)) row += 1 # Chunk Write Retries tk.Label(form_frame, text="Chunk Write Retries:").grid(row=row, column=0, sticky=tk.W, pady=5) self.global_chunk_retries_entry = tk.Entry(form_frame, width=40) self.global_chunk_retries_entry.grid(row=row, column=1, pady=5) self.global_chunk_retries_entry.insert(0, str(self.manager.global_config.max_chunk_write_retries)) row += 1 tk.Button(form_frame, text="Apply Global Settings", command=self._apply_global).grid(row=row, column=1, pady=20) def _create_models_tab(self, parent: tk.Frame) -> None: """Create flash models management tab.""" # Left side: model list list_frame = tk.Frame(parent) list_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=10) tk.Label(list_frame, text="Flash Models:", font=("Arial", 10, "bold")).pack(anchor=tk.W) self.models_listbox = tk.Listbox(list_frame, height=20) self.models_listbox.pack(fill=tk.BOTH, expand=True, pady=(5, 0)) self.models_listbox.bind("<>", self._on_model_select) # Buttons btn_frame = tk.Frame(list_frame) btn_frame.pack(fill=tk.X, pady=(5, 0)) tk.Button(btn_frame, text="New Model", command=self._new_model).pack(side=tk.LEFT, padx=2) tk.Button(btn_frame, text="Delete Model", command=self._delete_model).pack(side=tk.LEFT, padx=2) # Right side: model editor/details self.model_editor_frame = tk.LabelFrame(parent, text="Model Details", padx=10, pady=10) self.model_editor_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(0, 10), pady=10) self._refresh_models_list() self._show_model_editor(None) # Start with empty editor def _create_targets_tab(self, parent: tk.Frame) -> None: """Create targets management tab.""" # Left side: target list list_frame = tk.Frame(parent) list_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=10) tk.Label(list_frame, text="FPGA Targets:", font=("Arial", 10, "bold")).pack(anchor=tk.W) self.targets_listbox = tk.Listbox(list_frame, height=20) self.targets_listbox.pack(fill=tk.BOTH, expand=True, pady=(5, 0)) self.targets_listbox.bind("<>", self._on_target_select) # Buttons btn_frame = tk.Frame(list_frame) btn_frame.pack(fill=tk.X, pady=(5, 0)) tk.Button(btn_frame, text="New Target", command=self._new_target).pack(side=tk.LEFT, padx=2) tk.Button(btn_frame, text="Delete Target", command=self._delete_target).pack(side=tk.LEFT, padx=2) # Right side: target editor/details self.target_editor_frame = tk.LabelFrame(parent, text="Target Details", padx=10, pady=10) self.target_editor_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(0, 10), pady=10) self._refresh_targets_list() self._show_target_editor(None) # Start with empty editor # Global Config Methods def _apply_global(self) -> None: """Apply global configuration changes.""" try: self.manager.global_config.ip = self.global_ip_entry.get().strip() self.manager.global_config.port = int(self.global_port_entry.get().strip()) self.manager.global_config.srio_base = int(self.global_srio_entry.get().strip(), 16) self.manager.global_config.fpga_base = int(self.global_fpga_entry.get().strip(), 16) self.manager.global_config.fpga_sector = int(self.global_sector_entry.get().strip(), 16) self.manager.global_config.default_target = self.global_default_combo.get() # Retry configuration self.manager.global_config.max_tftp_retries = int(self.global_tftp_retries_entry.get().strip()) self.manager.global_config.max_register_retries = int(self.global_register_retries_entry.get().strip()) self.manager.global_config.max_chunk_write_retries = int(self.global_chunk_retries_entry.get().strip()) messagebox.showinfo("Success", "Global settings applied successfully", parent=self.dialog) except ValueError as e: messagebox.showerror("Error", f"Invalid input: {e}", parent=self.dialog) # Models Methods def _refresh_models_list(self) -> None: """Refresh models listbox.""" self.models_listbox.delete(0, tk.END) for id_model, model in sorted(self.manager.models.items()): self.models_listbox.insert(tk.END, f"[{id_model}] {model.model} - {model.description}") def _on_model_select(self, event) -> None: """Display selected model in editor.""" selection = self.models_listbox.curselection() if not selection: return models_list = sorted(self.manager.models.items()) id_model, model = models_list[selection[0]] self._show_model_editor(model) def _show_model_editor(self, model: Optional[FlashModel]) -> None: """Show model editor form.""" # Clear frame for widget in self.model_editor_frame.winfo_children(): widget.destroy() # Create form form = tk.Frame(self.model_editor_frame) form.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) row = 0 # Model ID tk.Label(form, text="Model ID:").grid(row=row, column=0, sticky=tk.W, pady=3) self.model_id_entry = tk.Entry(form, width=35) self.model_id_entry.grid(row=row, column=1, pady=3, sticky=tk.W) if model: self.model_id_entry.insert(0, str(model.id_model)) self.model_id_entry.config(state='readonly') row += 1 # Model Name tk.Label(form, text="Model Name:").grid(row=row, column=0, sticky=tk.W, pady=3) self.model_name_entry = tk.Entry(form, width=35) self.model_name_entry.grid(row=row, column=1, pady=3, sticky=tk.W) if model: self.model_name_entry.insert(0, model.model) row += 1 # Description tk.Label(form, text="Description:").grid(row=row, column=0, sticky=tk.W, pady=3) self.model_desc_entry = tk.Entry(form, width=35) self.model_desc_entry.grid(row=row, column=1, pady=3, sticky=tk.W) if model: self.model_desc_entry.insert(0, model.description) row += 1 # Flash Type tk.Label(form, text="Flash Type:").grid(row=row, column=0, sticky=tk.W, pady=3) self.model_type_entry = tk.Entry(form, width=35) self.model_type_entry.grid(row=row, column=1, pady=3, sticky=tk.W) if model: self.model_type_entry.insert(0, str(model.flash_type)) row += 1 # 4-byte Addressing tk.Label(form, text="4-byte Addressing:").grid(row=row, column=0, sticky=tk.W, pady=3) self.model_addr4_var = tk.BooleanVar(value=model.is_4byte_addressing if model else False) tk.Checkbutton(form, variable=self.model_addr4_var).grid(row=row, column=1, sticky=tk.W, pady=3) row += 1 # Num Sectors tk.Label(form, text="Sectors:").grid(row=row, column=0, sticky=tk.W, pady=3) self.model_sectors_entry = tk.Entry(form, width=35) self.model_sectors_entry.grid(row=row, column=1, pady=3, sticky=tk.W) if model: self.model_sectors_entry.insert(0, str(model.num_sectors)) row += 1 # Golden Start tk.Label(form, text="Golden Start (hex):").grid(row=row, column=0, sticky=tk.W, pady=3) self.model_gold_start_entry = tk.Entry(form, width=35) self.model_gold_start_entry.grid(row=row, column=1, pady=3, sticky=tk.W) if model: self.model_gold_start_entry.insert(0, f"0x{model.golden_start:08X}") row += 1 # Golden Stop tk.Label(form, text="Golden Stop (hex):").grid(row=row, column=0, sticky=tk.W, pady=3) self.model_gold_stop_entry = tk.Entry(form, width=35) self.model_gold_stop_entry.grid(row=row, column=1, pady=3, sticky=tk.W) if model: self.model_gold_stop_entry.insert(0, f"0x{model.golden_stop:08X}") row += 1 # User Start tk.Label(form, text="User Start (hex):").grid(row=row, column=0, sticky=tk.W, pady=3) self.model_user_start_entry = tk.Entry(form, width=35) self.model_user_start_entry.grid(row=row, column=1, pady=3, sticky=tk.W) if model: self.model_user_start_entry.insert(0, f"0x{model.user_start:08X}") row += 1 # User Stop tk.Label(form, text="User Stop (hex):").grid(row=row, column=0, sticky=tk.W, pady=3) self.model_user_stop_entry = tk.Entry(form, width=35) self.model_user_stop_entry.grid(row=row, column=1, pady=3, sticky=tk.W) if model: self.model_user_stop_entry.insert(0, f"0x{model.user_stop:08X}") row += 1 # Test Address tk.Label(form, text="Test Address (hex, opt):").grid(row=row, column=0, sticky=tk.W, pady=3) self.model_test_addr_entry = tk.Entry(form, width=35) self.model_test_addr_entry.grid(row=row, column=1, pady=3, sticky=tk.W) if model and model.test_address: self.model_test_addr_entry.insert(0, f"0x{model.test_address:08X}") row += 1 # Buttons btn_frame = tk.Frame(form) btn_frame.grid(row=row, column=0, columnspan=2, pady=15) tk.Button(btn_frame, text="Save Model", command=lambda: self._save_model(model), width=12).pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="Clear", command=lambda: self._show_model_editor(None), width=12).pack(side=tk.LEFT, padx=5) def _new_model(self) -> None: """Prepare editor for new model.""" self.models_listbox.selection_clear(0, tk.END) self._show_model_editor(None) def _save_model(self, original_model: Optional[FlashModel]) -> None: """Save model from editor.""" try: if original_model: id_model = original_model.id_model else: id_model = int(self.model_id_entry.get().strip()) if id_model in self.manager.models: messagebox.showerror("Error", f"Model ID {id_model} already exists.", parent=self.dialog) return test_addr_str = self.model_test_addr_entry.get().strip() test_addr = int(test_addr_str, 16) if test_addr_str else None new_model = FlashModel( id_model=id_model, model=self.model_name_entry.get().strip(), description=self.model_desc_entry.get().strip(), flash_type=int(self.model_type_entry.get().strip()), is_4byte_addressing=self.model_addr4_var.get(), num_sectors=int(self.model_sectors_entry.get().strip()), golden_start=int(self.model_gold_start_entry.get().strip(), 16), golden_stop=int(self.model_gold_stop_entry.get().strip(), 16), user_start=int(self.model_user_start_entry.get().strip(), 16), user_stop=int(self.model_user_stop_entry.get().strip(), 16), test_address=test_addr, ) self.manager.add_model(new_model) self._refresh_models_list() self._show_model_editor(new_model) # Select the saved model in list for idx, (id_m, m) in enumerate(sorted(self.manager.models.items())): if id_m == id_model: self.models_listbox.selection_clear(0, tk.END) self.models_listbox.selection_set(idx) self.models_listbox.see(idx) break messagebox.showinfo("Success", f"Model '{new_model.model}' saved successfully", parent=self.dialog) except ValueError as e: messagebox.showerror("Error", f"Invalid input: {e}", parent=self.dialog) def _delete_model(self) -> None: """Delete selected model.""" selection = self.models_listbox.curselection() if not selection: messagebox.showwarning("No Selection", "Please select a model to delete.", parent=self.dialog) return models_list = sorted(self.manager.models.items()) id_model, model = models_list[selection[0]] # Check if model is in use in_use = [t.id_target for t in self.manager.targets.values() if t.id_model == id_model] if in_use: messagebox.showerror("Cannot Delete", f"Model is in use by targets: {', '.join(in_use)}", parent=self.dialog) return if messagebox.askyesno("Confirm Delete", f"Delete model '{model.model}'?", parent=self.dialog): self.manager.delete_model(id_model) self._refresh_models_list() self._show_model_editor(None) # Targets Methods def _refresh_targets_list(self) -> None: """Refresh targets listbox.""" self.targets_listbox.delete(0, tk.END) for id_target, target in sorted(self.manager.targets.items()): self.targets_listbox.insert(tk.END, f"{target.id_target} - {target.description}") def _on_target_select(self, event) -> None: """Display selected target in editor.""" selection = self.targets_listbox.curselection() if not selection: return targets_list = sorted(self.manager.targets.items()) id_target, target = targets_list[selection[0]] self._show_target_editor(target) def _show_target_editor(self, target: Optional[FlashTarget]) -> None: """Show target editor form.""" # Clear frame for widget in self.target_editor_frame.winfo_children(): widget.destroy() # Create form form = tk.Frame(self.target_editor_frame) form.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) row = 0 # Target ID tk.Label(form, text="Target ID:").grid(row=row, column=0, sticky=tk.W, pady=3) self.target_id_entry = tk.Entry(form, width=35) self.target_id_entry.grid(row=row, column=1, pady=3, sticky=tk.W) if target: self.target_id_entry.insert(0, target.id_target) self.target_id_entry.config(state='readonly') row += 1 # Description tk.Label(form, text="Description:").grid(row=row, column=0, sticky=tk.W, pady=3) self.target_desc_entry = tk.Entry(form, width=35) self.target_desc_entry.grid(row=row, column=1, pady=3, sticky=tk.W) if target: self.target_desc_entry.insert(0, target.description) row += 1 # Slot Address tk.Label(form, text="Slot Address (hex):").grid(row=row, column=0, sticky=tk.W, pady=3) self.target_slot_entry = tk.Entry(form, width=35) self.target_slot_entry.grid(row=row, column=1, pady=3, sticky=tk.W) if target: self.target_slot_entry.insert(0, f"0x{target.slot_address:02X}") row += 1 # Architecture tk.Label(form, text="Architecture:").grid(row=row, column=0, sticky=tk.W, pady=3) self.target_arch_combo = ttk.Combobox(form, width=32, values=["Xilinx", "RFIF", "Other"]) self.target_arch_combo.grid(row=row, column=1, pady=3, sticky=tk.W) if target: self.target_arch_combo.set(target.architecture) row += 1 # Name tk.Label(form, text="Name:").grid(row=row, column=0, sticky=tk.W, pady=3) self.target_name_entry = tk.Entry(form, width=35) self.target_name_entry.grid(row=row, column=1, pady=3, sticky=tk.W) if target: self.target_name_entry.insert(0, target.name) row += 1 # File Prefix tk.Label(form, text="File Prefix:").grid(row=row, column=0, sticky=tk.W, pady=3) self.target_prefix_entry = tk.Entry(form, width=35) self.target_prefix_entry.grid(row=row, column=1, pady=3, sticky=tk.W) if target: self.target_prefix_entry.insert(0, target.file_prefix) row += 1 # Model Selection tk.Label(form, text="Flash Model:").grid(row=row, column=0, sticky=tk.W, pady=3) self.target_model_combo = ttk.Combobox(form, width=32, state="readonly") self.target_model_combo.grid(row=row, column=1, pady=3, sticky=tk.W) model_choices = [f"[{id_}] {m.model} - {m.description}" for id_, m in sorted(self.manager.models.items())] self.target_model_combo['values'] = model_choices if target: for idx, (id_, m) in enumerate(sorted(self.manager.models.items())): if id_ == target.id_model: self.target_model_combo.current(idx) break row += 1 # Golden Binary Path tk.Label(form, text="Golden Binary (opt):").grid(row=row, column=0, sticky=tk.W, pady=3) golden_binary_frame = tk.Frame(form) golden_binary_frame.grid(row=row, column=1, pady=3, sticky=tk.W) self.target_golden_binary_entry = tk.Entry(golden_binary_frame, width=25) self.target_golden_binary_entry.pack(side=tk.LEFT) tk.Button(golden_binary_frame, text="Browse", command=lambda: self._browse_target_binary('golden')).pack(side=tk.LEFT, padx=5) if target and target.golden_binary_path: self.target_golden_binary_entry.insert(0, target.golden_binary_path) row += 1 # User Binary Path tk.Label(form, text="User Binary (opt):").grid(row=row, column=0, sticky=tk.W, pady=3) user_binary_frame = tk.Frame(form) user_binary_frame.grid(row=row, column=1, pady=3, sticky=tk.W) self.target_user_binary_entry = tk.Entry(user_binary_frame, width=25) self.target_user_binary_entry.pack(side=tk.LEFT) tk.Button(user_binary_frame, text="Browse", command=lambda: self._browse_target_binary('user')).pack(side=tk.LEFT, padx=5) if target and target.user_binary_path: self.target_user_binary_entry.insert(0, target.user_binary_path) row += 1 # Buttons btn_frame = tk.Frame(form) btn_frame.grid(row=row, column=0, columnspan=2, pady=15) tk.Button(btn_frame, text="Save Target", command=lambda: self._save_target(target), width=12).pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="Clear", command=lambda: self._show_target_editor(None), width=12).pack(side=tk.LEFT, padx=5) def _browse_target_binary(self, area_type: str) -> None: """Browse for binary file. Args: area_type: 'golden' or 'user' """ path = filedialog.askopenfilename(title=f"Select {area_type} binary file") if path: if area_type == 'golden': self.target_golden_binary_entry.delete(0, tk.END) self.target_golden_binary_entry.insert(0, path) else: self.target_user_binary_entry.delete(0, tk.END) self.target_user_binary_entry.insert(0, path) def _new_target(self) -> None: """Prepare editor for new target.""" self.targets_listbox.selection_clear(0, tk.END) self._show_target_editor(None) def _save_target(self, original_target: Optional[FlashTarget]) -> None: """Save target from editor.""" try: if original_target: id_target = original_target.id_target else: id_target = self.target_id_entry.get().strip() if not id_target: messagebox.showerror("Error", "Target ID is required.", parent=self.dialog) return if id_target in self.manager.targets: messagebox.showerror("Error", f"Target '{id_target}' already exists.", parent=self.dialog) return # Extract model ID from combobox selection model_str = self.target_model_combo.get() if not model_str: messagebox.showerror("Error", "Please select a model.", parent=self.dialog) return # Parse "[ID] ..." format id_model = int(model_str.split(']')[0].strip('[')) golden_binary_path = self.target_golden_binary_entry.get().strip() or None user_binary_path = self.target_user_binary_entry.get().strip() or None new_target = FlashTarget( id_target=id_target, description=self.target_desc_entry.get().strip(), slot_address=int(self.target_slot_entry.get().strip(), 16), architecture=self.target_arch_combo.get(), name=self.target_name_entry.get().strip(), file_prefix=self.target_prefix_entry.get().strip(), id_model=id_model, golden_binary_path=golden_binary_path, user_binary_path=user_binary_path, ) self.manager.add_target(new_target) self._refresh_targets_list() self.refresh_callback() self._show_target_editor(new_target) # Select the saved target in list for idx, (id_t, t) in enumerate(sorted(self.manager.targets.items())): if id_t == id_target: self.targets_listbox.selection_clear(0, tk.END) self.targets_listbox.selection_set(idx) self.targets_listbox.see(idx) break # Refresh default target combo in global tab self.global_default_combo['values'] = list(self.manager.targets.keys()) messagebox.showinfo("Success", f"Target '{new_target.id_target}' saved successfully", parent=self.dialog) except ValueError as e: messagebox.showerror("Error", f"Invalid input: {e}", parent=self.dialog) def _delete_target(self) -> None: """Delete selected target.""" selection = self.targets_listbox.curselection() if not selection: messagebox.showwarning("No Selection", "Please select a target to delete.", parent=self.dialog) return targets_list = sorted(self.manager.targets.items()) id_target, target = targets_list[selection[0]] if messagebox.askyesno("Confirm Delete", f"Delete target '{target.id_target}'?", parent=self.dialog): self.manager.delete_target(id_target) self._refresh_targets_list() self.refresh_callback() self._show_target_editor(None) # Refresh default target combo in global tab self.global_default_combo['values'] = list(self.manager.targets.keys()) def _save_and_close(self) -> None: """Save configuration and close dialog.""" self.manager.save() self.refresh_callback() messagebox.showinfo("Success", "Configuration saved successfully", parent=self.dialog) self.dialog.destroy() def main() -> None: root = tk.Tk() app = FlasherGUI(root) root.mainloop() if __name__ == "__main__": main()