# 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 }