PLC_Generation/IO Tree Configuration Generator/models/festo_solenoid_boilerplate_model.py
2025-08-05 14:38:54 +04:00

181 lines
7.1 KiB
Python

#!/usr/bin/env python3
"""
Festo Solenoid Module Boilerplate Model
======================================
Model for Festo Solenoid modules (VAEM-L1-S-8).
Supports configuring module name, parent module, port ID (should be even for M12DR masters), port address, and basic settings.
"""
from typing import Dict
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from datetime import datetime
import os
@dataclass
class FestoSolenoidConfig:
"""Configuration for a Festo solenoid module instance."""
name: str # Module name (e.g., "UL11_13_SOL1")
parent_module: str = "Master" # Parent IO-Link master module
parent_port_id: str = "4" # Port on the IO-Link master (should be even for M12DR: 2,4,6,8)
port_address: str = "0" # IO-Link port address
inhibited: bool = False
major_fault: bool = False
# Note: For M12DR masters, parent_port_id must be even (2,4,6,8)
class FestoSolenoidGenerator:
"""Generator for Festo solenoid module XML."""
def __init__(self, config: FestoSolenoidConfig):
self.config = config
self.boilerplate_path = os.path.join("boilerplate", "Festo_Solenoids_Module.L5X")
self.tree = None
self.root = None
def load_boilerplate(self):
"""Load the Festo solenoid boilerplate template."""
if not os.path.exists(self.boilerplate_path):
raise FileNotFoundError(f"Boilerplate file not found: {self.boilerplate_path}")
self.tree = ET.parse(self.boilerplate_path)
self.root = self.tree.getroot()
def update_module_name(self):
"""Update the module name throughout the XML."""
# Update in root attributes
self.root.set("TargetName", self.config.name)
# Update Module element
module = self.root.find(".//Module[@Use='Target']")
if module is not None:
module.set("Name", self.config.name)
def update_parent_module(self):
"""Update parent module references."""
module = self.root.find(".//Module[@Use='Target']")
if module is not None:
module.set("ParentModule", self.config.parent_module)
module.set("ParentModPortId", self.config.parent_port_id)
def update_port_address(self):
"""Update the IO-Link port address."""
port = self.root.find(".//Port[@Type='IO-Link']")
if port is not None:
port.set("Address", self.config.port_address)
def update_inhibited_status(self):
"""Update the inhibited and major fault status."""
module = self.root.find(".//Module[@Use='Target']")
if module is not None:
module.set("Inhibited", str(self.config.inhibited).lower())
module.set("MajorFault", str(self.config.major_fault).lower())
def update_export_date(self):
"""Update the export date to current time."""
export_date = datetime.now().strftime("%a %b %d %H:%M:%S %Y")
self.root.set("ExportDate", export_date)
def apply_updates(self):
"""Apply all updates to the boilerplate."""
self.update_module_name()
self.update_parent_module()
self.update_port_address()
self.update_inhibited_status()
self.update_export_date()
def save(self, output_path: str):
"""Save the updated module to file."""
if self.tree is None:
raise RuntimeError("No boilerplate loaded. Call load_boilerplate() first.")
# Save with proper formatting and preserve CDATA sections
xml_string = ET.tostring(self.root, encoding='unicode')
# Fix CDATA wrapper for L5K data - ElementTree strips CDATA sections
import re
# Pattern to find L5K data that needs CDATA wrapper
l5k_pattern = r'(<Data Format="L5K">)(\s*\[.*?\]|\s*\(.*?\))\s*(</Data>)'
def replace_with_cdata(match):
opening_tag = match.group(1)
data_content = match.group(2).strip()
closing_tag = match.group(3)
# Add proper indentation and line breaks
return f'{opening_tag}\n<![CDATA[{data_content}]]>\n{closing_tag}'
# Apply CDATA wrapper to L5K data
xml_string = re.sub(l5k_pattern, replace_with_cdata, xml_string, flags=re.DOTALL | re.MULTILINE)
# Write the corrected XML
with open(output_path, 'w', encoding='utf-8') as f:
f.write('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n')
f.write(xml_string)
def get_xml_string(self) -> str:
"""Get the XML as a string."""
if self.tree is None:
raise RuntimeError("No boilerplate loaded. Call load_boilerplate() first.")
return ET.tostring(self.root, encoding='unicode')
# ------------------------------------------------------------------
# Convenience helper used by EnhancedMCMGenerator's factory dispatch.
# ------------------------------------------------------------------
@classmethod
def from_mapping(cls, mapping: Dict[str, str]) -> "FestoSolenoidGenerator":
"""Create and fully configure a solenoid generator from the Excel-derived
entry (a plain dict). The structure expected is the one produced in
EnhancedMCMGenerator._organize_modules_by_type()."""
cfg = FestoSolenoidConfig(
name=mapping["name"],
parent_module=mapping["parent_module"],
parent_port_id=mapping["parent_port_id"],
port_address=mapping["port_address"],
)
# Optional: Validate even port for M12DR
if "M12DR" in mapping.get("model", "") and int(cfg.parent_port_id) % 2 != 0:
raise ValueError(f"Festo solenoid must connect to even port on M12DR, got {cfg.parent_port_id}")
gen = cls(cfg)
gen.load_boilerplate()
gen.apply_updates()
return gen
def create_festo_solenoid(name: str, parent_module: str = "Master", parent_port_id: str = "4",
port_address: str = "0") -> FestoSolenoidConfig:
"""Factory function to create a Festo solenoid configuration."""
return FestoSolenoidConfig(
name=name,
parent_module=parent_module,
parent_port_id=parent_port_id,
port_address=port_address,
)
# Example usage
if __name__ == "__main__":
# Example: Create a Festo solenoid with custom configuration
config = create_festo_solenoid(
name="UL11_13_SOL1",
parent_module="UL11_13_FIO1",
parent_port_id="4",
port_address="0",
)
generator = FestoSolenoidGenerator(config)
generator.load_boilerplate()
generator.apply_updates()
generator.save("generated/UL11_13_SOL1.L5X")
print(f"Generated Festo solenoid module: {config.name}")
print(f"Parent module: {config.parent_module}")
print(f"Port: {config.parent_port_id}")
print(f"Address: {config.port_address}")