SXXXXXXX_PyDownloadFwViaSRIO/pydownloadfwviasrio/gui/gui.py
2026-01-22 17:19:12 +01:00

1207 lines
52 KiB
Python

"""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("<<ComboboxSelected>>", 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("<<ListboxSelect>>", 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("<<ListboxSelect>>", 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()