1207 lines
52 KiB
Python
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()
|