2025-08-18 13:20:34 +04:00

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