157 lines
6.3 KiB
Python
157 lines
6.3 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
import xml.etree.ElementTree as ET
|
|
from typing import Any
|
|
|
|
import pandas as pd
|
|
|
|
from ..utils.common import natural_sort_key
|
|
from ..utils.tag_utils import normalize_desca, epc_base_from_term
|
|
|
|
__all__ = ["create_inputs_routine"]
|
|
|
|
|
|
def create_inputs_routine(routines: ET.Element, epc_df: pd.DataFrame, ignore_estop1ok: bool = False) -> None:
|
|
"""Append the INPUTS routine (R010_INPUTS) to the given `<Routines>` element.
|
|
|
|
The logic mirrors the original generate_l5x.py implementation but uses
|
|
shared helpers for normalization and EPC grouping.
|
|
|
|
Args:
|
|
routines: The XML element to append the routine to
|
|
epc_df: DataFrame containing EPC data
|
|
ignore_estop1ok: If True, skip generation of ESTOP1OK tags
|
|
"""
|
|
|
|
routine = ET.SubElement(routines, "Routine")
|
|
routine.set("Name", "R010_INPUTS")
|
|
routine.set("Type", "RLL")
|
|
|
|
rll_content = ET.SubElement(routine, "RLLContent")
|
|
|
|
rung_num = 0
|
|
|
|
# ===== SECTION 1: STATUS TAGS =====
|
|
# MCM EPB Status (config-driven inputs)
|
|
from ..config import get_config
|
|
cfg = get_config()
|
|
status_inputs = list(getattr(cfg.routines, 'mcm_epb_status_inputs', []))
|
|
status_tag = getattr(cfg.routines, 'mcm_epb_status_tag', 'MCM_EPB_STATUS')
|
|
if not status_inputs:
|
|
status_inputs = []
|
|
rung = ET.SubElement(rll_content, "Rung")
|
|
rung.set("Number", str(rung_num))
|
|
rung.set("Type", "N")
|
|
text = ET.SubElement(rung, "Text")
|
|
xic_chain = ''.join(f"XIC({addr})" for addr in status_inputs)
|
|
text.text = f"{xic_chain}OTE({status_tag});" if xic_chain else "NOP();"
|
|
rung_num += 1
|
|
|
|
# Only SI terminals participate in EPC status/OK logic
|
|
si_epc_df = epc_df[epc_df["TERM"].str.startswith("SI", na=False)] if "TERM" in epc_df.columns else epc_df.iloc[0:0]
|
|
|
|
# Group EPCs by TAGNAME (device) - ensure TAGNAME column exists
|
|
if "TAGNAME" not in si_epc_df.columns:
|
|
si_epc_df = si_epc_df.copy()
|
|
si_epc_df["TAGNAME"] = ""
|
|
epc_groups = si_epc_df.groupby("TAGNAME") if len(si_epc_df) > 0 else []
|
|
|
|
# ------------------------------------------------------------------
|
|
# STATUS tags (for EPC devices only)
|
|
# ------------------------------------------------------------------
|
|
for tagname, group in sorted(epc_groups, key=lambda x: natural_sort_key(x[0])):
|
|
# Skip ESTOP devices when building STATUS tags
|
|
if "ESTOP" in group.iloc[0]["DESCA"]:
|
|
continue
|
|
|
|
# Bucket rows into EPC1 and EPC2 based on terminal index
|
|
epc_pairs: dict[str, list[pd.Series]] = {}
|
|
for _, row in group.iterrows():
|
|
base = epc_base_from_term(str(row["TERM"]))
|
|
if base is None:
|
|
continue
|
|
epc_pairs.setdefault(base, []).append(row)
|
|
|
|
# For each EPC1/2 pair that has at least two rows (main + secondary)
|
|
for epc_base, epc_rows in sorted(epc_pairs.items()):
|
|
# Sort rows so main appears before _2
|
|
epc_rows.sort(key=lambda r: r["DESCA"])
|
|
|
|
main_row = next((r for r in epc_rows if not str(r["DESCA"]).endswith("_2")), None)
|
|
sec_row = next((r for r in epc_rows if str(r["DESCA"]).endswith("_2")), None)
|
|
if main_row is None or sec_row is None:
|
|
continue # Need both for STATUS
|
|
|
|
rung = ET.SubElement(rll_content, "Rung")
|
|
rung.set("Number", str(rung_num))
|
|
rung.set("Type", "N")
|
|
text = ET.SubElement(rung, "Text")
|
|
|
|
xic_chain = f"XIC({main_row['IO_PATH']})XIC({sec_row['IO_PATH']})"
|
|
device_base = "_".join(str(main_row["DESCA"]).split("_")[:2])
|
|
status_tag = f"{device_base}_{epc_base}_ESTOP_STATUS"
|
|
text.text = f"{xic_chain}OTE({status_tag});"
|
|
rung_num += 1
|
|
|
|
# ------------------------------------------------------------------
|
|
# ESTOP_OK tags (for ESTOP devices)
|
|
# ------------------------------------------------------------------
|
|
if not ignore_estop1ok:
|
|
for tagname, group in sorted(epc_groups, key=lambda x: natural_sort_key(x[0])):
|
|
if "ESTOP" not in group.iloc[0]["DESCA"]:
|
|
continue
|
|
|
|
for _, row in group.sort_values("DESCA").iterrows():
|
|
rung = ET.SubElement(rll_content, "Rung")
|
|
rung.set("Number", str(rung_num))
|
|
rung.set("Type", "N")
|
|
text = ET.SubElement(rung, "Text")
|
|
|
|
norm_desca = normalize_desca(str(row["DESCA"]))
|
|
estop_ok_tag = f"{norm_desca}_OK"
|
|
text.text = f"XIC({row['IO_PATH']})OTE({estop_ok_tag});"
|
|
rung_num += 1
|
|
|
|
# ------------------------------------------------------------------
|
|
# EPC_ESTOP_OK tags (for EPC devices)
|
|
# ------------------------------------------------------------------
|
|
for tagname, group in sorted(epc_groups, key=lambda x: natural_sort_key(x[0])):
|
|
if "ESTOP" in group.iloc[0]["DESCA"]:
|
|
continue
|
|
|
|
for _, row in group.sort_values("DESCA").iterrows():
|
|
rung = ET.SubElement(rll_content, "Rung")
|
|
rung.set("Number", str(rung_num))
|
|
rung.set("Type", "N")
|
|
text = ET.SubElement(rung, "Text")
|
|
|
|
norm_desca = normalize_desca(str(row["DESCA"]))
|
|
estop_ok_tag = f"{norm_desca}_ESTOP_OK"
|
|
text.text = f"XIC({row['IO_PATH']})OTE({estop_ok_tag});"
|
|
rung_num += 1
|
|
|
|
# --- Plugin wrapper so modern generator can execute this routine ---
|
|
from ..plugin_system import RoutinePlugin
|
|
|
|
|
|
class InputsRoutinePlugin(RoutinePlugin):
|
|
name = "inputs"
|
|
description = "Generates the R010_INPUTS routine"
|
|
category = "safety"
|
|
|
|
def can_generate(self) -> bool:
|
|
try:
|
|
return not self.context.data_loader.epc.empty
|
|
except Exception:
|
|
return False
|
|
|
|
def generate(self) -> bool:
|
|
params = self.context.metadata.get("params", {}) if isinstance(self.context.metadata, dict) else {}
|
|
ignore_flag = bool(params.get("ignore_estop1ok", False))
|
|
create_inputs_routine(
|
|
self.context.routines_element,
|
|
self.context.data_loader.epc,
|
|
ignore_estop1ok=ignore_flag,
|
|
)
|
|
return True |