From 66c5a076aba98ea06e00a2b3eeea247ed7769442 Mon Sep 17 00:00:00 2001 From: Miguel Date: Sun, 20 Apr 2025 13:22:11 +0200 Subject: [PATCH] Adicion del procesamiento de OBs --- ToUpload/x0_main.py | 107 ++++-- ToUpload/x1_to_json.py | 683 +++++++++++++++++++++------------- ToUpload/x2_process.py | 485 ++++++++++++++++--------- ToUpload/x3_generate_scl.py | 707 +++++++++++++++++++++++------------- log.txt | Bin 0 -> 1933740 bytes paste.py | 273 -------------- x0_main.py | 256 +++++-------- x1_to_json.py | 198 ++++++---- x2_process.py | 148 ++++---- x3_generate_scl.py | 655 +++++++++++++++++++++++---------- 10 files changed, 2032 insertions(+), 1480 deletions(-) create mode 100644 log.txt diff --git a/ToUpload/x0_main.py b/ToUpload/x0_main.py index f14f918..950a973 100644 --- a/ToUpload/x0_main.py +++ b/ToUpload/x0_main.py @@ -3,7 +3,8 @@ import subprocess import os import sys import locale -import glob # <--- Importar glob para buscar archivos +import glob # <--- Importar glob para buscar archivos + # (Función get_console_encoding y variable CONSOLE_ENCODING como en la respuesta anterior) def get_console_encoding(): @@ -11,12 +12,14 @@ def get_console_encoding(): try: return locale.getpreferredencoding(False) except Exception: - return 'cp1252' + return "cp1252" + CONSOLE_ENCODING = get_console_encoding() # Descomenta la siguiente línea si quieres ver la codificación detectada: # print(f"Detected console encoding: {CONSOLE_ENCODING}") + # (Función run_script como en la respuesta anterior, usando CONSOLE_ENCODING) def run_script(script_name, xml_arg): """Runs a given script with the specified XML file argument.""" @@ -24,12 +27,14 @@ def run_script(script_name, xml_arg): command = [sys.executable, script_path, xml_arg] print(f"\n--- Running {script_name} with argument: {xml_arg} ---") try: - result = subprocess.run(command, - check=True, - capture_output=True, - text=True, - encoding=CONSOLE_ENCODING, - errors='replace') # 'replace' para evitar errores + result = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + encoding=CONSOLE_ENCODING, + errors="replace", + ) # 'replace' para evitar errores # Imprimir stdout y stderr # Eliminar saltos de línea extra al final si existen @@ -49,26 +54,35 @@ def run_script(script_name, xml_arg): except subprocess.CalledProcessError as e: print(f"Error running {script_name}:") print(f"Return code: {e.returncode}") - stdout_decoded = e.stdout.decode(CONSOLE_ENCODING, errors='replace').strip() if isinstance(e.stdout, bytes) else (e.stdout or "").strip() - stderr_decoded = e.stderr.decode(CONSOLE_ENCODING, errors='replace').strip() if isinstance(e.stderr, bytes) else (e.stderr or "").strip() + stdout_decoded = ( + e.stdout.decode(CONSOLE_ENCODING, errors="replace").strip() + if isinstance(e.stdout, bytes) + else (e.stdout or "").strip() + ) + stderr_decoded = ( + e.stderr.decode(CONSOLE_ENCODING, errors="replace").strip() + if isinstance(e.stderr, bytes) + else (e.stderr or "").strip() + ) if stdout_decoded: - print("--- Stdout ---") - print(stdout_decoded) + print("--- Stdout ---") + print(stdout_decoded) if stderr_decoded: - print("--- Stderr ---") - print(stderr_decoded) + print("--- Stderr ---") + print(stderr_decoded) print("--------------") return False except Exception as e: print(f"An unexpected error occurred while running {script_name}: {e}") return False + # --- NUEVA FUNCIÓN PARA SELECCIONAR ARCHIVO --- def select_xml_file(): """Busca archivos .xml, los lista y pide al usuario que elija uno.""" print("No XML file specified. Searching for XML files in current directory...") # Buscar archivos .xml en el directorio actual (.) - xml_files = sorted(glob.glob('*.xml')) # sorted para orden alfabético + xml_files = sorted(glob.glob("*.xml")) # sorted para orden alfabético if not xml_files: print("Error: No .xml files found in the current directory.") @@ -80,7 +94,9 @@ def select_xml_file(): while True: try: - choice = input(f"Enter the number of the file to process (1-{len(xml_files)}): ") + choice = input( + f"Enter the number of the file to process (1-{len(xml_files)}): " + ) choice_num = int(choice) if 1 <= choice_num <= len(xml_files): selected_file = xml_files[choice_num - 1] @@ -90,9 +106,11 @@ def select_xml_file(): print("Invalid choice. Please enter a number from the list.") except ValueError: print("Invalid input. Please enter a number.") - except EOFError: # Manejar si la entrada se cierra inesperadamente - print("\nSelection cancelled.") - sys.exit(1) + except EOFError: # Manejar si la entrada se cierra inesperadamente + print("\nSelection cancelled.") + sys.exit(1) + + # --- FIN NUEVA FUNCIÓN --- @@ -100,30 +118,36 @@ if __name__ == "__main__": # Imports necesarios para esta sección import os import sys - import glob # Asegúrate de que glob esté importado al principio del archivo + import glob # Asegúrate de que glob esté importado al principio del archivo # Directorio base donde buscar los archivos XML (relativo al script) base_search_dir = "XML Project" - script_dir = os.path.dirname(__file__) # Directorio donde está x0_main.py + script_dir = os.path.dirname(__file__) # Directorio donde está x0_main.py xml_project_dir = os.path.join(script_dir, base_search_dir) print(f"Buscando archivos XML recursivamente en: '{xml_project_dir}'") # Verificar si el directorio 'XML Project' existe if not os.path.isdir(xml_project_dir): - print(f"Error: El directorio '{xml_project_dir}' no existe o no es un directorio.") - print("Por favor, crea el directorio 'XML Project' en la misma carpeta que este script y coloca tus archivos XML dentro.") + print( + f"Error: El directorio '{xml_project_dir}' no existe o no es un directorio." + ) + print( + "Por favor, crea el directorio 'XML Project' en la misma carpeta que este script y coloca tus archivos XML dentro." + ) sys.exit(1) # Buscar todos los archivos .xml recursivamente dentro de xml_project_dir # Usamos os.path.join para construir la ruta de búsqueda correctamente # y '**/*.xml' para la recursividad con glob - search_pattern = os.path.join(xml_project_dir, '**', '*.xml') + search_pattern = os.path.join(xml_project_dir, "**", "*.xml") xml_files_found = glob.glob(search_pattern, recursive=True) if not xml_files_found: - print(f"No se encontraron archivos XML en '{xml_project_dir}' o sus subdirectorios.") - sys.exit(0) # Salir limpiamente si no hay archivos + print( + f"No se encontraron archivos XML en '{xml_project_dir}' o sus subdirectorios." + ) + sys.exit(0) # Salir limpiamente si no hay archivos print(f"Se encontraron {len(xml_files_found)} archivos XML para procesar:") # Ordenar para un procesamiento predecible (opcional) @@ -141,7 +165,9 @@ if __name__ == "__main__": processed_count = 0 failed_count = 0 for xml_filepath in xml_files_found: - print(f"\n--- Iniciando pipeline para: {os.path.relpath(xml_filepath, script_dir)} ---") + print( + f"\n--- Iniciando pipeline para: {os.path.relpath(xml_filepath, script_dir)} ---" + ) # Usar la ruta absoluta para evitar problemas si los scripts cambian de directorio absolute_xml_filepath = os.path.abspath(xml_filepath) @@ -150,26 +176,37 @@ if __name__ == "__main__": # La función run_script ya está definida en tu script x0_main.py success = True if not run_script(script1, absolute_xml_filepath): - print(f"\nPipeline falló en el script '{script1}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}") + print( + f"\nPipeline falló en el script '{script1}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}" + ) success = False elif not run_script(script2, absolute_xml_filepath): - print(f"\nPipeline falló en el script '{script2}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}") + print( + f"\nPipeline falló en el script '{script2}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}" + ) success = False elif not run_script(script3, absolute_xml_filepath): - print(f"\nPipeline falló en el script '{script3}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}") + print( + f"\nPipeline falló en el script '{script3}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}" + ) success = False if success: - print(f"--- Pipeline completado exitosamente para: {os.path.relpath(xml_filepath, script_dir)} ---") + print( + f"--- Pipeline completado exitosamente para: {os.path.relpath(xml_filepath, script_dir)} ---" + ) processed_count += 1 else: failed_count += 1 - print(f"--- Pipeline falló para: {os.path.relpath(xml_filepath, script_dir)} ---") - + print( + f"--- Pipeline falló para: {os.path.relpath(xml_filepath, script_dir)} ---" + ) print("\n--- Resumen Final del Procesamiento ---") print(f"Total de archivos XML encontrados: {len(xml_files_found)}") - print(f"Archivos procesados exitosamente por el pipeline completo: {processed_count}") + print( + f"Archivos procesados exitosamente por el pipeline completo: {processed_count}" + ) print(f"Archivos que fallaron en algún punto del pipeline: {failed_count}") print("---------------------------------------") xml_filename = None @@ -217,4 +254,4 @@ if __name__ == "__main__": else: print("\nPipeline failed at script:", script2) else: - print("\nPipeline failed at script:", script1) \ No newline at end of file + print("\nPipeline failed at script:", script1) diff --git a/ToUpload/x1_to_json.py b/ToUpload/x1_to_json.py index 74e78d8..71ac19b 100644 --- a/ToUpload/x1_to_json.py +++ b/ToUpload/x1_to_json.py @@ -46,6 +46,7 @@ def get_multilingual_text(element, default_lang="en-US", fallback_lang="it-IT"): print(f"Advertencia: Error extrayendo MultilingualText: {e}") return "" + def get_symbol_name(symbol_element): # (Sin cambios respecto a la versión anterior) if symbol_element is None: @@ -57,6 +58,7 @@ def get_symbol_name(symbol_element): print(f"Advertencia: Excepción en get_symbol_name: {e}") return None + def parse_access(access_element): # (Sin cambios respecto a la versión anterior) if access_element is None: @@ -149,6 +151,7 @@ def parse_access(access_element): return info return info + def parse_part(part_element): # (Sin cambios respecto a la versión anterior) if part_element is None: @@ -184,6 +187,7 @@ def parse_part(part_element): "negated_pins": negated_pins, } + def parse_call(call_element): # (Mantiene la corrección para DB de instancia) if call_element is None: @@ -243,8 +247,10 @@ def parse_call(call_element): call_data["instance_scope"] = instance_scope return call_data + # SCL (Structured Text) Parser + def reconstruct_scl_from_tokens(st_node): """ Reconstruye SCL desde , mejorando el manejo de @@ -263,10 +269,10 @@ def reconstruct_scl_from_tokens(st_node): scl_parts.append(elem.get("Text", "")) elif tag == "Blank": # Añadir espacios simples, evitar múltiples si ya hay uno antes/después - if not scl_parts or not scl_parts[-1].endswith(' '): - scl_parts.append(" " * int(elem.get("Num", 1))) - elif int(elem.get("Num", 1)) > 1: # Añadir extras si son más de 1 - scl_parts.append(" " * (int(elem.get("Num", 1))-1)) + if not scl_parts or not scl_parts[-1].endswith(" "): + scl_parts.append(" " * int(elem.get("Num", 1))) + elif int(elem.get("Num", 1)) > 1: # Añadir extras si son más de 1 + scl_parts.append(" " * (int(elem.get("Num", 1)) - 1)) elif tag == "NewLine": # Limpiar espacios antes del salto de línea real if scl_parts: @@ -274,9 +280,17 @@ def reconstruct_scl_from_tokens(st_node): scl_parts.append("\n") elif tag == "Access": scope = elem.get("Scope") - access_str = f"/*_ERR_Scope_{scope}_*/" # Fallback más informativo + access_str = f"/*_ERR_Scope_{scope}_*/" # Fallback más informativo - if scope in ["GlobalVariable", "LocalVariable", "TempVariable", "InOutVariable", "InputVariable", "OutputVariable", "ConstantVariable"]: # Tipos comunes de variables + if scope in [ + "GlobalVariable", + "LocalVariable", + "TempVariable", + "InOutVariable", + "InputVariable", + "OutputVariable", + "ConstantVariable", + ]: # Tipos comunes de variables symbol_elem = elem.xpath("./st:Symbol", namespaces=ns) if symbol_elem: components = symbol_elem[0].xpath("./st:Component", namespaces=ns) @@ -288,11 +302,18 @@ def reconstruct_scl_from_tokens(st_node): symbol_text_parts.append(".") # Reconstrucción de comillas (heurística) - has_quotes_elem = comp.xpath("../st:BooleanAttribute[@Name='HasQuotes']/text()", namespaces=ns) - has_quotes = has_quotes_elem and has_quotes_elem[0].lower() == "true" - is_temp = name.startswith('#') + has_quotes_elem = comp.xpath( + "../st:BooleanAttribute[@Name='HasQuotes']/text()", + namespaces=ns, + ) + has_quotes = ( + has_quotes_elem and has_quotes_elem[0].lower() == "true" + ) + is_temp = name.startswith("#") - if has_quotes or (i == 0 and not is_temp): # Comillas si HasQuotes o primer componente (no temp) + if has_quotes or ( + i == 0 and not is_temp + ): # Comillas si HasQuotes o primer componente (no temp) symbol_text_parts.append(f'"{name}"') else: symbol_text_parts.append(name) @@ -300,17 +321,24 @@ def reconstruct_scl_from_tokens(st_node): # Manejar índices de array (RECURSIVO) index_access = comp.xpath("./st:Access", namespaces=ns) if index_access: - # Llama recursivamente para obtener el texto de cada índice - indices_text = [reconstruct_scl_from_tokens(idx_node) for idx_node in index_access] - symbol_text_parts.append(f"[{','.join(indices_text)}]") + # Llama recursivamente para obtener el texto de cada índice + indices_text = [ + reconstruct_scl_from_tokens(idx_node) + for idx_node in index_access + ] + symbol_text_parts.append(f"[{','.join(indices_text)}]") access_str = "".join(symbol_text_parts) elif scope == "LiteralConstant": constant_elem = elem.xpath("./st:Constant", namespaces=ns) if constant_elem: - val_elem = constant_elem[0].xpath("./st:ConstantValue/text()", namespaces=ns) - type_elem = constant_elem[0].xpath("./st:ConstantType/text()", namespaces=ns) + val_elem = constant_elem[0].xpath( + "./st:ConstantValue/text()", namespaces=ns + ) + type_elem = constant_elem[0].xpath( + "./st:ConstantType/text()", namespaces=ns + ) const_type = type_elem[0] if type_elem else "" const_val = val_elem[0] if val_elem else "_ERR_CONSTVAL_" @@ -322,7 +350,7 @@ def reconstruct_scl_from_tokens(st_node): # elif const_type == "LTime": access_str = f"LT#{const_val}" # ... otros tipos ... else: - access_str = "/*_ERR_NOCONST_*/" + access_str = "/*_ERR_NOCONST_*/" # --- Añadir más manejo de scopes aquí si es necesario --- # elif scope == "Call": access_str = reconstruct_call(elem) # elif scope == "Expression": access_str = reconstruct_expression(elem) @@ -330,39 +358,48 @@ def reconstruct_scl_from_tokens(st_node): scl_parts.append(access_str) elif tag == "Comment" or tag == "LineComment": - comment_text = "".join(elem.xpath(".//text()")).strip() - if tag == "Comment": - scl_parts.append(f"(* {comment_text} *)") - else: - scl_parts.append(f"// {comment_text}") + comment_text = "".join(elem.xpath(".//text()")).strip() + if tag == "Comment": + scl_parts.append(f"(* {comment_text} *)") + else: + scl_parts.append(f"// {comment_text}") # else: Ignorar otros nodos # Unir partes, limpiar espacios extra alrededor de operadores y saltos de línea full_scl = "".join(scl_parts) - # Re-indentar líneas después de IF/THEN, etc. (Simplificado) output_lines = [] indent_level = 0 - for line in full_scl.split('\n'): + for line in full_scl.split("\n"): line = line.strip() - if not line: continue # Saltar líneas vacías + if not line: + continue # Saltar líneas vacías # Reducir indentación antes de procesar END_IF, ELSE, etc. (simplificado) - if line.startswith(('END_IF', 'END_WHILE', 'END_FOR', 'END_CASE', 'ELSE', 'ELSIF')): - indent_level = max(0, indent_level - 1) + if line.startswith( + ("END_IF", "END_WHILE", "END_FOR", "END_CASE", "ELSE", "ELSIF") + ): + indent_level = max(0, indent_level - 1) - output_lines.append(" " * indent_level + line) # Aplicar indentación + output_lines.append(" " * indent_level + line) # Aplicar indentación # Aumentar indentación después de IF, WHILE, FOR, CASE, ELSE, ELSIF (simplificado) - if line.endswith('THEN') or line.endswith('DO') or line.endswith('OF') or line == 'ELSE': - indent_level += 1 + if ( + line.endswith("THEN") + or line.endswith("DO") + or line.endswith("OF") + or line == "ELSE" + ): + indent_level += 1 # Nota: Esto no maneja bloques BEGIN/END dentro de SCL return "\n".join(output_lines) + # STL (Statement List) Parser + def get_access_text(access_element): """Reconstruye una representación textual simple de un Access en STL.""" if access_element is None: @@ -379,7 +416,9 @@ def get_access_text(access_element): for comp in components: name = comp.get("Name", "_ERR_COMP_") # CORREGIDO: Añadido namespaces=ns - has_quotes_elem = comp.xpath("../stl:BooleanAttribute[@Name='HasQuotes']/text()", namespaces=ns) + has_quotes_elem = comp.xpath( + "../stl:BooleanAttribute[@Name='HasQuotes']/text()", namespaces=ns + ) has_quotes = has_quotes_elem and has_quotes_elem[0].lower() == "true" # Usar nombre tal cual por ahora @@ -389,8 +428,8 @@ def get_access_text(access_element): # CORREGIDO: Añadido namespaces=ns index_access = comp.xpath("./stl:Access", namespaces=ns) if index_access: - indices = [get_access_text(ia) for ia in index_access] - parts.append(f"[{','.join(indices)}]") + indices = [get_access_text(ia) for ia in index_access] + parts.append(f"[{','.join(indices)}]") return ".".join(parts) @@ -400,14 +439,18 @@ def get_access_text(access_element): if constant_elem: # CORREGIDO: Añadido namespaces=ns val_elem = constant_elem[0].xpath("./stl:ConstantValue/text()", namespaces=ns) - type_elem = constant_elem[0].xpath("./stl:ConstantType/text()", namespaces=ns) # Obtener tipo para mejor formato + type_elem = constant_elem[0].xpath( + "./stl:ConstantType/text()", namespaces=ns + ) # Obtener tipo para mejor formato const_type = type_elem[0] if type_elem else "" const_val = val_elem[0] if val_elem else "_ERR_CONST_" # Añadir prefijo de tipo si es necesario (ej. T# , L#) - Simplificado - if const_type == "Time": return f"T#{const_val}" - if const_type == "ARef": return f"{const_val}" # No necesita prefijo + if const_type == "Time": + return f"T#{const_val}" + if const_type == "ARef": + return f"{const_val}" # No necesita prefijo # Añadir más tipos si es necesario - return const_val # Valor directo para otros tipos + return const_val # Valor directo para otros tipos # Intenta reconstruir etiqueta # CORREGIDO: Añadido namespaces=ns @@ -436,7 +479,9 @@ def get_access_text(access_element): # Formatear ancho width_map = {"Bit": "X", "Byte": "B", "Word": "W", "Double": "D"} - width_char = width_map.get(width, width[0] if width else "?") # Usa primera letra si no mapeado + width_char = width_map.get( + width, width[0] if width else "?" + ) # Usa primera letra si no mapeado return f"{area}{width_char}[{reg},{p_format_offset}]" @@ -446,49 +491,66 @@ def get_access_text(access_element): if address_elem: area = address_elem[0].get("Area", "??") bit_offset_str = address_elem[0].get("BitOffset", "0") - addr_type_str = address_elem[0].get("Type", "Bool") # Obtener tipo para ancho + addr_type_str = address_elem[0].get("Type", "Bool") # Obtener tipo para ancho try: - bit_offset = int(bit_offset_str) - byte_offset = bit_offset // 8 - bit_in_byte = bit_offset % 8 - # Determinar ancho basado en tipo (simplificación) - addr_width = "X" # Default a Bit - if addr_type_str == "Byte": addr_width = "B" - elif addr_type_str == "Word": addr_width = "W" - elif addr_type_str in ["DWord", "DInt"]: addr_width = "D" - # Añadir más tipos si es necesario (Real, etc.) + bit_offset = int(bit_offset_str) + byte_offset = bit_offset // 8 + bit_in_byte = bit_offset % 8 + # Determinar ancho basado en tipo (simplificación) + addr_width = "X" # Default a Bit + if addr_type_str == "Byte": + addr_width = "B" + elif addr_type_str == "Word": + addr_width = "W" + elif addr_type_str in ["DWord", "DInt"]: + addr_width = "D" + # Añadir más tipos si es necesario (Real, etc.) - # Mapear Area para STL estándar - area_map = { "Input": "I", "Output": "Q", "Memory": "M", - "PeripheryInput": "PI", "PeripheryOutput": "PQ", - "DB": "DB", "DI": "DI", "Local": "L", # L no siempre válido aquí - "Timer": "T", "Counter": "C" } - stl_area = area_map.get(area, area) + # Mapear Area para STL estándar + area_map = { + "Input": "I", + "Output": "Q", + "Memory": "M", + "PeripheryInput": "PI", + "PeripheryOutput": "PQ", + "DB": "DB", + "DI": "DI", + "Local": "L", # L no siempre válido aquí + "Timer": "T", + "Counter": "C", + } + stl_area = area_map.get(area, area) - # Manejar DB/DI que necesitan número de bloque - if stl_area in ["DB", "DI"]: - block_num = address_elem[0].get("BlockNumber") - if block_num: - return f"{stl_area}{block_num}.{stl_area}{addr_width}{byte_offset}.{bit_in_byte}" # Ej: DB1.DBX0.1 - else: # Acceso con registro DB/DI - return f"{stl_area}{addr_width}{byte_offset}.{bit_in_byte}" # Ej: DBX0.1 - elif stl_area in ["T", "C"]: - return f"{stl_area}{byte_offset}" # Los timers/contadores solo usan el número - else: # I, Q, M, L, PI, PQ - return f"{stl_area}{addr_width}{byte_offset}.{bit_in_byte}" # Ej: M10.1, I0.0 + # Manejar DB/DI que necesitan número de bloque + if stl_area in ["DB", "DI"]: + block_num = address_elem[0].get("BlockNumber") + if block_num: + return f"{stl_area}{block_num}.{stl_area}{addr_width}{byte_offset}.{bit_in_byte}" # Ej: DB1.DBX0.1 + else: # Acceso con registro DB/DI + return f"{stl_area}{addr_width}{byte_offset}.{bit_in_byte}" # Ej: DBX0.1 + elif stl_area in ["T", "C"]: + return f"{stl_area}{byte_offset}" # Los timers/contadores solo usan el número + else: # I, Q, M, L, PI, PQ + return f"{stl_area}{addr_width}{byte_offset}.{bit_in_byte}" # Ej: M10.1, I0.0 except ValueError: - return f"{area}?{bit_offset_str}?" + return f"{area}?{bit_offset_str}?" + + return f"_{scope}_?" # Fallback - return f"_{scope}_?" # Fallback def get_comment_text(comment_element): """Extrae texto de un LineComment o Comment.""" - if comment_element is None: return "" + if comment_element is None: + return "" # Usar get_multilingual_text si los comentarios son multilingües # Si no, extraer texto directamente - ml_texts = comment_element.xpath(".//mlt:MultilingualTextItem/mlt:AttributeList/mlt:Text/text()", - namespaces={'mlt': "http://www.siemens.com/automation/Openness/SW/Interface/v5"}) # Asumiendo ns + ml_texts = comment_element.xpath( + ".//mlt:MultilingualTextItem/mlt:AttributeList/mlt:Text/text()", + namespaces={ + "mlt": "http://www.siemens.com/automation/Openness/SW/Interface/v5" + }, + ) # Asumiendo ns if ml_texts: # Podrías intentar obtener un idioma específico o simplemente el primero return ml_texts[0].strip() if ml_texts else "" @@ -497,6 +559,7 @@ def get_comment_text(comment_element): text_nodes = comment_element.xpath("./text()") return "".join(text_nodes).strip() + def reconstruct_stl_from_statementlist(statement_list_node): """Reconstruye el código STL como una cadena de texto desde .""" if statement_list_node is None: @@ -508,11 +571,13 @@ def reconstruct_stl_from_statementlist(statement_list_node): for stmt in statements: line_parts = [] - line_comment = "" # Comentario al final de la línea + line_comment = "" # Comentario al final de la línea # 1. Comentarios al inicio de la línea (como líneas separadas //) # CORREGIDO: Añadido namespaces=ns - initial_comments = stmt.xpath("child::stl:Comment | child::stl:LineComment", namespaces=ns) + initial_comments = stmt.xpath( + "child::stl:Comment | child::stl:LineComment", namespaces=ns + ) for comm in initial_comments: comment_text = get_comment_text(comm) if comment_text: @@ -531,70 +596,185 @@ def reconstruct_stl_from_statementlist(statement_list_node): label_str = f"{label_name_nodes[0]}:" # Buscar comentarios DENTRO de LabelDeclaration pero después de Label # CORREGIDO: Añadido namespaces=ns - label_comments = label_decl[0].xpath("./stl:Comment | ./stl:LineComment", namespaces=ns) + label_comments = label_decl[0].xpath( + "./stl:Comment | ./stl:LineComment", namespaces=ns + ) for lcomm in label_comments: comment_text = get_comment_text(lcomm) - if comment_text: line_comment += f" // {comment_text}" # Añadir al comentario de línea + if comment_text: + line_comment += ( + f" // {comment_text}" # Añadir al comentario de línea + ) # 3. Token de Instrucción STL # CORREGIDO: Añadido namespaces=ns instruction_token = stmt.xpath("./stl:StlToken", namespaces=ns) instruction_str = "" if instruction_token: - token_text = instruction_token[0].get("Text", "_ERR_TOKEN_") - instruction_str = token_text - # Comentarios asociados directamente al token - # CORREGIDO: Añadido namespaces=ns - token_comments = instruction_token[0].xpath("./stl:Comment | ./stl:LineComment", namespaces=ns) - for tcomm in token_comments: - comment_text = get_comment_text(tcomm) - if comment_text: line_comment += f" // {comment_text}" # Añadir al comentario de línea + token_text = instruction_token[0].get("Text", "_ERR_TOKEN_") + instruction_str = token_text + # Comentarios asociados directamente al token + # CORREGIDO: Añadido namespaces=ns + token_comments = instruction_token[0].xpath( + "./stl:Comment | ./stl:LineComment", namespaces=ns + ) + for tcomm in token_comments: + comment_text = get_comment_text(tcomm) + if comment_text: + line_comment += ( + f" // {comment_text}" # Añadir al comentario de línea + ) # 4. Acceso/Operando STL # CORREGIDO: Añadido namespaces=ns access_elem = stmt.xpath("./stl:Access", namespaces=ns) access_str = "" if access_elem: - access_text = get_access_text(access_elem[0]) - access_str = access_text - # Comentarios DENTRO del Access (pueden ser de línea o bloque) - # CORREGIDO: Añadido namespaces=ns - access_comments = access_elem[0].xpath("child::stl:LineComment | child::stl:Comment", namespaces=ns) - for acc_comm in access_comments: - comment_text = get_comment_text(acc_comm) - if comment_text: line_comment += f" // {comment_text}" # Añadir al comentario de línea + access_text = get_access_text(access_elem[0]) + access_str = access_text + # Comentarios DENTRO del Access (pueden ser de línea o bloque) + # CORREGIDO: Añadido namespaces=ns + access_comments = access_elem[0].xpath( + "child::stl:LineComment | child::stl:Comment", namespaces=ns + ) + for acc_comm in access_comments: + comment_text = get_comment_text(acc_comm) + if comment_text: + line_comment += ( + f" // {comment_text}" # Añadir al comentario de línea + ) # Construir la línea: Etiqueta (si hay) + Tab + Instrucción + Espacio + Operando (si hay) + Comentario(s) current_line = "" if label_str: current_line += label_str if instruction_str: - if current_line: # Si ya había etiqueta, añadir tabulador + if current_line: # Si ya había etiqueta, añadir tabulador current_line += "\t" current_line += instruction_str if access_str: - if current_line: # Si ya había algo, añadir espacio + if current_line: # Si ya había algo, añadir espacio current_line += " " - current_line += access_str + current_line += access_str if line_comment: # Añadir espacio antes del comentario si hay código en la línea if current_line.strip(): current_line += f" {line_comment}" - else: # Si la línea estaba vacía (solo comentarios iniciales), poner el comentario de línea - current_line = line_comment + else: # Si la línea estaba vacía (solo comentarios iniciales), poner el comentario de línea + current_line = line_comment # Añadir la línea construida solo si no está vacía if current_line.strip(): - stl_lines.append(current_line.rstrip()) # Eliminar espacios finales + stl_lines.append(current_line.rstrip()) # Eliminar espacios finales return "\n".join(stl_lines) + +# DB Parser + + +def parse_interface_members(member_elements): + """ + Parsea recursivamente una lista de elementos de una interfaz o estructura. + Maneja miembros simples, structs anidados y arrays con valores iniciales. + """ + members_data = [] + if not member_elements: + return members_data + + for member in member_elements: + member_name = member.get("Name") + member_dtype = member.get("Datatype") + member_remanence = member.get("Remanence", "NonRetain") # Default si no existe + member_accessibility = member.get("Accessibility", "Public") # Default + + if not member_name or not member_dtype: + print( + f"Advertencia: Miembro sin nombre o tipo de dato encontrado. Saltando." + ) + continue + + member_info = { + "name": member_name, + "datatype": member_dtype, + "remanence": member_remanence, + "accessibility": member_accessibility, + "start_value": None, # Para valores simples o structs/arrays inicializados globalmente + "comment": None, + "children": [], # Para structs + "array_elements": {}, # Para arrays (índice -> valor) + } + + # Extraer comentario del miembro + # Usar namespace iface + comment_node = member.xpath("./iface:Comment", namespaces=ns) + if comment_node: + # Llama a get_multilingual_text que ya maneja el namespace iface internamente + member_info["comment"] = get_multilingual_text(comment_node[0]) + + # Extraer valor inicial (para tipos simples) + # Usar namespace iface + start_value_node = member.xpath("./iface:StartValue", namespaces=ns) + if start_value_node: + constant_name = start_value_node[0].get("ConstantName") + if constant_name: + member_info["start_value"] = constant_name + else: + member_info["start_value"] = ( + start_value_node[0].text + if start_value_node[0].text is not None + else "" + ) + + # --- Manejar Structs Anidados --- + # Usar namespace iface + nested_sections = member.xpath( + "./iface:Sections/iface:Section/iface:Member", namespaces=ns + ) + if nested_sections: + member_info["children"] = parse_interface_members( + nested_sections + ) # Llamada recursiva + + # --- Manejar Arrays --- + if member_dtype.lower().startswith("array["): + # Usar namespace iface + subelements = member.xpath("./iface:Subelement", namespaces=ns) + for sub in subelements: + path = sub.get("Path") + # Usar namespace iface + sub_start_value_node = sub.xpath("./iface:StartValue", namespaces=ns) + if path and sub_start_value_node: + constant_name = sub_start_value_node[0].get("ConstantName") + value = ( + constant_name + if constant_name + else ( + sub_start_value_node[0].text + if sub_start_value_node[0].text is not None + else "" + ) + ) + member_info["array_elements"][path] = value + else: + # Usar namespace iface + sub_comment_node = sub.xpath("./iface:Comment", namespaces=ns) + if path and sub_comment_node: + # member_info["array_comments"][path] = get_multilingual_text(sub_comment_node[0]) + pass + + members_data.append(member_info) + + return members_data + + # --- Main Parsing Function --- + def parse_network(network_element): """ Parsea una red, extrae lógica y añade conexiones EN implícitas. - Maneja wires con múltiples destinos. + Maneja wires con múltiples destinos. (Función original adaptada para namespaces) """ if network_element is None: return { @@ -607,15 +787,16 @@ def parse_network(network_element): network_id = network_element.get("ID") - # --- Extracción Título/Comentario (sin cambios respecto a la última versión) --- + # Extracción Título/Comentario (usar namespace iface para MultilingualText) title_element = network_element.xpath( - ".//*[local-name()='MultilingualText'][@CompositionName='Title']" + ".//iface:MultilingualText[@CompositionName='Title']", namespaces=ns ) network_title = ( get_multilingual_text(title_element[0]) if title_element else f"Network {network_id}" ) + # Asume que el comentario está en ObjectList dentro de CompileUnit comment_element = network_element.xpath( "./*[local-name()='ObjectList']/*[local-name()='MultilingualText'][@CompositionName='Comment']" ) @@ -623,31 +804,31 @@ def parse_network(network_element): get_multilingual_text(comment_element[0]) if comment_element else "" ) + # Buscar FlgNet usando namespace flg flgnet_list = network_element.xpath(".//flg:FlgNet", namespaces=ns) if not flgnet_list: - # print(f"Advertencia: FlgNet no encontrado en Red ID={network_id}. Puede estar vacía o ser comentario.") return { "id": network_id, "title": network_title, "comment": network_comment, "logic": [], + "language": "Unknown", "error": "FlgNet not found", } flgnet = flgnet_list[0] - # 1. Parsear Access, Parts y Calls (sin cambios) + # 1. Parsear Access, Parts y Calls (llaman a funciones que ya usan ns) access_map = { acc_info["uid"]: acc_info - for acc in flgnet.xpath(".//flg:Access", namespaces=ns) + for acc in flgnet.xpath(".//flg:Access", namespaces=ns) # Usa ns if (acc_info := parse_access(acc)) and acc_info["type"] != "unknown" } parts_and_calls_map = {} + # Usa ns instruction_elements = flgnet.xpath(".//flg:Part | .//flg:Call", namespaces=ns) for element in instruction_elements: parsed_info = None - tag_name = etree.QName( - element.tag - ).localname # Obtener nombre local de la etiqueta + tag_name = etree.QName(element.tag).localname if tag_name == "Part": parsed_info = parse_part(element) elif tag_name == "Call": @@ -659,85 +840,50 @@ def parse_network(network_element): f"Advertencia: Se ignoró un Part/Call inválido en la red {network_id}" ) - # --- 2. Parsear Wires (MODIFICADO para multi-destino) --- - wire_connections = defaultdict( - list - ) # (dest_uid, dest_pin) -> [(src_uid, src_pin), ...] - source_connections = defaultdict( - list - ) # (src_uid, src_pin) -> [(dest_uid, dest_pin), ...] - eno_outputs = defaultdict( - list - ) # src_uid -> [(dest_uid, dest_pin), ...] (conexiones DESDE eno) - - flg_ns_uri = ns["flg"] # Cache namespace URI + # 2. Parsear Wires (con namespaces) + wire_connections = defaultdict(list) + source_connections = defaultdict(list) + eno_outputs = defaultdict(list) + # Cachear QNames con namespace flg + flg_ns_uri = ns["flg"] qname_powerrail = etree.QName(flg_ns_uri, "Powerrail") qname_identcon = etree.QName(flg_ns_uri, "IdentCon") qname_namecon = etree.QName(flg_ns_uri, "NameCon") - + # Usa ns for wire in flgnet.xpath(".//flg:Wire", namespaces=ns): children = wire.getchildren() if len(children) < 2: - continue # Ignorar wires sin fuente y al menos un destino - + continue source_elem = children[0] source_uid, source_pin = None, None - - # Determinar fuente if source_elem.tag == qname_powerrail: source_uid, source_pin = "POWERRAIL", "out" elif source_elem.tag == qname_identcon: - source_uid, source_pin = ( - source_elem.get("UId"), - "value", - ) # Acceso a variable/constante + source_uid, source_pin = source_elem.get("UId"), "value" elif source_elem.tag == qname_namecon: - source_uid, source_pin = source_elem.get("UId"), source_elem.get( - "Name" - ) # Salida de instrucción - + source_uid, source_pin = source_elem.get("UId"), source_elem.get("Name") if source_uid is None: - continue # No se pudo determinar la fuente - - source_info = (source_uid, source_pin) # Par de fuente - - # Iterar sobre TODOS los posibles destinos (desde el segundo hijo en adelante) + continue + source_info = (source_uid, source_pin) for dest_elem in children[1:]: dest_uid, dest_pin = None, None - - # Determinar destino if dest_elem.tag == qname_identcon: - dest_uid, dest_pin = ( - dest_elem.get("UId"), - "value", - ) # Entrada a variable/constante (Coil, etc.) + dest_uid, dest_pin = dest_elem.get("UId"), "value" elif dest_elem.tag == qname_namecon: - dest_uid, dest_pin = dest_elem.get("UId"), dest_elem.get( - "Name" - ) # Entrada a instrucción - - # Guardar conexiones si son válidas + dest_uid, dest_pin = dest_elem.get("UId"), dest_elem.get("Name") if dest_uid is not None and dest_pin is not None: - # Mapa de Conexiones (Destino -> [Fuentes]) dest_key = (dest_uid, dest_pin) if source_info not in wire_connections[dest_key]: wire_connections[dest_key].append(source_info) - - # Mapa de Fuentes (Fuente -> [Destinos]) 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) - - # Registrar conexiones que SALEN de un pin 'eno' 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) - # else: # Debug opcional si un elemento no es destino válido - # print(f"Advertencia: Elemento en Wire {wire.get('UId')} no es destino válido: {etree.tostring(dest_elem)}") - # --- FIN MODIFICACIÓN Wire --- - # 3. Construcción Lógica Inicial (sin cambios) + # 3. Construcción Lógica Inicial (sin cambios en lógica, pero verificar llamadas) all_logic_steps = {} functional_block_types = [ "Move", @@ -751,7 +897,13 @@ def parse_network(network_element): "Se", "Sd", "BLKMOV", - ] + "TON", + "TOF", + "TP", + "CTU", + "CTD", + "CTUD", + ] # Añadidos timers/counters SCL rlo_generators = [ "Contact", "O", @@ -765,46 +917,42 @@ def parse_network(network_element): "Xor", "PBox", "NBox", - ] + "Not", + ] # Añadido Not for instruction_uid, instruction_info in parts_and_calls_map.items(): - # Copiar info básica instruction_repr = {"instruction_uid": instruction_uid, **instruction_info} instruction_repr["inputs"] = {} instruction_repr["outputs"] = {} - - # --- INICIO: Manejo Especial SdCoil y otros Timers --- original_type = instruction_info["type"] current_type = original_type - input_pin_mapping = {} # Mapa XML pin -> JSON pin - output_pin_mapping = {} # Mapa XML pin -> JSON pin - + input_pin_mapping = {} + output_pin_mapping = {} + # --- Manejo Especial Tipos --- if original_type == "SdCoil": - print( - f" Advertencia: Reinterpretando 'SdCoil' (UID: {instruction_uid}) como 'Se' (Pulse Timer)." - ) - current_type = "Se" # Tratarlo como Se (TP) + current_type = "Se" + input_pin_mapping = {"in": "s", "operand": "timer", "value": "tv"} + output_pin_mapping = {"out": "q"} + elif original_type in ["Se", "Sd", "TON", "TOF", "TP"]: input_pin_mapping = { - "in": "s", # Pin XML 'in' mapea a JSON 's' (Start) - "operand": "timer", # Pin XML 'operand' mapea a JSON 'timer' (Instance) - "value": "tv", # Pin XML 'value' mapea a JSON 'tv' (Time Value) + "s": "s", + "in": "in", + "tv": "tv", + "pt": "pt", + "r": "r", + "timer": "timer", } - output_pin_mapping = {"out": "q"} # Pin XML 'out' mapea a JSON 'q' (Output) - elif original_type in ["Se", "Sd"]: - # Mapear pines estándar de Se/Sd para consistencia con TP/TON - input_pin_mapping = {"s": "s", "tv": "tv", "r": "r", "timer": "timer"} - output_pin_mapping = { - "q": "q", - "rt": "rt", # "rtbcd": "rtbcd" (ignorar BCD) + output_pin_mapping = {"q": "q", "Q": "Q", "rt": "rt", "ET": "ET"} + elif original_type in ["CTU", "CTD", "CTUD"]: + input_pin_mapping = { + "cu": "CU", + "cd": "CD", + "r": "R", + "ld": "LD", + "pv": "PV", + "counter": "counter", } - # Añadir otros mapeos si son necesarios para otros bloques (ej. Contadores) - # elif original_type == "CTU": - # input_pin_mapping = {"cu": "cu", "r": "r", "pv": "pv", "counter": "counter"} # 'counter' es inventado para instancia - # output_pin_mapping = {"qu": "qu", "cv": "cv"} - # --- FIN Manejo Especial --- - - instruction_repr["type"] = current_type # Actualizar el tipo si se cambió - - # Mapear Entradas usando el mapeo de pines + output_pin_mapping = {"qu": "QU", "qd": "QD", "cv": "CV"} + instruction_repr["type"] = current_type possible_input_pins = set( [ "en", @@ -825,14 +973,14 @@ def parse_network(network_element): "ld", "pre", "SRCBLK", + "PT", ] - ) # Ampliar con pines conocidos + ) # Añadido PT for xml_pin_name in possible_input_pins: dest_key = (instruction_uid, xml_pin_name) if dest_key in wire_connections: sources_list = wire_connections[dest_key] input_sources_repr = [] - # ... (lógica existente para obtener input_sources_repr de sources_list) ... for source_uid, source_pin in sources_list: if source_uid == "POWERRAIL": input_sources_repr.append({"type": "powerrail"}) @@ -841,36 +989,38 @@ def parse_network(network_element): elif source_uid in parts_and_calls_map: source_instr_info = parts_and_calls_map[source_uid] source_original_type = source_instr_info["type"] - # Obtener el mapeo de salida para el tipo de la fuente (si existe) source_output_mapping = {} if source_original_type == "SdCoil": source_output_mapping = {"out": "q"} - elif source_original_type in ["Se", "Sd"]: - source_output_mapping = {"q": "q", "rt": "rt"} - - # Usar el pin mapeado si existe, sino el original - mapped_source_pin = source_output_mapping.get(source_pin, source_pin) - - input_sources_repr.append({ - "type": "connection", - "source_instruction_type": source_original_type, # Guardar tipo original puede ser útil - "source_instruction_uid": source_uid, - "source_pin": mapped_source_pin # <-- USAR PIN MAPEADO - }) + elif source_original_type in ["Se", "Sd", "TON", "TOF", "TP"]: + source_output_mapping = { + "q": "q", + "Q": "Q", + "rt": "rt", + "ET": "ET", + } + elif source_original_type in ["CTU", "CTD", "CTUD"]: + source_output_mapping = {"qu": "QU", "qd": "QD", "cv": "CV"} + mapped_source_pin = source_output_mapping.get( + source_pin, source_pin + ) + input_sources_repr.append( + { + "type": "connection", + "source_instruction_type": source_original_type, + "source_instruction_uid": source_uid, + "source_pin": mapped_source_pin, + } + ) else: input_sources_repr.append( {"type": "unknown_source", "uid": source_uid} ) - - # Usar el nombre de pin mapeado para el JSON json_pin_name = input_pin_mapping.get(xml_pin_name, xml_pin_name) - if len(input_sources_repr) == 1: instruction_repr["inputs"][json_pin_name] = input_sources_repr[0] elif len(input_sources_repr) > 1: instruction_repr["inputs"][json_pin_name] = input_sources_repr - - # Mapear Salidas usando el mapeo de pines possible_output_pins = set( [ "out", @@ -886,17 +1036,15 @@ def parse_network(network_element): "cvbcd", "QU", "QD", + "ET", ] - ) + ) # Añadido ET for xml_pin_name in possible_output_pins: source_key = (instruction_uid, xml_pin_name) if source_key in source_connections: - # Usar el nombre de pin mapeado para el JSON json_pin_name = output_pin_mapping.get(xml_pin_name, xml_pin_name) - if json_pin_name not in instruction_repr["outputs"]: instruction_repr["outputs"][json_pin_name] = [] - for dest_uid, dest_pin in source_connections[source_key]: if dest_uid in access_map: if ( @@ -906,10 +1054,9 @@ def parse_network(network_element): instruction_repr["outputs"][json_pin_name].append( access_map[dest_uid] ) - all_logic_steps[instruction_uid] = instruction_repr - # 4. Inferencia EN (sin cambios) + # 4. Inferencia EN (sin cambios en lógica) processed_blocks_en_inference = set() something_changed = True inference_passes = 0 @@ -930,8 +1077,8 @@ def parse_network(network_element): for i, instruction in enumerate(ordered_logic_list_for_en): part_uid = instruction["instruction_uid"] part_type_original = ( - instruction["type"].replace("_scl", "").replace("_error", "") - ) + instruction["type"].replace(SCL_SUFFIX, "").replace("_error", "") + ) # Usa SCL_SUFFIX if ( part_type_original in functional_block_types and "en" not in instruction["inputs"] @@ -943,7 +1090,9 @@ def parse_network(network_element): prev_instr = ordered_logic_list_for_en[j] prev_uid = prev_instr["instruction_uid"] prev_type_original = ( - prev_instr["type"].replace("_scl", "").replace("_error", "") + prev_instr["type"] + .replace(SCL_SUFFIX, "") + .replace("_error", "") ) if prev_type_original in rlo_generators: inferred_en_source = { @@ -979,7 +1128,7 @@ def parse_network(network_element): processed_blocks_en_inference.add(part_uid) something_changed = True - # 5. Añadir lógica ENO interesante (sin cambios) + # 5. Añadir lógica ENO interesante (sin cambios en lógica) for source_instr_uid, eno_destinations in eno_outputs.items(): if source_instr_uid not in all_logic_steps: continue @@ -1025,17 +1174,30 @@ def parse_network(network_element): if interesting_eno_logic: all_logic_steps[source_instr_uid]["eno_logic"] = interesting_eno_logic - # 6. Ordenar y Devolver (sin cambios) + # 6. Ordenar y Devolver network_logic_final = [ all_logic_steps[uid] for uid in sorted_uids_for_en if uid in all_logic_steps ] + # Determinar lenguaje de la red para devolverlo + network_lang = "Unknown" + if network_element is not None: + attr_list_net = network_element.xpath("./*[local-name()='AttributeList']") + if attr_list_net: + lang_node_net = attr_list_net[0].xpath( + "./*[local-name()='ProgrammingLanguage']/text()" + ) + if lang_node_net: + network_lang = lang_node_net[0].strip() + return { "id": network_id, "title": network_title, "comment": network_comment, + "language": network_lang, "logic": network_logic_final, } + def convert_xml_to_json(xml_filepath, json_filepath): print(f"Iniciando conversión de '{xml_filepath}' a '{json_filepath}'...") if not os.path.exists(xml_filepath): @@ -1047,25 +1209,37 @@ def convert_xml_to_json(xml_filepath, json_filepath): tree = etree.parse(xml_filepath, parser) root = tree.getroot() print("Paso 1: Parseo XML completado.") - print("Paso 2: Buscando el bloque SW.Blocks.FC...") # Asume FC primero - block_list = root.xpath("//*[local-name()='SW.Blocks.FC']") - block_type_found = "FC" - if not block_list: - block_list = root.xpath( - "//*[local-name()='SW.Blocks.FB']" - ) # Busca FB si no hay FC - block_type_found = "FB" - if not block_list: - print("Error Crítico: No se encontró ni .") - return - else: - print( - "Advertencia: Se encontró en lugar de ." - ) - the_block = block_list[0] - print( - f"Paso 2: Bloque SW.Blocks.{block_type_found} encontrado (ID={the_block.get('ID')})." - ) + print("Paso 2: Buscando el bloque SW.Blocks.FC, SW.Blocks.FB o SW.Blocks.GlobalDB...") + # --- MODIFICADO: Buscar FC, FB o GlobalDB --- + block_list = root.xpath("//*[local-name()='SW.Blocks.FC' or local-name()='SW.Blocks.FB' or local-name()='SW.Blocks.GlobalDB']") + block_type_found = None + the_block = None + + if block_list: + the_block = block_list[0] + # Obtener el nombre real de la etiqueta encontrada + block_tag_name = etree.QName(the_block.tag).localname + if block_tag_name == "SW.Blocks.FC": + block_type_found = "FC" + elif block_tag_name == "SW.Blocks.FB": + block_type_found = "FB" + elif block_tag_name == "SW.Blocks.GlobalDB": + block_type_found = "GlobalDB" # Identificar el tipo DB + print(f"Paso 2: Bloque {block_tag_name} encontrado (ID={the_block.get('ID')}).") + else: + # Mensaje de error más específico y añadimos depuración + print("Error Crítico: No se encontró el elemento raíz del bloque (, o ) usando XPath.") + # --- Añadir Debugging --- + print(f"DEBUG: Tag del elemento raíz del XML: {root.tag}") + print(f"DEBUG: Primeros hijos del raíz:") + for i, child in enumerate(root.getchildren()): + if i < 5: # Imprimir solo los primeros 5 para no saturar + print(f"DEBUG: - Hijo {i+1}: {child.tag}") + else: + print("DEBUG: - ... (más hijos)") + break + # --- Fin Debugging --- + return # Salir si no se encuentra el bloque principal print("Paso 3: Extrayendo atributos del bloque...") attribute_list_node = the_block.xpath("./*[local-name()='AttributeList']") block_name_val, block_number_val, block_lang_val = "Unknown", None, "Unknown" @@ -1241,24 +1415,30 @@ def convert_xml_to_json(xml_filepath, json_filepath): reconstructed_stl = f"// STL extraction failed for Network {network_id}: StatementList node not found.\n" if statement_list_node: - print(f" Reconstruyendo STL desde StatementList para red {network_id}...") + print( + f" Reconstruyendo STL desde StatementList para red {network_id}..." + ) # Llama a la nueva función de reconstrucción STL - reconstructed_stl = reconstruct_stl_from_statementlist(statement_list_node[0]) + reconstructed_stl = reconstruct_stl_from_statementlist( + statement_list_node[0] + ) # print(f" ... STL reconstruido (parcial):\n{reconstructed_stl[:200]}...") # Preview opcional else: - print(f" Advertencia: No se encontró nodo para red STL {network_id}.") + print( + f" Advertencia: No se encontró nodo para red STL {network_id}." + ) # Guardar como un chunk de texto crudo parsed_network_data = { "id": network_id, "title": network_title, "comment": network_comment, - "language": "STL", # Indicar que es STL + "language": "STL", # Indicar que es STL "logic": [ { - "instruction_uid": f"STL_{network_id}", # UID inventado - "type": "RAW_STL_CHUNK", # Nuevo tipo para identificarlo - "stl": reconstructed_stl, # Guardar el texto reconstruido + "instruction_uid": f"STL_{network_id}", # UID inventado + "type": "RAW_STL_CHUNK", # Nuevo tipo para identificarlo + "stl": reconstructed_stl, # Guardar el texto reconstruido } ], } @@ -1340,6 +1520,7 @@ def convert_xml_to_json(xml_filepath, json_filepath): traceback.print_exc() print("--- Fin Traceback ---") + if __name__ == "__main__": # Imports necesarios solo para la ejecución como script principal import argparse @@ -1351,27 +1532,29 @@ if __name__ == "__main__": description="Convert Simatic XML (LAD/FBD/SCL/STL) to simplified JSON. Expects XML filepath as argument." ) parser.add_argument( - "xml_filepath", # Argumento posicional obligatorio + "xml_filepath", # Argumento posicional obligatorio help="Path to the input XML file passed from the main script (x0_main.py).", ) - args = parser.parse_args() # Parsea los argumentos de sys.argv + args = parser.parse_args() # Parsea los argumentos de sys.argv - xml_input_file = args.xml_filepath # Obtiene la ruta del argumento + xml_input_file = args.xml_filepath # Obtiene la ruta del argumento # Verificar si el archivo de entrada existe (es una buena práctica aunque x0 lo haga) if not os.path.exists(xml_input_file): print(f"Error Crítico (x1): Archivo XML no encontrado: '{xml_input_file}'") - sys.exit(1) # Salir si el archivo no existe + sys.exit(1) # Salir si el archivo no existe # Derivar nombre base para archivo de salida JSON # El archivo JSON se guardará en el mismo directorio que el XML de entrada xml_filename_base = os.path.splitext(os.path.basename(xml_input_file))[0] - output_dir = os.path.dirname(xml_input_file) # Directorio del XML de entrada + output_dir = os.path.dirname(xml_input_file) # Directorio del XML de entrada # Asegurarse de que el directorio de salida exista (aunque debería si el XML existe) os.makedirs(output_dir, exist_ok=True) json_output_file = os.path.join(output_dir, f"{xml_filename_base}_simplified.json") - print(f"(x1) Convirtiendo: '{os.path.relpath(xml_input_file)}' -> '{os.path.relpath(json_output_file)}'") + print( + f"(x1) Convirtiendo: '{os.path.relpath(xml_input_file)}' -> '{os.path.relpath(json_output_file)}'" + ) # Llamar a la función principal de conversión del script # Asumiendo que tu función principal se llama convert_xml_to_json(input_path, output_path) @@ -1380,6 +1563,6 @@ if __name__ == "__main__": except Exception as e: print(f"Error Crítico (x1) durante la conversión de '{xml_input_file}': {e}") import traceback - traceback.print_exc() - sys.exit(1) # Salir con error si la función principal falla + traceback.print_exc() + sys.exit(1) # Salir con error si la función principal falla diff --git a/ToUpload/x2_process.py b/ToUpload/x2_process.py index fd124d8..a0f3f93 100644 --- a/ToUpload/x2_process.py +++ b/ToUpload/x2_process.py @@ -5,29 +5,30 @@ import os import copy import traceback import re -import importlib -import sys -import sympy # Import sympy +import importlib +import sys +import sympy # Import sympy # Import necessary components from processors directory from processors.processor_utils import ( - format_variable_name, # Keep if used outside processors - sympy_expr_to_scl, # Needed for IF grouping and maybe others + format_variable_name, # Keep if used outside processors + sympy_expr_to_scl, # Needed for IF grouping and maybe others # get_target_scl_name might be used here? Unlikely. ) -from processors.symbol_manager import SymbolManager # Import the manager +from processors.symbol_manager import SymbolManager # Import the manager # --- Constantes y Configuración --- # SCL_SUFFIX = "_scl" # Old suffix -SCL_SUFFIX = "_sympy_processed" # New suffix to indicate processing method +SCL_SUFFIX = "_sympy_processed" # New suffix to indicate processing method GROUPED_COMMENT = "// Logic included in grouped IF" -SIMPLIFIED_IF_COMMENT = "// Simplified IF condition by script" # May still be useful +SIMPLIFIED_IF_COMMENT = "// Simplified IF condition by script" # May still be useful # Global data dictionary (consider passing 'data' as argument if needed elsewhere) # It's currently used by process_group_ifs implicitly via the outer scope, # which works but passing it explicitly might be cleaner. data = {} + def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data): """ Busca condiciones (ya procesadas -> tienen expr SymPy en sympy_map) @@ -37,24 +38,45 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data): (Esta es la implementación de la función como la tenías en el archivo original) """ instr_uid = instruction["instruction_uid"] - instr_type_original = instruction.get("type", "").replace(SCL_SUFFIX, "").replace("_error", "") + instr_type_original = ( + instruction.get("type", "").replace(SCL_SUFFIX, "").replace("_error", "") + ) made_change = False # Check if this instruction *could* generate a condition suitable for grouping # It must have been processed by the new SymPy method if ( - not instruction.get("type", "").endswith(SCL_SUFFIX) # Check if processed by new method + not instruction.get("type", "").endswith( + SCL_SUFFIX + ) # Check if processed by new method or "_error" in instruction.get("type", "") or instruction.get("grouped", False) - or instr_type_original not in [ # Original types that produce boolean results - "Contact", "O", "Eq", "Ne", "Gt", "Lt", "Ge", "Le", "PBox", "NBox", "And", "Xor", "Not" # Add others like comparison + or instr_type_original + not in [ # Original types that produce boolean results + "Contact", + "O", + "Eq", + "Ne", + "Gt", + "Lt", + "Ge", + "Le", + "PBox", + "NBox", + "And", + "Xor", + "Not", # Add others like comparison ] ): return False # Avoid reagruping if SCL already contains a complex IF (less likely now) current_scl = instruction.get("scl", "") - if current_scl.strip().startswith("IF") and "END_IF;" in current_scl and GROUPED_COMMENT not in current_scl: + if ( + current_scl.strip().startswith("IF") + and "END_IF;" in current_scl + and GROUPED_COMMENT not in current_scl + ): return False # *** Get the SymPy expression for the condition *** @@ -62,20 +84,34 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data): sympy_condition_expr = sympy_map.get(map_key_out) # No SymPy expression found or trivial conditions - if sympy_condition_expr is None or sympy_condition_expr in [sympy.true, sympy.false]: + if sympy_condition_expr is None or sympy_condition_expr in [ + sympy.true, + sympy.false, + ]: return False # --- Find consumer instructions (logic similar to before) --- grouped_instructions_cores = [] consumer_instr_list = [] - network_logic = next((net["logic"] for net in data["networks"] if net["id"] == network_id), []) - if not network_logic: return False + network_logic = next( + (net["logic"] for net in data["networks"] if net["id"] == network_id), [] + ) + if not network_logic: + return False - groupable_types = [ # Types whose *final SCL* we want to group - "Move", "Add", "Sub", "Mul", "Div", "Mod", "Convert", - "Call_FC", "Call_FB", # Assuming these generate final SCL in their processors now + groupable_types = [ # Types whose *final SCL* we want to group + "Move", + "Add", + "Sub", + "Mul", + "Div", + "Mod", + "Convert", + "Call_FC", + "Call_FB", # Assuming these generate final SCL in their processors now # SCoil/RCoil might also be groupable if their SCL is final assignment - "SCoil", "RCoil" + "SCoil", + "RCoil", ] for consumer_instr in network_logic: @@ -84,31 +120,45 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data): continue consumer_en = consumer_instr.get("inputs", {}).get("en") - consumer_type = consumer_instr.get("type", "") # Current type suffix matters - consumer_type_original = consumer_type.replace(SCL_SUFFIX, "").replace("_error", "") + consumer_type = consumer_instr.get("type", "") # Current type suffix matters + consumer_type_original = consumer_type.replace(SCL_SUFFIX, "").replace( + "_error", "" + ) is_enabled_by_us = False - if ( isinstance(consumer_en, dict) and consumer_en.get("type") == "connection" and - consumer_en.get("source_instruction_uid") == instr_uid and - consumer_en.get("source_pin") == "out"): + if ( + isinstance(consumer_en, dict) + and consumer_en.get("type") == "connection" + and consumer_en.get("source_instruction_uid") == instr_uid + and consumer_en.get("source_pin") == "out" + ): is_enabled_by_us = True # Check if consumer is groupable AND has its final SCL generated # The suffix check needs adjustment based on how terminating processors set it. # Assuming processors like Move, Add, Call, SCoil, RCoil NOW generate final SCL and add a suffix. - if ( is_enabled_by_us and consumer_type.endswith(SCL_SUFFIX) and # Or a specific "final_scl" suffix - consumer_type_original in groupable_types ): + if ( + is_enabled_by_us + and consumer_type.endswith(SCL_SUFFIX) # Or a specific "final_scl" suffix + and consumer_type_original in groupable_types + ): consumer_scl = consumer_instr.get("scl", "") # Extract core SCL (logic is similar, maybe simpler if SCL is cleaner now) core_scl = None if consumer_scl: - # If consumer SCL itself is an IF generated by EN, take the body - if consumer_scl.strip().startswith("IF"): - match = re.search(r"THEN\s*(.*?)\s*END_IF;", consumer_scl, re.DOTALL | re.IGNORECASE) - core_scl = match.group(1).strip() if match else None - elif not consumer_scl.strip().startswith("//"): # Otherwise, take the whole line if not comment - core_scl = consumer_scl.strip() + # If consumer SCL itself is an IF generated by EN, take the body + if consumer_scl.strip().startswith("IF"): + match = re.search( + r"THEN\s*(.*?)\s*END_IF;", + consumer_scl, + re.DOTALL | re.IGNORECASE, + ) + core_scl = match.group(1).strip() if match else None + elif not consumer_scl.strip().startswith( + "//" + ): # Otherwise, take the whole line if not comment + core_scl = consumer_scl.strip() if core_scl: grouped_instructions_cores.append(core_scl) @@ -116,15 +166,19 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data): # --- If groupable consumers found --- if len(grouped_instructions_cores) > 1: - print(f"INFO: Agrupando {len(grouped_instructions_cores)} instr. bajo condición de {instr_type_original} UID {instr_uid}") + print( + f"INFO: Agrupando {len(grouped_instructions_cores)} instr. bajo condición de {instr_type_original} UID {instr_uid}" + ) # *** Simplify the SymPy condition *** try: - #simplified_expr = sympy.simplify_logic(sympy_condition_expr, force=True) - simplified_expr = sympy.logic.boolalg.to_dnf(sympy_condition_expr, simplify=True) + # simplified_expr = sympy.simplify_logic(sympy_condition_expr, force=True) + simplified_expr = sympy.logic.boolalg.to_dnf( + sympy_condition_expr, simplify=True + ) except Exception as e: print(f"Error simplifying condition for grouping UID {instr_uid}: {e}") - simplified_expr = sympy_condition_expr # Fallback + simplified_expr = sympy_condition_expr # Fallback # *** Convert simplified condition to SCL string *** condition_scl_simplified = sympy_expr_to_scl(simplified_expr, symbol_manager) @@ -132,7 +186,9 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data): # *** Build the grouped IF SCL *** scl_grouped_lines = [f"IF {condition_scl_simplified} THEN"] for core_line in grouped_instructions_cores: - indented_core = "\n".join([f" {line.strip()}" for line in core_line.splitlines()]) + indented_core = "\n".join( + [f" {line.strip()}" for line in core_line.splitlines()] + ) scl_grouped_lines.append(indented_core) scl_grouped_lines.append("END_IF;") final_grouped_scl = "\n".join(scl_grouped_lines) @@ -147,18 +203,19 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data): return made_change + def load_processors(processors_dir="processors"): """ Escanea el directorio, importa módulos, construye el mapa y una lista ordenada por prioridad. """ processor_map = {} - processor_list_unsorted = [] # Lista para guardar (priority, type_name, func) - default_priority = 10 # Prioridad si no se define en get_processor_info + processor_list_unsorted = [] # Lista para guardar (priority, type_name, func) + default_priority = 10 # Prioridad si no se define en get_processor_info if not os.path.isdir(processors_dir): print(f"Error: Directorio de procesadores no encontrado: '{processors_dir}'") - return processor_map, [] # Devuelve mapa vacío y lista vacía + return processor_map, [] # Devuelve mapa vacío y lista vacía print(f"Cargando procesadores desde: '{processors_dir}'") processors_package = os.path.basename(processors_dir) @@ -170,7 +227,9 @@ def load_processors(processors_dir="processors"): try: module = importlib.import_module(full_module_name) - if hasattr(module, 'get_processor_info') and callable(module.get_processor_info): + if hasattr(module, "get_processor_info") and callable( + module.get_processor_info + ): processor_info = module.get_processor_info() info_list = [] if isinstance(processor_info, dict): @@ -178,29 +237,51 @@ def load_processors(processors_dir="processors"): elif isinstance(processor_info, list): info_list = processor_info else: - print(f" Advertencia: get_processor_info en {full_module_name} devolvió tipo inesperado. Se ignora.") + print( + f" Advertencia: get_processor_info en {full_module_name} devolvió tipo inesperado. Se ignora." + ) continue for info in info_list: - if isinstance(info, dict) and 'type_name' in info and 'processor_func' in info: - type_name = info['type_name'].lower() - processor_func = info['processor_func'] + if ( + isinstance(info, dict) + and "type_name" in info + and "processor_func" in info + ): + type_name = info["type_name"].lower() + processor_func = info["processor_func"] # Obtener prioridad, usar default si no existe - priority = info.get('priority', default_priority) + priority = info.get("priority", default_priority) if callable(processor_func): if type_name in processor_map: - print(f" Advertencia: '{type_name}' en {full_module_name} sobrescribe definición anterior.") + print( + f" Advertencia: '{type_name}' en {full_module_name} sobrescribe definición anterior." + ) processor_map[type_name] = processor_func # Añadir a la lista para ordenar - processor_list_unsorted.append({'priority': priority, 'type_name': type_name, 'func': processor_func}) - print(f" - Cargado '{type_name}' (Prio: {priority}) desde {module_name_rel}.py") + processor_list_unsorted.append( + { + "priority": priority, + "type_name": type_name, + "func": processor_func, + } + ) + print( + f" - Cargado '{type_name}' (Prio: {priority}) desde {module_name_rel}.py" + ) else: - print(f" Advertencia: 'processor_func' para '{type_name}' en {full_module_name} no es callable.") + print( + f" Advertencia: 'processor_func' para '{type_name}' en {full_module_name} no es callable." + ) else: - print(f" Advertencia: Entrada inválida en {full_module_name}: {info}") + print( + f" Advertencia: Entrada inválida en {full_module_name}: {info}" + ) else: - print(f" Advertencia: Módulo {module_name_rel}.py no tiene 'get_processor_info'.") + print( + f" Advertencia: Módulo {module_name_rel}.py no tiene 'get_processor_info'." + ) except ImportError as e: print(f"Error importando {full_module_name}: {e}") @@ -209,22 +290,24 @@ def load_processors(processors_dir="processors"): traceback.print_exc() # Ordenar la lista por prioridad (menor primero) - processor_list_sorted = sorted(processor_list_unsorted, key=lambda x: x['priority']) + processor_list_sorted = sorted(processor_list_unsorted, key=lambda x: x["priority"]) print(f"\nTotal de tipos de procesadores cargados: {len(processor_map)}") - print(f"Orden de procesamiento por prioridad: {[item['type_name'] for item in processor_list_sorted]}") + print( + f"Orden de procesamiento por prioridad: {[item['type_name'] for item in processor_list_sorted]}" + ) # Devolver el mapa (para lookup rápido si es necesario) y la lista ordenada return processor_map, processor_list_sorted + # --- Bucle Principal de Procesamiento (Modificado para STL) --- def process_json_to_scl(json_filepath): """ - Lee el JSON simplificado, aplica los procesadores dinámicamente cargados - siguiendo un orden de prioridad (ignorando redes STL), y guarda el JSON procesado. + Lee JSON simplificado, aplica procesadores dinámicos (ignorando redes STL y bloques DB), + y guarda JSON procesado. """ - global data # Necesario para que load_processors y process_group_ifs (definidas fuera) puedan acceder a ella. - # Considerar pasar 'data' como argumento si es posible refactorizar. + global data if not os.path.exists(json_filepath): print(f"Error: JSON no encontrado: {json_filepath}") @@ -232,48 +315,83 @@ def process_json_to_scl(json_filepath): print(f"Cargando JSON desde: {json_filepath}") try: with open(json_filepath, "r", encoding="utf-8") as f: - data = json.load(f) # Carga en 'data' global + data = json.load(f) except Exception as e: print(f"Error al cargar JSON: {e}") traceback.print_exc() return - # --- Carga dinámica de procesadores --- + # --- Obtener lenguaje del bloque principal --- + block_language = data.get("language", "Unknown") + block_type = data.get("block_type", "Unknown") # FC, FB, GlobalDB + print(f"Procesando bloque tipo: {block_type}, Lenguaje principal: {block_language}") + + # --- SI ES UN DB, SALTAR EL PROCESAMIENTO LÓGICO --- + if block_language == "DB": + print( + "INFO: El bloque es un Data Block (DB). Saltando procesamiento lógico de x2." + ) + # Simplemente guardamos una copia (o el mismo archivo si no se requiere sufijo) + output_filename = json_filepath.replace( + "_simplified.json", "_simplified_processed.json" + ) + print(f"Guardando JSON de DB (sin cambios lógicos) en: {output_filename}") + try: + with open(output_filename, "w", encoding="utf-8") as f: + json.dump(data, f, indent=4, ensure_ascii=False) + print("Guardado de DB completado.") + except Exception as e: + print(f"Error Crítico al guardar JSON del DB: {e}") + traceback.print_exc() + return # <<< SALIR TEMPRANO PARA DBs + + # --- SI NO ES DB, CONTINUAR CON EL PROCESAMIENTO LÓGICO (FC/FB) --- + print("INFO: El bloque es FC/FB. Iniciando procesamiento lógico...") + script_dir = os.path.dirname(__file__) - processors_dir_path = os.path.join(script_dir, 'processors') + processors_dir_path = os.path.join(script_dir, "processors") processor_map, sorted_processors = load_processors(processors_dir_path) if not processor_map: print("Error crítico: No se cargaron procesadores. Abortando.") return - # --- Crear mapas de acceso por red --- network_access_maps = {} - # (La lógica para llenar network_access_maps no cambia, puedes copiarla de tu original) + # Crear mapas de acceso por red (copiado/adaptado de versión anterior) for network in data.get("networks", []): net_id = network["id"] current_access_map = {} for instr in network.get("logic", []): - for _, source in instr.get("inputs", {}).items(): - sources_to_check = (source if isinstance(source, list) else ([source] if isinstance(source, dict) else [])) - for src in sources_to_check: - if (isinstance(src, dict) and src.get("uid") and src.get("type") in ["variable", "constant"]): - current_access_map[src["uid"]] = src - for _, dest_list in instr.get("outputs", {}).items(): - if isinstance(dest_list, list): - for dest in dest_list: - if (isinstance(dest, dict) and dest.get("uid") and dest.get("type") in ["variable", "constant"]): - current_access_map[dest["uid"]] = dest + for _, source in instr.get("inputs", {}).items(): + sources_to_check = ( + source + if isinstance(source, list) + else ([source] if isinstance(source, dict) else []) + ) + for src in sources_to_check: + if ( + isinstance(src, dict) + and src.get("uid") + and src.get("type") in ["variable", "constant"] + ): + current_access_map[src["uid"]] = src + for _, dest_list in instr.get("outputs", {}).items(): + if isinstance(dest_list, list): + for dest in dest_list: + if ( + isinstance(dest, dict) + and dest.get("uid") + and dest.get("type") in ["variable", "constant"] + ): + current_access_map[dest["uid"]] = dest network_access_maps[net_id] = current_access_map - # --- Inicializar mapa SymPy y SymbolManager --- symbol_manager = SymbolManager() sympy_map = {} - max_passes = 30 passes = 0 processing_complete = False - print("\n--- Iniciando Bucle de Procesamiento Iterativo (con SymPy y prioridad) ---") + print("\n--- Iniciando Bucle de Procesamiento Iterativo (FC/FB) ---") while passes < max_passes and not processing_complete: passes += 1 made_change_in_base_pass = False @@ -284,90 +402,99 @@ def process_json_to_scl(json_filepath): # --- FASE 1: Procesadores Base (Ignorando STL) --- print(f" Fase 1 (SymPy Base - Orden por Prioridad):") - num_sympy_processed_this_pass = 0 + num_sympy_processed_this_pass = 0 # Resetear contador para el pase for processor_info in sorted_processors: - current_type_name = processor_info['type_name'] - func_to_call = processor_info['func'] - + current_type_name = processor_info["type_name"] + func_to_call = processor_info["func"] for network in data.get("networks", []): network_id = network["id"] - network_lang = network.get("language", "LAD") # Obtener lenguaje de la red - - # *** IGNORAR REDES STL EN ESTA FASE *** + network_lang = network.get("language", "LAD") if network_lang == "STL": - continue # Saltar al siguiente network + continue # Saltar STL access_map = network_access_maps.get(network_id, {}) network_logic = network.get("logic", []) - for instruction in network_logic: instr_uid = instruction.get("instruction_uid") instr_type_original = instruction.get("type", "Unknown") - - # Saltar si ya procesado, error, agrupado o es chunk STL/SCL/Unsupported - if (instr_type_original.endswith(SCL_SUFFIX) + if ( + instr_type_original.endswith(SCL_SUFFIX) or "_error" in instr_type_original or instruction.get("grouped", False) - or instr_type_original in ["RAW_STL_CHUNK", "RAW_SCL_CHUNK", "UNSUPPORTED_LANG"]): + or instr_type_original + in ["RAW_STL_CHUNK", "RAW_SCL_CHUNK", "UNSUPPORTED_LANG"] + ): continue - - # Determinar tipo efectivo (como antes) lookup_key = instr_type_original.lower() effective_type_name = lookup_key if instr_type_original == "Call": - block_type = instruction.get("block_type", "").upper() - if block_type == "FC": effective_type_name = "call_fc" - elif block_type == "FB": effective_type_name = "call_fb" + block_type = instruction.get("block_type", "").upper() + if block_type == "FC": + effective_type_name = "call_fc" + elif block_type == "FB": + effective_type_name = "call_fb" - # Llamar al procesador si coincide el tipo if effective_type_name == current_type_name: try: - # Pasa sympy_map, symbol_manager y data - changed = func_to_call(instruction, network_id, sympy_map, symbol_manager, data) + changed = func_to_call( + instruction, network_id, sympy_map, symbol_manager, data + ) if changed: made_change_in_base_pass = True num_sympy_processed_this_pass += 1 except Exception as e: - print(f"ERROR(SymPy Base) al procesar {instr_type_original} UID {instr_uid}: {e}") - traceback.print_exc() - instruction["scl"] = f"// ERROR en SymPy procesador base: {e}" - instruction["type"] = instr_type_original + "_error" - made_change_in_base_pass = True # Marcar cambio aunque sea error - print(f" -> {num_sympy_processed_this_pass} instrucciones (no STL) procesadas con SymPy.") - + print( + f"ERROR(SymPy Base) al procesar {instr_type_original} UID {instr_uid}: {e}" + ) + traceback.print_exc() + instruction["scl"] = ( + f"// ERROR en SymPy procesador base: {e}" + ) + instruction["type"] = instr_type_original + "_error" + made_change_in_base_pass = True + print( + f" -> {num_sympy_processed_this_pass} instrucciones (no STL) procesadas con SymPy." + ) # --- FASE 2: Agrupación IF (Ignorando STL) --- - if made_change_in_base_pass or passes == 1: + if ( + made_change_in_base_pass or passes == 1 + ): # Ejecutar siempre en el primer pase print(f" Fase 2 (Agrupación IF con Simplificación):") - num_grouped_this_pass = 0 + num_grouped_this_pass = 0 # Resetear contador para el pase for network in data.get("networks", []): network_id = network["id"] - network_lang = network.get("language", "LAD") # Obtener lenguaje - - # *** IGNORAR REDES STL EN ESTA FASE *** + network_lang = network.get("language", "LAD") if network_lang == "STL": - continue # Saltar red STL - + continue # Saltar STL network_logic = network.get("logic", []) for instruction in network_logic: try: - # Llama a process_group_ifs (que necesita acceso a 'data' global o pasado) - group_changed = process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data) - if group_changed: - made_change_in_group_pass = True - num_grouped_this_pass += 1 + group_changed = process_group_ifs( + instruction, network_id, sympy_map, symbol_manager, data + ) + if group_changed: + made_change_in_group_pass = True + num_grouped_this_pass += 1 except Exception as e: - print(f"ERROR(GroupLoop) al intentar agrupar desde UID {instruction.get('instruction_uid')}: {e}") - traceback.print_exc() - print(f" -> {num_grouped_this_pass} agrupaciones realizadas (en redes no STL).") - + print( + f"ERROR(GroupLoop) al intentar agrupar desde UID {instruction.get('instruction_uid')}: {e}" + ) + traceback.print_exc() + print( + f" -> {num_grouped_this_pass} agrupaciones realizadas (en redes no STL)." + ) # --- Comprobar si se completó el procesamiento --- if not made_change_in_base_pass and not made_change_in_group_pass: - print(f"\n--- No se hicieron más cambios en el pase {passes}. Proceso iterativo completado. ---") + print( + f"\n--- No se hicieron más cambios en el pase {passes}. Proceso iterativo completado. ---" + ) processing_complete = True else: - print(f"--- Fin Pase {passes}: {num_sympy_processed_this_pass} proc SymPy, {num_grouped_this_pass} agrup. Continuando...") + print( + f"--- Fin Pase {passes}: {num_sympy_processed_this_pass} proc SymPy, {num_grouped_this_pass} agrup. Continuando..." + ) # --- Comprobar límite de pases --- if passes == max_passes and not processing_complete: @@ -376,46 +503,51 @@ def process_json_to_scl(json_filepath): # --- FIN BUCLE ITERATIVO --- # --- Verificación Final (Ajustada para RAW_STL_CHUNK) --- - print("\n--- Verificación Final de Instrucciones No Procesadas ---") + print("\n--- Verificación Final de Instrucciones No Procesadas (FC/FB) ---") unprocessed_count = 0 unprocessed_details = [] - # Añadir RAW_STL_CHUNK a los tipos ignorados - ignored_types = ['raw_scl_chunk', 'unsupported_lang', 'raw_stl_chunk'] # Añadido raw_stl_chunk - + ignored_types = [ + "raw_scl_chunk", + "unsupported_lang", + "raw_stl_chunk", + ] # Añadido raw_stl_chunk for network in data.get("networks", []): - network_id = network.get("id", "Unknown ID") - network_title = network.get("title", f"Network {network_id}") - network_lang = network.get("language", "LAD") # Obtener lenguaje - - # No verificar instrucciones dentro de redes STL, ya que no se procesan - if network_lang == "STL": - continue - - for instruction in network.get("logic", []): - instr_uid = instruction.get("instruction_uid", "Unknown UID") - instr_type = instruction.get("type", "Unknown Type") - is_grouped = instruction.get("grouped", False) - - # Condición revisada para ignorar los chunks crudos - if (not instr_type.endswith(SCL_SUFFIX) and - "_error" not in instr_type and - not is_grouped and - instr_type.lower() not in ignored_types): # Verifica contra lista actualizada - unprocessed_count += 1 - unprocessed_details.append( - f" - Red '{network_title}' (ID: {network_id}, Lang: {network_lang}), " - f"Instrucción UID: {instr_uid}, Tipo: '{instr_type}'" - ) - + network_id = network.get("id", "Unknown ID") + network_title = network.get("title", f"Network {network_id}") + network_lang = network.get("language", "LAD") + if network_lang == "STL": + continue # No verificar redes STL + for instruction in network.get("logic", []): + instr_uid = instruction.get("instruction_uid", "Unknown UID") + instr_type = instruction.get("type", "Unknown Type") + is_grouped = instruction.get("grouped", False) + if ( + not instr_type.endswith(SCL_SUFFIX) + and "_error" not in instr_type + and not is_grouped + and instr_type.lower() not in ignored_types + ): + unprocessed_count += 1 + unprocessed_details.append( + f" - Red '{network_title}' (ID: {network_id}, Lang: {network_lang}), " + f"Instrucción UID: {instr_uid}, Tipo: '{instr_type}'" + ) if unprocessed_count > 0: - print(f"ADVERTENCIA: Se encontraron {unprocessed_count} instrucciones (no STL) que parecen no haber sido procesadas:") - for detail in unprocessed_details: print(detail) + print( + f"ADVERTENCIA: Se encontraron {unprocessed_count} instrucciones (no STL) que parecen no haber sido procesadas:" + ) + for detail in unprocessed_details: + print(detail) else: - print("INFO: Todas las instrucciones relevantes (no STL) parecen haber sido procesadas o agrupadas.") + print( + "INFO: Todas las instrucciones relevantes (no STL) parecen haber sido procesadas o agrupadas." + ) # --- Guardar JSON Final --- - output_filename = json_filepath.replace("_simplified.json", "_simplified_processed.json") - print(f"\nGuardando JSON procesado en: {output_filename}") + output_filename = json_filepath.replace( + "_simplified.json", "_simplified_processed.json" + ) + print(f"\nGuardando JSON procesado (FC/FB) en: {output_filename}") try: with open(output_filename, "w", encoding="utf-8") as f: json.dump(data, f, indent=4, ensure_ascii=False) @@ -424,6 +556,7 @@ def process_json_to_scl(json_filepath): print(f"Error Crítico al guardar JSON procesado: {e}") traceback.print_exc() + # --- Ejecución (sin cambios) --- if __name__ == "__main__": # Imports necesarios solo para la ejecución como script principal @@ -436,43 +569,55 @@ if __name__ == "__main__": description="Process simplified JSON (_simplified.json) to embed SCL logic (SymPy version). Expects original XML filepath as argument." ) parser.add_argument( - "source_xml_filepath", # Argumento posicional obligatorio + "source_xml_filepath", # Argumento posicional obligatorio help="Path to the original source XML file (passed from x0_main.py, used to derive JSON input name).", ) - args = parser.parse_args() # Parsea los argumentos de sys.argv + args = parser.parse_args() # Parsea los argumentos de sys.argv - source_xml_file = args.source_xml_filepath # Obtiene la ruta del XML original + source_xml_file = args.source_xml_filepath # Obtiene la ruta del XML original # Verificar si el archivo XML original existe (como referencia, útil para depuración) # No es estrictamente necesario para la lógica aquí, pero ayuda a confirmar if not os.path.exists(source_xml_file): - print(f"Advertencia (x2): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON correspondiente.") - # No salir necesariamente, pero es bueno saberlo. + print( + f"Advertencia (x2): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON correspondiente." + ) + # No salir necesariamente, pero es bueno saberlo. # Derivar nombre del archivo JSON de entrada (_simplified.json) xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0] # Asumir que el JSON simplificado está en el mismo directorio que el XML original - input_dir = os.path.dirname(source_xml_file) # Directorio del XML original + input_dir = os.path.dirname(source_xml_file) # Directorio del XML original input_json_file = os.path.join(input_dir, f"{xml_filename_base}_simplified.json") # Determinar el nombre esperado del archivo JSON procesado de salida - output_json_file = os.path.join(input_dir, f"{xml_filename_base}_simplified_processed.json") - - print(f"(x2) Procesando: '{os.path.relpath(input_json_file)}' -> '{os.path.relpath(output_json_file)}'") + output_json_file = os.path.join( + input_dir, f"{xml_filename_base}_simplified_processed.json" + ) + print( + f"(x2) Procesando: '{os.path.relpath(input_json_file)}' -> '{os.path.relpath(output_json_file)}'" + ) # Verificar si el archivo JSON de entrada (_simplified.json) EXISTE antes de procesar if not os.path.exists(input_json_file): - print(f"Error Fatal (x2): El archivo de entrada JSON simplificado no existe: '{input_json_file}'") - print(f"Asegúrate de que 'x1_to_json.py' se ejecutó correctamente para '{os.path.relpath(source_xml_file)}'.") - sys.exit(1) # Salir si el archivo necesario no está + print( + f"Error Fatal (x2): El archivo de entrada JSON simplificado no existe: '{input_json_file}'" + ) + print( + f"Asegúrate de que 'x1_to_json.py' se ejecutó correctamente para '{os.path.relpath(source_xml_file)}'." + ) + sys.exit(1) # Salir si el archivo necesario no está else: # Llamar a la función principal de procesamiento del script # Asumiendo que tu función principal se llama process_json_to_scl(input_json_path) try: process_json_to_scl(input_json_file) except Exception as e: - print(f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}") + print( + f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}" + ) import traceback + traceback.print_exc() - sys.exit(1) # Salir con error si la función principal falla \ No newline at end of file + sys.exit(1) # Salir con error si la función principal falla diff --git a/ToUpload/x3_generate_scl.py b/ToUpload/x3_generate_scl.py index 0551f51..e90d74e 100644 --- a/ToUpload/x3_generate_scl.py +++ b/ToUpload/x3_generate_scl.py @@ -5,290 +5,481 @@ import os import re import argparse import sys -import traceback # Importar traceback para errores +import traceback # Importar traceback para errores # --- Importar Utilidades y Constantes (Asumiendo ubicación) --- try: # Intenta importar desde el paquete de procesadores si está estructurado así from processors.processor_utils import format_variable_name + # Definir SCL_SUFFIX aquí o importarlo si está centralizado - SCL_SUFFIX = "_sympy_processed" # Asegúrate que coincida con x2_process.py - GROUPED_COMMENT = "// Logic included in grouped IF" # Opcional, si se usa para filtrar + SCL_SUFFIX = "_sympy_processed" # Asegúrate que coincida con x2_process.py + GROUPED_COMMENT = ( + "// Logic included in grouped IF" # Opcional, si se usa para filtrar + ) except ImportError: - print("Advertencia: No se pudo importar 'format_variable_name' desde processors.processor_utils.") - print("Usando una implementación local básica (¡PUEDE FALLAR CON NOMBRES COMPLEJOS!).") + print( + "Advertencia: No se pudo importar 'format_variable_name' desde processors.processor_utils." + ) + print( + "Usando una implementación local básica (¡PUEDE FALLAR CON NOMBRES COMPLEJOS!)." + ) + # Implementación local BÁSICA como fallback (MENOS RECOMENDADA) def format_variable_name(name): - if not name: return "_INVALID_NAME_" - if name.startswith('"') and name.endswith('"'): return name # Mantener comillas + if not name: + return "_INVALID_NAME_" + if name.startswith('"') and name.endswith('"'): + return name # Mantener comillas prefix = "#" if name.startswith("#") else "" - if prefix: name = name[1:] - if name and name[0].isdigit(): name = "_" + name + if prefix: + name = name[1:] + if name and name[0].isdigit(): + name = "_" + name name = re.sub(r"[^a-zA-Z0-9_]", "_", name) return prefix + name + SCL_SUFFIX = "_sympy_processed" GROUPED_COMMENT = "// Logic included in grouped IF" -# --- Función Principal de Generación SCL --- +# para formatear valores iniciales +def format_scl_start_value(value, datatype): + """Formatea un valor para la inicialización SCL según el tipo.""" + if value is None: + return None + datatype_lower = datatype.lower() if datatype else "" + value_str = str(value) + if "bool" in datatype_lower: + return "TRUE" if value_str.lower() == "true" else "FALSE" + elif "string" in datatype_lower: + escaped_value = value_str.replace("'", "''") + if escaped_value.startswith("'") and escaped_value.endswith("'"): + escaped_value = escaped_value[1:-1] + return f"'{escaped_value}'" + elif "char" in datatype_lower: # Añadido Char + escaped_value = value_str.replace("'", "''") + if escaped_value.startswith("'") and escaped_value.endswith("'"): + escaped_value = escaped_value[1:-1] + return f"'{escaped_value}'" + elif any( + t in datatype_lower + for t in [ + "int", + "byte", + "word", + "dint", + "dword", + "lint", + "lword", + "sint", + "usint", + "uint", + "udint", + "ulint", + ] + ): # Ampliado + try: + return str(int(value_str)) + except ValueError: + if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str): + return value_str + return f"'{value_str}'" # O como string si no es entero ni símbolo + elif "real" in datatype_lower or "lreal" in datatype_lower: + try: + f_val = float(value_str) + s_val = str(f_val) + if "." not in s_val and "e" not in s_val.lower(): + s_val += ".0" + return s_val + except ValueError: + if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str): + return value_str + return f"'{value_str}'" + elif "time" in datatype_lower: # Añadido Time, S5Time, LTime + # Quitar T#, LT#, S5T# si existen + prefix = "" + if value_str.upper().startswith("T#"): + prefix = "T#" + value_str = value_str[2:] + elif value_str.upper().startswith("LT#"): + prefix = "LT#" + value_str = value_str[3:] + elif value_str.upper().startswith("S5T#"): + prefix = "S5T#" + value_str = value_str[4:] + # Devolver con el prefijo correcto o T# por defecto si no había + if prefix: + return f"{prefix}{value_str}" + elif "s5time" in datatype_lower: + return f"S5T#{value_str}" + elif "ltime" in datatype_lower: + return f"LT#{value_str}" + else: + return f"T#{value_str}" # Default a TIME + elif "date" in datatype_lower: # Añadido Date, DT, TOD + if value_str.upper().startswith("D#"): + return value_str + elif "dt" in datatype_lower or "date_and_time" in datatype_lower: + if value_str.upper().startswith("DT#"): + return value_str + else: + return f"DT#{value_str}" # Añadir prefijo DT# + elif "tod" in datatype_lower or "time_of_day" in datatype_lower: + if value_str.upper().startswith("TOD#"): + return value_str + else: + return f"TOD#{value_str}" # Añadir prefijo TOD# + else: + return f"D#{value_str}" # Default a Date + # Fallback genérico + else: + if re.match( + r'^[a-zA-Z_][a-zA-Z0-9_."#\[\]]+$', value_str + ): # Permitir más caracteres en símbolos/tipos + # Si es un UDT o Struct complejo, podría venir con comillas, quitarlas + if value_str.startswith('"') and value_str.endswith('"'): + return value_str[1:-1] + return value_str + else: + escaped_value = value_str.replace("'", "''") + return f"'{escaped_value}'" + + +# --- NUEVA FUNCIÓN RECURSIVA para generar declaraciones SCL (VAR/STRUCT/ARRAY) --- +def generate_scl_declarations(variables, indent_level=1): + """Genera las líneas SCL para declarar variables, structs y arrays.""" + scl_lines = [] + indent = " " * indent_level + for var in variables: + var_name_scl = format_variable_name(var.get("name")) + var_dtype_raw = var.get("datatype", "VARIANT") + # Limpiar comillas de tipos de datos UDT ("MyType" -> MyType) + var_dtype = ( + var_dtype_raw.strip('"') + if var_dtype_raw.startswith('"') and var_dtype_raw.endswith('"') + else var_dtype_raw + ) + + var_comment = var.get("comment") + start_value = var.get("start_value") + children = var.get("children") # Para structs + array_elements = var.get("array_elements") # Para arrays + + # Manejar tipos de datos Array especiales + array_match = re.match(r"(Array\[.*\]\s+of\s+)(.*)", var_dtype, re.IGNORECASE) + base_type_for_init = var_dtype + declaration_dtype = var_dtype + if array_match: + array_prefix = array_match.group(1) + base_type_raw = array_match.group(2).strip() + # Limpiar comillas del tipo base del array + base_type_for_init = ( + base_type_raw.strip('"') + if base_type_raw.startswith('"') and base_type_raw.endswith('"') + else base_type_raw + ) + declaration_dtype = ( + f'{array_prefix}"{base_type_for_init}"' + if '"' not in base_type_raw + else f"{array_prefix}{base_type_raw}" + ) # Reconstruir con comillas si es UDT + + # Reconstruir declaración con comillas si es UDT y no array + elif ( + not array_match and var_dtype != base_type_for_init + ): # Es un tipo que necesita comillas (UDT) + declaration_dtype = f'"{var_dtype}"' + + declaration_line = f"{indent}{var_name_scl} : {declaration_dtype}" + init_value = None + + # ---- Arrays ---- + if array_elements: + # Ordenar índices (asumiendo que son numéricos) + try: + sorted_indices = sorted(array_elements.keys(), key=int) + except ValueError: + sorted_indices = sorted( + array_elements.keys() + ) # Fallback a orden alfabético + + init_values = [ + format_scl_start_value(array_elements[idx], base_type_for_init) + for idx in sorted_indices + ] + valid_inits = [v for v in init_values if v is not None] + if valid_inits: + init_value = f"[{', '.join(valid_inits)}]" + + # ---- Structs ---- + elif children: + # No añadir comentario // Struct aquí, es redundante + scl_lines.append(declaration_line) # Añadir línea de declaración base + scl_lines.append(f"{indent}STRUCT") + scl_lines.extend(generate_scl_declarations(children, indent_level + 1)) + scl_lines.append(f"{indent}END_STRUCT;") + if var_comment: + scl_lines.append(f"{indent}// {var_comment}") + scl_lines.append("") # Línea extra + continue # Saltar resto para Struct + + # ---- Tipos Simples ---- + else: + if start_value is not None: + init_value = format_scl_start_value(start_value, var_dtype) + + # Añadir inicialización si existe + if init_value: + declaration_line += f" := {init_value}" + declaration_line += ";" + if var_comment: + declaration_line += f" // {var_comment}" + scl_lines.append(declaration_line) + + return scl_lines + + +# --- Función Principal de Generación SCL --- def generate_scl(processed_json_filepath, output_scl_filepath): - """Genera un archivo SCL a partir del JSON procesado por x2_process (versión SymPy).""" + """Genera un archivo SCL a partir del JSON procesado (FC/FB o DB).""" if not os.path.exists(processed_json_filepath): - print(f"Error: Archivo JSON procesado no encontrado en '{processed_json_filepath}'") + print( + f"Error: Archivo JSON procesado no encontrado en '{processed_json_filepath}'" + ) return print(f"Cargando JSON procesado desde: {processed_json_filepath}") try: - with open(processed_json_filepath, 'r', encoding='utf-8') as f: + with open(processed_json_filepath, "r", encoding="utf-8") as f: data = json.load(f) except Exception as e: print(f"Error al cargar o parsear JSON: {e}") traceback.print_exc() return - # --- Extracción de Información del Bloque --- - block_name = data.get('block_name', 'UnknownBlock') - block_number = data.get('block_number') - block_lang_original = data.get('language', 'LAD') # Lenguaje original - # Determinar tipo de bloque SCL (Asumir FB si no se especifica) - # Idealmente, x1_to_json.py guardaría esto en data['block_type_scl'] = 'FC' o 'FB' - block_type_scl = data.get('block_type_scl', 'FUNCTION_BLOCK') - block_comment = data.get('block_comment', '') - - # Usar format_variable_name para el nombre del bloque en SCL - scl_block_name = format_variable_name(block_name) - print(f"Generando SCL para {block_type_scl}: {scl_block_name} (Original: {block_name})") - - # --- Identificación de Variables Temporales y Estáticas --- - # La detección basada en regex sobre el SCL final debería seguir funcionando - temp_vars = set() - stat_vars = set() - # Regex mejorado para capturar variables temporales que empiezan con # o _temp_ - # y estáticas (si usas un prefijo como 'stat_' o para bits de memoria de flanco) - temp_pattern = re.compile(r'"?#(_temp_[a-zA-Z0-9_]+)"?|"?(_temp_[a-zA-Z0-9_]+)"?') # Captura con o sin # - stat_pattern = re.compile(r'"?(stat_[a-zA-Z0-9_]+)"?') # Para memorias de flanco si usan prefijo 'stat_' - - edge_memory_bits = set() # Para detectar bits de memoria de flanco por nombre - - for network in data.get('networks', []): - for instruction in network.get('logic', []): - scl_code = instruction.get('scl', '') - # Buscar también en _edge_mem_update_scl si existe - edge_update_code = instruction.get('_edge_mem_update_scl','') - code_to_scan = (scl_code if scl_code else '') + '\n' + (edge_update_code if edge_update_code else '') - - if code_to_scan: - # Buscar #_temp_... o _temp_... - found_temps = temp_pattern.findall(code_to_scan) - for temp_tuple in found_temps: - # findall devuelve tuplas por los grupos de captura, tomar el no vacío - temp_name = next((t for t in temp_tuple if t), None) - if temp_name: - temp_vars.add("#"+temp_name if not temp_name.startswith("#") else temp_name) # Asegurar que empiece con # - - # Buscar estáticas (ej: stat_...) - found_stats = stat_pattern.findall(code_to_scan) - stat_vars.update(found_stats) - - # Identificar explícitamente bits de memoria usados por PBox/NBox - # Asumiendo que el nombre se guarda en el JSON (requiere ajuste en x1/x2) - # if instruction.get("type","").startswith(("PBox", "NBox")): - # mem_bit_info = instruction.get("inputs", {}).get("bit") - # if mem_bit_info and mem_bit_info.get("type") == "variable": - # edge_memory_bits.add(format_variable_name(mem_bit_info.get("name"))) - - - print(f"Variables temporales (#_temp_...) detectadas: {len(temp_vars)}") - # Si se detectan memorias de flanco, añadirlas a stat_vars si no tienen prefijo 'stat_' - # stat_vars.update(edge_memory_bits - stat_vars) # Añadir solo las nuevas - print(f"Variables estáticas (stat_...) detectadas: {len(stat_vars)}") - - # --- Construcción del String SCL --- + # --- Extracción de Información del Bloque (Común) --- + block_name = data.get("block_name", "UnknownBlock") + block_number = data.get("block_number") + block_lang_original = data.get("language", "Unknown") # Será "DB" para Data Blocks + block_type = data.get("block_type", "Unknown") # FC, FB, GlobalDB + block_comment = data.get("block_comment", "") + scl_block_name = format_variable_name(block_name) # Nombre SCL seguro + print( + f"Generando SCL para: {block_type} '{scl_block_name}' (Original: {block_name}, Lang: {block_lang_original})" + ) scl_output = [] - # Cabecera del Bloque - scl_output.append(f"// Block Name (Original): {block_name}") - if block_number: scl_output.append(f"// Block Number: {block_number}") - scl_output.append(f"// Original Language: {block_lang_original}") - if block_comment: scl_output.append(f"// Block Comment: {block_comment}") - scl_output.append("") - scl_output.append(f"{block_type_scl} \"{scl_block_name}\"") - scl_output.append("{ S7_Optimized_Access := 'TRUE' }") - scl_output.append("VERSION : 0.1") - scl_output.append("") + # --- GENERACIÓN PARA DATA BLOCK (DB) --- + if block_lang_original == "DB": + print("Modo de generación: DATA_BLOCK") + scl_output.append(f"// Block Type: {block_type}") + scl_output.append(f"// Block Name (Original): {block_name}") + if block_number: + scl_output.append(f"// Block Number: {block_number}") + if block_comment: + scl_output.append(f"// Block Comment: {block_comment}") + scl_output.append("") + scl_output.append(f'DATA_BLOCK "{scl_block_name}"') + scl_output.append("{ S7_Optimized_Access := 'TRUE' }") # Asumir optimizado + scl_output.append("VERSION : 0.1") + scl_output.append("") + interface_data = data.get("interface", {}) + static_vars = interface_data.get("Static", []) + if static_vars: + scl_output.append("VAR") + scl_output.extend(generate_scl_declarations(static_vars, indent_level=1)) + scl_output.append("END_VAR") + scl_output.append("") + else: + print( + "Advertencia: No se encontró sección 'Static' o está vacía en la interfaz del DB." + ) + scl_output.append("VAR") + scl_output.append("END_VAR") + scl_output.append("") + scl_output.append("BEGIN") + scl_output.append("") + scl_output.append("END_DATA_BLOCK") - # Declaraciones de Interfaz (Implementación básica) - interface_sections = ["Input", "Output", "InOut", "Static", "Temp", "Constant", "Return"] - interface_data = data.get('interface', {}) + # --- GENERACIÓN PARA FUNCTION BLOCK / FUNCTION (FC/FB) --- + else: + print("Modo de generación: FUNCTION_BLOCK / FUNCTION") + scl_block_keyword = "FUNCTION_BLOCK" if block_type == "FB" else "FUNCTION" + # Cabecera del Bloque + scl_output.append(f"// Block Type: {block_type}") + scl_output.append(f"// Block Name (Original): {block_name}") + if block_number: + scl_output.append(f"// Block Number: {block_number}") + scl_output.append(f"// Original Language: {block_lang_original}") + if block_comment: + scl_output.append(f"// Block Comment: {block_comment}") + scl_output.append("") + # Manejar tipo de retorno para FUNCTION + return_type = "Void" # Default + interface_data = data.get("interface", {}) + if scl_block_keyword == "FUNCTION" and interface_data.get("Return"): + return_member = interface_data["Return"][ + 0 + ] # Asumir un solo valor de retorno + return_type_raw = return_member.get("datatype", "Void") + return_type = ( + return_type_raw.strip('"') + if return_type_raw.startswith('"') and return_type_raw.endswith('"') + else return_type_raw + ) + # Añadir comillas si es UDT + if return_type != return_type_raw: + return_type = f'"{return_type}"' - for section_name in interface_sections: - scl_section_name = section_name - # Ajustar nombres de sección para SCL (Static -> STAT, Temp -> TEMP) - if section_name == "Static": scl_section_name = "STAT" - if section_name == "Temp": scl_section_name = "TEMP" # Usar VAR_TEMP para variables #temp - - vars_in_section = interface_data.get(section_name, []) - # No declarar VAR_TEMP aquí, se hará después con las detectadas/originales - if section_name == "Temp": continue - - # No declarar VAR_STAT aquí si ya lo hacemos abajo con las detectadas - if section_name == "Static" and stat_vars: continue - - - if vars_in_section or (section_name == "Static" and stat_vars): # Incluir STAT si hay detectadas - # Usar VAR para Input/Output/InOut/Constant/Return - var_keyword = "VAR" if section_name != "Static" else "VAR_STAT" - scl_output.append(f"{var_keyword}_{section_name.upper()}") - - for var in vars_in_section: - var_name = var.get('name') - var_dtype = var.get('datatype', 'VARIANT') # Default a VARIANT - if var_name: - # Usar format_variable_name CORRECTO - scl_name = format_variable_name(var_name) - scl_output.append(f" {scl_name} : {var_dtype};") - - # Declarar stat_vars detectadas si esta es la sección STAT - if section_name == "Static" and stat_vars: - for var_name in sorted(list(stat_vars)): - # Asumir Bool para stat_, podría necesitar inferencia - scl_output.append(f" {format_variable_name(var_name)} : Bool; // Auto-detected STAT") - - scl_output.append("END_VAR") - scl_output.append("") - - # Declaraciones Estáticas (Si no estaban en la interfaz y se detectaron) - # Esto es redundante si la sección VAR_STAT ya se generó arriba - # if stat_vars and not interface_data.get("Static"): - # scl_output.append("VAR_STAT") - # for var_name in sorted(list(stat_vars)): - # scl_output.append(f" {format_variable_name(var_name)} : Bool; // Auto-detected STAT") - # scl_output.append("END_VAR") - # scl_output.append("") - - - # Declaraciones Temporales (Interfaz Temp + _temp_ detectadas) - scl_output.append("VAR_TEMP") - declared_temps = set() - interface_temps = interface_data.get('Temp', []) - if interface_temps: - for var in interface_temps: - var_name = var.get('name') - var_dtype = var.get('datatype', 'VARIANT') - if var_name: - scl_name = format_variable_name(var_name) - scl_output.append(f" {scl_name} : {var_dtype};") - declared_temps.add(scl_name) # Marcar como declarada - - # Declarar las _temp_ generadas si no estaban ya en la interfaz Temp - if temp_vars: - for var_name in sorted(list(temp_vars)): - scl_name = format_variable_name(var_name) # #_temp_... - if scl_name not in declared_temps: - # Inferencia básica de tipo - inferred_type = "Bool" # Asumir Bool para la mayoría de temps de lógica - # Se podría mejorar si los procesadores añadieran info de tipo - scl_output.append(f" {scl_name} : {inferred_type}; // Auto-generated temporary") - declared_temps.add(scl_name) - scl_output.append("END_VAR") - scl_output.append("") - - # Cuerpo del Bloque - scl_output.append("BEGIN") - scl_output.append("") - - # Iterar por redes y lógica - for i, network in enumerate(data.get('networks', [])): - network_title = network.get('title', f'Network {network.get("id")}') - network_comment = network.get('comment', '') - network_lang = network.get('language', 'LAD') # O el lenguaje original - - scl_output.append(f" // Network {i+1}: {network_title} (Original Language: {network_lang})") - if network_comment: - for line in network_comment.splitlines(): - scl_output.append(f" // {line}") + scl_output.append( + f'{scl_block_keyword} "{scl_block_name}" : {return_type}' + if scl_block_keyword == "FUNCTION" + else f'{scl_block_keyword} "{scl_block_name}"' + ) + scl_output.append("{ S7_Optimized_Access := 'TRUE' }") + scl_output.append("VERSION : 0.1") scl_output.append("") - network_has_code = False + # Declaraciones de Interfaz FC/FB + section_order = [ + "Input", + "Output", + "InOut", + "Static", + "Temp", + "Constant", + ] # Return ya está en cabecera + declared_temps = set() + for section_name in section_order: + vars_in_section = interface_data.get(section_name, []) + if vars_in_section: + scl_section_keyword = f"VAR_{section_name.upper()}" + if section_name == "Static": + scl_section_keyword = "VAR_STAT" + if section_name == "Temp": + scl_section_keyword = "VAR_TEMP" + if section_name == "Constant": + scl_section_keyword = "CONSTANT" + scl_output.append(scl_section_keyword) + scl_output.extend( + generate_scl_declarations(vars_in_section, indent_level=1) + ) + if section_name == "Temp": + declared_temps.update( + format_variable_name(v.get("name")) + for v in vars_in_section + if v.get("name") + ) + scl_output.append("END_VAR") + scl_output.append("") - # --- NUEVO MANEJO STL con formato Markdown --- - if network_lang == "STL": - network_has_code = True # Marcar que la red tiene contenido - if network.get('logic') and isinstance(network['logic'], list) and len(network['logic']) > 0: - stl_chunk = network['logic'][0] - if stl_chunk.get("type") == "RAW_STL_CHUNK" and "stl" in stl_chunk: - raw_stl_code = stl_chunk["stl"] - # Añadir marcador de inicio (como comentario SCL para evitar errores) - scl_output.append(f" {'//'} ```STL") # Doble '//' para asegurar que sea comentario - # Escribir el código STL crudo, indentado + # Declaraciones VAR_TEMP adicionales detectadas + temp_vars = set() + temp_pattern = re.compile( + r'"?#(_temp_[a-zA-Z0-9_]+)"?|"?(_temp_[a-zA-Z0-9_]+)"?' + ) + for network in data.get("networks", []): + for instruction in network.get("logic", []): + scl_code = instruction.get("scl", "") + edge_update_code = instruction.get("_edge_mem_update_scl", "") + code_to_scan = ( + (scl_code if scl_code else "") + + "\n" + + (edge_update_code if edge_update_code else "") + ) + if code_to_scan: + found_temps = temp_pattern.findall(code_to_scan) + for temp_tuple in found_temps: + temp_name = next((t for t in temp_tuple if t), None) + if temp_name: + temp_vars.add( + "#" + temp_name + if not temp_name.startswith("#") + else temp_name + ) + additional_temps = sorted(list(temp_vars - declared_temps)) + if additional_temps: + if not interface_data.get("Temp"): + scl_output.append("VAR_TEMP") + for var_name in additional_temps: + scl_name = format_variable_name(var_name) + inferred_type = "Bool" # Asumir Bool + scl_output.append( + f" {scl_name} : {inferred_type}; // Auto-generated temporary" + ) + if not interface_data.get("Temp"): + scl_output.append("END_VAR") + scl_output.append("") + + # Cuerpo del Bloque FC/FB + scl_output.append("BEGIN") + scl_output.append("") + # Iterar por redes y lógica (como antes, incluyendo manejo STL Markdown) + for i, network in enumerate(data.get("networks", [])): + network_title = network.get("title", f'Network {network.get("id")}') + network_comment = network.get("comment", "") + network_lang = network.get("language", "LAD") + scl_output.append( + f" // Network {i+1}: {network_title} (Original Language: {network_lang})" + ) + if network_comment: + for line in network_comment.splitlines(): + scl_output.append(f" // {line}") + scl_output.append("") + network_has_code = False + if network_lang == "STL": + network_has_code = True + if ( + network.get("logic") + and network["logic"][0].get("type") == "RAW_STL_CHUNK" + ): + raw_stl_code = network["logic"][0].get( + "stl", "// ERROR: STL code missing" + ) + scl_output.append(f" {'//'} ```STL") for stl_line in raw_stl_code.splitlines(): - # Añadir indentación estándar de SCL - scl_output.append(f" {stl_line}") # <-- STL sin comentar - # Añadir marcador de fin (como comentario SCL) + scl_output.append(f" {stl_line}") scl_output.append(f" {'//'} ```") else: - scl_output.append(" // ERROR: Contenido STL inesperado en JSON.") - else: - scl_output.append(" // ERROR: No se encontró lógica STL en JSON para esta red.") - scl_output.append("") # Línea en blanco después de la red STL - # --- FIN NUEVO MANEJO STL con formato Markdown --- - else: - - # Iterar sobre la 'logica' de la red - for instruction in network.get('logic', []): - instruction_type = instruction.get("type", "") - scl_code = instruction.get('scl', "") # Obtener SCL generado por x2 - - # Saltar instrucciones agrupadas - if instruction.get("grouped", False): - continue - - # Escribir SCL si es un tipo procesado y tiene código relevante - # (Ignorar comentarios de depuración de SymPy) - if instruction_type.endswith(SCL_SUFFIX) and scl_code: - is_internal_sympy_comment_only = scl_code.strip().startswith("// SymPy") or \ - scl_code.strip().startswith("// PBox SymPy processed") or \ - scl_code.strip().startswith("// NBox SymPy processed") - # O podría ser más genérico: ignorar cualquier línea que solo sea comentario SCL - is_only_comment = all(line.strip().startswith("//") for line in scl_code.splitlines()) - - - # Escribir solo si NO es un comentario interno de SymPy O si es un bloque IF (que sí debe escribirse) - if not is_only_comment or scl_code.strip().startswith("IF"): - network_has_code = True - for line in scl_code.splitlines(): - # Añadir indentación estándar - scl_output.append(f" {line}") - - # Incluir también tipos especiales directamente - elif instruction_type in ["RAW_SCL_CHUNK", "UNSUPPORTED_LANG"] and scl_code: - network_has_code = True - for line in scl_code.splitlines(): - scl_output.append(f" {line}") # Indentar - - # Podríamos añadir comentarios para errores si se desea - # elif "_error" in instruction_type: - # network_has_code = True - # scl_output.append(f" // ERROR processing instruction UID {instruction.get('instruction_uid')}: {instruction.get('scl', 'No details')}") - - + scl_output.append(" // ERROR: Contenido STL inesperado.") + else: # LAD, FBD, SCL, etc. + for instruction in network.get("logic", []): + instruction_type = instruction.get("type", "") + scl_code = instruction.get("scl", "") + is_grouped = instruction.get("grouped", False) + if is_grouped: + continue + if ( + instruction_type.endswith(SCL_SUFFIX) + or instruction_type in ["RAW_SCL_CHUNK", "UNSUPPORTED_LANG"] + ) and scl_code: + is_only_comment = all( + line.strip().startswith("//") + for line in scl_code.splitlines() + if line.strip() + ) + is_if_block = scl_code.strip().startswith("IF") + if not is_only_comment or is_if_block: + network_has_code = True + for line in scl_code.splitlines(): + scl_output.append(f" {line}") if network_has_code: - scl_output.append("") # Línea en blanco después del código de la red + scl_output.append("") else: scl_output.append(f" // Network did not produce printable SCL code.") scl_output.append("") + # Fin del bloque FC/FB + scl_output.append(f"END_{scl_block_keyword}") - # Fin del bloque - scl_output.append("END_FUNCTION_BLOCK") # O END_FUNCTION si es FC - - # --- Escritura del Archivo SCL --- + # --- Escritura del Archivo SCL (Común) --- print(f"Escribiendo archivo SCL en: {output_scl_filepath}") try: - with open(output_scl_filepath, 'w', encoding='utf-8') as f: + with open(output_scl_filepath, "w", encoding="utf-8") as f: for line in scl_output: - f.write(line + '\n') + f.write(line + "\n") print("Generación de SCL completada.") except Exception as e: print(f"Error al escribir el archivo SCL: {e}") @@ -301,47 +492,61 @@ if __name__ == "__main__": import argparse import os import sys - import traceback # Asegurarse que traceback está importado si se usa en generate_scl + import traceback # Asegurarse que traceback está importado si se usa en generate_scl # Configurar ArgumentParser para recibir la ruta del XML original obligatoria parser = argparse.ArgumentParser( description="Generate final SCL file from processed JSON (_simplified_processed.json). Expects original XML filepath as argument." ) parser.add_argument( - "source_xml_filepath", # Argumento posicional obligatorio + "source_xml_filepath", # Argumento posicional obligatorio help="Path to the original source XML file (passed from x0_main.py, used to derive input/output names).", ) - args = parser.parse_args() # Parsea los argumentos de sys.argv + args = parser.parse_args() # Parsea los argumentos de sys.argv - source_xml_file = args.source_xml_filepath # Obtiene la ruta del XML original + source_xml_file = args.source_xml_filepath # Obtiene la ruta del XML original # Verificar si el archivo XML original existe (como referencia) if not os.path.exists(source_xml_file): - print(f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON procesado.") - # No salir necesariamente. + print( + f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON procesado." + ) + # No salir necesariamente. # Derivar nombres de archivos de entrada (JSON procesado) y salida (SCL) xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0] # Asumir que los archivos están en el mismo directorio que el XML original - base_dir = os.path.dirname(source_xml_file) # Directorio del XML original + base_dir = os.path.dirname(source_xml_file) # Directorio del XML original - input_json_file = os.path.join(base_dir, f"{xml_filename_base}_simplified_processed.json") - output_scl_file = os.path.join(base_dir, f"{xml_filename_base}_simplified_processed.scl") + input_json_file = os.path.join( + base_dir, f"{xml_filename_base}_simplified_processed.json" + ) + output_scl_file = os.path.join( + base_dir, f"{xml_filename_base}_simplified_processed.scl" + ) - print(f"(x3) Generando SCL: '{os.path.relpath(input_json_file)}' -> '{os.path.relpath(output_scl_file)}'") + print( + f"(x3) Generando SCL: '{os.path.relpath(input_json_file)}' -> '{os.path.relpath(output_scl_file)}'" + ) # Verificar si el archivo JSON procesado de entrada EXISTE if not os.path.exists(input_json_file): - print(f"Error Fatal (x3): Archivo JSON procesado no encontrado: '{input_json_file}'") - print(f"Asegúrate de que 'x2_process.py' se ejecutó correctamente para '{os.path.relpath(source_xml_file)}'.") - sys.exit(1) # Salir si el archivo necesario no está + print( + f"Error Fatal (x3): Archivo JSON procesado no encontrado: '{input_json_file}'" + ) + print( + f"Asegúrate de que 'x2_process.py' se ejecutó correctamente para '{os.path.relpath(source_xml_file)}'." + ) + sys.exit(1) # Salir si el archivo necesario no está else: # Llamar a la función principal de generación SCL del script # Asumiendo que tu función principal se llama generate_scl(input_json_path, output_scl_path) try: generate_scl(input_json_file, output_scl_file) except Exception as e: - print(f"Error Crítico (x3) durante la generación de SCL desde '{input_json_file}': {e}") + print( + f"Error Crítico (x3) durante la generación de SCL desde '{input_json_file}': {e}" + ) # traceback ya debería estar importado si generate_scl lo necesita traceback.print_exc() - sys.exit(1) # Salir con error si la función principal falla \ No newline at end of file + sys.exit(1) # Salir con error si la función principal falla diff --git a/log.txt b/log.txt new file mode 100644 index 0000000000000000000000000000000000000000..0acaee1f41f400a5e99b9e1bf2388c1f36092e43 GIT binary patch literal 1933740 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 literal 0 HcmV?d00001 diff --git a/paste.py b/paste.py index 3410e82..e69de29 100644 --- a/paste.py +++ b/paste.py @@ -1,273 +0,0 @@ -# x3_generate_scl.py -# -*- coding: utf-8 -*- -import json -import os -import re -import argparse -import sys -import traceback - -# --- Importar Utilidades (mantener como estaba) --- -try: - from processors.processor_utils import format_variable_name - SCL_SUFFIX = "_sympy_processed" - GROUPED_COMMENT = "// Logic included in grouped IF" -except ImportError: - print("Advertencia: No se pudo importar 'format_variable_name'. Usando fallback.") - def format_variable_name(name): # Fallback BÁSICO - if not name: return "_INVALID_NAME_" - if name.startswith('"') and name.endswith('"'): return name - prefix = "#" if name.startswith("#") else "" - if prefix: name = name[1:] - if name and name[0].isdigit(): name = "_" + name - name = re.sub(r"[^a-zA-Z0-9_]", "_", name) - return prefix + name - SCL_SUFFIX = "_sympy_processed" - GROUPED_COMMENT = "// Logic included in grouped IF" - -# --- NUEVA FUNCIÓN para formatear valores iniciales SCL --- -def format_scl_start_value(value, datatype): - """Formatea un valor para la inicialización SCL según el tipo.""" - if value is None: return None - datatype_lower = datatype.lower() if datatype else "" - value_str = str(value) - - if "bool" in datatype_lower: - return "TRUE" if value_str.lower() == 'true' else "FALSE" - elif "string" in datatype_lower: - escaped_value = value_str.replace("'", "''") - if escaped_value.startswith("'") and escaped_value.endswith("'"): escaped_value = escaped_value[1:-1] - return f"'{escaped_value}'" - elif "char" in datatype_lower: # Añadido Char - escaped_value = value_str.replace("'", "''") - if escaped_value.startswith("'") and escaped_value.endswith("'"): escaped_value = escaped_value[1:-1] - return f"'{escaped_value}'" - elif any(t in datatype_lower for t in ["int", "byte", "word", "dint", "dword", "lint", "lword", "sint", "usint", "uint", "udint", "ulint"]): # Ampliado - try: return str(int(value_str)) - except ValueError: - if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str): return value_str - return f"'{value_str}'" # O como string si no es entero ni símbolo - elif "real" in datatype_lower or "lreal" in datatype_lower: - try: - f_val = float(value_str); s_val = str(f_val) - if '.' not in s_val and 'e' not in s_val.lower(): s_val += ".0" - return s_val - except ValueError: - if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str): return value_str - return f"'{value_str}'" - elif "time" in datatype_lower: # Añadido Time, S5Time, LTime - # Quitar T#, LT#, S5T# si existen - prefix = "" - if value_str.upper().startswith("T#"): prefix="T#"; value_str = value_str[2:] - elif value_str.upper().startswith("LT#"): prefix="LT#"; value_str = value_str[3:] - elif value_str.upper().startswith("S5T#"): prefix="S5T#"; value_str = value_str[4:] - # Devolver con el prefijo correcto o T# por defecto si no había - if prefix: return f"{prefix}{value_str}" - elif "s5time" in datatype_lower: return f"S5T#{value_str}" - elif "ltime" in datatype_lower: return f"LT#{value_str}" - else: return f"T#{value_str}" # Default a TIME - elif "date" in datatype_lower: # Añadido Date, DT, TOD - if value_str.upper().startswith("D#"): return value_str - elif "dt" in datatype_lower or "date_and_time" in datatype_lower: - if value_str.upper().startswith("DT#"): return value_str - else: return f"DT#{value_str}" # Añadir prefijo DT# - elif "tod" in datatype_lower or "time_of_day" in datatype_lower: - if value_str.upper().startswith("TOD#"): return value_str - else: return f"TOD#{value_str}" # Añadir prefijo TOD# - else: return f"D#{value_str}" # Default a Date - # Fallback genérico - else: - if re.match(r'^[a-zA-Z_][a-zA-Z0-9_."#\[\]]+$', value_str): # Permitir más caracteres en símbolos/tipos - # Si es un UDT o Struct complejo, podría venir con comillas, quitarlas - if value_str.startswith('"') and value_str.endswith('"'): - return value_str[1:-1] - return value_str - else: - escaped_value = value_str.replace("'", "''") - return f"'{escaped_value}'" - -# --- NUEVA FUNCIÓN RECURSIVA para generar declaraciones SCL (VAR/STRUCT/ARRAY) --- - -# --- Función Principal de Generación SCL (MODIFICADA) --- -def generate_scl(processed_json_filepath, output_scl_filepath): - """Genera un archivo SCL a partir del JSON procesado (FC/FB o DB).""" - - if not os.path.exists(processed_json_filepath): - print(f"Error: Archivo JSON procesado no encontrado en '{processed_json_filepath}'") - return - - print(f"Cargando JSON procesado desde: {processed_json_filepath}") - try: - with open(processed_json_filepath, 'r', encoding='utf-8') as f: - data = json.load(f) - except Exception as e: - print(f"Error al cargar o parsear JSON: {e}"); traceback.print_exc(); return - - # --- Extracción de Información del Bloque (Común) --- - block_name = data.get('block_name', 'UnknownBlock') - block_number = data.get('block_number') - block_lang_original = data.get('language', 'Unknown') # Será "DB" para Data Blocks - block_type = data.get('block_type', 'Unknown') # FC, FB, GlobalDB - block_comment = data.get('block_comment', '') - scl_block_name = format_variable_name(block_name) # Nombre SCL seguro - print(f"Generando SCL para: {block_type} '{scl_block_name}' (Original: {block_name}, Lang: {block_lang_original})") - scl_output = [] - - # --- GENERACIÓN PARA DATA BLOCK (DB) --- - if block_lang_original == "DB": - print("Modo de generación: DATA_BLOCK") - scl_output.append(f"// Block Type: {block_type}") - scl_output.append(f"// Block Name (Original): {block_name}") - if block_number: scl_output.append(f"// Block Number: {block_number}") - if block_comment: scl_output.append(f"// Block Comment: {block_comment}") - scl_output.append("") - scl_output.append(f"DATA_BLOCK \"{scl_block_name}\"") - scl_output.append("{ S7_Optimized_Access := 'TRUE' }") # Asumir optimizado - scl_output.append("VERSION : 0.1") - scl_output.append("") - interface_data = data.get('interface', {}) - static_vars = interface_data.get('Static', []) - if static_vars: - scl_output.append("VAR") - scl_output.extend(generate_scl_declarations(static_vars, indent_level=1)) - scl_output.append("END_VAR") - scl_output.append("") - else: - print("Advertencia: No se encontró sección 'Static' o está vacía en la interfaz del DB.") - scl_output.append("VAR"); scl_output.append("END_VAR"); scl_output.append("") - scl_output.append("BEGIN"); scl_output.append(""); scl_output.append("END_DATA_BLOCK") - - # --- GENERACIÓN PARA FUNCTION BLOCK / FUNCTION (FC/FB) --- - else: - print("Modo de generación: FUNCTION_BLOCK / FUNCTION") - scl_block_keyword = "FUNCTION_BLOCK" if block_type == "FB" else "FUNCTION" - # Cabecera del Bloque - scl_output.append(f"// Block Type: {block_type}") - scl_output.append(f"// Block Name (Original): {block_name}") - if block_number: scl_output.append(f"// Block Number: {block_number}") - scl_output.append(f"// Original Language: {block_lang_original}") - if block_comment: scl_output.append(f"// Block Comment: {block_comment}") - scl_output.append("") - # Manejar tipo de retorno para FUNCTION - return_type = "Void" # Default - interface_data = data.get('interface', {}) - if scl_block_keyword == "FUNCTION" and interface_data.get('Return'): - return_member = interface_data['Return'][0] # Asumir un solo valor de retorno - return_type_raw = return_member.get('datatype', 'Void') - return_type = return_type_raw.strip('"') if return_type_raw.startswith('"') and return_type_raw.endswith('"') else return_type_raw - # Añadir comillas si es UDT - if return_type != return_type_raw: return_type = f'"{return_type}"' - - scl_output.append(f"{scl_block_keyword} \"{scl_block_name}\" : {return_type}" if scl_block_keyword == "FUNCTION" else f"{scl_block_keyword} \"{scl_block_name}\"") - scl_output.append("{ S7_Optimized_Access := 'TRUE' }") - scl_output.append("VERSION : 0.1"); scl_output.append("") - - # Declaraciones de Interfaz FC/FB - section_order = ["Input", "Output", "InOut", "Static", "Temp", "Constant"] # Return ya está en cabecera - declared_temps = set() - for section_name in section_order: - vars_in_section = interface_data.get(section_name, []) - if vars_in_section: - scl_section_keyword = f"VAR_{section_name.upper()}" - if section_name == "Static": scl_section_keyword = "VAR_STAT" - if section_name == "Temp": scl_section_keyword = "VAR_TEMP" - if section_name == "Constant": scl_section_keyword = "CONSTANT" - scl_output.append(scl_section_keyword) - scl_output.extend(generate_scl_declarations(vars_in_section, indent_level=1)) - if section_name == "Temp": declared_temps.update(format_variable_name(v.get("name")) for v in vars_in_section if v.get("name")) - scl_output.append("END_VAR"); scl_output.append("") - - # Declaraciones VAR_TEMP adicionales detectadas - temp_vars = set() - temp_pattern = re.compile(r'"?#(_temp_[a-zA-Z0-9_]+)"?|"?(_temp_[a-zA-Z0-9_]+)"?') - for network in data.get('networks', []): - for instruction in network.get('logic', []): - scl_code = instruction.get('scl', ''); edge_update_code = instruction.get('_edge_mem_update_scl','') - code_to_scan = (scl_code if scl_code else '') + '\n' + (edge_update_code if edge_update_code else '') - if code_to_scan: - found_temps = temp_pattern.findall(code_to_scan) - for temp_tuple in found_temps: - temp_name = next((t for t in temp_tuple if t), None) - if temp_name: temp_vars.add("#"+temp_name if not temp_name.startswith("#") else temp_name) - additional_temps = sorted(list(temp_vars - declared_temps)) - if additional_temps: - if not interface_data.get("Temp"): scl_output.append("VAR_TEMP") - for var_name in additional_temps: - scl_name = format_variable_name(var_name); inferred_type = "Bool" # Asumir Bool - scl_output.append(f" {scl_name} : {inferred_type}; // Auto-generated temporary") - if not interface_data.get("Temp"): scl_output.append("END_VAR"); scl_output.append("") - - # Cuerpo del Bloque FC/FB - scl_output.append("BEGIN"); scl_output.append("") - # Iterar por redes y lógica (como antes, incluyendo manejo STL Markdown) - for i, network in enumerate(data.get('networks', [])): - network_title = network.get('title', f'Network {network.get("id")}') - network_comment = network.get('comment', '') - network_lang = network.get('language', 'LAD') - scl_output.append(f" // Network {i+1}: {network_title} (Original Language: {network_lang})") - if network_comment: - for line in network_comment.splitlines(): scl_output.append(f" // {line}") - scl_output.append("") - network_has_code = False - if network_lang == "STL": - network_has_code = True - if network.get('logic') and network['logic'][0].get("type") == "RAW_STL_CHUNK": - raw_stl_code = network['logic'][0].get("stl", "// ERROR: STL code missing") - scl_output.append(f" {'//'} ```STL") - for stl_line in raw_stl_code.splitlines(): scl_output.append(f" {stl_line}") - scl_output.append(f" {'//'} ```") - else: scl_output.append(" // ERROR: Contenido STL inesperado.") - else: # LAD, FBD, SCL, etc. - for instruction in network.get('logic', []): - instruction_type = instruction.get("type", ""); scl_code = instruction.get('scl', ''); is_grouped = instruction.get("grouped", False) - if is_grouped: continue - if (instruction_type.endswith(SCL_SUFFIX) or instruction_type in ["RAW_SCL_CHUNK", "UNSUPPORTED_LANG"]) and scl_code: - is_only_comment = all(line.strip().startswith("//") for line in scl_code.splitlines() if line.strip()) - is_if_block = scl_code.strip().startswith("IF") - if not is_only_comment or is_if_block: - network_has_code = True - for line in scl_code.splitlines(): scl_output.append(f" {line}") - if network_has_code: scl_output.append("") - else: scl_output.append(f" // Network did not produce printable SCL code."); scl_output.append("") - # Fin del bloque FC/FB - scl_output.append(f"END_{scl_block_keyword}") - - # --- Escritura del Archivo SCL (Común) --- - print(f"Escribiendo archivo SCL en: {output_scl_filepath}") - try: - with open(output_scl_filepath, 'w', encoding='utf-8') as f: - for line in scl_output: f.write(line + '\n') - print("Generación de SCL completada.") - except Exception as e: - print(f"Error al escribir el archivo SCL: {e}"); traceback.print_exc() - - -# --- Ejecución (sin cambios) --- -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Generate final SCL file from processed JSON (FC/FB/DB).") - parser.add_argument("source_xml_filepath", help="Path to the original source XML file.") - args = parser.parse_args() - source_xml_file = args.source_xml_filepath - - if not os.path.exists(source_xml_file): - print(f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON procesado.") - # Continuar, pero verificar existencia del JSON procesado - - xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0] - base_dir = os.path.dirname(source_xml_file) - input_json_file = os.path.join(base_dir, f"{xml_filename_base}_simplified_processed.json") - output_scl_file = os.path.join(base_dir, f"{xml_filename_base}_simplified_processed.scl") - - print(f"(x3) Generando SCL: '{os.path.relpath(input_json_file)}' -> '{os.path.relpath(output_scl_file)}'") - - if not os.path.exists(input_json_file): - print(f"Error Fatal (x3): Archivo JSON procesado no encontrado: '{input_json_file}'") - print(f"Asegúrate de que 'x1_to_json.py' y 'x2_process.py' se ejecutaron correctamente para '{os.path.relpath(source_xml_file)}'.") - sys.exit(1) - else: - try: - generate_scl(input_json_file, output_scl_file) - except Exception as e: - print(f"Error Crítico (x3) durante la generación de SCL desde '{input_json_file}': {e}") - traceback.print_exc() - sys.exit(1) \ No newline at end of file diff --git a/x0_main.py b/x0_main.py index 950a973..d8f081b 100644 --- a/x0_main.py +++ b/x0_main.py @@ -3,255 +3,163 @@ import subprocess import os import sys import locale -import glob # <--- Importar glob para buscar archivos +import glob - -# (Función get_console_encoding y variable CONSOLE_ENCODING como en la respuesta anterior) +# (Función get_console_encoding y variable CONSOLE_ENCODING como antes) def get_console_encoding(): """Obtiene la codificación preferida de la consola, con fallback.""" try: return locale.getpreferredencoding(False) except Exception: - return "cp1252" - + # Fallback común en Windows si falla getpreferredencoding + return "cp1252" # O prueba con 'utf-8' si cp1252 da problemas CONSOLE_ENCODING = get_console_encoding() -# Descomenta la siguiente línea si quieres ver la codificación detectada: # print(f"Detected console encoding: {CONSOLE_ENCODING}") - -# (Función run_script como en la respuesta anterior, usando CONSOLE_ENCODING) +# (Función run_script como antes, usando CONSOLE_ENCODING) def run_script(script_name, xml_arg): """Runs a given script with the specified XML file argument.""" - script_path = os.path.join(os.path.dirname(__file__), script_name) - command = [sys.executable, script_path, xml_arg] + # Asegurarse que la ruta al script sea absoluta o relativa al script actual + script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), script_name) + # Usar la ruta absoluta al ejecutable de Python actual + python_executable = sys.executable + command = [python_executable, script_path, xml_arg] # Usar la ruta absoluta de python print(f"\n--- Running {script_name} with argument: {xml_arg} ---") try: + # Ejecutar el proceso hijo result = subprocess.run( command, - check=True, - capture_output=True, - text=True, - encoding=CONSOLE_ENCODING, - errors="replace", - ) # 'replace' para evitar errores + check=True, # Lanza excepción si el script falla (return code != 0) + capture_output=True,# Captura stdout y stderr + text=True, # Decodifica stdout/stderr como texto + encoding=CONSOLE_ENCODING, # Usa la codificación detectada + errors='replace' # Reemplaza caracteres no decodificables + ) + + # Imprimir stdout y stderr si no están vacíos + stdout_clean = result.stdout.strip() if result.stdout else "" + stderr_clean = result.stderr.strip() if result.stderr else "" - # Imprimir stdout y stderr - # Eliminar saltos de línea extra al final si existen - stdout_clean = result.stdout.strip() - stderr_clean = result.stderr.strip() if stdout_clean: print(stdout_clean) if stderr_clean: - print("--- Stderr ---") - print(stderr_clean) - print("--------------") + # Imprimir stderr claramente para errores del script hijo + print(f"--- Stderr ({script_name}) ---", file=sys.stderr) # Imprimir en stderr + print(stderr_clean, file=sys.stderr) + print("--------------------------", file=sys.stderr) + print(f"--- {script_name} finished successfully ---") - return True + return True # Indicar éxito + except FileNotFoundError: - print(f"Error: Script '{script_path}' not found.") + # Error si el script python o el ejecutable no se encuentran + print(f"Error: Script '{script_path}' or Python executable '{python_executable}' not found.", file=sys.stderr) return False except subprocess.CalledProcessError as e: - print(f"Error running {script_name}:") - print(f"Return code: {e.returncode}") - stdout_decoded = ( - e.stdout.decode(CONSOLE_ENCODING, errors="replace").strip() - if isinstance(e.stdout, bytes) - else (e.stdout or "").strip() - ) - stderr_decoded = ( - e.stderr.decode(CONSOLE_ENCODING, errors="replace").strip() - if isinstance(e.stderr, bytes) - else (e.stderr or "").strip() - ) + # Error si el script hijo devuelve un código de error (ej., sys.exit(1)) + print(f"Error running {script_name}: Script returned non-zero exit code {e.returncode}.", file=sys.stderr) + + # Decodificar e imprimir stdout/stderr del proceso fallido + stdout_decoded = e.stdout.strip() if e.stdout else "" + stderr_decoded = e.stderr.strip() if e.stderr else "" + if stdout_decoded: - print("--- Stdout ---") - print(stdout_decoded) + print(f"--- Stdout ({script_name}) ---", file=sys.stderr) + print(stdout_decoded, file=sys.stderr) if stderr_decoded: - print("--- Stderr ---") - print(stderr_decoded) - print("--------------") - return False + print(f"--- Stderr ({script_name}) ---", file=sys.stderr) + print(stderr_decoded, file=sys.stderr) + print("--------------------------", file=sys.stderr) + return False # Indicar fallo except Exception as e: - print(f"An unexpected error occurred while running {script_name}: {e}") - return False + # Otros errores inesperados + print(f"An unexpected error occurred while running {script_name}: {e}", file=sys.stderr) + # Imprimir traceback para depuración + import traceback + traceback.print_exc(file=sys.stderr) + return False # Indicar fallo -# --- NUEVA FUNCIÓN PARA SELECCIONAR ARCHIVO --- -def select_xml_file(): - """Busca archivos .xml, los lista y pide al usuario que elija uno.""" - print("No XML file specified. Searching for XML files in current directory...") - # Buscar archivos .xml en el directorio actual (.) - xml_files = sorted(glob.glob("*.xml")) # sorted para orden alfabético - - if not xml_files: - print("Error: No .xml files found in the current directory.") - sys.exit(1) - - print("\nAvailable XML files:") - for i, filename in enumerate(xml_files, start=1): - print(f" {i}: {filename}") - - while True: - try: - choice = input( - f"Enter the number of the file to process (1-{len(xml_files)}): " - ) - choice_num = int(choice) - if 1 <= choice_num <= len(xml_files): - selected_file = xml_files[choice_num - 1] - print(f"Selected: {selected_file}") - return selected_file - else: - print("Invalid choice. Please enter a number from the list.") - except ValueError: - print("Invalid input. Please enter a number.") - except EOFError: # Manejar si la entrada se cierra inesperadamente - print("\nSelection cancelled.") - sys.exit(1) - - -# --- FIN NUEVA FUNCIÓN --- - +# --- NO SE NECESITA select_xml_file() si procesamos todos --- if __name__ == "__main__": - # Imports necesarios para esta sección - import os - import sys - import glob # Asegúrate de que glob esté importado al principio del archivo - + # --- PARTE 1: BUSCAR ARCHIVOS --- # Directorio base donde buscar los archivos XML (relativo al script) base_search_dir = "XML Project" - script_dir = os.path.dirname(__file__) # Directorio donde está x0_main.py + # Obtener la ruta absoluta del directorio donde está x0_main.py + script_dir = os.path.dirname(os.path.abspath(__file__)) xml_project_dir = os.path.join(script_dir, base_search_dir) print(f"Buscando archivos XML recursivamente en: '{xml_project_dir}'") # Verificar si el directorio 'XML Project' existe if not os.path.isdir(xml_project_dir): - print( - f"Error: El directorio '{xml_project_dir}' no existe o no es un directorio." - ) - print( - "Por favor, crea el directorio 'XML Project' en la misma carpeta que este script y coloca tus archivos XML dentro." - ) - sys.exit(1) + print(f"Error: El directorio '{xml_project_dir}' no existe o no es un directorio.", file=sys.stderr) + print("Por favor, crea el directorio 'XML Project' en la misma carpeta que este script y coloca tus archivos XML dentro.") + sys.exit(1) # Salir con error - # Buscar todos los archivos .xml recursivamente dentro de xml_project_dir - # Usamos os.path.join para construir la ruta de búsqueda correctamente - # y '**/*.xml' para la recursividad con glob + # Buscar todos los archivos .xml recursivamente search_pattern = os.path.join(xml_project_dir, "**", "*.xml") xml_files_found = glob.glob(search_pattern, recursive=True) if not xml_files_found: - print( - f"No se encontraron archivos XML en '{xml_project_dir}' o sus subdirectorios." - ) - sys.exit(0) # Salir limpiamente si no hay archivos + print(f"No se encontraron archivos XML en '{xml_project_dir}' o sus subdirectorios.") + sys.exit(0) # Salir limpiamente si no hay archivos print(f"Se encontraron {len(xml_files_found)} archivos XML para procesar:") - # Ordenar para un procesamiento predecible (opcional) - xml_files_found.sort() + xml_files_found.sort() # Ordenar para consistencia for xml_file in xml_files_found: - # Imprimir la ruta relativa desde el directorio del script para claridad print(f" - {os.path.relpath(xml_file, script_dir)}") - # Scripts a ejecutar en secuencia (asegúrate que los nombres son correctos) + # --- PARTE 2: PROCESAR CADA ARCHIVO --- + # Scripts a ejecutar en secuencia script1 = "x1_to_json.py" script2 = "x2_process.py" script3 = "x3_generate_scl.py" - # Procesar cada archivo encontrado processed_count = 0 failed_count = 0 - for xml_filepath in xml_files_found: - print( - f"\n--- Iniciando pipeline para: {os.path.relpath(xml_filepath, script_dir)} ---" - ) - # Usar la ruta absoluta para evitar problemas si los scripts cambian de directorio + # Procesar cada archivo encontrado en el bucle + for xml_filepath in xml_files_found: + relative_path = os.path.relpath(xml_filepath, script_dir) + print(f"\n--- Iniciando pipeline para: {relative_path} ---") + + # Usar la ruta absoluta para los scripts hijos absolute_xml_filepath = os.path.abspath(xml_filepath) - # Ejecutar los scripts en secuencia para el archivo actual - # La función run_script ya está definida en tu script x0_main.py + # Ejecutar los scripts en secuencia success = True if not run_script(script1, absolute_xml_filepath): - print( - f"\nPipeline falló en el script '{script1}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}" - ) + print(f"\nPipeline falló en el script '{script1}' para el archivo: {relative_path}", file=sys.stderr) success = False elif not run_script(script2, absolute_xml_filepath): - print( - f"\nPipeline falló en el script '{script2}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}" - ) + print(f"\nPipeline falló en el script '{script2}' para el archivo: {relative_path}", file=sys.stderr) success = False elif not run_script(script3, absolute_xml_filepath): - print( - f"\nPipeline falló en el script '{script3}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}" - ) + print(f"\nPipeline falló en el script '{script3}' para el archivo: {relative_path}", file=sys.stderr) success = False + # Actualizar contadores y mostrar estado if success: - print( - f"--- Pipeline completado exitosamente para: {os.path.relpath(xml_filepath, script_dir)} ---" - ) + print(f"--- Pipeline completado exitosamente para: {relative_path} ---") processed_count += 1 else: failed_count += 1 - print( - f"--- Pipeline falló para: {os.path.relpath(xml_filepath, script_dir)} ---" - ) + print(f"--- Pipeline falló para: {relative_path} ---", file=sys.stderr) # Indicar fallo + # --- PARTE 3: RESUMEN FINAL --- print("\n--- Resumen Final del Procesamiento ---") print(f"Total de archivos XML encontrados: {len(xml_files_found)}") - print( - f"Archivos procesados exitosamente por el pipeline completo: {processed_count}" - ) + print(f"Archivos procesados exitosamente por el pipeline completo: {processed_count}") print(f"Archivos que fallaron en algún punto del pipeline: {failed_count}") print("---------------------------------------") - xml_filename = None - # Comprobar si se pasó un argumento de línea de comandos - # sys.argv[0] es el nombre del script, sys.argv[1] sería el primer argumento - if len(sys.argv) > 1: - # Si hay argumentos, usar argparse para parsearlo (permite -h, etc.) - parser = argparse.ArgumentParser( - description="Run the Simatic XML processing pipeline." - ) - parser.add_argument( - "xml_file", - # Ya no necesitamos nargs='?' ni default aquí porque sabemos que hay un argumento - help="Path to the XML file to process.", - ) - # Parsear solo los argumentos conocidos, ignorar extras si los hubiera - args, unknown = parser.parse_known_args() - xml_filename = args.xml_file - print(f"XML file specified via argument: {xml_filename}") - else: - # Si no hay argumentos, llamar a la función interactiva - xml_filename = select_xml_file() - - # --- El resto del script continúa igual, usando xml_filename --- - - # Verificar si el archivo XML de entrada (seleccionado o pasado) existe - if not os.path.exists(xml_filename): - print(f"Error: Selected or specified XML file not found: {xml_filename}") + # Salir con código 0 si todo fue bien, 1 si hubo fallos + if failed_count > 0: sys.exit(1) - - print(f"\nStarting pipeline for: {xml_filename}") - - # Run scripts sequentially (asegúrate que los nombres son correctos) - script1 = "x1_to_json.py" - script2 = "x2_process.py" - script3 = "x3_generate_scl.py" - - if run_script(script1, xml_filename): - if run_script(script2, xml_filename): - if run_script(script3, xml_filename): - print("\nPipeline completed successfully.") - else: - print("\nPipeline failed at script:", script3) - else: - print("\nPipeline failed at script:", script2) else: - print("\nPipeline failed at script:", script1) + sys.exit(0) + +# --- FIN: Se elimina la lógica redundante que venía después del bucle --- \ No newline at end of file diff --git a/x1_to_json.py b/x1_to_json.py index 71ac19b..8d43e61 100644 --- a/x1_to_json.py +++ b/x1_to_json.py @@ -18,6 +18,8 @@ ns = { # --- Helper Functions --- +# ... (El resto de las funciones helper: get_multilingual_text, get_symbol_name, etc. permanecen igual) ... +# --- (Incluye aquí todas tus funciones helper sin cambios) --- def get_multilingual_text(element, default_lang="en-US", fallback_lang="it-IT"): # (Sin cambios respecto a la versión anterior) if element is None: @@ -247,9 +249,7 @@ def parse_call(call_element): call_data["instance_scope"] = instance_scope return call_data - -# SCL (Structured Text) Parser - +# ... (Incluye aquí las funciones reconstruct_scl_from_tokens, get_access_text, get_comment_text, reconstruct_stl_from_statementlist, parse_interface_members, parse_network SIN CAMBIOS) ... def reconstruct_scl_from_tokens(st_node): """ @@ -807,14 +807,19 @@ def parse_network(network_element): # Buscar FlgNet usando namespace flg flgnet_list = network_element.xpath(".//flg:FlgNet", namespaces=ns) if not flgnet_list: - return { - "id": network_id, - "title": network_title, - "comment": network_comment, - "logic": [], - "language": "Unknown", - "error": "FlgNet not found", - } + # Intentar buscar directamente si está en la raíz (caso SCL/STL simple?) + # Esta parte puede necesitar ajuste si FlgNet no es el contenedor principal + # para lógica SCL/STL tokenizada. + # Si no hay FlgNet, no podemos parsear lógica LAD/FBD. + # El parseo de SCL/STL se hace en convert_xml_to_json, así que aquí devolvemos error si no hay FlgNet. + return { + "id": network_id, + "title": network_title, + "comment": network_comment, + "logic": [], + "language": "Unknown", # Podríamos intentar leer el lenguaje aquí también + "error": "FlgNet not found for LAD/FBD parsing", + } flgnet = flgnet_list[0] # 1. Parsear Access, Parts y Calls (llaman a funciones que ya usan ns) @@ -885,6 +890,8 @@ def parse_network(network_element): # 3. Construcción Lógica Inicial (sin cambios en lógica, pero verificar llamadas) all_logic_steps = {} + # Define SCL_SUFFIX, por ejemplo, "_scl" o "_sympy_processed" + SCL_SUFFIX = "_sympy_processed" # Asegúrate que esto coincida con x2_process.py functional_block_types = [ "Move", "Add", @@ -1198,6 +1205,7 @@ def parse_network(network_element): } +# --- Main Conversion Function --- def convert_xml_to_json(xml_filepath, json_filepath): print(f"Iniciando conversión de '{xml_filepath}' a '{json_filepath}'...") if not os.path.exists(xml_filepath): @@ -1209,9 +1217,11 @@ def convert_xml_to_json(xml_filepath, json_filepath): tree = etree.parse(xml_filepath, parser) root = tree.getroot() print("Paso 1: Parseo XML completado.") - print("Paso 2: Buscando el bloque SW.Blocks.FC, SW.Blocks.FB o SW.Blocks.GlobalDB...") - # --- MODIFICADO: Buscar FC, FB o GlobalDB --- - block_list = root.xpath("//*[local-name()='SW.Blocks.FC' or local-name()='SW.Blocks.FB' or local-name()='SW.Blocks.GlobalDB']") + + # --- MODIFICADO: Buscar FC, FB, GlobalDB o OB --- + print("Paso 2: Buscando el bloque SW.Blocks.FC, SW.Blocks.FB, SW.Blocks.GlobalDB o SW.Blocks.OB...") + block_list = root.xpath("//*[local-name()='SW.Blocks.FC' or local-name()='SW.Blocks.FB' or local-name()='SW.Blocks.GlobalDB' or local-name()='SW.Blocks.OB']") # <-- Añadido OB + block_type_found = None the_block = None @@ -1224,11 +1234,13 @@ def convert_xml_to_json(xml_filepath, json_filepath): elif block_tag_name == "SW.Blocks.FB": block_type_found = "FB" elif block_tag_name == "SW.Blocks.GlobalDB": - block_type_found = "GlobalDB" # Identificar el tipo DB + block_type_found = "GlobalDB" + elif block_tag_name == "SW.Blocks.OB": # <-- Añadido caso OB + block_type_found = "OB" # <-- Establecer tipo OB print(f"Paso 2: Bloque {block_tag_name} encontrado (ID={the_block.get('ID')}).") else: - # Mensaje de error más específico y añadimos depuración - print("Error Crítico: No se encontró el elemento raíz del bloque (, o ) usando XPath.") + # Mensaje de error actualizado + print("Error Crítico: No se encontró el elemento raíz del bloque (, , o ) usando XPath.") # --- Añadir Debugging --- print(f"DEBUG: Tag del elemento raíz del XML: {root.tag}") print(f"DEBUG: Primeros hijos del raíz:") @@ -1240,6 +1252,7 @@ def convert_xml_to_json(xml_filepath, json_filepath): break # --- Fin Debugging --- return # Salir si no se encuentra el bloque principal + print("Paso 3: Extrayendo atributos del bloque...") attribute_list_node = the_block.xpath("./*[local-name()='AttributeList']") block_name_val, block_number_val, block_lang_val = "Unknown", None, "Unknown" @@ -1255,7 +1268,9 @@ def convert_xml_to_json(xml_filepath, json_filepath): lang_node = attr_list.xpath( "./*[local-name()='ProgrammingLanguage']/text()" ) - block_lang_val = lang_node[0].strip() if lang_node else block_lang_val + # Para DBs, el lenguaje principal es 'DB'. Para OBs/FCs/FBs puede ser SCL, STL, LAD, FBD etc. + block_lang_val = lang_node[0].strip() if lang_node else ("DB" if block_type_found == "GlobalDB" else "Unknown") + print( f"Paso 3: Atributos: Nombre='{block_name_val}', Número={block_number_val}, Lenguaje='{block_lang_val}'" ) @@ -1263,6 +1278,10 @@ def convert_xml_to_json(xml_filepath, json_filepath): print( f"Advertencia: No se encontró AttributeList para el bloque {block_type_found}." ) + # Asignar 'DB' como lenguaje si es un GlobalDB y no se encontró explícitamente + if block_type_found == "GlobalDB": + block_lang_val = "DB" + block_comment_val = "" comment_node_list = the_block.xpath( "./*[local-name()='ObjectList']/*[local-name()='MultilingualText'][@CompositionName='Comment']" @@ -1270,44 +1289,57 @@ def convert_xml_to_json(xml_filepath, json_filepath): if comment_node_list: block_comment_val = get_multilingual_text(comment_node_list[0]) print(f"Paso 3b: Comentario bloque: '{block_comment_val[:50]}...'") + + # --- MODIFICADO: Añadir block_type al resultado --- result = { "block_name": block_name_val, "block_number": block_number_val, - "language": block_lang_val, + "language": block_lang_val, # Lenguaje del bloque (SCL, LAD, DB, etc.) + "block_type": block_type_found, # Tipo de bloque (FC, FB, GlobalDB, OB) "block_comment": block_comment_val, "interface": {}, "networks": [], } + print("Paso 4: Extrayendo la interfaz del bloque...") - 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] - print("Paso 4: Nodo Interface encontrado.") - for section in interface_node.xpath(".//iface:Section", namespaces=ns): - section_name = section.get("Name") - if not section_name: - continue - members = [] - for member in section.xpath("./iface:Member", namespaces=ns): - member_name = member.get("Name") - member_dtype = member.get("Datatype") - 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: Interface sin secciones iface:Section válidas.") - else: - print( - "Advertencia: No se encontró dentro de ." - ) + # La estructura de la interfaz suele ser la misma para FC/FB/OB y la sección Static para DB + # Si Interface está dentro de AttributeList + interface_node_list = attribute_list_node[0].xpath(".//*[local-name()='Interface']") if attribute_list_node else [] + + if interface_node_list: + interface_node = interface_node_list[0] + print("Paso 4: Nodo Interface encontrado.") + # Usar parse_interface_members para extraer todas las secciones + # Esta función ya maneja structs anidados y arrays + all_sections = interface_node.xpath(".//iface:Section", namespaces=ns) + for section in all_sections: + section_name = section.get("Name") + if not section_name: + continue + # Obtener los miembros directos de esta sección + # Asegurarse de no obtener miembros de secciones anidadas accidentalmente + members_in_section = section.xpath("./iface:Member", namespaces=ns) + if members_in_section: + result["interface"][section_name] = parse_interface_members(members_in_section) + + if not result["interface"]: + print("Advertencia: Interface encontrada pero sin secciones iface:Section válidas.") + else: + # Para GlobalDB, la interfaz podría no estar explícita o ser solo la sección Static + if block_type_found == "GlobalDB": + # Intentar buscar directamente los miembros bajo Sections/Section Name="Static" + static_members = the_block.xpath(".//iface:Section[@Name='Static']/iface:Member", namespaces=ns) + if static_members: + print("Paso 4: Encontrada sección Static para GlobalDB.") + result["interface"]["Static"] = parse_interface_members(static_members) + else: + print("Advertencia: No se encontró sección 'Static' para GlobalDB.") + else: # FC/FB/OB + print(f"Advertencia: No se encontró para bloque {block_type_found}.") + if not result["interface"]: - print("Advertencia: No se pudo extraer información de la interfaz.") + print("Advertencia: No se pudo extraer información de la interfaz.") + print("Paso 5: Extrayendo y PROCESANDO lógica de redes (CompileUnits)...") networks_processed_count = 0 @@ -1315,6 +1347,7 @@ def convert_xml_to_json(xml_filepath, json_filepath): object_list_node = the_block.xpath("./*[local-name()='ObjectList']") if object_list_node: + # Buscar CompileUnits (para FC/FB/OB) compile_units = object_list_node[0].xpath( "./*[local-name()='SW.Blocks.CompileUnit']" ) @@ -1443,9 +1476,8 @@ def convert_xml_to_json(xml_filepath, json_filepath): ], } - elif programming_language in ["LAD", "FBD"]: - # Para LAD/FBD, llamar a parse_network (que espera FlgNet dentro de NetworkSource) - # parse_network ya maneja su propio título/comentario si es necesario, pero podemos pasar los extraídos + elif programming_language in ["LAD", "FBD", "GRAPH"]: # GRAPH también usa FlgNet + # Para LAD/FBD/GRAPH, llamar a parse_network # Nota: parse_network espera el *CompileUnit* element, no el NetworkSource parsed_network_data = parse_network(network_elem) if parsed_network_data: @@ -1453,19 +1485,42 @@ def convert_xml_to_json(xml_filepath, json_filepath): programming_language # Asegurar que el lenguaje se guarda ) if parsed_network_data.get("error"): + # Si parse_network devuelve error (ej. no encontró FlgNet), lo registramos print( f" Error al parsear red {programming_language} ID={network_id}: {parsed_network_data['error']}" ) - # parsed_network_data = None # Descomentar para omitir redes con error - else: + # Si es un error esperado para este lenguaje (ej. GRAPH sin FlgNet?), creamos placeholder + if "FlgNet not found" in parsed_network_data.get("error", ""): + parsed_network_data = { + "id": network_id, + "title": network_title, + "comment": network_comment, + "language": programming_language, + "logic": [{"instruction_uid": f"PLC_{network_id}", "type": "UNSUPPORTED_CONTENT", "info": f"Contenido {programming_language} sin FlgNet"}], + "error": parsed_network_data.get("error") # Mantener el error original + } + else: + print(f" Red {programming_language} ID={network_id} parseada.") + + else: # parse_network devolvió None (error interno) print( f" Error: parse_network devolvió None para red {programming_language} ID={network_id}" ) + # Crear placeholder de error + parsed_network_data = { + "id": network_id, + "title": network_title, + "comment": network_comment, + "language": programming_language, + "logic": [{"instruction_uid": f"ERR_{network_id}", "type": "PARSING_ERROR", "info": "parse_network returned None"}], + "error": "parse_network returned None" + } + else: # Manejar otros lenguajes o casos inesperados print( - f" Advertencia: Lenguaje no soportado '{programming_language}' en red ID={network_id}. Creando placeholder." + f" Advertencia: Lenguaje no soportado o inesperado '{programming_language}' en red ID={network_id}. Creando placeholder." ) parsed_network_data = { "id": network_id, @@ -1476,9 +1531,10 @@ def convert_xml_to_json(xml_filepath, json_filepath): { "instruction_uid": f"UNS_{network_id}", "type": "UNSUPPORTED_LANG", - "scl": f"// Network {network_id} uses unsupported language: {programming_language}\n", + "info": f"Network {network_id} uses unsupported language: {programming_language}", } ], + "error": f"Unsupported language: {programming_language}" } # Añadir la red procesada (si es válida) al resultado @@ -1487,17 +1543,21 @@ def convert_xml_to_json(xml_filepath, json_filepath): # --- Fin del bucle for network_elem --- - if networks_processed_count == 0: + if networks_processed_count == 0 and block_type_found != "GlobalDB": print( - "Advertencia: ObjectList no contenía elementos SW.Blocks.CompileUnit." + f"Advertencia: ObjectList para bloque {block_type_found} no contenía elementos SW.Blocks.CompileUnit." ) - else: - print("Advertencia: No se encontró ObjectList para el bloque.") + # Para DBs, no esperamos CompileUnits + elif block_type_found == "GlobalDB": + print("Paso 5: Saltando búsqueda de CompileUnits para GlobalDB.") + else: # No se encontró ObjectList + print(f"Advertencia: No se encontró ObjectList para el bloque {block_type_found}.") + print("Paso 6: Escribiendo el resultado en el archivo JSON...") if not result["interface"]: print("ADVERTENCIA FINAL: 'interface' está vacía.") - if not result["networks"]: + if not result["networks"] and block_type_found != "GlobalDB": print("ADVERTENCIA FINAL: 'networks' está vacía.") try: with open(json_filepath, "w", encoding="utf-8") as f: @@ -1510,6 +1570,19 @@ def convert_xml_to_json(xml_filepath, json_filepath): ) except TypeError as e: print(f"Error Crítico: Problema al serializar a JSON. Error: {e}") + print("--- Datos problemáticos (parcial) ---") + # Intentar imprimir partes del diccionario para depurar + for k, v in result.items(): + try: + json.dumps({k: v}) # Intentar serializar cada parte + except TypeError: + print(f"Error serializando clave '{k}': {type(v)}") + if isinstance(v, list) and v: + print(f" Primer elemento tipo: {type(v[0])}") + elif isinstance(v, dict) and v: + print(f" Primeras claves: {list(v.keys())[:5]}") + print("--- Fin Datos problemáticos ---") + except etree.XMLSyntaxError as e: print( f"Error Crítico: Sintaxis XML inválida en '{xml_filepath}'. Detalles: {e}" @@ -1529,7 +1602,7 @@ if __name__ == "__main__": # Configurar ArgumentParser para recibir la ruta del XML obligatoria parser = argparse.ArgumentParser( - description="Convert Simatic XML (LAD/FBD/SCL/STL) to simplified JSON. Expects XML filepath as argument." + description="Convert Simatic XML (LAD/FBD/SCL/STL/OB/DB) to simplified JSON. Expects XML filepath as argument." # Actualizada descripción ) parser.add_argument( "xml_filepath", # Argumento posicional obligatorio @@ -1557,12 +1630,11 @@ if __name__ == "__main__": ) # Llamar a la función principal de conversión del script - # Asumiendo que tu función principal se llama convert_xml_to_json(input_path, output_path) try: convert_xml_to_json(xml_input_file, json_output_file) except Exception as e: print(f"Error Crítico (x1) durante la conversión de '{xml_input_file}': {e}") - import traceback + import traceback # Asegurarse de que traceback está importado aquí traceback.print_exc() - sys.exit(1) # Salir con error si la función principal falla + sys.exit(1) # Salir con error si la función principal falla \ No newline at end of file diff --git a/x2_process.py b/x2_process.py index a0f3f93..748b417 100644 --- a/x2_process.py +++ b/x2_process.py @@ -18,17 +18,14 @@ from processors.processor_utils import ( from processors.symbol_manager import SymbolManager # Import the manager # --- Constantes y Configuración --- -# SCL_SUFFIX = "_scl" # Old suffix SCL_SUFFIX = "_sympy_processed" # New suffix to indicate processing method GROUPED_COMMENT = "// Logic included in grouped IF" SIMPLIFIED_IF_COMMENT = "// Simplified IF condition by script" # May still be useful -# Global data dictionary (consider passing 'data' as argument if needed elsewhere) -# It's currently used by process_group_ifs implicitly via the outer scope, -# which works but passing it explicitly might be cleaner. +# Global data dictionary data = {} - +# --- (Incluye aquí las funciones process_group_ifs y load_processors SIN CAMBIOS) --- def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data): """ Busca condiciones (ya procesadas -> tienen expr SymPy en sympy_map) @@ -112,6 +109,9 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data): # SCoil/RCoil might also be groupable if their SCL is final assignment "SCoil", "RCoil", + "BLKMOV", # Added BLKMOV + "TON", "TOF", "TP", "Se", "Sd", # Added timers + "CTU", "CTD", "CTUD", # Added counters ] for consumer_instr in network_logic: @@ -135,26 +135,26 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data): is_enabled_by_us = True # Check if consumer is groupable AND has its final SCL generated - # The suffix check needs adjustment based on how terminating processors set it. - # Assuming processors like Move, Add, Call, SCoil, RCoil NOW generate final SCL and add a suffix. if ( is_enabled_by_us - and consumer_type.endswith(SCL_SUFFIX) # Or a specific "final_scl" suffix + and consumer_type.endswith(SCL_SUFFIX) # Check if processed and consumer_type_original in groupable_types ): consumer_scl = consumer_instr.get("scl", "") - # Extract core SCL (logic is similar, maybe simpler if SCL is cleaner now) + # Extract core SCL core_scl = None if consumer_scl: # If consumer SCL itself is an IF generated by EN, take the body if consumer_scl.strip().startswith("IF"): match = re.search( - r"THEN\s*(.*?)\s*END_IF;", + r"IF\s+.*?THEN\s*(.*?)\s*END_IF;", # More robust regex consumer_scl, re.DOTALL | re.IGNORECASE, ) core_scl = match.group(1).strip() if match else None + # If body contains another IF, maybe don't group? (optional complexity) + # if core_scl and core_scl.strip().startswith("IF"): core_scl = None elif not consumer_scl.strip().startswith( "//" ): # Otherwise, take the whole line if not comment @@ -300,8 +300,7 @@ def load_processors(processors_dir="processors"): # Devolver el mapa (para lookup rápido si es necesario) y la lista ordenada return processor_map, processor_list_sorted - -# --- Bucle Principal de Procesamiento (Modificado para STL) --- +# --- Bucle Principal de Procesamiento (Modificado para STL y tipo de bloque) --- def process_json_to_scl(json_filepath): """ Lee JSON simplificado, aplica procesadores dinámicos (ignorando redes STL y bloques DB), @@ -321,15 +320,14 @@ def process_json_to_scl(json_filepath): traceback.print_exc() return - # --- Obtener lenguaje del bloque principal --- - block_language = data.get("language", "Unknown") - block_type = data.get("block_type", "Unknown") # FC, FB, GlobalDB - print(f"Procesando bloque tipo: {block_type}, Lenguaje principal: {block_language}") + # --- MODIFICADO: Obtener tipo de bloque (FC, FB, GlobalDB, OB) --- + block_type = data.get("block_type", "Unknown") # FC, FB, GlobalDB, OB + print(f"Procesando bloque tipo: {block_type}, Lenguaje principal: {data.get('language', 'Unknown')}") - # --- SI ES UN DB, SALTAR EL PROCESAMIENTO LÓGICO --- - if block_language == "DB": + # --- MODIFICADO: SI ES UN GlobalDB, SALTAR EL PROCESAMIENTO LÓGICO --- + if block_type == "GlobalDB": # <-- Comprobar tipo de bloque print( - "INFO: El bloque es un Data Block (DB). Saltando procesamiento lógico de x2." + "INFO: El bloque es un Data Block (GlobalDB). Saltando procesamiento lógico de x2." ) # Simplemente guardamos una copia (o el mismo archivo si no se requiere sufijo) output_filename = json_filepath.replace( @@ -345,8 +343,8 @@ def process_json_to_scl(json_filepath): traceback.print_exc() return # <<< SALIR TEMPRANO PARA DBs - # --- SI NO ES DB, CONTINUAR CON EL PROCESAMIENTO LÓGICO (FC/FB) --- - print("INFO: El bloque es FC/FB. Iniciando procesamiento lógico...") + # --- SI NO ES DB (FC, FB, OB), CONTINUAR CON EL PROCESAMIENTO LÓGICO --- + print(f"INFO: El bloque es {block_type}. Iniciando procesamiento lógico...") # <-- Mensaje actualizado script_dir = os.path.dirname(__file__) processors_dir_path = os.path.join(script_dir, "processors") @@ -391,7 +389,7 @@ def process_json_to_scl(json_filepath): passes = 0 processing_complete = False - print("\n--- Iniciando Bucle de Procesamiento Iterativo (FC/FB) ---") + print(f"\n--- Iniciando Bucle de Procesamiento Iterativo ({block_type}) ---") # <-- Mensaje actualizado while passes < max_passes and not processing_complete: passes += 1 made_change_in_base_pass = False @@ -408,34 +406,44 @@ def process_json_to_scl(json_filepath): func_to_call = processor_info["func"] for network in data.get("networks", []): network_id = network["id"] - network_lang = network.get("language", "LAD") - if network_lang == "STL": - continue # Saltar STL + network_lang = network.get("language", "LAD") # Lenguaje de la red + if network_lang == "STL": # Saltar redes STL + continue access_map = network_access_maps.get(network_id, {}) network_logic = network.get("logic", []) for instruction in network_logic: instr_uid = instruction.get("instruction_uid") - instr_type_original = instruction.get("type", "Unknown") + # Usar el tipo *actual* de la instrucción para el lookup + instr_type_current = instruction.get("type", "Unknown") + + # Saltar si ya está procesado, es error, agrupado, o tipo crudo if ( - instr_type_original.endswith(SCL_SUFFIX) - or "_error" in instr_type_original + instr_type_current.endswith(SCL_SUFFIX) + or "_error" in instr_type_current or instruction.get("grouped", False) - or instr_type_original - in ["RAW_STL_CHUNK", "RAW_SCL_CHUNK", "UNSUPPORTED_LANG"] + or instr_type_current + in ["RAW_STL_CHUNK", "RAW_SCL_CHUNK", "UNSUPPORTED_LANG", "UNSUPPORTED_CONTENT", "PARSING_ERROR"] ): continue - lookup_key = instr_type_original.lower() - effective_type_name = lookup_key - if instr_type_original == "Call": - block_type = instruction.get("block_type", "").upper() - if block_type == "FC": - effective_type_name = "call_fc" - elif block_type == "FB": - effective_type_name = "call_fb" + # El lookup usa el tipo actual (que aún no tiene el sufijo) + lookup_key = instr_type_current.lower() + effective_type_name = lookup_key + + # Mapeo especial para llamadas FC/FB + if instr_type_current == "Call": + call_block_type = instruction.get("block_type", "").upper() + if call_block_type == "FC": + effective_type_name = "call_fc" + elif call_block_type == "FB": + effective_type_name = "call_fb" + # Añadir otros tipos de llamada si es necesario + + # Si el tipo efectivo coincide con el procesador actual if effective_type_name == current_type_name: try: + # Pasar 'data' a la función del procesador changed = func_to_call( instruction, network_id, sympy_map, symbol_manager, data ) @@ -444,22 +452,24 @@ def process_json_to_scl(json_filepath): num_sympy_processed_this_pass += 1 except Exception as e: print( - f"ERROR(SymPy Base) al procesar {instr_type_original} UID {instr_uid}: {e}" + f"ERROR(SymPy Base) al procesar {instr_type_current} UID {instr_uid}: {e}" ) traceback.print_exc() instruction["scl"] = ( f"// ERROR en SymPy procesador base: {e}" ) - instruction["type"] = instr_type_original + "_error" - made_change_in_base_pass = True + # Añadir sufijo de error al tipo actual + instruction["type"] = instr_type_current + "_error" + made_change_in_base_pass = True # Se hizo un cambio (marcar como error) print( f" -> {num_sympy_processed_this_pass} instrucciones (no STL) procesadas con SymPy." ) + # --- FASE 2: Agrupación IF (Ignorando STL) --- if ( made_change_in_base_pass or passes == 1 - ): # Ejecutar siempre en el primer pase + ): # Ejecutar siempre en el primer pase o si hubo cambios print(f" Fase 2 (Agrupación IF con Simplificación):") num_grouped_this_pass = 0 # Resetear contador para el pase for network in data.get("networks", []): @@ -468,19 +478,30 @@ def process_json_to_scl(json_filepath): if network_lang == "STL": continue # Saltar STL network_logic = network.get("logic", []) - for instruction in network_logic: - try: - group_changed = process_group_ifs( - instruction, network_id, sympy_map, symbol_manager, data - ) - if group_changed: - made_change_in_group_pass = True - num_grouped_this_pass += 1 - except Exception as e: - print( - f"ERROR(GroupLoop) al intentar agrupar desde UID {instruction.get('instruction_uid')}: {e}" - ) - traceback.print_exc() + # Iterar en orden por UID puede ser más estable para agrupación + uids_in_network = sorted([instr.get("instruction_uid", "Z") for instr in network_logic if instr.get("instruction_uid")]) + for uid_to_process in uids_in_network: + instruction = next((instr for instr in network_logic if instr.get("instruction_uid") == uid_to_process), None) + if not instruction: continue + + # Saltar si ya está agrupada, es error, etc. + if instruction.get("grouped") or "_error" in instruction.get("type", ""): + continue + # La agrupación sólo aplica a instrucciones que generan condiciones booleanas + # y que ya fueron procesadas (tienen el sufijo) + if instruction.get("type", "").endswith(SCL_SUFFIX): + try: + group_changed = process_group_ifs( + instruction, network_id, sympy_map, symbol_manager, data + ) + if group_changed: + made_change_in_group_pass = True + num_grouped_this_pass += 1 + except Exception as e: + print( + f"ERROR(GroupLoop) al intentar agrupar desde UID {instruction.get('instruction_uid')}: {e}" + ) + traceback.print_exc() print( f" -> {num_grouped_this_pass} agrupaciones realizadas (en redes no STL)." ) @@ -503,14 +524,16 @@ def process_json_to_scl(json_filepath): # --- FIN BUCLE ITERATIVO --- # --- Verificación Final (Ajustada para RAW_STL_CHUNK) --- - print("\n--- Verificación Final de Instrucciones No Procesadas (FC/FB) ---") + print(f"\n--- Verificación Final de Instrucciones No Procesadas ({block_type}) ---") # <-- Mensaje actualizado unprocessed_count = 0 unprocessed_details = [] ignored_types = [ "raw_scl_chunk", "unsupported_lang", "raw_stl_chunk", - ] # Añadido raw_stl_chunk + "unsupported_content", # Añadido de x1 + "parsing_error", # Añadido de x1 + ] for network in data.get("networks", []): network_id = network.get("id", "Unknown ID") network_title = network.get("title", f"Network {network_id}") @@ -547,7 +570,7 @@ def process_json_to_scl(json_filepath): output_filename = json_filepath.replace( "_simplified.json", "_simplified_processed.json" ) - print(f"\nGuardando JSON procesado (FC/FB) en: {output_filename}") + print(f"\nGuardando JSON procesado ({block_type}) en: {output_filename}") # <-- Mensaje actualizado try: with open(output_filename, "w", encoding="utf-8") as f: json.dump(data, f, indent=4, ensure_ascii=False) @@ -557,7 +580,7 @@ def process_json_to_scl(json_filepath): traceback.print_exc() -# --- Ejecución (sin cambios) --- +# --- Ejecución (sin cambios en esta parte) --- if __name__ == "__main__": # Imports necesarios solo para la ejecución como script principal import argparse @@ -577,12 +600,10 @@ if __name__ == "__main__": source_xml_file = args.source_xml_filepath # Obtiene la ruta del XML original # Verificar si el archivo XML original existe (como referencia, útil para depuración) - # No es estrictamente necesario para la lógica aquí, pero ayuda a confirmar if not os.path.exists(source_xml_file): print( f"Advertencia (x2): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON correspondiente." ) - # No salir necesariamente, pero es bueno saberlo. # Derivar nombre del archivo JSON de entrada (_simplified.json) xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0] @@ -610,14 +631,13 @@ if __name__ == "__main__": sys.exit(1) # Salir si el archivo necesario no está else: # Llamar a la función principal de procesamiento del script - # Asumiendo que tu función principal se llama process_json_to_scl(input_json_path) try: process_json_to_scl(input_json_file) except Exception as e: print( f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}" ) - import traceback + import traceback # Asegurar que traceback está importado traceback.print_exc() - sys.exit(1) # Salir con error si la función principal falla + sys.exit(1) # Salir con error si la función principal falla \ No newline at end of file diff --git a/x3_generate_scl.py b/x3_generate_scl.py index e90d74e..e20b9d2 100644 --- a/x3_generate_scl.py +++ b/x3_generate_scl.py @@ -46,24 +46,23 @@ except ImportError: # para formatear valores iniciales def format_scl_start_value(value, datatype): """Formatea un valor para la inicialización SCL según el tipo.""" + # Add initial debug print + # print(f"DEBUG format_scl_start_value: value='{value}', datatype='{datatype}'") if value is None: - return None + return None # Retornar None si no hay valor datatype_lower = datatype.lower() if datatype else "" value_str = str(value) - if "bool" in datatype_lower: - return "TRUE" if value_str.lower() == "true" else "FALSE" - elif "string" in datatype_lower: - escaped_value = value_str.replace("'", "''") - if escaped_value.startswith("'") and escaped_value.endswith("'"): - escaped_value = escaped_value[1:-1] - return f"'{escaped_value}'" - elif "char" in datatype_lower: # Añadido Char - escaped_value = value_str.replace("'", "''") - if escaped_value.startswith("'") and escaped_value.endswith("'"): - escaped_value = escaped_value[1:-1] - return f"'{escaped_value}'" - elif any( + # Intentar quitar comillas si existen (para manejar "TRUE" vs TRUE) + if value_str.startswith('"') and value_str.endswith('"') and len(value_str) > 1: + value_str_unquoted = value_str[1:-1] + elif value_str.startswith("'") and value_str.endswith("'") and len(value_str) > 1: + value_str_unquoted = value_str[1:-1] + else: + value_str_unquoted = value_str + + # --- Integer-like types --- + if any( t in datatype_lower for t in [ "int", @@ -79,72 +78,169 @@ def format_scl_start_value(value, datatype): "udint", "ulint", ] - ): # Ampliado + ): try: - return str(int(value_str)) + # Intentar convertir el valor (sin comillas) a entero + return str(int(value_str_unquoted)) except ValueError: - if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str): - return value_str - return f"'{value_str}'" # O como string si no es entero ni símbolo + # Si no es un entero válido, podría ser una constante simbólica + if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str_unquoted): + return value_str_unquoted # Devolver como símbolo + + # --- Fallback for non-integer, non-symbol --- + print( + f"DEBUG format_scl_start_value: Fallback for int-like. value_str_unquoted='{repr(value_str_unquoted)}', datatype='{datatype}'" + ) # More debug + # MODIFIED FALLBACK: Escape newlines and use repr() for safety before formatting + try: + # Escape backslashes and single quotes properly for SCL string literal + escaped_for_scl = value_str_unquoted.replace("\\", "\\\\").replace( + "'", "''" + ) + # Remove potential newlines that break Python f-string; SCL strings usually don't span lines implicitly + escaped_for_scl = escaped_for_scl.replace("\n", "").replace("\r", "") + # Format as SCL string literal + formatted_scl_string = f"'{escaped_for_scl}'" + print( + f"DEBUG format_scl_start_value: Fallback result='{formatted_scl_string}'" + ) + return formatted_scl_string + except Exception as format_exc: + print( + f"ERROR format_scl_start_value: Exception during fallback formatting: {format_exc}" + ) + return f"'ERROR_FORMATTING_{value_str_unquoted[:20]}'" # Return an error string + + # --- Other types (Bool, Real, String, Char, Time, Date, etc.) --- + elif "bool" in datatype_lower: + # Comparar sin importar mayúsculas/minúsculas y sin comillas + return "TRUE" if value_str_unquoted.lower() == "true" else "FALSE" + elif "string" in datatype_lower: + # Usar el valor sin comillas originales y escapar las internas + escaped_value = value_str_unquoted.replace("'", "''") + return f"'{escaped_value}'" + elif "char" in datatype_lower: + # Usar el valor sin comillas originales y escapar las internas + escaped_value = value_str_unquoted.replace("'", "''") + # SCL usa comillas simples para Char. Asegurar que sea un solo caracter si es posible? + # Por ahora, solo formatear. Longitud se verifica en TIA. + return f"'{escaped_value}'" elif "real" in datatype_lower or "lreal" in datatype_lower: try: - f_val = float(value_str) + # Intentar convertir a float + f_val = float(value_str_unquoted) s_val = str(f_val) + # Asegurar que tenga punto decimal si es entero if "." not in s_val and "e" not in s_val.lower(): s_val += ".0" return s_val except ValueError: - if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str): - return value_str - return f"'{value_str}'" - elif "time" in datatype_lower: # Añadido Time, S5Time, LTime - # Quitar T#, LT#, S5T# si existen + # Podría ser constante simbólica + if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str_unquoted): + return value_str_unquoted + print( + f"Advertencia: Valor '{value_str}' no reconocido como real o símbolo para tipo {datatype}. Devolviendo como string." + ) + # Use the robust fallback formatting here too + escaped_for_scl = ( + value_str_unquoted.replace("\\", "\\\\") + .replace("'", "''") + .replace("\n", "") + .replace("\r", "") + ) + return f"'{escaped_for_scl}'" + elif "time" in datatype_lower: + # Quitar prefijos y añadir el correcto según el tipo específico prefix = "" - if value_str.upper().startswith("T#"): + val_to_use = value_str_unquoted # Usar valor sin comillas + if val_to_use.upper().startswith("T#"): prefix = "T#" - value_str = value_str[2:] - elif value_str.upper().startswith("LT#"): + val_to_use = val_to_use[2:] + elif val_to_use.upper().startswith("LT#"): prefix = "LT#" - value_str = value_str[3:] - elif value_str.upper().startswith("S5T#"): + val_to_use = val_to_use[3:] + elif val_to_use.upper().startswith("S5T#"): prefix = "S5T#" - value_str = value_str[4:] - # Devolver con el prefijo correcto o T# por defecto si no había - if prefix: - return f"{prefix}{value_str}" - elif "s5time" in datatype_lower: - return f"S5T#{value_str}" + val_to_use = val_to_use[4:] + + if "s5time" in datatype_lower: + return f"S5T#{val_to_use}" elif "ltime" in datatype_lower: - return f"LT#{value_str}" + return f"LT#{val_to_use}" else: - return f"T#{value_str}" # Default a TIME - elif "date" in datatype_lower: # Añadido Date, DT, TOD - if value_str.upper().startswith("D#"): - return value_str - elif "dt" in datatype_lower or "date_and_time" in datatype_lower: - if value_str.upper().startswith("DT#"): - return value_str - else: - return f"DT#{value_str}" # Añadir prefijo DT# + return f"T#{val_to_use}" # Default a TIME + elif "date" in datatype_lower: + val_to_use = value_str_unquoted + # Handle DTL first as it's longer + if "dtl" in datatype_lower or "date_and_time" in datatype_lower: + prefix = "DTL#" if val_to_use.upper().startswith("DTL#") else "DTL#" + val_to_use = ( + val_to_use[4:] if val_to_use.upper().startswith("DTL#") else val_to_use + ) + return f"{prefix}{val_to_use}" + elif "dt" in datatype_lower: + prefix = "DT#" if val_to_use.upper().startswith("DT#") else "DT#" + val_to_use = ( + val_to_use[3:] if val_to_use.upper().startswith("DT#") else val_to_use + ) + return f"{prefix}{val_to_use}" elif "tod" in datatype_lower or "time_of_day" in datatype_lower: - if value_str.upper().startswith("TOD#"): - return value_str - else: - return f"TOD#{value_str}" # Añadir prefijo TOD# - else: - return f"D#{value_str}" # Default a Date - # Fallback genérico + prefix = "TOD#" if val_to_use.upper().startswith("TOD#") else "TOD#" + val_to_use = ( + val_to_use[4:] if val_to_use.upper().startswith("TOD#") else val_to_use + ) + return f"{prefix}{val_to_use}" + else: # Default a Date D# + prefix = "D#" if val_to_use.upper().startswith("D#") else "D#" + val_to_use = ( + val_to_use[2:] if val_to_use.upper().startswith("D#") else val_to_use + ) + return f"{prefix}{val_to_use}" + + # --- Fallback for completely unknown types or complex structures --- else: + # Si es un nombre válido (posiblemente UDT, constante global, etc.), devolverlo tal cual + # Ajustar regex para permitir más caracteres si es necesario if re.match( - r'^[a-zA-Z_][a-zA-Z0-9_."#\[\]]+$', value_str - ): # Permitir más caracteres en símbolos/tipos - # Si es un UDT o Struct complejo, podría venir con comillas, quitarlas - if value_str.startswith('"') and value_str.endswith('"'): + r'^[a-zA-Z_#"][a-zA-Z0-9_."#\[\]%]+$', value_str + ): # Permitir % para accesos tipo %DB1.DBD0 + # Quitar comillas externas si es un UDT o struct complejo + if ( + value_str.startswith('"') + and value_str.endswith('"') + and len(value_str) > 1 + ): return value_str[1:-1] + # Mantener comillas si es acceso a DB ("DB_Name".Var) + if '"' in value_str and "." in value_str and value_str.count('"') == 2: + return value_str + # Si no tiene comillas y es un nombre simple o acceso #temp o %I0.0 etc + if not value_str.startswith('"') and not value_str.startswith("'"): + # Formatear nombres simples, pero dejar accesos % y # tal cual + if value_str.startswith("#") or value_str.startswith("%"): + return value_str + else: + # return format_variable_name(value_str) # Evitar formatear aquí, puede ser una constante + return value_str # Return as is if it looks symbolic + # Devolver el valor original si tiene comillas internas o estructura compleja no manejada arriba return value_str else: - escaped_value = value_str.replace("'", "''") - return f"'{escaped_value}'" + # Si no parece un nombre/símbolo/acceso, tratarlo como string (último recurso) + print( + f"DEBUG format_scl_start_value: Fallback final. value_str_unquoted='{repr(value_str_unquoted)}', datatype='{datatype}'" + ) + # Use the robust fallback formatting + escaped_for_scl = ( + value_str_unquoted.replace("\\", "\\\\") + .replace("'", "''") + .replace("\n", "") + .replace("\r", "") + ) + return f"'{escaped_for_scl}'" + + +# ... (generate_scl_declarations and generate_scl function remain the same as the previous version) ... +# --- (Incluye aquí las funciones generate_scl_declarations y generate_scl SIN CAMBIOS respecto a la respuesta anterior) --- # --- NUEVA FUNCIÓN RECURSIVA para generar declaraciones SCL (VAR/STRUCT/ARRAY) --- @@ -155,87 +251,132 @@ def generate_scl_declarations(variables, indent_level=1): for var in variables: var_name_scl = format_variable_name(var.get("name")) var_dtype_raw = var.get("datatype", "VARIANT") - # Limpiar comillas de tipos de datos UDT ("MyType" -> MyType) - var_dtype = ( - var_dtype_raw.strip('"') - if var_dtype_raw.startswith('"') and var_dtype_raw.endswith('"') - else var_dtype_raw - ) - var_comment = var.get("comment") start_value = var.get("start_value") children = var.get("children") # Para structs array_elements = var.get("array_elements") # Para arrays - # Manejar tipos de datos Array especiales - array_match = re.match(r"(Array\[.*\]\s+of\s+)(.*)", var_dtype, re.IGNORECASE) - base_type_for_init = var_dtype - declaration_dtype = var_dtype - if array_match: - array_prefix = array_match.group(1) - base_type_raw = array_match.group(2).strip() - # Limpiar comillas del tipo base del array - base_type_for_init = ( - base_type_raw.strip('"') - if base_type_raw.startswith('"') and base_type_raw.endswith('"') - else base_type_raw + # Limpiar comillas del tipo de dato si es UDT/String/etc. + var_dtype_cleaned = var_dtype_raw + if isinstance(var_dtype_raw, str): + if var_dtype_raw.startswith('"') and var_dtype_raw.endswith('"'): + var_dtype_cleaned = var_dtype_raw[1:-1] + # Manejar caso 'Array [...] of "MyUDT"' + array_match = re.match( + r'(Array\[.*\]\s+of\s+)"(.*)"', var_dtype_raw, re.IGNORECASE ) - declaration_dtype = ( - f'{array_prefix}"{base_type_for_init}"' - if '"' not in base_type_raw - else f"{array_prefix}{base_type_raw}" - ) # Reconstruir con comillas si es UDT + if array_match: + var_dtype_cleaned = f"{array_match.group(1)}{array_match.group(2)}" # Quitar comillas del tipo base - # Reconstruir declaración con comillas si es UDT y no array - elif ( - not array_match and var_dtype != base_type_for_init - ): # Es un tipo que necesita comillas (UDT) - declaration_dtype = f'"{var_dtype}"' + # Determinar tipo base para inicialización (importante para arrays) + base_type_for_init = var_dtype_cleaned + array_prefix_for_decl = "" + if var_dtype_cleaned.lower().startswith("array["): + match = re.match( + r"(Array\[.*\]\s+of\s+)(.*)", var_dtype_cleaned, re.IGNORECASE + ) + if match: + array_prefix_for_decl = match.group(1) + base_type_for_init = match.group(2).strip() + + # Construir tipo de dato para la declaración SCL + declaration_dtype = var_dtype_raw # Usar el raw por defecto + # Si es UDT o tipo complejo que requiere comillas y no es array simple + if base_type_for_init != var_dtype_cleaned and not array_prefix_for_decl: + # Poner comillas si no las tiene ya el tipo base + if not base_type_for_init.startswith('"'): + declaration_dtype = f'"{base_type_for_init}"' + else: + declaration_dtype = base_type_for_init # Ya tiene comillas + # Si es array de UDT/complejo, reconstruir con comillas en el tipo base + elif array_prefix_for_decl and base_type_for_init != var_dtype_cleaned: + if not base_type_for_init.startswith('"'): + declaration_dtype = f'{array_prefix_for_decl}"{base_type_for_init}"' + else: + declaration_dtype = f"{array_prefix_for_decl}{base_type_for_init}" declaration_line = f"{indent}{var_name_scl} : {declaration_dtype}" - init_value = None + init_value_scl = None # ---- Arrays ---- if array_elements: - # Ordenar índices (asumiendo que son numéricos) + # Ordenar índices (asumiendo que son numéricos '0', '1', ...) try: - sorted_indices = sorted(array_elements.keys(), key=int) + # Extraer números de los índices string + indices_numeric = {int(k): v for k, v in array_elements.items()} + sorted_indices = sorted(indices_numeric.keys()) + # Mapear de nuevo a string para buscar valor + sorted_indices_str = [str(k) for k in sorted_indices] except ValueError: - sorted_indices = sorted( - array_elements.keys() - ) # Fallback a orden alfabético + # Fallback a orden alfabético si los índices no son números + print( + f"Advertencia: Índices de array no numéricos para '{var_name_scl}'. Usando orden alfabético." + ) + sorted_indices_str = sorted(array_elements.keys()) - init_values = [ - format_scl_start_value(array_elements[idx], base_type_for_init) - for idx in sorted_indices - ] + init_values = [] + for idx_str in sorted_indices_str: + try: + formatted_val = format_scl_start_value( + array_elements[idx_str], base_type_for_init + ) + init_values.append(formatted_val) + except Exception as e_fmt: + print( + f"ERROR: Falló formateo para índice {idx_str} de array '{var_name_scl}'. Valor: {array_elements[idx_str]}. Error: {e_fmt}" + ) + init_values.append(f"/*ERR_FMT_{idx_str}*/") # Placeholder de error + + # Filtrar Nones que pueden venir de format_scl_start_value si el valor era None valid_inits = [v for v in init_values if v is not None] if valid_inits: - init_value = f"[{', '.join(valid_inits)}]" + # Si todos los valores son iguales y es un array grande, podríamos usar notación x(value) + # Simplificación: por ahora, listar todos + init_value_scl = f"[{', '.join(valid_inits)}]" + elif array_elements: # Si había elementos pero todos formatearon a None + print( + f"Advertencia: Todos los valores iniciales para array '{var_name_scl}' son None o inválidos." + ) # ---- Structs ---- elif children: - # No añadir comentario // Struct aquí, es redundante - scl_lines.append(declaration_line) # Añadir línea de declaración base + # El valor inicial de un struct se maneja recursivamente dentro + # Añadir comentario? Puede ser redundante. + scl_lines.append( + declaration_line + ) # Añadir línea de declaración base STRUCT scl_lines.append(f"{indent}STRUCT") + # Llamada recursiva para los miembros internos scl_lines.extend(generate_scl_declarations(children, indent_level + 1)) scl_lines.append(f"{indent}END_STRUCT;") - if var_comment: + if var_comment: # Comentario después de END_STRUCT scl_lines.append(f"{indent}// {var_comment}") - scl_lines.append("") # Línea extra - continue # Saltar resto para Struct + scl_lines.append("") # Línea extra para legibilidad + continue # Saltar el resto de la lógica para este struct # ---- Tipos Simples ---- else: if start_value is not None: - init_value = format_scl_start_value(start_value, var_dtype) + try: + init_value_scl = format_scl_start_value( + start_value, base_type_for_init + ) # Usar tipo base + except Exception as e_fmt_simple: + print( + f"ERROR: Falló formateo para valor simple de '{var_name_scl}'. Valor: {start_value}. Error: {e_fmt_simple}" + ) + init_value_scl = f"/*ERR_FMT_SIMPLE*/" # Placeholder + + # Añadir inicialización si existe y no es None + if init_value_scl is not None: + declaration_line += f" := {init_value_scl}" - # Añadir inicialización si existe - if init_value: - declaration_line += f" := {init_value}" declaration_line += ";" + + # Añadir comentario si existe if var_comment: declaration_line += f" // {var_comment}" + scl_lines.append(declaration_line) return scl_lines @@ -243,7 +384,7 @@ def generate_scl_declarations(variables, indent_level=1): # --- Función Principal de Generación SCL --- def generate_scl(processed_json_filepath, output_scl_filepath): - """Genera un archivo SCL a partir del JSON procesado (FC/FB o DB).""" + """Genera un archivo SCL a partir del JSON procesado (FC/FB/OB o DB).""" # Actualizado if not os.path.exists(processed_json_filepath): print( @@ -263,33 +404,41 @@ def generate_scl(processed_json_filepath, output_scl_filepath): # --- Extracción de Información del Bloque (Común) --- block_name = data.get("block_name", "UnknownBlock") block_number = data.get("block_number") - block_lang_original = data.get("language", "Unknown") # Será "DB" para Data Blocks - block_type = data.get("block_type", "Unknown") # FC, FB, GlobalDB + # block_lang_original = data.get("language", "Unknown") # Lenguaje original (SCL, LAD, DB...) + block_type = data.get( + "block_type", "Unknown" + ) # Tipo de bloque (FC, FB, GlobalDB, OB) <-- Usar este block_comment = data.get("block_comment", "") scl_block_name = format_variable_name(block_name) # Nombre SCL seguro print( - f"Generando SCL para: {block_type} '{scl_block_name}' (Original: {block_name}, Lang: {block_lang_original})" + f"Generando SCL para: {block_type} '{scl_block_name}' (Original: {block_name})" # Quitado lenguaje original del log ) scl_output = [] - # --- GENERACIÓN PARA DATA BLOCK (DB) --- - if block_lang_original == "DB": + # --- MODIFICADO: GENERACIÓN PARA DATA BLOCK (GlobalDB) --- + if block_type == "GlobalDB": # <-- Comprobar tipo de bloque print("Modo de generación: DATA_BLOCK") scl_output.append(f"// Block Type: {block_type}") scl_output.append(f"// Block Name (Original): {block_name}") if block_number: scl_output.append(f"// Block Number: {block_number}") if block_comment: - scl_output.append(f"// Block Comment: {block_comment}") + # Dividir comentarios largos en múltiples líneas + comment_lines = block_comment.splitlines() + scl_output.append(f"// Block Comment:") + for line in comment_lines: + scl_output.append(f"// {line}") scl_output.append("") scl_output.append(f'DATA_BLOCK "{scl_block_name}"') scl_output.append("{ S7_Optimized_Access := 'TRUE' }") # Asumir optimizado scl_output.append("VERSION : 0.1") scl_output.append("") interface_data = data.get("interface", {}) + # En DBs, la sección relevante suele ser 'Static' static_vars = interface_data.get("Static", []) if static_vars: scl_output.append("VAR") + # Usar la función recursiva para generar declaraciones scl_output.extend(generate_scl_declarations(static_vars, indent_level=1)) scl_output.append("END_VAR") scl_output.append("") @@ -297,182 +446,288 @@ def generate_scl(processed_json_filepath, output_scl_filepath): print( "Advertencia: No se encontró sección 'Static' o está vacía en la interfaz del DB." ) + # Añadir bloque VAR vacío si no hay variables scl_output.append("VAR") scl_output.append("END_VAR") scl_output.append("") scl_output.append("BEGIN") - scl_output.append("") + scl_output.append( + " // Los Data Blocks no tienen código ejecutable en BEGIN/END" + ) scl_output.append("END_DATA_BLOCK") - # --- GENERACIÓN PARA FUNCTION BLOCK / FUNCTION (FC/FB) --- + # --- MODIFICADO: GENERACIÓN PARA FC/FB/OB --- else: - print("Modo de generación: FUNCTION_BLOCK / FUNCTION") - scl_block_keyword = "FUNCTION_BLOCK" if block_type == "FB" else "FUNCTION" + # Determinar palabra clave SCL + scl_block_keyword = "FUNCTION_BLOCK" # Default + if block_type == "FC": + scl_block_keyword = "FUNCTION" + elif block_type == "OB": + scl_block_keyword = "ORGANIZATION_BLOCK" + elif block_type == "FB": + scl_block_keyword = "FUNCTION_BLOCK" + else: # Fallback + print( + f"Advertencia: Tipo de bloque desconocido '{block_type}', usando FUNCTION_BLOCK." + ) + scl_block_keyword = "FUNCTION_BLOCK" # O quizás lanzar error? + + print(f"Modo de generación: {scl_block_keyword}") + # Cabecera del Bloque scl_output.append(f"// Block Type: {block_type}") scl_output.append(f"// Block Name (Original): {block_name}") if block_number: scl_output.append(f"// Block Number: {block_number}") - scl_output.append(f"// Original Language: {block_lang_original}") + # Indicar lenguaje original de las redes si es relevante + original_net_langs = set( + n.get("language", "Unknown") for n in data.get("networks", []) + ) + scl_output.append( + f"// Original Network Languages: {', '.join(l for l in original_net_langs if l != 'Unknown')}" + ) if block_comment: - scl_output.append(f"// Block Comment: {block_comment}") + comment_lines = block_comment.splitlines() + scl_output.append(f"// Block Comment:") + for line in comment_lines: + scl_output.append(f"// {line}") scl_output.append("") - # Manejar tipo de retorno para FUNCTION + + # Manejar tipo de retorno para FUNCTION (FC) return_type = "Void" # Default interface_data = data.get("interface", {}) if scl_block_keyword == "FUNCTION" and interface_data.get("Return"): - return_member = interface_data["Return"][ - 0 - ] # Asumir un solo valor de retorno + # Asumir un solo valor de retorno + return_member = interface_data["Return"][0] return_type_raw = return_member.get("datatype", "Void") + # Limpiar comillas si es UDT/String return_type = ( - return_type_raw.strip('"') - if return_type_raw.startswith('"') and return_type_raw.endswith('"') + return_type_raw[1:-1] + if isinstance(return_type_raw, str) + and return_type_raw.startswith('"') + and return_type_raw.endswith('"') else return_type_raw ) - # Añadir comillas si es UDT - if return_type != return_type_raw: + # Añadir comillas si es UDT y no las tenía + if ( + return_type != return_type_raw + and not return_type_raw.lower().startswith("array") + ): return_type = f'"{return_type}"' + else: # Mantener raw si es tipo básico o ya tenía comillas + return_type = return_type_raw - scl_output.append( - f'{scl_block_keyword} "{scl_block_name}" : {return_type}' - if scl_block_keyword == "FUNCTION" - else f'{scl_block_keyword} "{scl_block_name}"' - ) - scl_output.append("{ S7_Optimized_Access := 'TRUE' }") + # Línea de declaración del bloque + if scl_block_keyword == "FUNCTION": + scl_output.append(f'{scl_block_keyword} "{scl_block_name}" : {return_type}') + else: # FB y OB + scl_output.append(f'{scl_block_keyword} "{scl_block_name}"') + + # Atributos y versión + scl_output.append("{ S7_Optimized_Access := 'TRUE' }") # Asumir optimizado scl_output.append("VERSION : 0.1") scl_output.append("") - # Declaraciones de Interfaz FC/FB - section_order = [ - "Input", - "Output", - "InOut", - "Static", - "Temp", - "Constant", - ] # Return ya está en cabecera - declared_temps = set() + # Declaraciones de Interfaz (Input, Output, InOut, Static, Temp, Constant) + # Orden estándar SCL + section_order = ["Input", "Output", "InOut", "Static", "Temp", "Constant"] + declared_temps = set() # Para rastrear temps ya declaradas + has_declarations = False + for section_name in section_order: vars_in_section = interface_data.get(section_name, []) if vars_in_section: + has_declarations = True + # Mapeo de nombres de sección JSON a palabras clave SCL VAR_ scl_section_keyword = f"VAR_{section_name.upper()}" if section_name == "Static": - scl_section_keyword = "VAR_STAT" + scl_section_keyword = "VAR_STAT" # Para FBs if section_name == "Temp": scl_section_keyword = "VAR_TEMP" if section_name == "Constant": - scl_section_keyword = "CONSTANT" + scl_section_keyword = "CONSTANT" # CONSTANT no usa VAR_ + scl_output.append(scl_section_keyword) + # Usar la función recursiva para generar declaraciones scl_output.extend( generate_scl_declarations(vars_in_section, indent_level=1) ) + # Añadir END_VAR (o END_CONSTANT) + scl_output.append( + "END_VAR" if section_name != "Constant" else "END_CONSTANT" + ) + scl_output.append("") # Línea en blanco + + # Guardar nombres de Temp declarados explícitamente if section_name == "Temp": declared_temps.update( format_variable_name(v.get("name")) for v in vars_in_section if v.get("name") ) - scl_output.append("END_VAR") - scl_output.append("") - - # Declaraciones VAR_TEMP adicionales detectadas - temp_vars = set() + # Declaraciones VAR_TEMP adicionales (auto-detectadas) + # Buscar variables que empiecen con #_temp_ en el SCL generado + temp_vars_detected = set() + # Patrón para encontrar #variable o "#variable" temp_pattern = re.compile( - r'"?#(_temp_[a-zA-Z0-9_]+)"?|"?(_temp_[a-zA-Z0-9_]+)"?' - ) + r'"?(#\w+)"?' + ) # Busca # seguido de caracteres alfanuméricos + for network in data.get("networks", []): for instruction in network.get("logic", []): + # Revisar el SCL final y el SCL de actualización de memoria si existe scl_code = instruction.get("scl", "") - edge_update_code = instruction.get("_edge_mem_update_scl", "") + edge_update_code = instruction.get( + "_edge_mem_update_scl", "" + ) # Para flancos code_to_scan = ( (scl_code if scl_code else "") + "\n" + (edge_update_code if edge_update_code else "") ) + if code_to_scan: + # Usar findall para encontrar todas las ocurrencias found_temps = temp_pattern.findall(code_to_scan) - for temp_tuple in found_temps: - temp_name = next((t for t in temp_tuple if t), None) + for temp_name in found_temps: + # findall devuelve el grupo capturado (#...) if temp_name: - temp_vars.add( - "#" + temp_name - if not temp_name.startswith("#") - else temp_name - ) - additional_temps = sorted(list(temp_vars - declared_temps)) + temp_vars_detected.add(temp_name) + + # Filtrar las que ya estaban declaradas + additional_temps = sorted(list(temp_vars_detected - declared_temps)) + if additional_temps: - if not interface_data.get("Temp"): + print(f"INFO: Detectadas {len(additional_temps)} VAR_TEMP adicionales.") + # Si no se declaró la sección Temp antes, añadirla ahora + if "Temp" not in interface_data or not interface_data["Temp"]: scl_output.append("VAR_TEMP") - for var_name in additional_temps: - scl_name = format_variable_name(var_name) - inferred_type = "Bool" # Asumir Bool + + for temp_name in additional_temps: + # Formatear por si acaso, aunque el patrón ya debería dar #nombre + scl_name = format_variable_name(temp_name) + # Inferir tipo (Bool es lo más común para temporales internos) + # Se podría mejorar si el nombre da pistas (ej. _temp_r para Real) + inferred_type = "Bool" # Asumir Bool por defecto scl_output.append( f" {scl_name} : {inferred_type}; // Auto-generated temporary" ) - if not interface_data.get("Temp"): + + # Si abrimos la sección aquí, cerrarla + if "Temp" not in interface_data or not interface_data["Temp"]: scl_output.append("END_VAR") scl_output.append("") - # Cuerpo del Bloque FC/FB + # --- Cuerpo del Bloque (BEGIN...END) --- scl_output.append("BEGIN") scl_output.append("") - # Iterar por redes y lógica (como antes, incluyendo manejo STL Markdown) + # Iterar por redes y lógica (incluyendo manejo STL/SCL crudo) for i, network in enumerate(data.get("networks", [])): - network_title = network.get("title", f'Network {network.get("id")}') + network_title = network.get( + "title", f'Network {network.get("id", i+1)}' + ) # Usar i+1 si falta ID network_comment = network.get("comment", "") - network_lang = network.get("language", "LAD") + network_lang = network.get("language", "LAD") # Lenguaje original de la red scl_output.append( f" // Network {i+1}: {network_title} (Original Language: {network_lang})" ) if network_comment: + # Indentar comentarios de red for line in network_comment.splitlines(): - scl_output.append(f" // {line}") - scl_output.append("") + scl_output.append(f" // {line}") + scl_output.append("") # Línea en blanco antes del código de red + network_has_code = False + logic_in_network = network.get("logic", []) + + if not logic_in_network: + scl_output.append(f" // Network {i+1} has no logic elements.") + scl_output.append("") + continue + + # --- Manejo Especial Redes STL --- if network_lang == "STL": - network_has_code = True - if ( - network.get("logic") - and network["logic"][0].get("type") == "RAW_STL_CHUNK" - ): - raw_stl_code = network["logic"][0].get( + # Asumir que la lógica STL está en el primer elemento como RAW_STL_CHUNK + if logic_in_network[0].get("type") == "RAW_STL_CHUNK": + network_has_code = True + raw_stl_code = logic_in_network[0].get( "stl", "// ERROR: STL code missing" ) - scl_output.append(f" {'//'} ```STL") + # Incrustar STL como comentario multi-línea o delimitado + scl_output.append(f" // --- BEGIN STL Network {i+1} ---") + # Comentar cada línea STL for stl_line in raw_stl_code.splitlines(): - scl_output.append(f" {stl_line}") - scl_output.append(f" {'//'} ```") + scl_output.append(f" // {stl_line}") + scl_output.append(f" // --- END STL Network {i+1} ---") + scl_output.append("") # Línea en blanco después else: - scl_output.append(" // ERROR: Contenido STL inesperado.") - else: # LAD, FBD, SCL, etc. - for instruction in network.get("logic", []): + scl_output.append( + f" // ERROR: Contenido STL inesperado en Network {i+1}." + ) + scl_output.append("") + + # --- Manejo Redes SCL/LAD/FBD procesadas --- + else: + # Iterar por las instrucciones procesadas + for instruction in logic_in_network: instruction_type = instruction.get("type", "") scl_code = instruction.get("scl", "") is_grouped = instruction.get("grouped", False) + + # Saltar instrucciones agrupadas (su lógica está en el IF) if is_grouped: continue + + # Incluir SCL si la instrucción fue procesada o es un chunk crudo/error/placeholder if ( instruction_type.endswith(SCL_SUFFIX) - or instruction_type in ["RAW_SCL_CHUNK", "UNSUPPORTED_LANG"] + or instruction_type + in [ + "RAW_SCL_CHUNK", + "UNSUPPORTED_LANG", + "UNSUPPORTED_CONTENT", + "PARSING_ERROR", + ] + or "_error" in instruction_type # Incluir errores comentados ) and scl_code: + + # Comprobar si el SCL es solo un comentario (a menos que sea un bloque IF) is_only_comment = all( line.strip().startswith("//") for line in scl_code.splitlines() if line.strip() ) is_if_block = scl_code.strip().startswith("IF") - if not is_only_comment or is_if_block: + + # Añadir el SCL indentado si no es solo un comentario (o si es un IF/Error) + if ( + not is_only_comment + or is_if_block + or "_error" in instruction_type + or instruction_type + in [ + "UNSUPPORTED_LANG", + "UNSUPPORTED_CONTENT", + "PARSING_ERROR", + ] + ): network_has_code = True for line in scl_code.splitlines(): - scl_output.append(f" {line}") - if network_has_code: + scl_output.append(f" {line}") # Indentar código + # Añadir línea en blanco después de cada bloque SCL para legibilidad + scl_output.append("") + + # Si la red no produjo código SCL imprimible (ej. solo lógica interna) + if ( + not network_has_code and network_lang != "STL" + ): # No añadir para STL ya comentado + scl_output.append( + f" // Network {i+1} did not produce printable SCL code." + ) scl_output.append("") - else: - scl_output.append(f" // Network did not produce printable SCL code.") - scl_output.append("") - # Fin del bloque FC/FB - scl_output.append(f"END_{scl_block_keyword}") + + # Fin del bloque FC/FB/OB + scl_output.append(f"END_{scl_block_keyword}") # <-- Usar keyword determinada # --- Escritura del Archivo SCL (Común) --- print(f"Escribiendo archivo SCL en: {output_scl_filepath}") @@ -492,7 +747,7 @@ if __name__ == "__main__": import argparse import os import sys - import traceback # Asegurarse que traceback está importado si se usa en generate_scl + import traceback # Asegurarse que traceback está importado # Configurar ArgumentParser para recibir la ruta del XML original obligatoria parser = argparse.ArgumentParser( @@ -511,7 +766,6 @@ if __name__ == "__main__": print( f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON procesado." ) - # No salir necesariamente. # Derivar nombres de archivos de entrada (JSON procesado) y salida (SCL) xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0] @@ -521,8 +775,9 @@ if __name__ == "__main__": input_json_file = os.path.join( base_dir, f"{xml_filename_base}_simplified_processed.json" ) + # Cambiar extensión de salida a .scl output_scl_file = os.path.join( - base_dir, f"{xml_filename_base}_simplified_processed.scl" + base_dir, f"{xml_filename_base}_generated.scl" # Cambiado nombre de salida ) print( @@ -540,13 +795,13 @@ if __name__ == "__main__": sys.exit(1) # Salir si el archivo necesario no está else: # Llamar a la función principal de generación SCL del script - # Asumiendo que tu función principal se llama generate_scl(input_json_path, output_scl_path) try: generate_scl(input_json_file, output_scl_file) + sys.exit(0) # Salir con éxito explícitamente except Exception as e: print( f"Error Crítico (x3) durante la generación de SCL desde '{input_json_file}': {e}" ) - # traceback ya debería estar importado si generate_scl lo necesita + # traceback ya debería estar importado traceback.print_exc() sys.exit(1) # Salir con error si la función principal falla