#!/usr/bin/env python3 """ Turck Hub Module Boilerplate Model ================================== Model for Turck Hub (TBIL-M1-16DXP) modules with support for different configurations. Supports Chute_Load, Chute_Chute, Load_Chute, and PDP_FIOH variants. Important Constraints: - Port addresses must be even numbers only (0, 2, 4, 6, 8, 10, 12, 14) - Maximum port address is 14 """ from typing import Dict, Optional, List from typing import 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, IOPathMapping @dataclass class TurckHubModuleConfig: """Configuration for a Turck Hub module instance.""" name: str # Module name (e.g., "Chute_Load_Hub1") variant: str # Module variant: "Chute_Load", "Chute_Chute", "Load_Chute", or "PDP_FIOH" parent_module: str = "D2CMaster" parent_port_id: str = "4" port_address: str = "2" # Port address: Must be even number (0, 2, 4, 6, 8, 10, 12, 14) inhibited: bool = False major_fault: bool = False input_comments: Optional[Dict[str, str]] = None # Key: operand (e.g., ".PROCESSDATAIN.CONNECTOR_4_A_PIN_4"), Value: comment output_comments: Optional[Dict[str, str]] = None # Key: operand (e.g., ".PROCESSDATAOUT.CONNECTOR_3_B_PIN_2"), Value: comment def __post_init__(self): """Validate configuration after initialization.""" self._validate_port_address() def _validate_port_address(self): """Validate that port address is an even number between 0-14.""" try: addr = int(self.port_address) if addr < 0 or addr > 14: raise ValueError(f"Port address must be between 0-14, got: {addr}") if addr % 2 != 0: raise ValueError(f"Port address must be even number, got: {addr}") except ValueError as e: if "invalid literal" in str(e): raise ValueError(f"Port address must be a valid integer, got: '{self.port_address}'") raise class TurckHubModuleGenerator: """Generator for Turck Hub module XML with different variant support. Note: Port addresses must be even numbers between 0-14 (0, 2, 4, 6, 8, 10, 12, 14). """ # Mapping of variants to boilerplate filenames VARIANT_BOILERPLATE_MAP = { "Chute_Load": "Chute_Load_Hub_Module.L5X", "Chute_Chute": "Chute_Chute_Hub_Module.L5X", "Load_Chute": "Load_Chute_Hub_Module.L5X", "PDP_FIOH": "PDP_FIOH_Module.L5X", "FL_Hub": "FL_Hub_Module.L5X" } # Default port addresses for each variant (FIOH must be on 6 or 14) VARIANT_PORT_ADDRESSES = { "Chute_Load": "6", # Fixed: was "2", now proper FIOH address "Chute_Chute": "6", # Fixed: was "0", now proper FIOH address "Load_Chute": "14", # Fixed: was "8", now proper FIOH address "PDP_FIOH": "6", # PDP FIOH modules default to address 6 "FL_Hub": "6" # FL Hub modules default to address 6 } def __init__(self, config: TurckHubModuleConfig): self.config = config # Determine the correct boilerplate file self.boilerplate_filename = self._determine_boilerplate_filename() self.boilerplate_path = os.path.join("boilerplate", self.boilerplate_filename) # Set default port address if not specified if not self.config.port_address: self.config.port_address = self.VARIANT_PORT_ADDRESSES[self.config.variant] self.tree = None self.root = None def _determine_boilerplate_filename(self) -> str: """Determine the boilerplate filename to use. Priority: 1. If "FL" is in the module name, use FL_Hub_Module.L5X 2. Check for module-specific boilerplate: {module_name}_Module.L5X 3. Fall back to variant-based boilerplate """ # First, check if "FL" is in the module name if "FL" in self.config.name.upper(): fl_hub_filename = "FL_Hub_Module.L5X" print(f" {self.config.name}: Detected 'FL' in name, using FL_Hub boilerplate {fl_hub_filename}") return fl_hub_filename # Second, try module-specific boilerplate module_specific_filename = f"{self.config.name}_Module.L5X" module_specific_path = os.path.join("boilerplate", module_specific_filename) if os.path.exists(module_specific_path): print(f" {self.config.name} (FIOH {self.config.variant}): Using module-specific boilerplate {module_specific_filename}") return module_specific_filename # Fall back to variant-based boilerplate if self.config.variant not in self.VARIANT_BOILERPLATE_MAP: raise ValueError(f"Unsupported variant: {self.config.variant}. Supported variants: {list(self.VARIANT_BOILERPLATE_MAP.keys())}") fallback_filename = self.VARIANT_BOILERPLATE_MAP[self.config.variant] print(f" {self.config.name} (FIOH {self.config.variant}): Using variant boilerplate {fallback_filename}") return fallback_filename def load_boilerplate(self): """Load the appropriate boilerplate template based on variant.""" 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 port address.""" port = self.root.find(".//Port[@Type='IO-Link']") if port is not None: port.set("Address", self.config.port_address) else: print(f" ERROR: Could not find IO-Link port for {self.config.name}") def update_inhibited_status(self): """Update the inhibited status.""" module = self.root.find(".//Module[@Use='Target']") if module is not None: module.set("Inhibited", "true" if self.config.inhibited else "false") module.set("MajorFault", "true" if self.config.major_fault else "false") def update_input_comments(self): """Update input tag comments.""" if self.config.input_comments: input_tag = self.root.find(".//Connection[@Name='_2004250069802D0028802D005304']/InputTag") if input_tag is not None: # Find or create Comments section input_comments = input_tag.find("Comments") if input_comments is None: # Create Comments section as the first child input_comments = ET.Element("Comments") input_tag.insert(0, input_comments) else: # Clear existing comments input_comments.clear() # Add new comments for operand, comment_text in self.config.input_comments.items(): comment = ET.SubElement(input_comments, "Comment") comment.set("Operand", operand) comment.text = comment_text def update_output_comments(self): """Update output tag comments.""" if self.config.output_comments: output_tag = self.root.find(".//Connection[@Name='_2004250069802D0028802D005304']/OutputTag") if output_tag is not None: # Find or create Comments section output_comments = output_tag.find("Comments") if output_comments is None: # Create Comments section as the first child output_comments = ET.Element("Comments") output_tag.insert(0, output_comments) else: # Clear existing comments output_comments.clear() # Add new comments for operand, comment_text in self.config.output_comments.items(): comment = ET.SubElement(output_comments, "Comment") comment.set("Operand", operand) comment.text = comment_text 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_input_comments() self.update_output_comments() self.update_export_date() def save(self, output_path: str): """Save the updated module to file, preserving CDATA sections.""" if self.tree is None: raise RuntimeError("No boilerplate loaded. Call load_boilerplate() first.") # Read the original boilerplate file to preserve formatting and CDATA with open(self.boilerplate_path, 'r', encoding='utf-8') as f: original_content = f.read() # Apply our updates by doing string replacements on the original content updated_content = self._apply_updates_to_content(original_content) # Write the updated content with open(output_path, 'w', encoding='utf-8') as f: f.write(updated_content) def _apply_updates_to_content(self, content: str) -> str: """Apply updates to the original XML content via string replacement.""" import re # Update TargetName in root element content = re.sub( r'TargetName="[^"]*"', f'TargetName="{self.config.name}"', content ) # Update ExportDate export_date = datetime.now().strftime("%a %b %d %H:%M:%S %Y") content = re.sub( r'ExportDate="[^"]*"', f'ExportDate="{export_date}"', content ) # Update Module Name content = re.sub( r' str: """Update input comments in the content string.""" import re # First try to find existing Comments section pattern = r'(]*>\s*)(.*?)()' def replace_comments(match): start = match.group(1) end = match.group(3) # Determine base indentation from existing block m_indent = re.search(r"\n(\s*)<", start) base_indent = m_indent.group(1) if m_indent else " " # 12 spaces as fallback # Build comments exactly like boiler-plate pieces = [] for operand, txt in self.config.input_comments.items(): pieces.extend([ f"{base_indent}", f"{base_indent} ", f"{base_indent}" ]) return f"{start}\n" + "\n".join(pieces) + f"\n{base_indent}{end}" # Try to replace existing Comments section new_content = re.sub(pattern, replace_comments, content, flags=re.DOTALL) # If no replacement was made, we need to create the Comments section if new_content == content: # Find InputTag and insert Comments section input_tag_pattern = r'(]*>)(\s*"] for operand, txt in self.config.input_comments.items(): pieces.extend([ f"{base_indent}", f"{base_indent} ", f"{base_indent}" ]) pieces.append(f"{base_indent}") return "\n".join(pieces) + data_start new_content = re.sub(input_tag_pattern, insert_comments, content, flags=re.DOTALL) return new_content def _update_output_comments_in_content(self, content: str) -> str: """Update output comments in the content string.""" import re # First try to find existing Comments section pattern = r'(]*>\s*)(.*?)()' def replace_comments(match): start = match.group(1) end = match.group(3) import re m_indent = re.search(r"\n(\s*)<", start) base_indent = m_indent.group(1) if m_indent else " " pieces = [] for operand, txt in self.config.output_comments.items(): pieces.extend([ f"{base_indent}", f"{base_indent} ", f"{base_indent}" ]) return f"{start}\n" + "\n".join(pieces) + f"\n{base_indent}{end}" # Try to replace existing Comments section new_content = re.sub(pattern, replace_comments, content, flags=re.DOTALL) # If no replacement was made, we need to create the Comments section if new_content == content: # Find OutputTag and insert Comments section output_tag_pattern = r'(]*>)(\s*"] for operand, txt in self.config.output_comments.items(): pieces.extend([ f"{base_indent}", f"{base_indent} ", f"{base_indent}" ]) pieces.append(f"{base_indent}") return "\n".join(pieces) + data_start new_content = re.sub(output_tag_pattern, insert_comments, content, flags=re.DOTALL) return new_content 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') # ------------------------------------------------------------------ # Factory helper for EnhancedMCMGenerator refactor # ------------------------------------------------------------------ @classmethod def from_excel(cls, module_data: "ModuleData") -> "TurckHubModuleGenerator": """Create, configure, and return generator directly from Excel data.""" variant = _determine_variant(module_data) parent_module, parent_port_id, port_address = _parent_info(module_data) input_comments, output_comments = _extract_comments(module_data) cfg = create_turck_hub_module( name=module_data.tagname, variant=variant, parent_module=parent_module, parent_port_id=parent_port_id, port_address=port_address, input_comments=input_comments if input_comments else None, output_comments=output_comments if output_comments else None, ) gen = cls(cfg) gen.load_boilerplate() gen.apply_updates() return gen def get_valid_port_addresses() -> List[str]: """Get list of valid port addresses for Turck Hub modules. Returns: List of valid port addresses: ['0', '2', '4', '6', '8', '10', '12', '14'] """ return [str(i) for i in range(0, 15, 2)] def create_turck_hub_module(name: str, variant: str, parent_module: str = "D2CMaster", parent_port_id: str = "4", port_address: str = None, input_comments: Optional[Dict[str, str]] = None, output_comments: Optional[Dict[str, str]] = None, inhibited: bool = False, major_fault: bool = False) -> TurckHubModuleConfig: """Factory function to create a Turck Hub module configuration. Args: name: Module name variant: One of "Chute_Load", "Chute_Chute", "Load_Chute", "PDP_FIOH" parent_module: Parent module name parent_port_id: Parent module port ID port_address: IO-Link port address - must be even number (0, 2, 4, 6, 8, 10, 12, 14) If None, uses variant default input_comments: Dict of input tag comments output_comments: Dict of output tag comments inhibited: Whether module starts inhibited major_fault: Whether module starts with major fault Returns: TurckHubModuleConfig instance Raises: ValueError: If port_address is not a valid even number between 0-14 """ return TurckHubModuleConfig( name=name, variant=variant, parent_module=parent_module, parent_port_id=parent_port_id, port_address=port_address, input_comments=input_comments, output_comments=output_comments, inhibited=inhibited, major_fault=major_fault ) # Helper functions to get default comment structures for each variant def get_chute_load_default_input_comments() -> Dict[str, str]: """Get default input comments for Chute_Load variant.""" return { ".PROCESSDATAIN.CONNECTOR_4_A_PIN_4": "PE", ".PROCESSDATAIN.CONNECTOR_3_A_PIN_4": "PB In", ".PROCESSDATAIN.CONNECTOR_2_A_PIN_4": "PE 50", ".PROCESSDATAIN.CONNECTOR_6_A_PIN_4": "PB In", ".PROCESSDATAIN.CONNECTOR_5_A_PIN_4": "PE" } def get_chute_load_default_output_comments() -> Dict[str, str]: """Get default output comments for Chute_Load variant.""" return { ".PROCESSDATAOUT.CONNECTOR_3_B_PIN_2": "PB LT Out", ".PROCESSDATAOUT.CONNECTOR_1_B_PIN_2": "Beacon Segment2", ".PROCESSDATAOUT.CONNECTOR_1_A_PIN_4": "Beacon Segment1", ".PROCESSDATAOUT.CONNECTOR_8_A_PIN_4": "Sol", ".PROCESSDATAOUT.CONNECTOR_7_B_PIN_2": "Beacon Segment2", ".PROCESSDATAOUT.CONNECTOR_7_A_PIN_4": "Beacon Segment1" } def get_chute_chute_default_input_comments() -> Dict[str, str]: """Get default input comments for Chute_Chute variant.""" return { ".PROCESSDATAIN.CONNECTOR_4_A_PIN_4": "PE 100", ".PROCESSDATAIN.CONNECTOR_3_A_PIN_4": "PE 100", ".PROCESSDATAIN.CONNECTOR_2_A_PIN_4": "PE 50", ".PROCESSDATAIN.CONNECTOR_1_A_PIN_4": "PE 50", ".PROCESSDATAIN.CONNECTOR_6_A_PIN_4": "PB In", ".PROCESSDATAIN.CONNECTOR_5_A_PIN_4": "PB In" } def get_chute_chute_default_output_comments() -> Dict[str, str]: """Get default output comments for Chute_Chute variant.""" return { ".PROCESSDATAOUT.CONNECTOR_8_A_PIN_4": "Sol", ".PROCESSDATAOUT.CONNECTOR_7_A_PIN_4": "Sol" } def get_load_chute_default_input_comments() -> Dict[str, str]: """Get default input comments for Load_Chute variant.""" return { ".PROCESSDATAIN.CONNECTOR_4_A_PIN_4": "PB In", ".PROCESSDATAIN.CONNECTOR_3_A_PIN_4": "PE 100", ".PROCESSDATAIN.CONNECTOR_1_A_PIN_4": "PE 50", ".PROCESSDATAIN.CONNECTOR_6_A_PIN_4": "PE", ".PROCESSDATAIN.CONNECTOR_5_A_PIN_4": "PB In" } def get_load_chute_default_output_comments() -> Dict[str, str]: """Get default output comments for Load_Chute variant.""" return { ".PROCESSDATAOUT.CONNECTOR_4_B_PIN_2": "PB Out", ".PROCESSDATAOUT.CONNECTOR_2_B_PIN_2": "Beacon Segment2", ".PROCESSDATAOUT.CONNECTOR_2_A_PIN_4": "Beacon Segment1", ".PROCESSDATAOUT.CONNECTOR_8_B_PIN_2": "Beacon Segment2", ".PROCESSDATAOUT.CONNECTOR_8_A_PIN_4": "Beacon Segment1", ".PROCESSDATAOUT.CONNECTOR_7_A_PIN_4": "Sol" } def get_pdp_fioh_default_input_comments() -> Dict[str, str]: """Get default input comments for PDP_FIOH variant.""" return { ".PROCESSDATAIN.CONNECTOR_1_A_PIN_4": "Circuit Breaker 1", ".PROCESSDATAIN.CONNECTOR_1_B_PIN_2": "Circuit Breaker 2", ".PROCESSDATAIN.CONNECTOR_2_A_PIN_4": "Circuit Breaker 3", ".PROCESSDATAIN.CONNECTOR_2_B_PIN_2": "Circuit Breaker 4", ".PROCESSDATAIN.CONNECTOR_3_A_PIN_4": "Circuit Breaker 5", ".PROCESSDATAIN.CONNECTOR_3_B_PIN_2": "Circuit Breaker 6", ".PROCESSDATAIN.CONNECTOR_4_A_PIN_4": "Circuit Breaker 7", ".PROCESSDATAIN.CONNECTOR_4_B_PIN_2": "Circuit Breaker 8", ".PROCESSDATAIN.CONNECTOR_5_A_PIN_4": "Circuit Breaker 9", ".PROCESSDATAIN.CONNECTOR_5_B_PIN_2": "Circuit Breaker 10", ".PROCESSDATAIN.CONNECTOR_6_A_PIN_4": "Circuit Breaker 11", ".PROCESSDATAIN.CONNECTOR_6_B_PIN_2": "Circuit Breaker 12", ".PROCESSDATAIN.CONNECTOR_7_A_PIN_4": "Circuit Breaker 13", ".PROCESSDATAIN.CONNECTOR_7_B_PIN_2": "Circuit Breaker 14", ".PROCESSDATAIN.CONNECTOR_8_A_PIN_4": "Circuit Breaker 15", ".PROCESSDATAIN.CONNECTOR_8_B_PIN_2": "Circuit Breaker 16" } # -------------------------------------------------------------------------------- # Helper logic migrated from EnhancedMCMGenerator for behaviour parity # -------------------------------------------------------------------------------- def _determine_variant(module_data: "ModuleData") -> str: """Determine Turck hub variant based on module name and DESC patterns.""" # Check for PDP FIOH modules first (name-based detection) module_name = module_data.tagname.upper() if "PDP" in module_name and "FIOH" in module_name: return "PDP_FIOH" # Build terminal to description mapping terminal_desc: Dict[str, str] = {} for m in module_data.io_mappings: if m.terminal and m.description: terminal_desc[m.terminal.upper()] = m.description.upper() io4 = terminal_desc.get("IO4", "") io10 = terminal_desc.get("IO10", "") io11 = terminal_desc.get("IO11", "") io12 = terminal_desc.get("IO12", "") # Check for Chute_Load variant: JR1 in IO11 or IO10 if "JR1" in io11 or "JR1" in io10: return "Chute_Load" # Check for Chute_Chute variant: PR1 in IO4 or IO12 if "PR1" in io4 or "PR1" in io12: return "Chute_Chute" # Check for Load_Chute variant (different from Chute_Load) # This might need additional logic - for now keeping as separate case # You may need to specify the exact criteria for Load_Chute # Default to Chute_Load for other cases return "Chute_Load" def _extract_comments(module_data: "ModuleData") -> tuple[Dict[str, str], Dict[str, str]]: input_comments: Dict[str, str] = {} output_comments: Dict[str, str] = {} for m in module_data.io_mappings: if not (m.io_path and m.description): continue comment_text = m.description # For PDP FIOH modules, map IO terminals to connector operands if module_data.tagname.upper().find("PDP") != -1 and module_data.tagname.upper().find("FIOH1") != -1: operand = _map_pdp_io_to_connector(m.terminal, m.signal) if operand: if m.signal.upper() == "I": input_comments[operand] = comment_text elif m.signal.upper() == "O": output_comments[operand] = comment_text continue # Original logic for other variants if ":" in m.io_path: _, io_part = m.io_path.split(":", 1) else: io_part = m.io_path # fallback io_part_up = io_part.upper() if "I.PROCESSDATAIN." in io_part_up: connector = io_part_up.split("I.PROCESSDATAIN.", 1)[1] input_comments[f".PROCESSDATAIN.{connector}"] = comment_text elif "O.PROCESSDATAOUT." in io_part_up: connector = io_part_up.split("O.PROCESSDATAOUT.", 1)[1] output_comments[f".PROCESSDATAOUT.{connector}"] = comment_text else: # Handle path without colon by checking substrings if "I.PROCESSDATAIN." in io_part_up: connector = io_part_up.split("I.PROCESSDATAIN.", 1)[1] input_comments[f".PROCESSDATAIN.{connector}"] = comment_text elif "O.PROCESSDATAOUT." in io_part_up: connector = io_part_up.split("O.PROCESSDATAOUT.", 1)[1] output_comments[f".PROCESSDATAOUT.{connector}"] = comment_text return input_comments, output_comments def _map_pdp_io_to_connector(terminal: str, signal: str) -> str: """Map PDP FIOH IO terminal to connector operand. Examples: - IO00 -> .PROCESSDATAIN.CONNECTOR_1_A_PIN_4 - IO01 -> .PROCESSDATAIN.CONNECTOR_1_B_PIN_2 - IO02 -> .PROCESSDATAIN.CONNECTOR_2_A_PIN_4 - etc. """ if not terminal.upper().startswith("IO"): return "" try: io_num = int(terminal.upper().replace("IO", "")) except ValueError: return "" # Calculate connector number (1-based) connector_num = (io_num // 2) + 1 # Determine pin type based on even/odd if io_num % 2 == 0: pin_type = "A_PIN_4" else: pin_type = "B_PIN_2" # Build operand if signal.upper() == "I": return f".PROCESSDATAIN.CONNECTOR_{connector_num}_{pin_type}" elif signal.upper() == "O": return f".PROCESSDATAOUT.CONNECTOR_{connector_num}_{pin_type}" else: return "" def _parent_info(module_data: "ModuleData") -> tuple[str, str, str]: """Mimic EnhancedMCMGenerator._get_parent_info for a single FIOH module.""" # Extract port address from terminal - use the actual terminal number term = module_data.terminal.upper() if module_data.terminal else "" if term.startswith("IO"): # Extract the numeric part directly (IO4 -> 4, IO12 -> 12, IO14 -> 14) try: port_address = term[2:] # Get everything after "IO" except: port_address = "4" # Default to channel 4 if parsing fails else: port_address = "4" # Default to channel 4 if not an IO terminal # The parent_module should be set from Excel data (e.g., "PDP1_FIO1") parent_module = module_data.parent_module if not parent_module: raise ValueError(f"FIOH module {module_data.tagname} missing parent module information") # For FIOH modules connected via IO-Link, always use port 4 (the IO-Link port on M12DR) # The port_address is different - it's the address on the IO-Link network parent_port_id = "4" return parent_module, parent_port_id, port_address # Example usage if __name__ == "__main__": # Example: Create a Chute_Load hub module chute_load_config = create_turck_hub_module( name="Chute_Load_Hub1", variant="Chute_Load", parent_module="D2CMaster", parent_port_id="4", input_comments={ ".PROCESSDATAIN.CONNECTOR_4_A_PIN_4": "Emergency Stop", ".PROCESSDATAIN.CONNECTOR_3_A_PIN_4": "Reset Button", ".PROCESSDATAIN.CONNECTOR_2_A_PIN_4": "Photo Eye 1", ".PROCESSDATAIN.CONNECTOR_6_A_PIN_4": "Photo Eye 2" }, output_comments={ ".PROCESSDATAOUT.CONNECTOR_3_B_PIN_2": "Status Light", ".PROCESSDATAOUT.CONNECTOR_1_B_PIN_2": "Warning Beacon", ".PROCESSDATAOUT.CONNECTOR_8_A_PIN_4": "Conveyor Motor" } ) generator = TurckHubModuleGenerator(chute_load_config) generator.load_boilerplate() generator.apply_updates() generator.save("generated_projects/Chute_Load_Hub1.L5X") # Example: Create a Chute_Chute hub module with defaults chute_chute_config = create_turck_hub_module( name="Chute_Chute_Hub2", variant="Chute_Chute", input_comments=get_chute_chute_default_input_comments(), output_comments=get_chute_chute_default_output_comments() ) generator2 = TurckHubModuleGenerator(chute_chute_config) generator2.load_boilerplate() generator2.apply_updates() generator2.save("generated_projects/Chute_Chute_Hub2.L5X") # Example: Create a Load_Chute hub module load_chute_config = create_turck_hub_module( name="Load_Chute_Hub3", variant="Load_Chute", parent_module="IOLMMaster1", parent_port_id="2", input_comments=get_load_chute_default_input_comments(), output_comments=get_load_chute_default_output_comments() ) generator3 = TurckHubModuleGenerator(load_chute_config) generator3.load_boilerplate() generator3.apply_updates() generator3.save("generated_projects/Load_Chute_Hub3.L5X") # Example: Create a PDP_FIOH hub module pdp_fioh_config = create_turck_hub_module( name="PDP1_FIOH1", variant="PDP_FIOH", parent_module="SLOT2_EN4TR", parent_port_id="2", input_comments=get_pdp_fioh_default_input_comments() ) generator4 = TurckHubModuleGenerator(pdp_fioh_config) generator4.load_boilerplate() generator4.apply_updates() generator4.save("generated_projects/PDP1_FIOH1.L5X") print(f"Generated Chute_Load hub module: {chute_load_config.name}") print(f"Generated Chute_Chute hub module: {chute_chute_config.name}") print(f"Generated Load_Chute hub module: {load_chute_config.name}") print(f"Generated PDP_FIOH hub module: {pdp_fioh_config.name}")