Files
Zynq_Flasher/src/config_manager.py
T
yuysh 4453fc8e12 feat: validate JTAG Zynq part matches config.zynq_part (default XC7Z100)
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).
2026-06-11 17:38:27 +08:00

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