2025-09-11 00:08:34 +04:00

155 lines
6.2 KiB
Python

"""
Generate R040_APF routine from DESC_IP data.
"""
import xml.etree.ElementTree as ET
import pandas as pd
from ..utils.common import format_xml_to_match_original, natural_sort_key
from ..plugin_system import RoutinePlugin, register_plugin
def generate_apf_routine(data_loader) -> str:
"""Generate R040_APF routine from NETWORK sheet data.
Args:
data_loader: DataLoader instance with access to NETWORK sheet
Returns:
Formatted XML string for R040_APF routine
"""
# Get APF data from NETWORK sheet
apf_df = data_loader.apf
# Sort by Name for deterministic output
if 'Name' in apf_df.columns:
apf_df = apf_df.sort_values('Name', key=lambda x: [natural_sort_key(name) for name in x])
else:
# This shouldn't happen with NETWORK sheet data
return format_xml_to_match_original(ET.tostring(ET.Element("Routine", attrib={"Name": "R040_APF", "Type": "RLL"}), encoding='unicode'))
# Create routine XML structure (config-driven name)
try:
from ..config import get_config
routine_name = get_config().routines.name_map.get('apf', 'R040_APF')
except Exception:
routine_name = 'R040_APF'
routine = ET.Element("Routine", attrib={"Name": routine_name, "Type": "RLL"})
rll_content = ET.SubElement(routine, "RLLContent")
# Rung 0: Comment rung
rung0 = ET.SubElement(rll_content, "Rung", attrib={"Number": "0", "Type": "N"})
comment = ET.SubElement(rung0, "Comment")
comment.text = """APF (Variable Frequency Drive) Instantiation Routine
These VFDs are connected to the DPMs for their Ethernet Communication"""
text0 = ET.SubElement(rung0, "Text")
text0.text = "NOP();"
# Generate AOI_APF calls for each module
rung_number = 1
for _, apf_row in apf_df.iterrows():
# APF name comes from Name column in NETWORK sheet
if 'Name' not in apf_row or pd.isna(apf_row['Name']):
continue
apf_name = str(apf_row['Name']).strip()
if not apf_name:
continue
# Extract DPM name from DPM column (from NETWORK sheet)
dpm_name = None
if 'DPM' in apf_row and pd.notna(apf_row['DPM']) and str(apf_row['DPM']).strip():
dpm_name = str(apf_row['DPM']).strip()
else:
# This should not happen with proper NETWORK sheet data
print(f"Warning: No DPM specified for APF {apf_name}, using MCM")
dpm_name = "MCM"
print(f" APF {apf_name} -> DPM {dpm_name}")
# Create rung for this APF module
rung = ET.SubElement(rll_content, "Rung", attrib={"Number": str(rung_number), "Type": "N"})
text = ET.SubElement(rung, "Text")
# Generate AOI_APF call with MOVE instruction
# Pattern: AOI_APF({APF}.AOI,{APF}.HMI,{APF}.CTRL,{APF},{APF}:I,{APF}:O,MCM01.CTRL,{DPM}.CTRL,{APF}:I.In_0,1,Horn_Beacon)MOVE({APF}.CTRL.STS.Log,{APF}.CTRL.STS.Log);
# Config-driven control tags and horn tag lookup
from ..config import get_config
rc = get_config().routines
mcm_ctrl = getattr(rc, 'mcm_ctrl_tag', 'MCM.CTRL')
in_def = getattr(rc, 'apf_input_default', 'In_0')
# Look up horn beacon for this APF from horn mappings
horn_mappings = data_loader.horn_mappings
beacon_name = horn_mappings.get(apf_name)
if not beacon_name:
# Fallback to configured default or NO_Horn
horn_tag = getattr(rc, 'no_horn_tag_name', 'NO_Horn')
print(f" Warning: No horn mapping found for {apf_name}, using {horn_tag}")
else:
# Look up the actual IO path for this beacon in DESC_IP
desc_ip = data_loader.desc_ip
beacon_rows = desc_ip[desc_ip['DESCA'].astype(str).str.contains(beacon_name, case=False, na=False)]
if not beacon_rows.empty:
beacon_row = beacon_rows.iloc[0]
io_path = beacon_row.get('IO_PATH', '')
if io_path and str(io_path).strip():
horn_tag = str(io_path).strip()
print(f" Horn mapping: {apf_name} -> {beacon_name} -> {horn_tag}")
else:
# No IO_PATH found, use beacon name as boolean tag
horn_tag = beacon_name
print(f" Horn mapping: {apf_name} -> {beacon_name} (no IO_PATH, using as boolean)")
else:
# Beacon not found in DESC_IP, use beacon name as boolean tag
horn_tag = beacon_name
print(f" Horn mapping: {apf_name} -> {beacon_name} (not found in DESC_IP, using as boolean)")
aoi_call = (
f"AOI_APF({apf_name}.AOI,{apf_name}.HMI,{apf_name}.CTRL,{apf_name},{apf_name}:I,{apf_name}:O,"
f"{mcm_ctrl},{dpm_name}.CTRL,{apf_name}:I.{in_def},1,{horn_tag})"
f"MOVE({apf_name}.CTRL.STS.Log,{apf_name}.CTRL.STS.Log);"
)
text.text = aoi_call
rung_number += 1
# Convert to string and format to match Rockwell L5X format
xml_str = ET.tostring(routine, encoding='unicode')
return format_xml_to_match_original(xml_str)
def _get_apf_data(data_loader) -> pd.DataFrame:
"""Helper to get APF data from DataLoader."""
return data_loader.apf
def append_apf_routine(routines_element: ET.Element, data_loader):
"""Append APF routine to routines element."""
routine_content = generate_apf_routine(data_loader)
routine_element = ET.fromstring(routine_content)
# Append to routines element
routines_element.append(routine_element)
class ApfRoutinePlugin(RoutinePlugin):
name = "apf"
description = "Generates the APF routine"
category = "device"
def can_generate(self) -> bool:
return not self.context.data_loader.apf.empty
def generate(self) -> bool:
xml_text = generate_apf_routine(self.context.data_loader)
if not xml_text:
return False
self.context.routines_element.append(ET.fromstring(xml_text))
return True
register_plugin(ApfRoutinePlugin)