import xml.etree.ElementTree as ET import json import os import sys class SiemensLadderDoc: def __init__(self): self.function_calls_map = {} # Mapa de función -> llamadas self.all_variables = set() # Todas las variables usadas self.all_function_calls = set() # Todas las llamadas a funciones self.variable_details = {} # Detalles y tipos de variables def extract_semantics(self, xml_file): """Extrae la semántica principal del archivo XML de Siemens""" try: tree = ET.parse(xml_file) root = tree.getroot() except Exception as e: return f"Error processing XML: {str(e)}" # Extraer información del bloque block_info = {} block = root.find(".//SW.Blocks.FC") or root.find(".//SW.Blocks.FB") if block is None: return "No function block found in the file" # Extraer atributos básicos attr_list = block.find("AttributeList") if attr_list is not None: for attr in attr_list: if attr.tag in ["Name", "Number", "ProgrammingLanguage"]: block_info[attr.tag] = attr.text # Extraer interface (inputs, outputs, temp variables) interface_section = block.find(".//Interface") if interface_section is not None: block_info["Interface"] = self.extract_interface(interface_section) # Guardar detalles de los tipos de variables for section_name, members in block_info["Interface"].items(): for member in members: var_name = member["Name"] var_type = member["Datatype"] self.variable_details[var_name] = { "type": var_type, "section": section_name, } # Procesar todas las redes compile_units = block.findall(".//SW.Blocks.CompileUnit") networks = [] block_name = block_info.get("Name", "Unknown") self.function_calls_map[block_name] = [] for i, unit in enumerate(compile_units): network = self.process_network(unit, i + 1, block_name) networks.append(network) # Actualizar todas las variables y llamadas for var in network["variables"]: self.all_variables.add(var) for call in network["calls"]: self.all_function_calls.add(call) if call not in self.function_calls_map[block_name]: self.function_calls_map[block_name].append(call) block_info["Networks"] = networks # Añadir resúmenes globales block_info["all_variables"] = sorted(list(self.all_variables)) block_info["all_function_calls"] = sorted(list(self.all_function_calls)) return block_info def extract_interface(self, interface): """Extrae la información de interfaz (entradas, salidas, variables temporales)""" result = {} # Buscar secciones directamente for section_name in ["Input", "Output", "InOut", "Temp", "Constant", "Return"]: section = interface.find(f".//Section[@Name='{section_name}']") if section is not None: result[section_name] = [] for member in section.findall("./Member"): result[section_name].append( {"Name": member.get("Name"), "Datatype": member.get("Datatype")} ) return result def process_network(self, network, index, parent_block): """Procesa una red para extraer sus elementos principales""" result = { "index": index, "title": self.get_network_title(network), "calls": [], "variables": [], "logic_elements": [], "wire_connections": [], # Nueva propiedad para conexiones } # Extraer el XML de la red para analizar la lógica network_source = network.find(".//NetworkSource") if network_source is not None: flg_net = network_source.find( ".//{http://www.siemens.com/automation/Openness/SW/NetworkSource/FlgNet/v4}FlgNet" ) if flg_net is not None: # Extraer partes y conexiones parts = flg_net.find( ".//{http://www.siemens.com/automation/Openness/SW/NetworkSource/FlgNet/v4}Parts" ) wires = flg_net.find( ".//{http://www.siemens.com/automation/Openness/SW/NetworkSource/FlgNet/v4}Wires" ) # Analizar partes if parts is not None: # Buscar accesos a variables access_elements = parts.findall( ".//{http://www.siemens.com/automation/Openness/SW/NetworkSource/FlgNet/v4}Access" ) for access in access_elements: scope = access.get("Scope") uid = access.get("UId") var_name = self.extract_variable_name(access) if var_name and var_name not in result["variables"]: result["variables"].append(var_name) # Buscar llamadas a funciones call_elements = parts.findall( ".//{http://www.siemens.com/automation/Openness/SW/NetworkSource/FlgNet/v4}Call" ) for call in call_elements: call_info = call.find("CallInfo") if call_info is not None: call_name = call_info.get("Name", "Unknown") if call_name not in result["calls"]: result["calls"].append(call_name) # Registra en el mapa de llamadas para el árbol if call_name not in self.function_calls_map: self.function_calls_map[call_name] = [] # Buscar elementos lógicos (dentro del elemento Part) part_elements = parts.findall( ".//{http://www.siemens.com/automation/Openness/SW/NetworkSource/FlgNet/v4}Part" ) for part in part_elements: part_name = part.get("Name") if part_name and part_name not in result["logic_elements"]: result["logic_elements"].append(part_name) # Analizar conexiones if wires is not None: wire_elements = wires.findall( ".//{http://www.siemens.com/automation/Openness/SW/NetworkSource/FlgNet/v4}Wire" ) for wire in wire_elements: uid = wire.get("UId") # Aquí podríamos guardar más detalles de las conexiones si se necesita result["wire_connections"].append(uid) return result def get_network_title(self, network): """Extrae el título de una red - Versión corregida sin XPath avanzado""" # Primer intento: Buscar título en italiano title_sections = network.findall( ".//MultilingualText[@CompositionName='Title']" ) for title_section in title_sections: items = title_section.findall(".//MultilingualTextItem") for item in items: attr_list = item.find("AttributeList") if attr_list is not None: culture = attr_list.find("Culture") text = attr_list.find("Text") # Priorizar italiano if ( culture is not None and culture.text == "it-IT" and text is not None and text.text ): return text.text # Cualquier otro texto if text is not None and text.text: return text.text return "Unnamed Network" def extract_variable_name(self, access_element): """Extrae el nombre de variable desde un elemento Access""" symbol = access_element.find("Symbol") if symbol is not None: components = [] for component in symbol.findall("Component"): components.append(component.get("Name", "")) return ".".join(components) return None def generate_call_tree(self): """Genera un árbol de llamadas en formato texto""" if not self.function_calls_map: return "No function calls found." def build_tree(function_name, depth=0, visited=None): if visited is None: visited = set() if function_name in visited: return f"{' ' * depth}└─ {function_name} (recursive call)\n" visited.add(function_name) result = f"{' ' * depth}└─ {function_name}\n" if function_name in self.function_calls_map: for called_function in self.function_calls_map[function_name]: result += build_tree(called_function, depth + 1, visited.copy()) return result tree = "Function Call Tree:\n" # Comenzar solo con los bloques principales (que no son llamados por otros) root_functions = set(self.function_calls_map.keys()) for func, calls in self.function_calls_map.items(): for call in calls: if call in root_functions: root_functions.remove(call) for main_function in root_functions: tree += build_tree(main_function) tree += "\n" return tree def generate_markdown(self, block_info): """Genera documentación en formato markdown""" if isinstance(block_info, str): return f"# Error\n\n{block_info}" block_name = block_info.get("Name", "Unknown") block_number = block_info.get("Number", "Unknown") md = f"# Function Block: {block_name} (FC{block_number})\n\n" md += f"**Programming Language:** {block_info.get('ProgrammingLanguage', 'Unknown')}\n\n" # Tabla de Contenido md += "## Table of Contents\n\n" md += "1. [Interface](#interface)\n" md += "2. [Summary of Variables](#summary-of-variables)\n" md += "3. [Summary of Function Calls](#summary-of-function-calls)\n" md += "4. [Networks Detail](#networks-detail)\n" md += "5. [Call Tree](#call-tree)\n\n" # Información de interfaz md += "## Interface\n\n" if "Interface" in block_info and block_info["Interface"]: for section_name, members in block_info["Interface"].items(): if members: md += f"### {section_name}\n\n" md += "| Name | Datatype |\n|------|----------|\n" for member in members: md += f"| {member['Name']} | {member['Datatype']} |\n" md += "\n" else: md += "_No interface information available_\n\n" # Resumen de variables md += "## Summary of Variables\n\n" if block_info.get("all_variables"): md += "All global variables used in this function block:\n\n" for var in block_info.get("all_variables"): md += f"- `{var}`\n" else: md += "_No variables found_\n\n" # Resumen de llamadas a funciones md += "\n## Summary of Function Calls\n\n" if block_info.get("all_function_calls"): md += "All functions called by this function block:\n\n" for func in block_info.get("all_function_calls"): md += f"- `{func}`\n" else: md += "_No function calls found_\n\n" # Detalles de redes networks = block_info.get("Networks", []) md += f"\n## Networks Detail ({len(networks)} networks)\n\n" for network in networks: md += f"### Network {network['index']}: {network['title']}\n\n" if network["calls"]: md += "**Function Calls:**\n" for call in network["calls"]: md += f"- `{call}`\n" md += "\n" if network["logic_elements"]: md += "**Logic Elements:**\n" for element in network["logic_elements"]: md += f"- {element}\n" md += "\n" if network["variables"]: md += "**Variables Used:**\n" for var in network["variables"]: md += f"- `{var}`\n" md += "\n" # Árbol de llamadas md += "## Call Tree\n\n" md += "```\n" md += self.generate_call_tree() md += "```\n" return md def generate_python_class(self, block_info): """Genera una representación en clase Python del bloque de función con semántica completa""" if isinstance(block_info, str): return f"# Error\n\n# {block_info}" block_name = block_info.get("Name", "Unknown") block_number = block_info.get("Number", "Unknown") py_code = f"class {block_name}:\n" py_code += " def __init__(self):\n" py_code += ( f" # Initialize variables for {block_name} (FC{block_number})\n" ) # Variables de interfaz if "Interface" in block_info: interface = block_info["Interface"] for section_name, members in interface.items(): if members: py_code += f" # {section_name} variables\n" for member in members: var_name = member["Name"] var_type = member["Datatype"] default_value = "None" if var_type == "Bool": default_value = "False" elif var_type in ["Int", "DInt", "Word", "DWord"]: default_value = "0" elif var_type == "Real": default_value = "0.0" py_code += ( f" self.{var_name} = {default_value} # {var_type}\n" ) py_code += "\n" # Variables globales usadas if block_info.get("all_variables"): py_code += " # Global variables used\n" for var in block_info.get("all_variables"): if ( "." in var ): # Si es una variable estructurada, solo inicializar la estructura principal struct_name = var.split(".")[0] if not any( v.startswith(f"self.{struct_name} =") for v in py_code.split("\n") ): py_code += f" self.{struct_name} = None\n" else: if not any( v.startswith(f"self.{var} =") for v in py_code.split("\n") ): # Buscar tipo de variable si existe en detalles default_value = "None" if var in self.variable_details: var_type = self.variable_details[var].get("type") if var_type == "Bool": default_value = "False" elif var_type in ["Int", "DInt", "Word", "DWord"]: default_value = "0" elif var_type == "Real": default_value = "0.0" py_code += f" self.{var} = {default_value}\n" py_code += "\n" # Agregar variables típicas para los sistemas blender si no se han añadido ya common_vars = [ ("AUX_FALSE", "False"), ("HMI_PID", "None"), ("Filler_Head_Variables", "None"), ("gIN_VoltageOk", "False"), ("M19000", "False"), ("gEmergencyPressed", "False"), ] for var_name, default_value in common_vars: if not any(v.startswith(f"self.{var_name} =") for v in py_code.split("\n")): py_code += f" self.{var_name} = {default_value}\n" # Método run principal py_code += " def run(self):\n" networks = block_info.get("Networks", []) if not networks: py_code += " pass # No networks found\n" for network in networks: title = ( network["title"] if network["title"] != "Unnamed Network" else f"Network {network['index']}" ) py_code += f" # Network {network['index']}: {title}\n" # Implementar lógica basada en los elementos del network if network["calls"]: for call in network["calls"]: py_code += f" self.{call}()\n" # Si es un llamado que requerimos implementar específicamente if call == "Clock_Signal": py_code += ( " # Generación de señales de reloj para el sistema\n" ) elif call == "BlenderCtrl_MachineInit": py_code += " # Inicialización de la máquina mezcladora\n" # Representar la lógica básica de contactos y bobinas element_logic = self._generate_element_logic(network) if element_logic: py_code += element_logic # Generar una descripción semántica basada en elementos y variables if network["variables"] and not network["calls"]: semantics = self._generate_network_semantics(network) if semantics: py_code += semantics py_code += "\n" # Métodos para cada función llamada if block_info.get("all_function_calls"): for func_call in block_info.get("all_function_calls"): py_code += f" def {func_call}(self):\n" # Implementaciones especiales para funciones conocidas if func_call == "Clock_Signal": py_code += " # Clock generation implementation\n" py_code += " # This generates the system timing signals\n" py_code += " self.Clock_10ms = not self.Clock_10ms\n" py_code += " \n" py_code += " # Generate 100ms and 1s clock signals\n" py_code += " if self.Clock_Counter % 10 == 0:\n" py_code += " self.Clock_100ms = not self.Clock_100ms\n" py_code += " \n" py_code += " if self.Clock_Counter % 100 == 0:\n" py_code += " self.Clock_1s = not self.Clock_1s\n" py_code += " self.Clock_Counter = 0\n" py_code += " \n" py_code += " self.Clock_Counter += 1\n" elif func_call == "BlenderCtrl_MachineInit": py_code += " # Initialize blender machine state\n" py_code += " if not self.MachineInitialized:\n" py_code += " self.MachineState = 0 # IDLE state\n" py_code += " self.SystemError = False\n" py_code += " self.MachineInitialized = True\n" else: py_code += f" # Implementation of {func_call}\n" py_code += " pass\n" py_code += "\n" return py_code def _generate_element_logic(self, network): """Genera código Python para la lógica de los elementos en una red""" logic_code = "" elements = network["logic_elements"] variables = network["variables"] # Lógica para contactos, bobinas y bloques BLKMOV if "Contact" in elements and variables: # Si hay contacto y bobina, crear una condición if if ("Coil" in elements or "RCoil" in elements) and len(variables) >= 2: input_var = variables[0] output_var = variables[-1] logic_code += f" if self.{input_var}:\n" if "RCoil" in elements: # Reset Coil - establece en False logic_code += ( f" self.{output_var} = False # Reset coil\n" ) else: logic_code += f" self.{output_var} = True\n" # Contacto negado (NBox) elif "NBox" in elements and len(variables) >= 2: input_var = variables[0] output_var = variables[-1] logic_code += f" if not self.{input_var}:\n" logic_code += f" self.{output_var} = True\n" # Operación OR elif "O" in elements and len(variables) >= 3: inputs = variables[:-1] # Todos excepto el último son entradas output = variables[-1] conditions = [] for var in inputs: conditions.append(f"self.{var}") logic_code += f" if {' or '.join(conditions)}:\n" logic_code += f" self.{output} = True\n" # Bloques Move y BLKMOV if ("Move" in elements or "BLKMOV" in elements) and len(variables) >= 2: source = variables[0] target = variables[-1] # Si el origen tiene un punto, es una estructura if "." in source and "." in target: src_struct = source.split(".")[0] src_field = source.split(".")[1] tgt_struct = target.split(".")[0] tgt_field = target.split(".")[1] logic_code += f" # Block move operation\n" logic_code += f" if self.{src_struct} is not None and self.{tgt_struct} is not None:\n" logic_code += f" self.{target} = self.{source}\n" else: logic_code += f" # Move operation\n" logic_code += f" self.{target} = self.{source}\n" return logic_code def _generate_network_semantics(self, network): """Genera una descripción semántica para una red basada en sus elementos y variables""" elements = network["logic_elements"] variables = network["variables"] semantics = "" # Verificar elementos específicos y generar código semántico if "Emergency" in " ".join(variables) or "EmergencyPressed" in " ".join( variables ): semantics += " # Control de parada de emergencia\n" if len(variables) >= 2: in_var = variables[0] out_var = variables[-1] semantics += f" if self.{in_var} and not self.M19000:\n" semantics += f" self.{out_var} = True\n" elif "Filler_Head" in " ".join(variables) or "FillerHead" in " ".join( variables ): semantics += " # Procesamiento de variables de cabezal de llenado\n" semantics += " if self.AUX_FALSE:\n" semantics += " # Implementación del BLKMOV para cabezal\n" semantics += " self.Filler_Head_Variables.FillerHead = self.HMI_PID.PPM303\n" semantics += " self.Block_Move_Err = self.resultado_operacion\n" elif "Pressure" in " ".join(variables) or "CO2" in " ".join(variables): semantics += " # Verificación de presión de CO2 y aire\n" semantics += " if self.Air_Pressure_OK and self.CO2_Pressure_OK:\n" semantics += " self.System_Pressure_OK = True\n" elif "Temperature" in " ".join(variables) or "Temp" in " ".join(variables): semantics += ( " # Control de temperatura del sistema de enfriamiento\n" ) semantics += " if self.Temperature_Current > self.Temperature_Max:\n" semantics += " self.Temperature_Alarm = True\n" elif "Tank" in " ".join(variables) or "Level" in " ".join(variables): semantics += " # Monitoreo de nivel de tanque\n" semantics += " if self.Tank_Level < self.Tank_Level_Min:\n" semantics += " self.Tank_Level_Low = True\n" elif "Reset" in " ".join(variables) and "Totalizer" in " ".join(variables): semantics += " # Reseteo de totalizador\n" semantics += " if self.Reset_Command:\n" semantics += " self.Totalizer_Value = 0\n" semantics += " self.Reset_Complete = True\n" elif "HMI" in " ".join(variables) or "Manual" in " ".join(variables): semantics += " # Control de interfaz HMI y modo manual\n" semantics += " if self.HMI_Manual_Mode_Requested:\n" semantics += " self.System_In_Manual_Mode = True\n" semantics += " self.System_In_Auto_Mode = False\n" return semantics def process_siemens_file(file_path, output_format="markdown"): """Procesa un archivo Siemens PLC y genera la documentación""" extractor = SiemensLadderDoc() block_info = extractor.extract_semantics(file_path) if output_format.lower() == "json": return json.dumps(block_info, indent=2) elif output_format.lower() == "markdown": return extractor.generate_markdown(block_info) elif output_format.lower() == "call_tree": extractor.extract_semantics(file_path) return extractor.generate_call_tree() elif output_format.lower() == "python": return extractor.generate_python_class(block_info) else: return "Unknown output format. Supported formats: markdown, json, call_tree, python" if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: python script.py [output_format]") sys.exit(1) file_path = sys.argv[1] output_format = sys.argv[2] if len(sys.argv) > 2 else "markdown" result = process_siemens_file(file_path, output_format) extension = ( "py" if output_format.lower() == "python" else "json" if output_format.lower() == "json" else "md" ) output_file = os.path.splitext(file_path)[0] + "." + extension with open(output_file, "w", encoding="utf-8") as f: f.write(result) print(f"Documentation generated: {output_file}")