210 lines
7.5 KiB
Python
210 lines
7.5 KiB
Python
#!/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 = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' + xml_string
|
|
|
|
# Fix CDATA sections that got stripped
|
|
import re
|
|
pattern = r'(<Data Format="L5K">)\s*(\[\[.*?\]\])\s*(</Data>)'
|
|
def fix_cdata(match):
|
|
start_tag = match.group(1)
|
|
content = match.group(2)
|
|
end_tag = match.group(3)
|
|
return f'{start_tag}\n<![CDATA[{content}]]>\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() |