183 lines
7.2 KiB
Python
183 lines
7.2 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
|
|
# Use project-specific boilerplate directory if set, otherwise default
|
|
boilerplate_dir = os.environ.get('MCM_BOILERPLATE_DIR', 'boilerplate')
|
|
self.boilerplate_path = os.path.join(boilerplate_dir, "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}") |