2025-09-04 22:37:52 +04:00

168 lines
6.9 KiB
Python

"""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)