2025-08-05 14:38:54 +04:00

147 lines
5.9 KiB
Python

from __future__ import annotations
import xml.etree.ElementTree as ET
import pandas as pd
from ..utils.common import natural_sort_key
__all__ = ["create_outputs_routine"]
def create_outputs_routine(routines: ET.Element, zones_df: pd.DataFrame, sto_df: pd.DataFrame) -> None:
"""Append OUTPUTS routine (R011_OUTPUTS) to the given `<Routines>` element.
Logic copied from original generate_l5x.py implementation with no
behavioural changes; only external dependencies (natural_sort_key)
imported from utilities.
"""
routine = ET.SubElement(routines, "Routine")
routine.set("Use", "Target")
routine.set("Name", "R011_OUTPUTS")
routine.set("Type", "RLL")
rll_content = ET.SubElement(routine, "RLLContent")
rung_num = 0
# Check if zones_df is empty or missing NAME column
if zones_df.empty:
print("Warning: zones DataFrame is empty, skipping outputs routine")
return
if "NAME" not in zones_df.columns:
print(f"Warning: zones DataFrame missing NAME column. Available columns: {list(zones_df.columns)}")
return
# Process each zone (sorted alphabetically)
for _, zone in zones_df.sort_values("NAME").iterrows():
# Skip MCM zone (no conventional device range)
if zone["NAME"] in ["MCM", "MCM01"]:
continue
if pd.isna(zone["START"]) or pd.isna(zone["END"]) or zone["START"] == "" or zone["END"] == "":
continue
zone_name = zone["NAME"].replace(" ", "_").replace("-", "_")
zone_ok_tag = f"EStop_{zone_name}_OK"
# Check if START and END follow numeric device pattern
try:
start_parts = zone["START"].split("_")
end_parts = zone["END"].split("_")
if len(start_parts) < 2 or len(end_parts) < 2:
print(f"Warning: Skipping zone {zone_name} in outputs - invalid device name format")
continue
start_prefix = start_parts[0]
start_num = int(start_parts[1])
end_prefix = end_parts[0]
end_num = int(end_parts[1])
except (ValueError, IndexError):
print(f"Warning: Skipping zone {zone_name} in outputs - unable to parse device numbers")
continue
# Get unique device outputs for this zone
zone_outputs: list[str] = []
# Collect all STO devices from the dataframe
for _, sto_row in sto_df.iterrows():
try:
device_parts = sto_row["TAGNAME"].split("_")
if len(device_parts) < 2:
continue
device_prefix = device_parts[0]
# Extract device number, handling A/B suffixes
device_num_str = device_parts[1]
device_num = int(device_num_str[:-1]) if device_num_str[-1] in ["A", "B"] else int(device_num_str)
except (ValueError, IndexError):
continue
# Check if this device is within the zone range
in_zone = False
if start_prefix == end_prefix:
if device_prefix == start_prefix and start_num <= device_num <= end_num:
in_zone = True
else:
if (
(device_prefix == start_prefix and device_num >= start_num)
or (device_prefix == end_prefix and device_num <= end_num)
or (device_prefix > start_prefix and device_prefix < end_prefix)
):
in_zone = True
if in_zone:
if "STO" in sto_row["DESCA"]:
zone_outputs.append(sto_row["IO_PATH"])
else:
zone_outputs.append(f"{sto_row['TAGNAME']}:SO.STOOutput")
# Create rung for zone
if zone_outputs:
# Zone has devices - create rung with interlock logic and device outputs
rung = ET.SubElement(rll_content, "Rung")
rung.set("Number", str(rung_num))
rung.set("Type", "N")
text = ET.SubElement(rung, "Text")
# Build XIC conditions - always include EStop_MCM01_OK first
xic_conditions = "XIC(EStop_MCM01_OK)"
# Add interlock if specified and different from MCM01
if pd.notna(zone["INTERLOCK"]) and zone["INTERLOCK"] != "":
interlock_zone = zone["INTERLOCK"].replace(" ", "_").replace("-", "_")
interlock_tag = f"EStop_{interlock_zone}_OK"
# Only add interlock if it's different from the hardcoded MCM01
if interlock_tag != "EStop_MCM01_OK":
xic_conditions = f"XIC({interlock_tag}){xic_conditions}"
# Add the zone's own OK tag
xic_conditions = f"{xic_conditions}XIC({zone_ok_tag})"
# Remove duplicates and filter out non-STO outputs
unique_outputs: list[str] = []
for output in zone_outputs:
if (":SO.STOOutput" in output or ":SO.Out00Output" in output) and output not in unique_outputs:
unique_outputs.append(output)
if len(unique_outputs) == 1:
output = unique_outputs[0]
text.text = f"{xic_conditions}OTE({output});"
elif len(unique_outputs) > 1:
ote_list = [f"OTE({output})" for output in sorted(unique_outputs, key=natural_sort_key)]
ote_chain = "[" + ",".join(ote_list) + "]"
text.text = f"{xic_conditions}{ote_chain};"
rung_num += 1
else:
# Zone has no devices - just set the zone OK tag (no interlock conditions)
rung = ET.SubElement(rll_content, "Rung")
rung.set("Number", str(rung_num))
rung.set("Type", "N")
text = ET.SubElement(rung, "Text")
text.text = f"OTE({zone_ok_tag});"
rung_num += 1