feat: initial Zynq XC7Z100 Flasher GUI project
Add cross-platform GUI application for programming Zynq devices with step-by-step workflow: - CustomTkinter GUI with dark/light theme support - 4-step workflow: Check Env → Flash → Bootloader → Main Program - YAML configuration with relative path resolution - Serial monitoring and boot log parsing - TFTP upload with CRC32 verification - SSH/serial reboot management - Vivado/Impact tool auto-detection Project structure: - app.py: Entry point - src/: Core modules (config, flash, bitstream, tftp, serial, gui) - config/: Default configuration template - .flasher_env/: Python virtual environment
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
"""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] = {
|
||||
"vitis_path": "",
|
||||
"zynq_ip": "192.168.100.11",
|
||||
"tftp_upload_name": "z7bin",
|
||||
"serial_port": "",
|
||||
"serial_baudrate": 115200,
|
||||
"bin_path": "",
|
||||
"elf_path": "",
|
||||
"bit_path": "",
|
||||
"reboot_timeout": 30,
|
||||
"ping_timeout": 5,
|
||||
"ping_count": 3,
|
||||
}
|
||||
|
||||
|
||||
@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.
|
||||
"""
|
||||
|
||||
vitis_path: str = ""
|
||||
zynq_ip: str = "192.168.100.11"
|
||||
tftp_upload_name: str = "z7bin"
|
||||
serial_port: str = ""
|
||||
serial_baudrate: int = 115200
|
||||
bin_path: str = ""
|
||||
elf_path: str = ""
|
||||
bit_path: str = ""
|
||||
reboot_timeout: int = 30
|
||||
ping_timeout: int = 5
|
||||
ping_count: int = 3
|
||||
|
||||
# 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 = {
|
||||
"vitis_path": self._relative_path(self.vitis_path),
|
||||
"zynq_ip": self.zynq_ip,
|
||||
"tftp_upload_name": self.tftp_upload_name,
|
||||
"serial_port": self.serial_port,
|
||||
"serial_baudrate": self.serial_baudrate,
|
||||
"bin_path": self._relative_path(self.bin_path),
|
||||
"elf_path": self._relative_path(self.elf_path),
|
||||
"bit_path": self._relative_path(self.bit_path),
|
||||
"reboot_timeout": self.reboot_timeout,
|
||||
"ping_timeout": self.ping_timeout,
|
||||
"ping_count": self.ping_count,
|
||||
}
|
||||
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
|
||||
Reference in New Issue
Block a user