- TftpSubStepFrame.set_step_skipped(): ◌ Skip in WARNING color - _reload_config(): suppress callbacks to avoid duplicate log/status wipe - _log_all_file_info(): removed clear_info(), updates in-place per-key - Removed duplicate _log_file_info method - main_frame grid: uniform col group for strict 7:3 ratio - FileSelector browse title shows expected file type hint - boot_wait_delay: default 10→30s - Step 4 IP fallback: ping first, then UDP scan, extra boot_delay retry
261 lines
8.1 KiB
Python
261 lines
8.1 KiB
Python
"""Configuration manager for Zynq Flasher GUI.
|
|
|
|
Handles loading and saving YAML configuration files.
|
|
All paths are stored relative to the config file's directory.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import yaml
|
|
|
|
|
|
DEFAULT_CONFIG: dict[str, Any] = {
|
|
"xilinx_path": "",
|
|
"zynq_ip": "192.168.100.11",
|
|
"zynq_part": "XC7Z100",
|
|
"tftp_upload_name": "z7bin",
|
|
"serial_port": "",
|
|
"serial_baudrate": 115200,
|
|
"bootloader_bit_path": "",
|
|
"bootloader_elf_path": "",
|
|
"bootloader_bin_path": "",
|
|
"fsbl_elf_path": "",
|
|
"flash_type": "qspi-x4-single",
|
|
"flash_model": "s25fl256s1",
|
|
"erase_all": False,
|
|
"download_verify": True,
|
|
"firmware_bin_path": "",
|
|
"reboot_timeout": 30,
|
|
"ping_timeout": 5,
|
|
"ping_count": 3,
|
|
"uart_delay": 3,
|
|
"inter_step_delay": 2,
|
|
"boot_wait_delay": 30,
|
|
"step_timeouts": {
|
|
"check_env": 120,
|
|
"flash_erase": 120,
|
|
"flash_program": 600,
|
|
"bitstream": 60,
|
|
"elf_download": 60,
|
|
"tftp_reboot": 120,
|
|
},
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class Config:
|
|
"""Application configuration loaded from a YAML file.
|
|
|
|
All file paths are stored as relative paths from the config file's
|
|
directory and resolved to absolute paths when accessed.
|
|
"""
|
|
|
|
xilinx_path: str = ""
|
|
zynq_ip: str = "192.168.100.11"
|
|
zynq_part: str = "XC7Z100"
|
|
tftp_upload_name: str = "z7bin"
|
|
serial_port: str = ""
|
|
serial_baudrate: int = 115200
|
|
bootloader_bit_path: str = ""
|
|
bootloader_elf_path: str = ""
|
|
bootloader_bin_path: str = ""
|
|
fsbl_elf_path: str = ""
|
|
flash_type: str = "qspi-x4-single"
|
|
flash_model: str = "s25fl256s1"
|
|
erase_all: bool = False
|
|
download_verify: bool = True
|
|
firmware_bin_path: str = ""
|
|
reboot_timeout: int = 30
|
|
ping_timeout: int = 5
|
|
ping_count: int = 3
|
|
uart_delay: int = 3
|
|
inter_step_delay: int = 2
|
|
boot_wait_delay: int = 30
|
|
step_timeouts: dict[str, int] = field(default_factory=lambda: {})
|
|
|
|
# Internal: path to the config file on disk
|
|
_config_path: Path = field(default=None, init=False, repr=False)
|
|
|
|
@classmethod
|
|
def from_file(cls, path: str | Path) -> Config:
|
|
"""Load configuration from a YAML file.
|
|
|
|
Args:
|
|
path: Path to the YAML configuration file.
|
|
|
|
Returns:
|
|
A Config instance populated with values from the file.
|
|
|
|
Raises:
|
|
FileNotFoundError: If the config file does not exist.
|
|
yaml.YAMLError: If the file contains invalid YAML.
|
|
"""
|
|
config_path = Path(path).resolve()
|
|
if not config_path.exists():
|
|
raise FileNotFoundError(f"Config file not found: {config_path}")
|
|
|
|
with open(config_path, "r", encoding="utf-8") as f:
|
|
data = yaml.safe_load(f) or {}
|
|
|
|
config = cls()
|
|
config._config_path = config_path
|
|
config._apply_data(data)
|
|
return config
|
|
|
|
@classmethod
|
|
def from_default(cls) -> Config:
|
|
"""Create a Config with default values (no file path).
|
|
|
|
Returns:
|
|
A Config instance with default values.
|
|
"""
|
|
return cls()
|
|
|
|
@classmethod
|
|
def create_default(cls, path: str | Path) -> Config:
|
|
"""Create a default config file and return a Config instance.
|
|
|
|
Args:
|
|
path: Where to write the default config file.
|
|
|
|
Returns:
|
|
A Config instance loaded from the newly created file.
|
|
"""
|
|
config_path = Path(path).resolve()
|
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with open(config_path, "w", encoding="utf-8") as f:
|
|
yaml.dump(DEFAULT_CONFIG, f, default_flow_style=False)
|
|
|
|
return cls.from_file(config_path)
|
|
|
|
def _apply_data(self, data: dict[str, Any]) -> None:
|
|
"""Apply configuration data from a dictionary.
|
|
|
|
Args:
|
|
data: Dictionary of configuration key-value pairs.
|
|
"""
|
|
for key, value in data.items():
|
|
if hasattr(self, key):
|
|
setattr(self, key, value)
|
|
|
|
def save(self) -> None:
|
|
"""Save the current configuration to the YAML file.
|
|
|
|
Resolves all paths to be relative to the config file's directory.
|
|
|
|
Raises:
|
|
RuntimeError: If no config file path is set.
|
|
"""
|
|
if self._config_path is None:
|
|
raise RuntimeError("No config file path set; use from_file() or create_default()")
|
|
|
|
data = self._to_dict()
|
|
with open(self._config_path, "w", encoding="utf-8") as f:
|
|
yaml.dump(data, f, default_flow_style=False)
|
|
|
|
def _to_dict(self) -> dict[str, Any]:
|
|
"""Convert configuration to a dictionary with relative paths.
|
|
|
|
Returns:
|
|
Dictionary representation of the config.
|
|
"""
|
|
data = {
|
|
"xilinx_path": self.xilinx_path,
|
|
"zynq_ip": self.zynq_ip,
|
|
"tftp_upload_name": self.tftp_upload_name,
|
|
"serial_port": self.serial_port,
|
|
"serial_baudrate": self.serial_baudrate,
|
|
"bootloader_bit_path": self._relative_path(self.bootloader_bit_path),
|
|
"bootloader_elf_path": self._relative_path(self.bootloader_elf_path),
|
|
"bootloader_bin_path": self._relative_path(self.bootloader_bin_path),
|
|
"fsbl_elf_path": self._relative_path(self.fsbl_elf_path),
|
|
"flash_type": self.flash_type,
|
|
"flash_model": self.flash_model,
|
|
"erase_all": self.erase_all,
|
|
"download_verify": self.download_verify,
|
|
"firmware_bin_path": self._relative_path(self.firmware_bin_path),
|
|
"reboot_timeout": self.reboot_timeout,
|
|
"ping_timeout": self.ping_timeout,
|
|
"ping_count": self.ping_count,
|
|
"uart_delay": self.uart_delay,
|
|
"inter_step_delay": self.inter_step_delay,
|
|
"boot_wait_delay": self.boot_wait_delay,
|
|
"step_timeouts": self.step_timeouts,
|
|
}
|
|
return data
|
|
|
|
def _relative_path(self, path: str) -> str:
|
|
"""Convert an absolute path to a relative path from the config file.
|
|
|
|
Uses :func:`os.path.relpath` which works across filesystem
|
|
boundaries (unlike :meth:`Path.relative_to`).
|
|
|
|
Args:
|
|
path: Absolute or relative path string.
|
|
|
|
Returns:
|
|
Relative path string, or the original if conversion fails.
|
|
"""
|
|
if not path:
|
|
return ""
|
|
try:
|
|
target = Path(path)
|
|
if target.is_absolute():
|
|
base = str(self._config_path.parent)
|
|
return os.path.relpath(str(target), base)
|
|
except (ValueError, OSError):
|
|
pass
|
|
return path
|
|
|
|
def resolve_path(self, relative_path: str) -> Path:
|
|
"""Resolve a relative config path to an absolute path.
|
|
|
|
Args:
|
|
relative_path: Path relative to the config file's directory.
|
|
|
|
Returns:
|
|
Absolute Path object.
|
|
"""
|
|
if not relative_path:
|
|
return Path()
|
|
return (self._config_path.parent / relative_path).resolve()
|
|
|
|
def validate_paths(self) -> list[str]:
|
|
"""Validate file paths in config and clear those that don't exist.
|
|
|
|
Checks each file path field, resolves it against the config
|
|
directory, and clears it if the target file does not exist
|
|
on the current filesystem.
|
|
|
|
Returns:
|
|
List of field names that were cleared.
|
|
"""
|
|
cleared: list[str] = []
|
|
path_fields = [
|
|
"bootloader_bit_path",
|
|
"bootloader_elf_path",
|
|
"bootloader_bin_path",
|
|
"fsbl_elf_path",
|
|
"firmware_bin_path",
|
|
]
|
|
for attr in path_fields:
|
|
val = getattr(self, attr, "")
|
|
if not val:
|
|
continue
|
|
resolved = self.resolve_path(val)
|
|
if not resolved.is_file():
|
|
setattr(self, attr, "")
|
|
cleared.append(attr)
|
|
return cleared
|
|
|
|
@property
|
|
def config_path(self) -> Path | None:
|
|
"""Return the path to the config file on disk."""
|
|
return self._config_path
|