From 1b727fa4bb5101763c4c0dd89f0f7d9af58785d9 Mon Sep 17 00:00:00 2001 From: Miguel Date: Fri, 18 Apr 2025 12:18:49 +0200 Subject: [PATCH] Varsion Base Utilizable --- BlenderRun_ProdTime_simplified.json | 33 ++- to_json.py | 436 ++++++++++------------------ 2 files changed, 178 insertions(+), 291 deletions(-) diff --git a/BlenderRun_ProdTime_simplified.json b/BlenderRun_ProdTime_simplified.json index 798a314..7bac087 100644 --- a/BlenderRun_ProdTime_simplified.json +++ b/BlenderRun_ProdTime_simplified.json @@ -2,6 +2,7 @@ "block_name": "BlenderRun_ProdTime", "block_number": 2040, "language": "LAD", + "block_comment": "", "interface": { "Temp": [ { @@ -48,6 +49,7 @@ { "id": "9", "title": "Seconds", + "comment": "", "logic": [ { "instruction_uid": "26", @@ -124,6 +126,7 @@ { "id": "1A", "title": "Reset Hours", + "comment": "", "logic": [ { "instruction_uid": "24", @@ -175,6 +178,7 @@ { "id": "2B", "title": "Seconds Counter", + "comment": "", "logic": [ { "instruction_uid": "26", @@ -251,6 +255,7 @@ { "id": "3C", "title": "Minute", + "comment": "", "logic": [ { "instruction_uid": "24", @@ -299,6 +304,7 @@ { "id": "4D", "title": "Minute Counter", + "comment": "", "logic": [ { "instruction_uid": "27", @@ -379,6 +385,7 @@ { "id": "5E", "title": "Hour", + "comment": "", "logic": [ { "instruction_uid": "24", @@ -427,6 +434,7 @@ { "id": "6F", "title": "Hour Counter", + "comment": "", "logic": [ { "instruction_uid": "30", @@ -536,6 +544,7 @@ { "id": "80", "title": "Counter reset", + "comment": "", "logic": [ { "instruction_uid": "29", @@ -665,6 +674,7 @@ { "id": "91", "title": "Running Seconds", + "comment": "", "logic": [ { "instruction_uid": "26", @@ -741,6 +751,7 @@ { "id": "A2", "title": "Running Minutes", + "comment": "", "logic": [ { "instruction_uid": "35", @@ -800,7 +811,15 @@ "name": "\"MOD60\"" } ] - } + }, + "eno_logic": [ + { + "target_pin": "pre", + "target_type": "instruction", + "target_uid": "37", + "target_name": "Eq" + } + ] }, { "instruction_uid": "37", @@ -938,6 +957,7 @@ { "id": "B3", "title": "Running Hours for Maintenance", + "comment": "", "logic": [ { "instruction_uid": "32", @@ -1016,7 +1036,15 @@ "name": "\"MOD60\"" } ] - } + }, + "eno_logic": [ + { + "target_pin": "pre", + "target_type": "instruction", + "target_uid": "35", + "target_name": "Eq" + } + ] }, { "instruction_uid": "35", @@ -1084,6 +1112,7 @@ { "id": "C4", "title": "Running Hours for Maintenance", + "comment": "", "logic": [ { "instruction_uid": "23", diff --git a/to_json.py b/to_json.py index b984d9b..039a8c3 100644 --- a/to_json.py +++ b/to_json.py @@ -2,27 +2,22 @@ import json import os from lxml import etree -import traceback # Para obtener detalles de excepciones +import traceback # --- Namespaces --- -# Namespaces usados comúnmente en los archivos XML de TIA Portal Openness ns = { - # Namespace principal para elementos de la interfaz del bloque (Input, Output, Temp, etc.) 'iface': 'http://www.siemens.com/automation/Openness/SW/Interface/v5', - # Namespace para elementos dentro de la lógica de red LAD/FBD (FlgNet) 'flg': 'http://www.siemens.com/automation/Openness/SW/NetworkSource/FlgNet/v4' - # Podrían añadirse otros si fueran necesarios (p.ej., para SCL, DBs) } # --- Helper Functions --- - +# (get_multilingual_text, get_symbol_name, parse_access, parse_part sin cambios) def get_multilingual_text(element, default_lang='en-US', fallback_lang='it-IT'): """ Intenta extraer texto de un elemento MultilingualText, priorizando idiomas. Busca directamente los Text dentro de Items/AttributeList/Text bajo las Culture especificadas. """ if element is None: - # print("DEBUG: get_multilingual_text llamado con element=None") return "" try: # Intenta encontrar el idioma por defecto @@ -30,7 +25,6 @@ def get_multilingual_text(element, default_lang='en-US', fallback_lang='it-IT'): f"/*[local-name()='AttributeList']/*[local-name()='Text']" text_items = element.xpath(xpath_expr) if text_items and text_items[0].text is not None: - # print(f"DEBUG: Texto encontrado para {default_lang}: {text_items[0].text.strip()}") return text_items[0].text.strip() # Si no, intenta encontrar el idioma de fallback @@ -38,17 +32,14 @@ def get_multilingual_text(element, default_lang='en-US', fallback_lang='it-IT'): f"/*[local-name()='AttributeList']/*[local-name()='Text']" text_items = element.xpath(xpath_expr) if text_items and text_items[0].text is not None: - # print(f"DEBUG: Texto encontrado para {fallback_lang}: {text_items[0].text.strip()}") return text_items[0].text.strip() # Si no, toma el primer texto que encuentre xpath_expr = f".//*[local-name()='MultilingualTextItem']/*[local-name()='AttributeList']/*[local-name()='Text']" text_items = element.xpath(xpath_expr) if text_items and text_items[0].text is not None: - # print(f"DEBUG: Texto encontrado (primer disponible): {text_items[0].text.strip()}") return text_items[0].text.strip() - # print("DEBUG: No se encontró ningún texto en MultilingualTextItem") return "" # Devuelve cadena vacía si no se encuentra nada except Exception as e: print(f"Advertencia: Error extrayendo MultilingualText desde {etree.tostring(element, encoding='unicode')[:100]}...: {e}") @@ -60,7 +51,6 @@ def get_symbol_name(symbol_element): Encapsula cada componente entre comillas dobles y los une con puntos. """ if symbol_element is None: - # print("DEBUG: get_symbol_name llamado con symbol_element=None") return None try: # Selecciona el atributo 'Name' de cada elemento 'Component' hijo directo del Symbol @@ -68,10 +58,8 @@ def get_symbol_name(symbol_element): if components: # Une los componentes, asegurándose de que cada uno esté entre comillas dobles full_name = ".".join(f'"{c}"' for c in components) - # print(f"DEBUG: Nombre de símbolo construido: {full_name}") return full_name else: - # print(f"Advertencia: No se encontraron 'Component' en Symbol: {etree.tostring(symbol_element, encoding='unicode')}") return None # Indica que no se pudo formar un nombre except Exception as e: print(f"Advertencia: Excepción en get_symbol_name para {etree.tostring(symbol_element, encoding='unicode')[:100]}...: {e}") @@ -84,67 +72,48 @@ def parse_access(access_element): Devuelve un diccionario con la información o None si hay un error crítico. """ if access_element is None: - # print("DEBUG: parse_access llamado con access_element=None") return None uid = access_element.get('UId') scope = access_element.get('Scope') - # print(f"DEBUG: Parseando Access UID={uid}, Scope={scope}") info = {'uid': uid, 'scope': scope, 'type': 'unknown'} # Inicializa info - # Intenta encontrar un elemento Symbol (indica una variable) symbol = access_element.xpath("./*[local-name()='Symbol']") - # Intenta encontrar un elemento Constant (indica un valor literal) constant = access_element.xpath("./*[local-name()='Constant']") if symbol: info['type'] = 'variable' info['name'] = get_symbol_name(symbol[0]) if info['name'] is None: - # Si get_symbol_name falló, marca como error y reporta info['type'] = 'error_parsing_symbol' print(f"Error: No se pudo parsear el nombre del símbolo para Access UID={uid}") - return info # Devolver la info con el error marcado - # print(f"DEBUG: Access UID={uid} es Variable: {info['name']}") + return info elif constant: info['type'] = 'constant' - # Extrae el tipo de dato de la constante const_type_elem = constant[0].xpath("./*[local-name()='ConstantType']") const_val_elem = constant[0].xpath("./*[local-name()='ConstantValue']") - - # Obtiene el texto del tipo de dato, o 'Unknown' si no está presente info['datatype'] = const_type_elem[0].text if const_type_elem and const_type_elem[0].text is not None else 'Unknown' - # Obtiene el texto del valor, o None si no está presente value_str = const_val_elem[0].text if const_val_elem and const_val_elem[0].text is not None else None if value_str is None: - # Si no hay valor, marca como error y reporta info['type'] = 'error_parsing_constant' info['value'] = None print(f"Error: Constante sin valor encontrada para Access UID={uid}") - return info # Devolver la info con el error marcado + return info - # Intenta inferir el tipo si es 'Unknown' if info['datatype'] == 'Unknown': val_lower = value_str.lower() if val_lower in ['true', 'false']: info['datatype'] = 'Bool' - elif value_str.isdigit() or (value_str.startswith('-') and value_str[1:].isdigit()): info['datatype'] = 'Int' # Asume Int base + elif value_str.isdigit() or (value_str.startswith('-') and value_str[1:].isdigit()): info['datatype'] = 'Int' elif '.' in value_str: - try: - float(value_str) - info['datatype'] = 'Real' # Asume Real base - except ValueError: pass # Sigue siendo Unknown si no parece número real - elif '#' in value_str: - info['datatype'] = 'TypedConstant' # Ej: DINT#60 - # print(f"DEBUG: Tipo de constante inferido para UID={uid}: {info['datatype']} (desde '{value_str}')") + try: float(value_str); info['datatype'] = 'Real' + except ValueError: pass + elif '#' in value_str: info['datatype'] = 'TypedConstant' - - # Intenta convertir el valor a un tipo Python nativo - info['value'] = value_str # Valor por defecto es el string original + info['value'] = value_str dtype_lower = info['datatype'].lower() - # Procesa el valor, quitando prefijos tipo 'DINT#' si existen val_str_processed = value_str.split('#')[-1] if '#' in value_str else value_str try: if dtype_lower in ['int', 'dint', 'udint', 'sint', 'usint', 'lint', 'ulint', 'word', 'dword', 'lword', 'byte']: @@ -153,32 +122,23 @@ def parse_access(access_element): info['value'] = val_str_processed.lower() == 'true' or val_str_processed == '1' elif dtype_lower in ['real', 'lreal']: info['value'] = float(val_str_processed) - # Para TypedConstant u otros, mantenemos el string original como valor, pero registramos el tipo elif dtype_lower == 'typedconstant': - # Podríamos intentar extraer el tipo y valor aquí si fuera necesario - info['value'] = value_str # Mantiene el string original - # Nota: Los tipos Time, Date, String, etc., se mantendrán como strings + info['value'] = value_str except (ValueError, TypeError) as e: - # Si la conversión falla, mantenemos el string original y reportamos print(f"Advertencia: No se pudo convertir el valor '{val_str_processed}' a tipo {dtype_lower} para UID={uid}. Se mantiene como string. Error: {e}") - info['value'] = value_str # Asegura que se mantenga el string original si falla la conversión - - # print(f"DEBUG: Access UID={uid} es Constante: Tipo={info['datatype']}, Valor={info['value']}") + info['value'] = value_str else: - # Si no es ni Symbol ni Constant, es una estructura desconocida info['type'] = 'unknown_structure' print(f"Advertencia: Access UID={uid} no es ni Symbol ni Constant.") - return info # Devolver la info con el tipo 'unknown_structure' + return info - # Verificación final: si es variable, debe tener nombre if info['type'] == 'variable' and info.get('name') is None: - # Esto no debería ocurrir si get_symbol_name funciona, pero es una salvaguarda print(f"Error Interno: parse_access terminó con tipo 'variable' pero sin nombre para UID {uid}.") - info['type'] = 'error_no_name' # Marca un estado de error específico + info['type'] = 'error_no_name' return info - return info # Devuelve el diccionario con la información parseada + return info def parse_part(part_element): """ @@ -187,34 +147,29 @@ def parse_part(part_element): Devuelve un diccionario con la información o None si el elemento es inválido. """ if part_element is None: - # print("DEBUG: parse_part llamado con part_element=None") return None uid = part_element.get('UId') name = part_element.get('Name') - # print(f"DEBUG: Parseando Part UID={uid}, Name={name}") if not uid or not name: print(f"Error: Part encontrado sin UID o Name: {etree.tostring(part_element, encoding='unicode')}") - return None # Ignora partes inválidas + return None - # Extrae los TemplateValue si existen (información adicional sobre la instrucción) template_values = {} try: - # Selecciona los atributos Name y Type de cada TemplateValue hijo for tv in part_element.xpath("./*[local-name()='TemplateValue']"): tv_name = tv.get('Name') tv_type = tv.get('Type') if tv_name and tv_type: template_values[tv_name] = tv_type - # print(f"DEBUG: TemplateValues para Part UID={uid}: {template_values}") except Exception as e: print(f"Advertencia: Error extrayendo TemplateValues para Part UID={uid}: {e}") return { 'uid': uid, 'name': name, - 'template_values': template_values + 'template_values': template_values # Mantenemos esto por si acaso, aunque no se use prominentemente } # --- Main Parsing Logic --- @@ -222,382 +177,285 @@ def parse_part(part_element): def parse_network(network_element): """ Parsea un elemento SW.Blocks.CompileUnit (representa una red de lógica) - y extrae su ID, título y la lógica interna simplificada en formato JSON. + y extrae su ID, título, comentario y la lógica interna simplificada en formato JSON, + incluyendo la lógica conectada a ENO si no es un simple EN->ENO. """ if network_element is None: print("Error: parse_network llamado con network_element=None") - return {'id': 'ERROR', 'title': 'Invalid Network Element', 'logic': [], 'error': 'Input element was None'} + return {'id': 'ERROR', 'title': 'Invalid Network Element', 'comment': '', 'logic': [], 'error': 'Input element was None'} network_id = network_element.get('ID') - # print(f"--- Parseando Red ID={network_id} ---") - # Extrae el título de la red usando la función auxiliar + # Extrae el título de la red title_element = network_element.xpath(".//*[local-name()='MultilingualText'][@CompositionName='Title']") network_title = get_multilingual_text(title_element[0]) if title_element else f"Network {network_id}" - # print(f"DEBUG: Título de la Red: {network_title}") - # Encuentra el contenedor FlgNet que tiene la lógica LAD/FBD - # Usa '//' para buscar en cualquier nivel descendiente y el namespace 'flg' + # *** NUEVO: Extrae el comentario de la red *** + network_comment = "" + comment_title_element = network_element.xpath("./*[local-name()='ObjectList']/*[local-name()='MultilingualText'][@CompositionName='Comment']") + if comment_title_element: + network_comment = get_multilingual_text(comment_title_element[0]) + # print(f"DEBUG: Comentario Red {network_id}: '{network_comment[:50]}...'") + + flgnet_list = network_element.xpath(".//flg:FlgNet", namespaces=ns) if not flgnet_list: print(f"Error: No se encontró FlgNet en la red ID={network_id}") - return {'id': network_id, 'title': network_title, 'logic': [], 'error': 'FlgNet not found'} - flgnet = flgnet_list[0] # Toma el primer FlgNet encontrado + return {'id': network_id, 'title': network_title, 'comment': network_comment, 'logic': [], 'error': 'FlgNet not found'} + flgnet = flgnet_list[0] - # 1. Parsea todos los Access (operandos) y Parts (instrucciones) dentro de FlgNet + # 1. Parsear Access y Parts access_map = {} for acc in flgnet.xpath(".//flg:Access", namespaces=ns): acc_info = parse_access(acc) if acc_info and 'uid' in acc_info: access_map[acc_info['uid']] = acc_info - else: - print(f"Advertencia: Se ignoró un Access inválido en la red {network_id}") parts_map = {} for part in flgnet.xpath(".//flg:Part", namespaces=ns): part_info = parse_part(part) if part_info and 'uid' in part_info: parts_map[part_info['uid']] = part_info - else: - print(f"Advertencia: Se ignoró un Part inválido en la red {network_id}") - # print(f"DEBUG: Red {network_id}: {len(access_map)} Access encontrados, {len(parts_map)} Parts encontrados.") - - # 2. Parsea todas las conexiones (Wires) para entender el flujo - wire_connections = {} # Diccionario: clave=(dest_uid, dest_pin), valor=lista de (source_uid, source_pin) - flg_ns_uri = ns['flg'] # Obtiene la URI del namespace 'flg' para comparaciones de tags + # 2. Parsear Wires y construir mapa de conexiones de entrada y *salida ENO* + wire_connections = {} # Clave=(dest_uid, dest_pin), Valor=lista de (source_uid, source_pin) + eno_outputs = {} # Clave=source_part_uid, Valor=lista de (dest_uid, dest_pin) + flg_ns_uri = ns['flg'] for wire in flgnet.xpath(".//flg:Wire", namespaces=ns): - # print(f"DEBUG: Procesando Wire: {etree.tostring(wire, encoding='unicode').strip()}") source_uid, source_pin, dest_uid, dest_pin = None, None, None, None - children = wire.getchildren() # Obtiene los hijos directos del Wire (conexiones) - - if len(children) < 2: - # print(f"Advertencia: Wire con menos de 2 hijos ignorado en red {network_id}: {etree.tostring(wire, encoding='unicode')}") - continue # Un wire necesita al menos un origen y un destino + children = wire.getchildren() + if len(children) < 2: continue source_elem, dest_elem = children[0], children[1] - # Determina la fuente de la conexión - # Utiliza etree.QName para comparar tags incluyendo el namespace - if source_elem.tag == etree.QName(flg_ns_uri, 'Powerrail'): - source_uid, source_pin = 'POWERRAIL', 'out' # Fuente es la barra de potencia - elif source_elem.tag == etree.QName(flg_ns_uri, 'IdentCon'): - # Conexión a un operando (Access) - source_uid, source_pin = source_elem.get('UId'), 'value' # Usamos 'value' como pin genérico para Access - elif source_elem.tag == etree.QName(flg_ns_uri, 'NameCon'): - # Conexión desde un pin específico de una instrucción (Part) - source_uid, source_pin = source_elem.get('UId'), source_elem.get('Name') - else: - # print(f"Advertencia: Tipo de fuente de Wire desconocido '{source_elem.tag}' en red {network_id}") - pass # Ignora fuentes desconocidas por ahora + # Determina la fuente + if source_elem.tag == etree.QName(flg_ns_uri, 'Powerrail'): source_uid, source_pin = 'POWERRAIL', 'out' + elif source_elem.tag == etree.QName(flg_ns_uri, 'IdentCon'): source_uid, source_pin = source_elem.get('UId'), 'value' + elif source_elem.tag == etree.QName(flg_ns_uri, 'NameCon'): source_uid, source_pin = source_elem.get('UId'), source_elem.get('Name') - # Determina el destino de la conexión - if dest_elem.tag == etree.QName(flg_ns_uri, 'IdentCon'): - # Conexión a un operando (Access) - Generalmente una asignación de salida - dest_uid, dest_pin = dest_elem.get('UId'), 'value' # Usamos 'value' como pin genérico - elif dest_elem.tag == etree.QName(flg_ns_uri, 'NameCon'): - # Conexión a un pin específico de una instrucción (Part) - Generalmente una entrada - dest_uid, dest_pin = dest_elem.get('UId'), dest_elem.get('Name') - else: - # print(f"Advertencia: Tipo de destino de Wire desconocido '{dest_elem.tag}' en red {network_id}") - pass # Ignora destinos desconocidos + # Determina el destino + if dest_elem.tag == etree.QName(flg_ns_uri, 'IdentCon'): dest_uid, dest_pin = dest_elem.get('UId'), 'value' + elif dest_elem.tag == etree.QName(flg_ns_uri, 'NameCon'): dest_uid, dest_pin = dest_elem.get('UId'), dest_elem.get('Name') - # Si tenemos un destino válido y una fuente válida, registra la conexión + # Registrar conexión de entrada normal if dest_uid and dest_pin and source_uid is not None: dest_key = (dest_uid, dest_pin) source_info = (source_uid, source_pin) - # print(f"DEBUG: Wire Conexión: De ({source_uid}, {source_pin}) a ({dest_uid}, {dest_pin})") + if dest_key not in wire_connections: wire_connections[dest_key] = [] + if source_info not in wire_connections[dest_key]: wire_connections[dest_key].append(source_info) - # Añade la fuente a la lista de fuentes para ese pin de destino - if dest_key not in wire_connections: - wire_connections[dest_key] = [] - if source_info not in wire_connections[dest_key]: # Evita duplicados si el XML fuera redundante - wire_connections[dest_key].append(source_info) + # *** NUEVO: Registrar conexiones que SALEN de un pin ENO *** + if source_pin == 'eno' and source_uid in parts_map: + if source_uid not in eno_outputs: eno_outputs[source_uid] = [] + eno_dest_info = (dest_uid, dest_pin) + if eno_dest_info not in eno_outputs[source_uid]: + eno_outputs[source_uid].append(eno_dest_info) + # print(f"DEBUG: Red {network_id} - ENO de {source_uid} conectado a ({dest_uid}, {dest_pin})") - # print(f"DEBUG: Red {network_id}: {len(wire_connections)} Conexiones de destino procesadas.") - # 3. Construye la representación lógica de cada instrucción (Part) - all_logic_steps = {} # Diccionario para almacenar la representación JSON de cada instrucción + # 3. Construir la representación lógica principal + all_logic_steps = {} for part_uid, part_info in parts_map.items(): - instruction_repr = { - 'instruction_uid': part_uid, - 'type': part_info['name'], - 'inputs': {}, # Pines de entrada de la instrucción - 'outputs': {} # Pines de salida de la instrucción - } + instruction_repr = {'instruction_uid': part_uid, 'type': part_info['name'], 'inputs': {}, 'outputs': {}} - # Busca todas las conexiones que *llegan* a esta instrucción (Part) + # Procesar Entradas (igual que antes) for (conn_dest_uid, conn_dest_pin), sources_list in wire_connections.items(): - if conn_dest_uid == part_uid: # Si el destino es esta instrucción - # print(f"DEBUG: Part UID={part_uid}, Pin Entrada='{conn_dest_pin}', Fuentes={sources_list}") - input_sources_repr = [] # Lista para representar las fuentes de este pin + if conn_dest_uid == part_uid: + input_sources_repr = [] for source_uid, source_pin in sources_list: - if source_uid == 'POWERRAIL': - input_sources_repr.append({'type': 'powerrail'}) - elif source_uid in access_map: - # La fuente es una variable o constante - input_sources_repr.append(access_map[source_uid]) + if source_uid == 'POWERRAIL': input_sources_repr.append({'type': 'powerrail'}) + elif source_uid in access_map: input_sources_repr.append(access_map[source_uid]) elif source_uid in parts_map: - # La fuente es la salida de otra instrucción - input_sources_repr.append({ - 'type': 'connection', - 'source_instruction_uid': source_uid, - 'source_instruction_type': parts_map[source_uid]['name'], - 'source_pin': source_pin - }) - else: - # Fuente desconocida (error o caso no manejado) - input_sources_repr.append({'type': 'unknown_source', 'uid': source_uid}) - print(f"Advertencia: Fuente desconocida UID={source_uid} encontrada como entrada para Part UID={part_uid}, Pin={conn_dest_pin}") + input_sources_repr.append({'type': 'connection', 'source_instruction_uid': source_uid, + 'source_instruction_type': parts_map[source_uid]['name'], 'source_pin': source_pin}) + else: input_sources_repr.append({'type': 'unknown_source', 'uid': source_uid}) - # Asigna la representación de las fuentes al pin de entrada correspondiente - # Si solo hay una fuente, la asigna directamente. - # Si hay múltiples fuentes (caso de ramas OR en LAD), las agrupa. - # NOTA: Esta lógica de OR_Branch es una simplificación y podría no cubrir todos los casos complejos. - # Asumimos que múltiples wires llegando al mismo pin de entrada implican un OR. - if len(input_sources_repr) == 1: - instruction_repr['inputs'][conn_dest_pin] = input_sources_repr[0] - elif len(input_sources_repr) > 1: - # Si hay varias fuentes para el mismo pin de entrada, las marcamos - # print(f"DEBUG: Múltiples fuentes para Part UID={part_uid}, Pin={conn_dest_pin}. Asumiendo OR.") - # Comprobamos si todas las fuentes son simples conexiones o powerrail - is_simple_or = all(s['type'] in ['powerrail', 'connection'] for s in input_sources_repr) - if is_simple_or: - # Si son conexiones simples, mantenemos la lista (implica OR) - instruction_repr['inputs'][conn_dest_pin] = input_sources_repr - else: - # Si hay operandos directos (variables/constantes), esto es más complejo. - # Por ahora, lo marcamos como una rama OR genérica. - # Una mejora sería analizar la estructura LAD/FBD más a fondo. - print(f"Advertencia: Rama OR compleja detectada para Part UID={part_uid}, Pin={conn_dest_pin}. Simplificando a lista.") - instruction_repr['inputs'][conn_dest_pin] = input_sources_repr - # else: no sources found for this input pin (podría ser normal si no está conectado) + if len(input_sources_repr) == 1: instruction_repr['inputs'][conn_dest_pin] = input_sources_repr[0] + elif len(input_sources_repr) > 1: instruction_repr['inputs'][conn_dest_pin] = input_sources_repr - - # Busca todas las conexiones que *salen* de esta instrucción y van a un Access (asignación) + # Procesar Salidas (igual que antes) for (conn_dest_uid, conn_dest_pin), sources_list in wire_connections.items(): for source_uid, source_pin in sources_list: - if source_uid == part_uid and conn_dest_uid in access_map: # Si la fuente es esta instrucción y el destino es un Access - # print(f"DEBUG: Part UID={part_uid}, Pin Salida='{source_pin}', Destino Access UID={conn_dest_uid}") - # Agrega el Access de destino a la lista de salidas de este pin - if source_pin not in instruction_repr['outputs']: - instruction_repr['outputs'][source_pin] = [] - # Evita añadir duplicados si el XML es redundante + if source_uid == part_uid and conn_dest_uid in access_map: + if source_pin not in instruction_repr['outputs']: instruction_repr['outputs'][source_pin] = [] if access_map[conn_dest_uid] not in instruction_repr['outputs'][source_pin]: instruction_repr['outputs'][source_pin].append(access_map[conn_dest_uid]) - # Almacena la representación JSON de la instrucción all_logic_steps[part_uid] = instruction_repr - # 4. Ordena las instrucciones por UID (intenta mantener un orden lógico si los UIDs son numéricos) - # Esto es una heurística, el orden real de ejecución depende del PLC. + + # *** NUEVO: Procesar y añadir lógica ENO "interesante" *** + for source_instr_uid, eno_destinations in eno_outputs.items(): + if source_instr_uid not in all_logic_steps: continue # Seguridad + + interesting_eno_logic = [] + for dest_uid, dest_pin in eno_destinations: + # Determinar si es una conexión directa a EN de otra instrucción + is_direct_en_connection = (dest_uid in parts_map and dest_pin == 'en') + + if not is_direct_en_connection: + # Si NO es directa a EN, es "interesante" + target_info = {'target_pin': dest_pin} + if dest_uid in parts_map: + target_info['target_type'] = 'instruction' + target_info['target_uid'] = dest_uid + target_info['target_name'] = parts_map[dest_uid]['name'] + elif dest_uid in access_map: + # El destino es una variable o constante + target_info['target_type'] = 'operand' + target_info['target_details'] = access_map[dest_uid] # Incluye toda la info del Access + else: + target_info['target_type'] = 'unknown' + target_info['target_uid'] = dest_uid + + interesting_eno_logic.append(target_info) + # print(f"DEBUG: Red {network_id} - ENO de {source_instr_uid}: Lógica interesante -> {target_info}") + + # Añadir la lista de lógica ENO interesante a la instrucción fuente, si existe + if interesting_eno_logic: + all_logic_steps[source_instr_uid]['eno_logic'] = interesting_eno_logic + + + # 4. Ordenar y finalizar try: - # Intenta convertir UIDs a enteros para ordenar numéricamente, - # poniendo los no numéricos al final. sorted_uids = sorted(all_logic_steps.keys(), key=lambda x: int(x) if x.isdigit() else float('inf')) except ValueError: - # Si falla la conversión a int (UIDs no numéricos complejos), ordena alfabéticamente. print(f"Advertencia: UIDs no puramente numéricos en red {network_id}. Ordenando alfabéticamente.") sorted_uids = sorted(all_logic_steps.keys()) - # Construye la lista final de lógica ordenada network_logic = [all_logic_steps[uid] for uid in sorted_uids if uid in all_logic_steps] - # print(f"--- Fin Parseo Red ID={network_id} ---") - return {'id': network_id, 'title': network_title, 'logic': network_logic} + # Devolver estructura de red con ID, título, comentario y lógica + return {'id': network_id, 'title': network_title, 'comment': network_comment, 'logic': network_logic} def convert_xml_to_json(xml_filepath, json_filepath): """ Función principal que orquesta la conversión del archivo XML de Openness - a un archivo JSON simplificado que representa la estructura del bloque FC. + a un archivo JSON simplificado que representa la estructura del bloque FC, + incluyendo comentarios y lógica ENO no trivial. """ print(f"Iniciando conversión de '{xml_filepath}' a '{json_filepath}'...") if not os.path.exists(xml_filepath): print(f"Error Crítico: Archivo XML no encontrado en '{xml_filepath}'") - return # Termina si el archivo no existe + return try: - # Parsea el archivo XML print("Paso 1: Parseando archivo XML...") - parser = etree.XMLParser(remove_blank_text=True) # Intenta limpiar espacios irrelevantes + parser = etree.XMLParser(remove_blank_text=True) tree = etree.parse(xml_filepath, parser) root = tree.getroot() print("Paso 1: Parseo XML completado.") - # Encuentra el bloque FC principal usando local-name() para evitar problemas de namespace - # // busca en todo el árbol print("Paso 2: Buscando el bloque SW.Blocks.FC...") fc_block_list = root.xpath("//*[local-name()='SW.Blocks.FC']") if not fc_block_list: print("Error Crítico: No se encontró el elemento en el archivo.") - return # Termina si no encuentra el bloque FC - fc_block = fc_block_list[0] # Asume que solo hay un bloque FC principal en este archivo + return + fc_block = fc_block_list[0] print(f"Paso 2: Bloque SW.Blocks.FC encontrado (ID={fc_block.get('ID')}).") - - # Extrae los atributos básicos del bloque desde su AttributeList - print("Paso 3: Extrayendo atributos del bloque (Nombre, Número, Lenguaje)...") - # Busca AttributeList como hijo directo de fc_block + print("Paso 3: Extrayendo atributos del bloque...") attribute_list_node = fc_block.xpath("./*[local-name()='AttributeList']") block_name_val = "Unknown" block_number_val = None block_lang_val = "Unknown" - if attribute_list_node: attr_list = attribute_list_node[0] - # Extrae Nombre name_node = attr_list.xpath("./*[local-name()='Name']/text()") if name_node: block_name_val = name_node[0].strip() - # Extrae Número num_node = attr_list.xpath("./*[local-name()='Number']/text()") if num_node and num_node[0].isdigit(): block_number_val = int(num_node[0]) - # Extrae Lenguaje lang_node = attr_list.xpath("./*[local-name()='ProgrammingLanguage']/text()") if lang_node: block_lang_val = lang_node[0].strip() print(f"Paso 3: Atributos extraídos: Nombre='{block_name_val}', Número={block_number_val}, Lenguaje='{block_lang_val}'") - else: - print("Advertencia: No se encontró AttributeList para el bloque FC.") + else: print("Advertencia: No se encontró AttributeList para el bloque FC.") + + # *** NUEVO: Extraer comentario del bloque *** + block_comment_val = "" + # El comentario del bloque suele estar en ObjectList > MultilingualText[@CompositionName='Comment'] + comment_node_list = fc_block.xpath("./*[local-name()='ObjectList']/*[local-name()='MultilingualText'][@CompositionName='Comment']") + if comment_node_list: + block_comment_val = get_multilingual_text(comment_node_list[0]) + print(f"Paso 3b: Comentario del bloque extraído: '{block_comment_val[:50]}...'") - # Inicializa la estructura del resultado JSON result = { "block_name": block_name_val, "block_number": block_number_val, "language": block_lang_val, - "interface": {}, # Diccionario para las secciones de la interfaz - "networks": [] # Lista para las redes lógicas + "block_comment": block_comment_val, # Añadido comentario del bloque + "interface": {}, + "networks": [] } - # Extrae la Interfaz del bloque print("Paso 4: Extrayendo la interfaz del bloque...") interface_found = False - # Busca Interface DENTRO de AttributeList (corrección clave) - if attribute_list_node: # Solo busca si encontramos AttributeList + if attribute_list_node: interface_node_list = attribute_list_node[0].xpath("./*[local-name()='Interface']") if interface_node_list: interface_node = interface_node_list[0] interface_found = True print("Paso 4: Nodo Interface encontrado dentro de AttributeList.") - # Ahora busca las secciones DENTRO de Interface usando el namespace 'iface' - section_count = 0 - # Usa './/' para buscar Sections en cualquier nivel dentro de Interface for section in interface_node.xpath(".//iface:Section", namespaces=ns): - section_count += 1 section_name = section.get('Name') members = [] - member_count = 0 - # Busca Members como hijos directos de Section usando 'iface' for member in section.xpath("./iface:Member", namespaces=ns): - member_count +=1 member_name = member.get('Name') member_dtype = member.get('Datatype') - if member_name and member_dtype: - members.append({"name": member_name, "datatype": member_dtype}) - else: - print(f"Advertencia: Miembro inválido encontrado en sección '{section_name}' (sin nombre o tipo).") + if member_name and member_dtype: members.append({"name": member_name, "datatype": member_dtype}) + if members: result["interface"][section_name] = members + if not result["interface"]: print("Advertencia: Nodo Interface encontrado, pero no contenía secciones iface:Section válidas.") + else: print("Advertencia: No se encontró el nodo DENTRO de .") - if members: - print(f"DEBUG: Interfaz - Sección '{section_name}' encontrada con {member_count} miembros.") - result["interface"][section_name] = members - # else: print(f"DEBUG: Interfaz - Sección '{section_name}' encontrada pero sin miembros válidos.") + if not interface_found and not result["interface"]: print("Advertencia: No se pudo extraer ninguna información de la interfaz.") - if section_count == 0: - print("Advertencia: Nodo Interface encontrado, pero no contenía secciones iface:Section válidas.") - else: - print("Advertencia: No se encontró el nodo DENTRO de .") - # else: # Ya se advirtió que no se encontró AttributeList - - if not interface_found and not result["interface"]: - print("Advertencia: No se pudo extraer ninguna información de la interfaz.") - - - # Extrae la lógica de las Redes (CompileUnits) print("Paso 5: Extrayendo la lógica de las redes (CompileUnits)...") networks_processed_count = 0 - # Busca ObjectList como hijo directo de fc_block object_list_node = fc_block.xpath("./*[local-name()='ObjectList']") if object_list_node: - # Busca CompileUnits dentro de ObjectList compile_units = object_list_node[0].xpath("./*[local-name()='SW.Blocks.CompileUnit']") print(f"Paso 5: Se encontraron {len(compile_units)} elementos SW.Blocks.CompileUnit.") for network_elem in compile_units: networks_processed_count += 1 print(f"DEBUG: Procesando red #{networks_processed_count} (ID={network_elem.get('ID')})...") - parsed_network = parse_network(network_elem) - # Solo añade la red si el parseo no devolvió un error grave - if parsed_network and parsed_network.get('error') is None: - result["networks"].append(parsed_network) + parsed_network = parse_network(network_elem) # Ahora parse_network incluye comentario + if parsed_network and parsed_network.get('error') is None: result["networks"].append(parsed_network) elif parsed_network: print(f"Error: Falló el parseo de la red ID={parsed_network.get('id')}: {parsed_network.get('error')}") - result["networks"].append(parsed_network) # Añadir incluso con error para depurar - else: - print(f"Error: parse_network devolvió None para un CompileUnit (ID={network_elem.get('ID')}).") + result["networks"].append(parsed_network) + else: print(f"Error: parse_network devolvió None para un CompileUnit (ID={network_elem.get('ID')}).") + if networks_processed_count == 0: print("Advertencia: ObjectList encontrado, pero no contenía SW.Blocks.CompileUnit.") + else: print("Advertencia: No se encontró ObjectList para el bloque FC.") - if networks_processed_count == 0: - print("Advertencia: ObjectList encontrado, pero no contenía SW.Blocks.CompileUnit.") - else: - print("Advertencia: No se encontró ObjectList para el bloque FC.") - - - # Escribe el resultado al archivo JSON print("Paso 6: Escribiendo el resultado en el archivo JSON...") - # Realiza chequeos finales antes de escribir - if not result["interface"]: - print("ADVERTENCIA FINAL: La sección 'interface' está vacía en el JSON resultante.") - if not result["networks"]: - print("ADVERTENCIA FINAL: La sección 'networks' está vacía en el JSON resultante.") + # Chequeos finales + if not result["interface"]: print("ADVERTENCIA FINAL: La sección 'interface' está vacía.") + if not result["networks"]: print("ADVERTENCIA FINAL: La sección 'networks' está vacía.") else: - # Verifica si se extrajeron nombres de variables en las redes - variable_names_found = any( - acc.get('type') == 'variable' and acc.get('name') is not None - for net in result.get('networks', []) if net.get('error') is None # Solo redes válidas - for instr in net.get('logic', []) - for pin_data in instr.get('inputs', {}).values() - # Maneja entradas que son listas (OR) o diccionarios - for acc in (pin_data if isinstance(pin_data, list) else [pin_data] if isinstance(pin_data, dict) else []) - if isinstance(acc, dict) # Asegura que acc sea un diccionario parseado - ) or any( - acc.get('type') == 'variable' and acc.get('name') is not None - for net in result.get('networks', []) if net.get('error') is None # Solo redes válidas - for instr in net.get('logic', []) - for pin_data_list in instr.get('outputs', {}).values() if isinstance(pin_data_list, list) - for acc in pin_data_list if isinstance(acc, dict) # Asegura que acc sea un diccionario - ) - if not variable_names_found: - print("ADVERTENCIA FINAL: No parece haberse extraído ningún nombre de variable válido en las redes procesadas.") - else: - print("INFO FINAL: Se detectaron nombres de variables en las redes procesadas.") + # Chequea si alguna instrucción tiene lógica ENO interesante + eno_logic_found = any(instr.get('eno_logic') for net in result.get('networks', []) if net.get('error') is None for instr in net.get('logic', [])) + if eno_logic_found: print("INFO FINAL: Se detectó lógica ENO interesante en al menos una instrucción.") + else: print("INFO FINAL: No se detectó lógica ENO interesante (solo conexiones directas ENO->EN o ENO no conectado).") - - # Escribe el archivo try: with open(json_filepath, 'w', encoding='utf-8') as f: - # Usa indent=4 para formato legible, ensure_ascii=False para caracteres UTF-8 json.dump(result, f, indent=4, ensure_ascii=False) print(f"Paso 6: Escritura completada.") print(f"Conversión finalizada con éxito. Archivo JSON guardado en: '{json_filepath}'") - except IOError as e: - print(f"Error Crítico: No se pudo escribir el archivo JSON en '{json_filepath}'. Error: {e}") - except TypeError as e: - print(f"Error Crítico: Problema al serializar datos a JSON (posiblemente datos no serializables). Error: {e}") - + except IOError as e: print(f"Error Crítico: No se pudo escribir el archivo JSON en '{json_filepath}'. Error: {e}") + except TypeError as e: print(f"Error Crítico: Problema al serializar datos a JSON. Error: {e}") except etree.XMLSyntaxError as e: print(f"Error Crítico: Error de sintaxis en el archivo XML '{xml_filepath}'. Detalles: {e}") except Exception as e: print(f"Error Crítico: Ocurrió un error inesperado durante el procesamiento: {e}") - print("--- Traceback ---") - traceback.print_exc() # Imprime la traza completa de la excepción - print("--- Fin Traceback ---") + print("--- Traceback ---"); traceback.print_exc(); print("--- Fin Traceback ---") # --- Punto de Entrada Principal --- if __name__ == "__main__": - # Define los nombres de los archivos de entrada y salida xml_file = 'BlenderRun_ProdTime.xml' json_file = 'BlenderRun_ProdTime_simplified.json' - - # Llama a la función principal de conversión convert_xml_to_json(xml_file, json_file) \ No newline at end of file