168 lines
6.9 KiB
Python
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) |