"""Flash profile management for multi-target systems. This module provides classes to manage flash profiles (targets) with different SRIO endpoints, IP addresses, and flash configurations. The structure follows a three-level hierarchy: 1. GlobalConfig: System-wide settings (IP, port, SRIO base, etc.) 2. FlashModel: Reusable FPGA model definitions (memory map, sectors, etc.) 3. FlashTarget: Specific targets referencing a model by ID Reference: Vecchia_app FpgaFlashProfile system and targets.ini format. """ from __future__ import annotations import configparser import json from dataclasses import dataclass, asdict, field from pathlib import Path from typing import List, Optional, Dict @dataclass class GlobalConfig: """Global system configuration. Attributes: ip: Default IP address for SRIO communication. port: Default port for TFTP/SRIO communication. srio_base: SRIO base address (hex value). fpga_base: FPGA base address (hex value). fpga_sector: FPGA sector size (hex value). smart: Smart mode flag (0 or 1). section: Default flash section (e.g., "APP1"). default_target: Default target ID to use. """ ip: str = "192.168.2.102" port: int = 50069 srio_base: int = 0x500000 fpga_base: int = 0x0 fpga_sector: int = 0x010000 smart: int = 1 section: str = "APP1" default_target: str = "AESA_RFIF" verify_after_write: bool = False # Read-back verification after each chunk write # Retry configuration (at different levels) max_tftp_retries: int = 3 # TFTP transport level (SimpleTFTPClient) max_register_retries: int = 500 # Register write verification level (srio_flash.py) max_chunk_write_retries: int = 3 # Flash chunk write+verify level (core.py) def to_dict(self) -> dict: """Convert to dictionary for JSON serialization.""" return asdict(self) @staticmethod def from_dict(data: dict) -> GlobalConfig: """Create GlobalConfig from dictionary.""" return GlobalConfig(**data) @dataclass class FlashModel: """FPGA flash memory model definition. Attributes: id_model: Unique model identifier (e.g., 0, 1, 2). model: Model name (e.g., "xcku040", "rfif"). description: Human-readable description. flash_type: Flash type identifier (0 or 1). is_4byte_addressing: True if 4-byte addressing, False for 3-byte. num_sectors: Total number of flash sectors. golden_start: Golden area start address. golden_stop: Golden area stop address. user_start: User area start address. user_stop: User area stop address. test_address: Optional test address for verification. """ id_model: int model: str description: str flash_type: int is_4byte_addressing: bool num_sectors: int golden_start: int golden_stop: int user_start: int user_stop: int test_address: Optional[int] = None def to_dict(self) -> dict: """Convert to dictionary for JSON serialization.""" return asdict(self) @staticmethod def from_dict(data: dict) -> FlashModel: """Create FlashModel from dictionary.""" return FlashModel(**data) @dataclass class FlashTarget: """Individual flash target configuration. Attributes: id_target: Unique target identifier (e.g., "EIF_FPGA1"). description: Human-readable description. slot_address: SRIO slot/endpoint address (hex value). architecture: Target architecture (e.g., "Xilinx", "RFIF"). name: Target name (same as id_target usually). file_prefix: Prefix for firmware files. id_model: Reference to FlashModel ID. golden_binary_path: Optional path to golden area firmware binary. user_binary_path: Optional path to user area firmware binary. """ id_target: str description: str slot_address: int architecture: str name: str file_prefix: str id_model: int golden_binary_path: Optional[str] = None user_binary_path: Optional[str] = None def to_dict(self) -> dict: """Convert to dictionary for JSON serialization.""" return asdict(self) @staticmethod def from_dict(data: dict) -> FlashTarget: """Create FlashTarget from dictionary.""" return FlashTarget(**data) # Legacy class for backward compatibility @dataclass class FlashProfile: """Legacy flash target configuration (deprecated). This class is kept for backward compatibility with older code. New code should use FlashTarget + FlashModel instead. Attributes: name: Human-readable profile name (e.g., "Primary Flash", "Secondary Flash"). slot_address: SRIO endpoint/slot address (hex string, e.g., "0x10"). ip: Target IP address (e.g., "192.168.1.100"). port: TFTP port on target (default 69). base_address: Flash base address (hex, e.g., 0x01000000). size: Total flash size in bytes (e.g., 16777216 for 16 MB). binary_path: Optional path to binary file associated with this profile. """ name: str slot_address: str ip: str port: int = 69 base_address: int = 0x01000000 size: int = 16 * 1024 * 1024 # 16 MB default binary_path: Optional[str] = None def to_dict(self) -> dict: """Convert to dictionary for JSON serialization.""" return asdict(self) @staticmethod def from_dict(data: dict) -> FlashProfile: """Create FlashProfile from dictionary.""" return FlashProfile(**data) class ProfileManager: """Manages flash targets, models, and global configuration. This manager handles the complete flash programming configuration including: - Global system settings (IP, port, SRIO addresses) - Flash models (FPGA types with memory maps) - Flash targets (specific FPGA instances) Supports both JSON and INI file formats for import/export. """ def __init__(self, config_path: Optional[Path] = None) -> None: """Initialize ProfileManager. Args: config_path: Path to JSON configuration file. Defaults to "flash_profiles.json". """ self.global_config = GlobalConfig() self.models: Dict[int, FlashModel] = {} self.targets: Dict[str, FlashTarget] = {} self.config_path = config_path or Path("flash_profiles.json") if self.config_path.exists(): self.load() def add_model(self, model: FlashModel) -> None: """Add or update a flash model.""" self.models[model.id_model] = model def add_target(self, target: FlashTarget) -> None: """Add or update a flash target.""" self.targets[target.id_target] = target def get_model(self, id_model: int) -> Optional[FlashModel]: """Get model by ID.""" return self.models.get(id_model) def get_target(self, id_target: str) -> Optional[FlashTarget]: """Get target by ID.""" return self.targets.get(id_target) def get_target_with_model(self, id_target: str) -> Optional[tuple[FlashTarget, FlashModel]]: """Get target together with its associated model. Args: id_target: Target ID to retrieve. Returns: Tuple of (FlashTarget, FlashModel) if found, None otherwise. """ target = self.get_target(id_target) if not target: return None model = self.get_model(target.id_model) if not model: return None return (target, model) def delete_model(self, id_model: int) -> bool: """Delete model by ID. Returns True if found and deleted.""" if id_model in self.models: del self.models[id_model] return True return False def delete_target(self, id_target: str) -> bool: """Delete target by ID. Returns True if found and deleted.""" if id_target in self.targets: del self.targets[id_target] return True return False def list_models(self) -> List[int]: """Return list of model IDs.""" return list(self.models.keys()) def list_targets(self) -> List[str]: """Return list of target IDs.""" return list(self.targets.keys()) def save(self) -> None: """Save configuration to JSON file.""" data = { "global_config": self.global_config.to_dict(), "models": {k: v.to_dict() for k, v in self.models.items()}, "targets": {k: v.to_dict() for k, v in self.targets.items()}, } with open(self.config_path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2) def load(self) -> None: """Load configuration from JSON file.""" with open(self.config_path, "r", encoding="utf-8") as f: data = json.load(f) # Load global config if "global_config" in data: self.global_config = GlobalConfig.from_dict(data["global_config"]) # Load models if "models" in data: self.models = { int(k): FlashModel.from_dict(v) for k, v in data["models"].items() } # Load targets if "targets" in data: self.targets = { k: FlashTarget.from_dict(v) for k, v in data["targets"].items() } def load_from_ini(self, ini_path: Path) -> None: """Load configuration from INI file (targets.ini format). Args: ini_path: Path to INI configuration file. """ parser = configparser.ConfigParser() parser.read(ini_path, encoding="utf-8") # Parse global config from [default] section if "default" in parser: section = parser["default"] self.global_config.ip = section.get("ip", self.global_config.ip).strip('"') self.global_config.port = section.getint("port", self.global_config.port) self.global_config.smart = section.getint("smart", self.global_config.smart) self.global_config.section = section.get("section", self.global_config.section) # Parse hex values if "srio_base" in section: self.global_config.srio_base = int(section["srio_base"].replace("$", ""), 16) if "fpga_base" in section: self.global_config.fpga_base = int(section["fpga_base"].replace("$", ""), 16) if "fpga_sector" in section: self.global_config.fpga_sector = int(section["fpga_sector"].replace("$", ""), 16) if "defTarget" in section: self.global_config.default_target = section["defTarget"] # Parse models from [MODEL-x] sections for section_name in parser.sections(): if section_name.startswith("MODEL-"): section = parser[section_name] id_model = section.getint("idModel") model = FlashModel( id_model=id_model, model=section.get("model", ""), description=section.get("description", ""), flash_type=section.getint("type", 0), is_4byte_addressing=bool(section.getint("3_4Byte", 0)), num_sectors=section.getint("numSector", 0), golden_start=int(section.get("goldenAddressStartArea", "0x0"), 16), golden_stop=int(section.get("goldenAddressStopArea", "0x0"), 16), user_start=int(section.get("userAddressStartArea", "0x0"), 16), user_stop=int(section.get("userAddresStopArea", "0x0"), 16), ) if "testAddress" in section: model.test_address = int(section["testAddress"], 16) self.add_model(model) # Parse targets from [TGT-x] sections for section_name in parser.sections(): if section_name.startswith("TGT-"): section = parser[section_name] id_target = section.get("idTgt", "") target = FlashTarget( id_target=id_target, description=section.get("description", ""), slot_address=int(section.get("slotAddress", "0x0"), 16), architecture=section.get("arch", ""), name=section.get("name", id_target), file_prefix=section.get("filePrefix", id_target), id_model=section.getint("idModel", 0), ) self.add_target(target) def export_to_ini(self, ini_path: Path) -> None: """Export configuration to INI file (targets.ini format). Args: ini_path: Path where to save INI file. """ parser = configparser.ConfigParser() # Write [default] section parser["default"] = { "port": str(self.global_config.port), "ip": f'"{self.global_config.ip}"', "smart": str(self.global_config.smart), "section": self.global_config.section, "srio_base": f"$0x{self.global_config.srio_base:X}", "fpga_base": f"$0x{self.global_config.fpga_base:X}", "fpga_sector": f"0x{self.global_config.fpga_sector:06X}", "defTarget": self.global_config.default_target, } # Write [MODEL-x] sections for id_model, model in sorted(self.models.items()): section_name = f"MODEL-{id_model}" parser[section_name] = { "idModel": str(model.id_model), "model": model.model, "description": model.description, "type": str(model.flash_type), "3_4Byte": "1" if model.is_4byte_addressing else "0", "numSector": str(model.num_sectors), "goldenAddressStartArea": f"0x{model.golden_start:08X}", "goldenAddressStopArea": f"0x{model.golden_stop:08X}", "userAddressStartArea": f"0x{model.user_start:08X}", "userAddresStopArea": f"0x{model.user_stop:08X}", } if model.test_address is not None: parser[section_name]["testAddress"] = f"0x{model.test_address:08X}" # Write [TGT-x] sections for idx, (id_target, target) in enumerate(sorted(self.targets.items())): section_name = f"TGT-{idx}" parser[section_name] = { "idTgt": target.id_target, "description": target.description, "slotAddress": f"0x{target.slot_address:02X}", "arch": target.architecture, "name": target.name, "filePrefix": target.file_prefix, "idModel": str(target.id_model), } with open(ini_path, "w", encoding="utf-8") as f: parser.write(f) def create_default_profiles() -> List[FlashProfile]: """Create sample legacy flash profiles for testing (deprecated). This function is kept for backward compatibility. Use ProfileManager.load_from_ini() for new code. """ return [ FlashProfile( name="Primary Flash", slot_address="0x10", ip="192.168.1.100", port=69, base_address=0x01000000, size=16 * 1024 * 1024, ), FlashProfile( name="Secondary Flash", slot_address="0x11", ip="192.168.1.100", port=69, base_address=0x02000000, size=16 * 1024 * 1024, ), FlashProfile( name="Localhost Emulator", slot_address="0xFF", ip="127.0.0.1", port=6969, base_address=0x00000000, size=16 * 1024 * 1024, ), ] def initialize_from_targets_ini( ini_path: Path = Path("_OLD/Vecchia_app/FpgaBeamMeUp/targets.ini"), json_path: Path = Path("flash_profiles.json") ) -> ProfileManager: """Initialize ProfileManager from legacy targets.ini file. This function reads the original targets.ini and creates a new JSON configuration file with all models and targets properly structured. Args: ini_path: Path to targets.ini file. json_path: Path where to save JSON configuration. Returns: Initialized ProfileManager with all data loaded. """ manager = ProfileManager(config_path=json_path) manager.load_from_ini(ini_path) manager.save() return manager