147 lines
5.9 KiB
Python
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 |