# ToUpload/x1_to_json.py # -*- coding: utf-8 -*- import json import argparse import os import sys import traceback import importlib from lxml import etree from collections import defaultdict # Puede ser necesario si load_parsers la usa import copy # Puede ser necesario si load_parsers la usa # Importar funciones comunes y namespaces desde el nuevo módulo de utils try: from parsers.parser_utils import ns, get_multilingual_text, parse_interface_members except ImportError as e: print( f"Error crítico: No se pudieron importar funciones desde parsers.parser_utils: {e}" ) print( "Asegúrate de que el directorio 'parsers' y 'parsers/parser_utils.py' existen y son correctos." ) sys.exit(1) # --- Cargador Dinámico de Parsers --- def load_parsers(parsers_dir="parsers"): """ Escanea el directorio de parsers, importa módulos y construye un mapa de lenguaje a función de parseo. """ parser_map = {} # Verificar si el directorio existe script_dir = os.path.dirname(__file__) parsers_dir_path = os.path.join(script_dir, parsers_dir) if not os.path.isdir(parsers_dir_path): print(f"Error: Directorio de parsers no encontrado: '{parsers_dir_path}'") return parser_map # Devuelve mapa vacío print(f"Cargando parsers desde: '{parsers_dir_path}'") parsers_package = os.path.basename(parsers_dir) for filename in os.listdir(parsers_dir_path): # Buscar archivos que empiecen con 'parse_' y terminen en '.py' # Excluir '__init__.py' y 'parser_utils.py' if ( filename.startswith("parse_") and filename.endswith(".py") and filename not in ["__init__.py", "parser_utils.py"] ): module_name_rel = filename[:-3] # Nombre sin .py (e.g., parse_lad_fbd) full_module_name = ( f"{parsers_package}.{module_name_rel}" # e.g., parsers.parse_lad_fbd ) try: # Importar el módulo dinámicamente module = importlib.import_module(full_module_name) # Verificar si el módulo tiene la función get_parser_info if hasattr(module, "get_parser_info") and callable( module.get_parser_info ): parser_info = module.get_parser_info() # Esperamos un diccionario con 'language' (lista) y 'parser_func' if ( isinstance(parser_info, dict) and "language" in parser_info and "parser_func" in parser_info ): languages = parser_info["language"] parser_func = parser_info["parser_func"] if isinstance(languages, list) and callable(parser_func): # Añadir la función al mapa para cada lenguaje que soporta for lang in languages: lang_upper = lang.upper() # Usar mayúsculas como clave if lang_upper in parser_map: print( f" Advertencia: Parser para '{lang_upper}' en {full_module_name} sobrescribe definición anterior." ) parser_map[lang_upper] = parser_func print( f" - Cargado parser para '{lang_upper}' desde {module_name_rel}.py" ) else: print( f" Advertencia: Formato inválido en get_parser_info de {full_module_name} (language debe ser lista, parser_func callable)." ) else: print( f" Advertencia: get_parser_info en {full_module_name} no devolvió el diccionario esperado." ) else: print( f" Advertencia: Módulo {module_name_rel}.py no tiene la función 'get_parser_info'." ) except ImportError as e: print(f"Error importando {full_module_name}: {e}") except Exception as e: print(f"Error procesando {full_module_name}: {e}") traceback.print_exc() print(f"\nTotal de lenguajes con parser cargado: {len(parser_map)}") print(f"Lenguajes soportados: {list(parser_map.keys())}") return parser_map # --- Función Principal de Conversión (Refactorizada) --- def convert_xml_to_json(xml_filepath, json_filepath, parser_map): """Convierte XML a JSON usando los parsers cargados dinámicamente.""" print(f"Iniciando conversión de '{xml_filepath}' a '{json_filepath}'...") if not os.path.exists(xml_filepath): print(f"Error Crítico: Archivo XML no encontrado: '{xml_filepath}'") return False # Indicar fallo try: print("Paso 1: Parseando archivo XML...") # Usar un parser que quite texto en blanco para simplificar XPath parser = etree.XMLParser(remove_blank_text=True) tree = etree.parse(xml_filepath, parser) root = tree.getroot() print("Paso 1: Parseo XML completado.") # --- Buscar bloque principal (FC, FB, GlobalDB, OB) --- print("Paso 2: Buscando el bloque SW.Blocks.FC/FB/GlobalDB/OB...") # Usar local-name() para ignorar namespaces en esta búsqueda inicial 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']" ) if ( not block_list ): # Intentar con namespace si el anterior falla (menos probable) ns_doc = { "doc": "http://www.siemens.com/automation/Openness/SW/Document/v5" } # Asumiendo este namespace block_list = root.xpath( "//doc:SW.Blocks.FC | //doc:SW.Blocks.FB | //doc:SW.Blocks.GlobalDB | //doc:SW.Blocks.OB", namespaces=ns_doc, ) block_type_found = None the_block = None if block_list: the_block = block_list[0] block_tag_name = etree.QName( the_block.tag ).localname # Obtener nombre local sin ns 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" elif block_tag_name == "SW.Blocks.OB": block_type_found = "OB" print( f"Paso 2: Bloque {block_tag_name} (Tipo: {block_type_found}) encontrado (ID={the_block.get('ID')})." ) else: print( "Error Crítico: No se encontró el elemento raíz del bloque ()." ) # Podríamos intentar buscar cualquier SW.Blocks.* como fallback? any_block = root.xpath("//*[starts-with(local-name(), 'SW.Blocks.')]") if any_block: print( f"Advertencia: Se encontró un bloque genérico: {etree.QName(any_block[0].tag).localname}. Intentando continuar..." ) the_block = any_block[0] block_type_found = "Unknown" # Marcar como desconocido else: return False # Fallo si no se encuentra ningún bloque # --- Extraer atributos del bloque --- print("Paso 3: Extrayendo atributos del bloque...") # AttributeList generalmente no tiene namespace propio attribute_list_node = the_block.xpath("./AttributeList") block_name_val, block_number_val, block_lang_val = "Unknown", None, "Unknown" if attribute_list_node: attr_list = attribute_list_node[0] # Name, Number, ProgrammingLanguage están directamente bajo AttributeList name_node = attr_list.xpath("./Name/text()") block_name_val = name_node[0].strip() if name_node else block_name_val num_node = attr_list.xpath("./Number/text()") try: block_number_val = int(num_node[0]) if num_node else None except (ValueError, TypeError): block_number_val = None # Mantener como None si no es entero lang_node = attr_list.xpath("./ProgrammingLanguage/text()") 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 Bloque='{block_lang_val}'" ) else: print( f"Advertencia: No se encontró AttributeList para el bloque {block_type_found}." ) if block_type_found == "GlobalDB": block_lang_val = "DB" # Asignar lenguaje DB si es GlobalDB # --- Extraer comentario del bloque --- # ObjectList -> MultilingualText[@CompositionName='Comment'] block_comment_val = "" # ObjectList tampoco suele tener namespace propio comment_node_list = the_block.xpath( "./ObjectList/MultilingualText[@CompositionName='Comment']" ) if comment_node_list: # Usar la función de utils que maneja los namespaces internos de MultilingualText block_comment_val = get_multilingual_text(comment_node_list[0]) print(f"Paso 3b: Comentario bloque: '{block_comment_val[:50]}...'") else: # Intentar buscar comentario en AttributeList como fallback? comment_attr_node = the_block.xpath("./AttributeList/Comment") if comment_attr_node: block_comment_val = get_multilingual_text(comment_attr_node[0]) print( f"Paso 3b (Fallback): Comentario bloque encontrado en AttributeList: '{block_comment_val[:50]}...'" ) # --- Crear diccionario resultado --- result = { "block_name": block_name_val, "block_number": block_number_val, "language": block_lang_val, # Lenguaje general del bloque "block_type": block_type_found, "block_comment": block_comment_val, "interface": {}, "networks": [], } # --- Extraer interfaz --- print("Paso 4: Extrayendo la interfaz del bloque...") # Interface está dentro de AttributeList (sin ns propio), pero sus hijos usan 'iface' interface_node_list = ( attribute_list_node[0].xpath("./Interface") if attribute_list_node else [] ) if interface_node_list: interface_node = interface_node_list[0] print("Paso 4: Nodo Interface encontrado.") # Sections/Section usan namespace iface all_sections = interface_node.xpath(".//iface:Section", namespaces=ns) if all_sections: processed_sections = set() for section in all_sections: section_name = section.get( "Name" ) # Input, Output, Static, Temp, etc. if not section_name or section_name in processed_sections: continue # Los Member dentro de Section usan namespace iface members_in_section = section.xpath("./iface:Member", namespaces=ns) if members_in_section: # Usar la función de utils para parsear miembros result["interface"][section_name] = parse_interface_members( members_in_section ) processed_sections.add(section_name) else: print( "Advertencia: Nodo Interface no contiene secciones ." ) if not result["interface"]: print( "Advertencia: Interface encontrada pero sin secciones procesables." ) else: # Manejo especial para DB si no hay explícita if block_type_found == "GlobalDB": # Buscar directamente la sección Static (que usa namespace iface) 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 (sin nodo Interface)." ) result["interface"]["Static"] = parse_interface_members( static_members ) else: print("Advertencia: No se encontró sección 'Static' para GlobalDB.") else: 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.") # --- Procesar redes (CompileUnits) --- print("Paso 5: Buscando y PROCESANDO redes (CompileUnits)...") networks_processed_count = 0 result["networks"] = [] # ObjectList y SW.Blocks.CompileUnit no suelen tener namespace propio object_list_node = the_block.xpath("./ObjectList") if object_list_node: compile_units = object_list_node[0].xpath("./SW.Blocks.CompileUnit") print( f"Paso 5: Se encontraron {len(compile_units)} elementos SW.Blocks.CompileUnit." ) # --- BUCLE PRINCIPAL DE PARSEO DE REDES (MODIFICADO) --- for network_elem in compile_units: networks_processed_count += 1 network_id = network_elem.get("ID") if not network_id: print("Advertencia: CompileUnit sin ID, saltando.") continue # Detectar lenguaje de la RED (puede diferir del lenguaje del bloque) # AttributeList/ProgrammingLanguage sin namespace network_lang = "LAD" # Default si no se encuentra net_attr_list = network_elem.xpath("./AttributeList") if net_attr_list: lang_node = net_attr_list[0].xpath("./ProgrammingLanguage/text()") if lang_node: network_lang = lang_node[0].strip() print( f" - Procesando Red ID={network_id}, Lenguaje Red={network_lang}" ) # --- Llamada al Parser Dinámico --- parser_func = parser_map.get( network_lang.upper() ) # Buscar parser por lenguaje parsed_network_data = None if parser_func: try: # Llamar a la función de parseo específica del lenguaje # Pasar el elemento XML de la red y los namespaces parsed_network_data = parser_func( network_elem ) # Pasar ns ya no es necesario si están en utils except Exception as e_parse: print( f" ERROR durante el parseo de Red {network_id} ({network_lang}): {e_parse}" ) traceback.print_exc() # Crear diccionario de error si el parser falla parsed_network_data = { "id": network_id, "language": network_lang, "logic": [], "error": f"Parser failed: {e_parse}", } else: # Lenguaje no soportado por ningún parser cargado print( f" Advertencia: Lenguaje de red '{network_lang}' no soportado por los parsers cargados." ) parsed_network_data = { "id": network_id, "language": network_lang, "logic": [], "error": f"Unsupported language: {network_lang}", } # --- Añadir Título y Comentario a la Red Parseada --- if parsed_network_data: # Usar get_multilingual_text de utils title_element = network_elem.xpath( ".//iface:MultilingualText[@CompositionName='Title']", namespaces=ns, ) parsed_network_data["title"] = ( get_multilingual_text(title_element[0]) if title_element else f"Network {network_id}" ) # Buscar comentario específico de la red comment_elem_net = network_elem.xpath( "./ObjectList/MultilingualText[@CompositionName='Comment']", namespaces=ns, ) if not comment_elem_net: # Fallback comment_elem_net = network_elem.xpath( ".//MultilingualText[@CompositionName='Comment']", namespaces=ns, ) parsed_network_data["comment"] = ( get_multilingual_text(comment_elem_net[0]) if comment_elem_net else "" ) # Añadir la red procesada (o con error) al resultado result["networks"].append(parsed_network_data) # --- Fin Bucle Redes --- if networks_processed_count == 0 and block_type_found != "GlobalDB": print( f"Advertencia: ObjectList para {block_type_found} sin SW.Blocks.CompileUnit." ) elif block_type_found == "GlobalDB": print("Paso 5: Saltando búsqueda de CompileUnits para GlobalDB (esperado).") else: print( f"Advertencia: No se encontró ObjectList para el bloque {block_type_found}." ) # --- Escribir JSON --- print("Paso 6: Escribiendo el resultado en el archivo JSON...") # Validaciones finales opcionales if not result["interface"]: print("ADVERTENCIA FINAL: 'interface' está vacía en el JSON.") if not result["networks"] and block_type_found != "GlobalDB": print("ADVERTENCIA FINAL: 'networks' está vacía en el JSON.") try: with open(json_filepath, "w", encoding="utf-8") as f: json.dump(result, f, indent=4, ensure_ascii=False) print("Paso 6: Escritura JSON completada.") print( f"Conversión finalizada. JSON guardado en: '{os.path.relpath(json_filepath)}'" ) return True # Indicar éxito except IOError as e: print( f"Error Crítico: No se pudo escribir JSON en '{json_filepath}'. Error: {e}" ) return False # Indicar fallo except TypeError as e: print( f"Error Crítico: Problema al serializar a JSON (posiblemente datos no serializables). Error: {e}" ) # Opcional: Imprimir una versión parcial o depurar 'result' # print("--- Datos antes de JSON DUMP (parcial) ---") # try: print(json.dumps({k: v for k, v in result.items() if k != 'networks'}, indent=2)) # Imprimir sin redes # except: print("No se pudo imprimir datos parciales.") return False # Indicar fallo except etree.XMLSyntaxError as e: print( f"Error Crítico: Sintaxis XML inválida en '{xml_filepath}'. Detalles: {e}" ) return False # Indicar fallo except Exception as e: print(f"Error Crítico: Error inesperado durante la conversión: {e}") traceback.print_exc() return False # Indicar fallo # --- Punto de Entrada Principal (__main__) --- if __name__ == "__main__": parser = argparse.ArgumentParser( description="Convert Simatic XML (LAD/FBD/SCL/STL/OB/DB) to simplified JSON using dynamic parsers." ) parser.add_argument( "xml_filepath", help="Path to the input XML file passed from the main script (x0_main.py).", ) args = parser.parse_args() xml_input_file = args.xml_filepath if not os.path.exists(xml_input_file): print( f"Error Crítico (x1): Archivo XML no encontrado: '{xml_input_file}'", file=sys.stderr, ) sys.exit(1) # --- Cargar Parsers Dinámicamente --- loaded_parsers = load_parsers() if not loaded_parsers: print("Error Crítico (x1): No se cargaron parsers. Abortando.", file=sys.stderr) sys.exit(1) # Derivar nombre de salida JSON xml_filename_base = os.path.splitext(os.path.basename(xml_input_file))[0] output_dir = os.path.dirname(xml_input_file) # Asegurarse que el directorio de salida exista (puede ser el mismo que el de entrada) 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)}'" ) # Llamar a la función de conversión principal success = convert_xml_to_json(xml_input_file, json_output_file, loaded_parsers) # Salir con código de error apropiado if success: sys.exit(0) # Éxito else: print( f"\nError durante la conversión de '{os.path.relpath(xml_input_file)}'.", file=sys.stderr, ) sys.exit(1) # Fallo