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 `` 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