549 lines
22 KiB
Python
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
|
|
}
|