LadderToSCL/LadderToPython.py

640 lines
27 KiB
Python

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 <xml_file> [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}")