Simatic_XML_Parser_to_SCL/parsers/parse_lad_fbd.py

549 lines
22 KiB
Python

# ToUpload/parsers/parse_lad_fbd.py
# -*- coding: utf-8 -*-
from lxml import etree
from collections import defaultdict
import copy
import traceback
# Importar desde las utilidades del parser
from .parser_utils import (
ns,
parse_access,
parse_part,
parse_call,
get_multilingual_text,
)
# Sufijo usado en x2 para identificar instrucciones procesadas (útil para EN/ENO)
SCL_SUFFIX = "_sympy_processed" # Asumimos que este es el sufijo de x2
def parse_lad_fbd_network(network_element):
"""
Parsea una red LAD/FBD/GRAPH, extrae lógica y añade conexiones EN/ENO implícitas.
Devuelve un diccionario representando la red para el JSON.
"""
if network_element is None:
return {
"id": "ERROR",
"title": "Invalid Network Element",
"logic": [],
"error": "Input element was None",
}
network_id = network_element.get("ID")
# Usar get_multilingual_text de utils
title_element = network_element.xpath(
".//iface:MultilingualText[@CompositionName='Title']", namespaces=ns
)
network_title = (
get_multilingual_text(title_element[0])
if title_element
else f"Network {network_id}"
)
comment_element = network_element.xpath(
"./ObjectList/MultilingualText[@CompositionName='Comment']", namespaces=ns
) # OJO: Path relativo a CompileUnit?
if not comment_element: # Intentar path alternativo si el anterior falla
comment_element = network_element.xpath(
".//MultilingualText[@CompositionName='Comment']", namespaces=ns
) # Más genérico dentro de la red
network_comment = (
get_multilingual_text(comment_element[0]) if comment_element else ""
)
# --- Determinar Lenguaje (ya que este parser maneja varios) ---
network_lang = "Unknown"
attr_list_net = network_element.xpath("./AttributeList")
if attr_list_net:
lang_node_net = attr_list_net[0].xpath("./ProgrammingLanguage/text()")
if lang_node_net:
network_lang = lang_node_net[0].strip()
# --- Buscar FlgNet ---
# Buscar NetworkSource y luego FlgNet (ambos usan namespace flg)
network_source_node = network_element.xpath(".//flg:NetworkSource", namespaces=ns)
flgnet = None
if network_source_node:
flgnet_list = network_source_node[0].xpath("./flg:FlgNet", namespaces=ns)
if flgnet_list:
flgnet = flgnet_list[0]
else: # Intentar buscar FlgNet directamente si no hay NetworkSource
flgnet_list = network_element.xpath(".//flg:FlgNet", namespaces=ns)
if flgnet_list:
flgnet = flgnet_list[0]
if flgnet is None:
return {
"id": network_id,
"title": network_title,
"comment": network_comment,
"language": network_lang,
"logic": [],
"error": "FlgNet not found inside NetworkSource or CompileUnit",
}
# 1. Parse Access, Parts, Calls (usan utils)
access_map = {}
# Corregir XPath para buscar Access dentro de FlgNet/Parts
for acc in flgnet.xpath(".//flg:Parts/flg:Access", namespaces=ns):
acc_info = parse_access(acc)
if acc_info and acc_info.get("uid") and "error" not in acc_info.get("type", ""):
access_map[acc_info["uid"]] = acc_info
elif acc_info:
print(
f"Advertencia: Ignorando Access inválido o con error UID={acc_info.get('uid')} en red {network_id}"
)
parts_and_calls_map = {}
# Corregir XPath para buscar Part y Call dentro de FlgNet/Parts
instruction_elements = flgnet.xpath(
".//flg:Parts/flg:Part | .//flg:Parts/flg:Call", namespaces=ns
)
for element in instruction_elements:
parsed_info = None
tag_name = etree.QName(element.tag).localname
if tag_name == "Part":
parsed_info = parse_part(element) # Usa utils
elif tag_name == "Call":
parsed_info = parse_call(element) # Usa utils
if (
parsed_info
and parsed_info.get("uid")
and "error" not in parsed_info.get("type", "")
):
parts_and_calls_map[parsed_info["uid"]] = parsed_info
elif parsed_info:
# Si parse_call/parse_part devolvió error, lo guardamos para tener el UID
print(
f"Advertencia: {tag_name} con error UID={parsed_info.get('uid')} en red {network_id}. Error: {parsed_info.get('error')}"
)
parts_and_calls_map[parsed_info["uid"]] = (
parsed_info # Guardar aunque tenga error
)
# 2. Parse Wires (lógica compleja, mantener aquí)
wire_connections = defaultdict(list) # destination -> [source1, source2]
source_connections = defaultdict(list) # source -> [dest1, dest2]
eno_outputs = defaultdict(list)
qname_powerrail = etree.QName(ns["flg"], "Powerrail")
qname_identcon = etree.QName(
ns["flg"], "IdentCon"
) # Conexión a/desde Access (variable/constante)
qname_namecon = etree.QName(
ns["flg"], "NameCon"
) # Conexión a/desde Part/Call (pin con nombre)
qname_openbranch = etree.QName(
ns["flg"], "Openbranch"
) # Rama abierta (normalmente ignorada o tratada como TRUE?)
qname_opencon = etree.QName(
ns["flg"], "OpenCon"
) # Conexión abierta (pin no conectado)
# Corregir XPath para buscar Wire dentro de FlgNet/Wires
for wire in flgnet.xpath(".//flg:Wires/flg:Wire", namespaces=ns):
children = wire.getchildren()
if len(children) < 2:
continue # Necesita al menos origen y destino
source_elem = children[0]
source_uid, source_pin = None, None
# Determinar origen
if source_elem.tag == qname_powerrail:
source_uid, source_pin = "POWERRAIL", "out"
elif source_elem.tag == qname_identcon: # Origen es una variable/constante
source_uid = source_elem.get("UId")
source_pin = "value" # Salida implícita de un Access
elif source_elem.tag == qname_namecon: # Origen es pin de instrucción
source_uid = source_elem.get("UId")
source_pin = source_elem.get("Name")
elif source_elem.tag == qname_openbranch:
# ¿Cómo manejar OpenBranch como fuente? Podría ser TRUE o una condición OR implícita
source_uid = "OPENBRANCH_" + wire.get(
"UId", "Unknown"
) # UID único para la rama
source_pin = "out"
print(
f"Advertencia: OpenBranch encontrado como fuente en Wire UID={wire.get('UId')} (Red {network_id}). Tratando como fuente especial."
)
# No lo añadimos a parts_and_calls_map, get_sympy_representation necesitará manejarlo
# Ignorar OpenCon como fuente (no tiene sentido)
if source_uid is None or source_pin is None:
# print(f"Advertencia: Fuente de wire inválida o no soportada: {source_elem.tag} en Wire UID={wire.get('UId')}")
continue
source_info = (source_uid, source_pin)
# Procesar destinos
for dest_elem in children[1:]:
dest_uid, dest_pin = None, None
if (
dest_elem.tag == qname_identcon
): # Destino es una variable/constante (asignación)
dest_uid = dest_elem.get("UId")
dest_pin = "value" # Entrada implícita de un Access
elif dest_elem.tag == qname_namecon: # Destino es pin de instrucción
dest_uid = dest_elem.get("UId")
dest_pin = dest_elem.get("Name")
# Ignorar Powerrail, OpenBranch, OpenCon como destinos válidos de conexión lógica principal
if dest_uid is not None and dest_pin is not None:
dest_key = (dest_uid, dest_pin)
if source_info not in wire_connections[dest_key]:
wire_connections[dest_key].append(source_info)
# Mapa inverso: source -> list of destinations
source_key = (source_uid, source_pin)
dest_info = (dest_uid, dest_pin)
if dest_info not in source_connections[source_key]:
source_connections[source_key].append(dest_info)
# Trackear salidas ENO específicamente si la fuente es una instrucción
if source_pin == "eno" and source_uid in parts_and_calls_map:
if dest_info not in eno_outputs[source_uid]:
eno_outputs[source_uid].append(dest_info)
# 3. Build Initial Logic Structure (incorporando errores)
all_logic_steps = {}
# Lista de tipos funcionales (usados para inferencia EN)
# Estos son los tipos *originales* de las instrucciones
functional_block_types = [
"Move",
"Add",
"Sub",
"Mul",
"Div",
"Mod",
"Convert",
"Call", # Call ya está aquí
"TON",
"TOF",
"TP",
"CTU",
"CTD",
"CTUD",
"BLKMOV", # Añadidos
"Se",
"Sd", # Estos son tipos LAD que se mapearán a timers SCL
]
# Lista de generadores RLO (usados para inferencia EN)
rlo_generators = [
"Contact",
"O",
"Eq",
"Ne",
"Gt",
"Lt",
"Ge",
"Le",
"And",
"Xor",
"PBox",
"NBox",
"Not",
]
# Iterar sobre UIDs válidos (los que se pudieron parsear, aunque sea con error)
valid_instruction_uids = list(parts_and_calls_map.keys())
for instruction_uid in valid_instruction_uids:
instruction_info = parts_and_calls_map[instruction_uid]
# Hacer copia profunda para no modificar el mapa original
instruction_repr = copy.deepcopy(instruction_info)
instruction_repr["instruction_uid"] = instruction_uid # Asegurar UID
instruction_repr["inputs"] = {}
instruction_repr["outputs"] = {}
# Si la instrucción ya tuvo un error de parseo, añadirlo aquí
if "error" in instruction_info:
instruction_repr["parsing_error"] = instruction_info["error"]
# No intentar poblar inputs/outputs si el parseo base falló
all_logic_steps[instruction_uid] = instruction_repr
continue
original_type = instruction_repr.get("type", "") # Tipo de la instrucción
# --- Poblar Entradas ---
# Lista base de pines posibles (podría obtenerse de XSDs o dinámicamente)
possible_input_pins = set(["en", "in", "in1", "in2", "pre"])
# Añadir pines dinámicamente basados en el tipo de instrucción
if original_type in ["Contact", "Coil", "SCoil", "RCoil", "SdCoil"]:
possible_input_pins.add("operand")
elif original_type in [
"Add",
"Sub",
"Mul",
"Div",
"Mod",
"Eq",
"Ne",
"Gt",
"Lt",
"Ge",
"Le",
]:
possible_input_pins.update(["in1", "in2"])
elif original_type in ["TON", "TOF", "TP"]:
possible_input_pins.update(["IN", "PT"]) # Pines SCL
elif original_type in ["Se", "Sd"]:
possible_input_pins.update(["s", "tv", "timer"]) # Pines LAD
elif original_type in ["CTU", "CTD", "CTUD"]:
possible_input_pins.update(["CU", "CD", "R", "LD", "PV"]) # Pines SCL/LAD
elif original_type in ["PBox", "NBox"]:
possible_input_pins.update(
["bit", "clk", "in"]
) # PBox/NBox usa 'in' y 'bit'
elif original_type == "BLKMOV":
possible_input_pins.add("SRCBLK")
elif original_type == "Move":
possible_input_pins.add("in")
elif original_type == "Convert":
possible_input_pins.add("in")
elif original_type == "Call":
# Para Calls, los nombres de los parámetros reales se definen en el XML
# El Xpath busca Parameter DENTRO de CallInfo, que está DENTRO de Call
call_xml_element_list = flgnet.xpath(
f".//flg:Parts/flg:Call[@UId='{instruction_uid}']", namespaces=ns
)
if call_xml_element_list:
call_xml_element = call_xml_element_list[0]
call_info_node_list = call_xml_element.xpath(
"./flg:CallInfo", namespaces=ns
)
if call_info_node_list:
call_param_names = call_info_node_list[0].xpath(
"./flg:Parameter/@Name", namespaces=ns
)
possible_input_pins.update(call_param_names)
# print(f"DEBUG Call UID={instruction_uid}: Params={call_param_names}")
else: # Fallback si no hay namespace (menos probable)
call_info_node_list_no_ns = call_xml_element.xpath("./CallInfo")
if call_info_node_list_no_ns:
possible_input_pins.update(
call_info_node_list_no_ns[0].xpath("./Parameter/@Name")
)
# Iterar sobre pines posibles y buscar conexiones
for pin_name in possible_input_pins:
dest_key = (instruction_uid, pin_name)
if dest_key in wire_connections:
sources_list = wire_connections[dest_key]
input_sources_repr = []
for source_uid, source_pin in sources_list:
source_repr = None
if source_uid == "POWERRAIL":
source_repr = {"type": "powerrail"}
elif source_uid.startswith("OPENBRANCH_"):
source_repr = {
"type": "openbranch",
"uid": source_uid,
} # Fuente especial
elif source_uid in access_map:
source_repr = copy.deepcopy(access_map[source_uid])
elif source_uid in parts_and_calls_map:
source_instr_info = parts_and_calls_map[source_uid]
source_repr = {
"type": "connection",
"source_instruction_type": source_instr_info.get(
"type", "Unknown"
), # Usar tipo base
"source_instruction_uid": source_uid,
"source_pin": source_pin,
}
else:
# Fuente desconocida (ni Access, ni Part/Call válido)
print(
f"Advertencia: Fuente desconocida UID={source_uid} conectada a {instruction_uid}.{pin_name}"
)
source_repr = {"type": "unknown_source", "uid": source_uid}
input_sources_repr.append(source_repr)
# Guardar la representación de la entrada (lista o dict)
instruction_repr["inputs"][pin_name] = (
input_sources_repr[0]
if len(input_sources_repr) == 1
else input_sources_repr
)
# --- Poblar Salidas (simplificado: solo conexiones a Access) ---
possible_output_pins = set(
[
"out",
"out1",
"Q",
"q",
"eno",
"RET_VAL",
"DSTBLK",
"rt",
"cv",
"QU",
"QD",
"ET", # Añadir pines de salida estándar SCL
]
)
if original_type == "BLKMOV":
possible_output_pins.add("DSTBLK")
if (
original_type == "Call"
): # Para Calls, las salidas dependen del bloque llamado
call_xml_element_list = flgnet.xpath(
f".//flg:Parts/flg:Call[@UId='{instruction_uid}']", namespaces=ns
)
if call_xml_element_list:
call_info_node_list = call_xml_element_list[0].xpath(
"./flg:CallInfo", namespaces=ns
)
if call_info_node_list:
# Buscar parámetros con Section="Output" o "InOut" o "Return"
output_param_names = call_info_node_list[0].xpath(
"./flg:Parameter[@Section='Output' or @Section='InOut' or @Section='Return']/@Name",
namespaces=ns,
)
possible_output_pins.update(output_param_names)
for pin_name in possible_output_pins:
source_key = (instruction_uid, pin_name)
if source_key in source_connections:
if pin_name not in instruction_repr["outputs"]:
instruction_repr["outputs"][pin_name] = []
for dest_uid, dest_pin in source_connections[source_key]:
if (
dest_uid in access_map
): # Solo registrar si va a una variable/constante
dest_operand_copy = copy.deepcopy(access_map[dest_uid])
if (
dest_operand_copy
not in instruction_repr["outputs"][pin_name]
):
instruction_repr["outputs"][pin_name].append(
dest_operand_copy
)
all_logic_steps[instruction_uid] = instruction_repr
# 4. Inferencia EN (modificado para usar tipos originales)
processed_blocks_en_inference = set()
try:
# Ordenar UIDs numéricamente si es posible
sorted_uids_for_en = sorted(
all_logic_steps.keys(),
key=lambda x: (
int(x) if isinstance(x, str) and x.isdigit() else float("inf")
),
)
except ValueError:
sorted_uids_for_en = sorted(all_logic_steps.keys()) # Fallback sort
ordered_logic_list_for_en = [
all_logic_steps[uid] for uid in sorted_uids_for_en if uid in all_logic_steps
]
for i, instruction in enumerate(ordered_logic_list_for_en):
part_uid = instruction["instruction_uid"]
# Usar el tipo original para la lógica de inferencia
part_type_original = (
instruction.get("type", "").replace(SCL_SUFFIX, "").replace("_error", "")
)
# Inferencia solo para tipos funcionales que no tengan EN explícito
if (
part_type_original in functional_block_types
and "en" not in instruction.get("inputs", {})
and part_uid not in processed_blocks_en_inference
and "error" not in part_type_original
): # No inferir para errores
inferred_en_source = None
# Buscar hacia atrás en la lista ordenada
if i > 0:
for j in range(i - 1, -1, -1):
prev_instr = ordered_logic_list_for_en[j]
if "error" in prev_instr.get("type", ""):
continue # Saltar errores previos
prev_uid = prev_instr["instruction_uid"]
prev_type_original = (
prev_instr.get("type", "")
.replace(SCL_SUFFIX, "")
.replace("_error", "")
)
if prev_type_original in rlo_generators: # Fuente RLO encontrada
inferred_en_source = {
"type": "connection",
"source_instruction_uid": prev_uid,
"source_instruction_type": prev_type_original, # Tipo original
"source_pin": "out",
}
break # Detener búsqueda
elif (
prev_type_original in functional_block_types
): # Bloque funcional previo
# Comprobar si este bloque tiene salida ENO conectada
if (prev_uid, "eno") in source_connections:
inferred_en_source = {
"type": "connection",
"source_instruction_uid": prev_uid,
"source_instruction_type": prev_type_original, # Tipo original
"source_pin": "eno",
}
# Si no tiene ENO conectado, el flujo RLO se detiene aquí
break # Detener búsqueda
elif prev_type_original in [
"Coil",
"SCoil",
"RCoil",
"SdCoil",
"SetCoil",
"ResetCoil",
]:
# Bobinas terminan el flujo RLO
break # Detener búsqueda
# Si no se encontró fuente, conectar a PowerRail
if inferred_en_source is None:
inferred_en_source = {"type": "powerrail"}
# Actualizar la instrucción EN el diccionario principal
if part_uid in all_logic_steps:
# Asegurar que inputs exista
if "inputs" not in all_logic_steps[part_uid]:
all_logic_steps[part_uid]["inputs"] = {}
all_logic_steps[part_uid]["inputs"]["en"] = inferred_en_source
processed_blocks_en_inference.add(part_uid)
# 5. Lógica ENO (añadir destinos ENO si existen)
for source_instr_uid, eno_destinations in eno_outputs.items():
if source_instr_uid in all_logic_steps and "error" not in all_logic_steps[
source_instr_uid
].get("type", ""):
all_logic_steps[source_instr_uid]["eno_destinations"] = eno_destinations
# 6. Ordenar y Devolver
final_logic_list = [
all_logic_steps[uid] for uid in sorted_uids_for_en if uid in all_logic_steps
]
return {
"id": network_id,
"title": network_title,
"comment": network_comment,
"language": network_lang, # Lenguaje original de la red
"logic": final_logic_list,
# No añadir 'error' aquí a menos que el parseo completo falle
}
# --- Función de Información del Parser ---
def get_parser_info():
"""Devuelve la información para este parser."""
# Este parser maneja LAD, FBD y GRAPH
return {
"language": ["LAD", "FBD", "GRAPH"], # Lista de lenguajes soportados
"parser_func": parse_lad_fbd_network, # Función a llamar
}