#!/usr/bin/env python3 """ Siemens EXTENDO Module Boilerplate Model ======================================== Model for Siemens EXTENDO modules (6ES7 158-3MU10-0XA0). Supports name, IP address, and parent module configuration. """ from typing import Optional, TYPE_CHECKING import xml.etree.ElementTree as ET from dataclasses import dataclass from datetime import datetime import os if TYPE_CHECKING: from excel_data_processor import ModuleData @dataclass class ExtendoModuleConfig: """Configuration for a Siemens EXTENDO module instance.""" name: str # Module name (e.g., "EXTENDO1") ip_address: str = "112.131.213.123" parent_module: str = "SLOT2_EN4TR" parent_port_id: str = "2" class ExtendoModuleGenerator: """Generates Siemens EXTENDO module configurations from boilerplate.""" def __init__(self, config: ExtendoModuleConfig): self.config = config self.boilerplate_filename = "EXTENDO_Module.L5X" self.boilerplate_path = os.path.join("boilerplate", self.boilerplate_filename) self.tree = None self.root = None def load_boilerplate(self): """Load the boilerplate XML file.""" 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 in the XML.""" # Update module name module = self.root.find(".//Module[@Use='Target']") if module is not None: module.set("Name", self.config.name) # Update target name in root self.root.set("TargetName", self.config.name) def update_ip_address(self): """Update the IP address in the module configuration.""" # Find the Ethernet port and update IP address port = self.root.find(".//Port[@Type='Ethernet']") if port is not None: port.set("Address", self.config.ip_address) def update_parent_module(self): """Update the parent module configuration.""" 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_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 configuration updates.""" self.update_module_name() self.update_ip_address() self.update_parent_module() self.update_export_date() def save(self, output_path: str): """Save the configured module to a file.""" if self.tree is None: raise ValueError("No boilerplate loaded. Call load_boilerplate() first.") # Create output directory if it doesn't exist output_dir = os.path.dirname(output_path) if output_dir and not os.path.exists(output_dir): os.makedirs(output_dir) # Write the XML to file with proper formatting self._indent(self.root) # Convert to string and preserve CDATA sections xml_string = ET.tostring(self.root, encoding='unicode') full_xml = '\n' + xml_string # Fix CDATA sections that got stripped import re pattern = r'()\s*(\[\[.*?\]\])\s*()' def fix_cdata(match): start_tag = match.group(1) content = match.group(2) end_tag = match.group(3) return f'{start_tag}\n\n{end_tag}' full_xml = re.sub(pattern, fix_cdata, full_xml, flags=re.DOTALL) # Save the corrected XML with open(output_path, 'w', encoding='utf-8') as f: f.write(full_xml) def _indent(self, elem, level=0): """Add proper indentation to XML elements.""" i = "\n" + level * " " if len(elem): if not elem.text or not elem.text.strip(): elem.text = i + " " if not elem.tail or not elem.tail.strip(): elem.tail = i for child in elem: self._indent(child, level + 1) if not child.tail or not child.tail.strip(): child.tail = i else: if level and (not elem.tail or not elem.tail.strip()): elem.tail = i # ------------------------------------------------------------------ # Helper for EnhancedMCMGenerator refactor # ------------------------------------------------------------------ @classmethod def from_excel(cls, module_data: 'ModuleData') -> 'ExtendoModuleGenerator': cfg = create_extendo_module( name=module_data.tagname, ip_address=module_data.ip_address or "112.131.213.123", parent_module="SLOT2_EN4TR", parent_port_id="2", ) gen = cls(cfg) gen.load_boilerplate() gen.apply_updates() return gen def create_extendo_module(name: str, ip_address: str = "112.131.213.123", parent_module: str = "SLOT2_EN4TR", parent_port_id: str = "2") -> ExtendoModuleConfig: """ Factory function to create a Siemens EXTENDO module configuration. Args: name: Module name (e.g., "EXTENDO1") ip_address: IP address for the module (default: "112.131.213.123") parent_module: Parent module name (default: "SLOT2_EN4TR") parent_port_id: Parent port ID (default: "2") Returns: ExtendoModuleConfig: Configured Siemens EXTENDO module """ return ExtendoModuleConfig( name=name, ip_address=ip_address, parent_module=parent_module, parent_port_id=parent_port_id ) def main(): """Example usage of the Siemens EXTENDO module generator.""" print("Siemens EXTENDO Module Generator Example") print("=" * 42) # Create Siemens EXTENDO module configuration config = create_extendo_module( name="EXTENDO1", ip_address="112.131.213.200", parent_module="SLOT2_EN4TR" ) # Generate the module generator = ExtendoModuleGenerator(config) generator.load_boilerplate() generator.apply_updates() # Save to generated folder os.makedirs("generated", exist_ok=True) output_file = f"generated/{config.name}.L5X" generator.save(output_file) print(f"Generated Siemens EXTENDO module: {output_file}") print(f" Name: {config.name}") print(f" IP Address: {config.ip_address}") print(f" Parent Module: {config.parent_module}") print(f" Parent Port: {config.parent_port_id}") print("\nModule Features:") print(" - Siemens ET 200SP remote I/O") print(" - Input data: 15 bytes (11 SINT data + connection info)") print(" - Output data: 8 bytes (8 SINT data)") print(" - Catalog Number: 6ES7 158-3MU10-0XA0") print(" - Ethernet/IP communication") print(" - Vendor: Siemens AG") if __name__ == "__main__": main()