Step 1 now compares detected part (e.g. XC7Z035) against config's zynq_part. If mismatch: log error, return False to block subsequent steps. Also returns False when no Zynq detected (was always True).
226 lines
6.9 KiB
Python
226 lines
6.9 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,
|
|
"firmware_bin_path": "",
|
|
"reboot_timeout": 30,
|
|
"ping_timeout": 5,
|
|
"ping_count": 3,
|
|
"uart_delay": 3,
|
|
"inter_step_delay": 2,
|
|
"boot_wait_delay": 10,
|
|
"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
|
|
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 = 10
|
|
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,
|
|
"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.
|
|
|
|
Args:
|
|
path: Absolute path string.
|
|
|
|
Returns:
|
|
Relative path string, or the original if conversion fails.
|
|
"""
|
|
if not path:
|
|
return ""
|
|
try:
|
|
target = Path(path)
|
|
if target.is_absolute():
|
|
return str(target.relative_to(self._config_path.parent))
|
|
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()
|
|
|
|
@property
|
|
def config_path(self) -> Path | None:
|
|
"""Return the path to the config file on disk."""
|
|
return self._config_path
|