"""FIOH (Field IO Hub) routine generation. Generates R031_FIOH routine with AOI_IO_BLOCK calls for each FIOH module. FIOH modules are identified by PARTNUMBER containing '5032-8IOLM12DR' and DESCA containing 'FIOH'. """ import xml.etree.ElementTree as ET import pandas as pd from ..utils.common import format_xml_to_match_original, natural_sort_key from ..plugin_system import RoutinePlugin, register_plugin def generate_fioh_routine(desc_ip_df: pd.DataFrame) -> str: """Generate R031_FIOH routine XML from DESC_IP data. Args: desc_ip_df: DataFrame with DESC_IP data containing TAGNAME, PARTNUMBER, and DESCA columns Returns: Formatted XML string for R031_FIOH routine """ # Extract FIOH modules by filtering for 5032-8IOLM12DR part number AND DESCA containing FIOH fioh_filter = (desc_ip_df['PARTNUMBER'].str.contains('5032-8IOLM12DR', na=False) & desc_ip_df['DESCA'].str.contains('FIOH', na=False)) fioh_df = desc_ip_df[fioh_filter].copy() # Remove duplicates and sort by DESCA (since that's the FIOH device name) if 'DESCA' in fioh_df.columns: fioh_df = fioh_df.drop_duplicates(subset=['DESCA']) fioh_df = fioh_df.sort_values('DESCA', key=lambda x: [natural_sort_key(name) for name in x]) else: # Fallback to Name/TAGNAME if DESCA not available if 'Name' in fioh_df.columns: fioh_df = fioh_df.drop_duplicates(subset=['Name']) fioh_df = fioh_df.sort_values('Name', key=lambda x: [natural_sort_key(name) for name in x]) else: fioh_df = fioh_df.drop_duplicates(subset=['TAGNAME']) fioh_df = fioh_df.sort_values('TAGNAME', key=lambda x: [natural_sort_key(name) for name in x]) # Create routine XML structure (config-driven name) try: from ..config import get_config routine_name = get_config().routines.name_map.get('fioh', 'R031_FIOH') except Exception: routine_name = 'R031_FIOH' routine = ET.Element("Routine", attrib={"Name": routine_name, "Type": "RLL"}) rll_content = ET.SubElement(routine, "RLLContent") # Rung 0: Documentation comment rung0 = ET.SubElement(rll_content, "Rung", attrib={"Number": "0", "Type": "N"}) comment = ET.SubElement(rung0, "Comment") comment.text = """FIOH (Field IO Hub) Instantiation Routine FIOHs are dependent on the masters for communication back to the DPM""" text0 = ET.SubElement(rung0, "Text") text0.text = "NOP();" # Generate AOI_IO_BLOCK calls for each FIOH module rung_number = 1 for _, fioh_row in fioh_df.iterrows(): # FIOH device name comes from DESCA column if 'DESCA' in fioh_row and pd.notna(fioh_row['DESCA']): fioh_name = str(fioh_row['DESCA']).strip() else: continue # Skip if no DESCA value # FIO parent name comes from Name/TAGNAME column if 'Name' in fioh_row and pd.notna(fioh_row['Name']): fio_name = str(fioh_row['Name']).strip() elif 'TAGNAME' in fioh_row and pd.notna(fioh_row['TAGNAME']): fio_name = str(fioh_row['TAGNAME']).strip() else: fio_name = "MCM" # Default fallback # Validate both names exist and are not empty if not fioh_name or not fio_name: continue print(f" FIOH {fioh_name} -> FIO {fio_name}") # Create rung for this FIOH module rung = ET.SubElement(rll_content, "Rung", attrib={"Number": str(rung_number), "Type": "N"}) text = ET.SubElement(rung, "Text") # Generate AOI_IO_BLOCK call with FIO controller reference # Pattern: AOI_IO_BLOCK({FIOH_NAME}.AOI,{FIOH_NAME}.HMI,{FIOH_NAME}.CTRL,MCM01.CTRL,{FIO_NAME}.CTRL,{FIOH_NAME}:I.ConnectionFaulted); try: from ..config import get_config mcm_ctrl = get_config().routines.mcm_ctrl_tag except Exception: mcm_ctrl = 'MCM.CTRL' aoi_call = f"AOI_IO_BLOCK({fioh_name}.AOI,{fioh_name}.HMI,{fioh_name}.CTRL,{mcm_ctrl},{fio_name}.CTRL.STS.Communication_Faulted,{fioh_name}:I.ConnectionFaulted);" text.text = aoi_call rung_number += 1 # Convert to string and format to match Rockwell L5X format xml_str = ET.tostring(routine, encoding='unicode') return format_xml_to_match_original(xml_str) def create_fioh_routine(routines_element: ET.Element, desc_ip_df: pd.DataFrame) -> None: """Create and append R031_FIOH routine to routines element. Args: routines_element: XML element where the routine should be added desc_ip_df: DataFrame with DESC_IP data """ # Generate the routine XML routine_xml = generate_fioh_routine(desc_ip_df) # Parse the XML string back to element routine_element = ET.fromstring(routine_xml) # Append to routines element routines_element.append(routine_element) def extract_fioh_from_desc_ip(desc_ip_df: pd.DataFrame) -> pd.DataFrame: """Extract FIOH device data from DESC_IP DataFrame. Args: desc_ip_df: DESC_IP DataFrame with PARTNUMBER and DESCA columns Returns: Filtered DataFrame containing only FIOH devices """ # Filter for FIOH part number AND DESCA containing FIOH fioh_filter = (desc_ip_df['PARTNUMBER'].str.contains('5032-8IOLM12DR', na=False) & desc_ip_df['DESCA'].str.contains('FIOH', na=False)) fioh_df = desc_ip_df[fioh_filter].copy() # Remove duplicates based on DESCA (since that's the FIOH device name) if 'DESCA' in fioh_df.columns: fioh_df = fioh_df.drop_duplicates(subset=['DESCA']) fioh_df = fioh_df.sort_values('DESCA', key=lambda x: [natural_sort_key(name) for name in x]) else: # Fallback to Name/TAGNAME if DESCA not available if 'Name' in fioh_df.columns: fioh_df = fioh_df.drop_duplicates(subset=['Name']) fioh_df = fioh_df.sort_values('Name', key=lambda x: [natural_sort_key(name) for name in x]) else: fioh_df = fioh_df.drop_duplicates(subset=['TAGNAME']) fioh_df = fioh_df.sort_values('TAGNAME', key=lambda x: [natural_sort_key(name) for name in x]) return fioh_df class FiohRoutinePlugin(RoutinePlugin): name = "fioh" description = "Generates the FIOH routine" category = "device" def can_generate(self) -> bool: return not self.context.data_loader.fioh.empty def generate(self) -> bool: desc_ip_df = self.context.data_loader.desc_ip xml_text = generate_fioh_routine(desc_ip_df) if not xml_text: return False self.context.routines_element.append(ET.fromstring(xml_text)) return True register_plugin(FiohRoutinePlugin)