307 lines
12 KiB
Python
307 lines
12 KiB
Python
import xml.etree.ElementTree as ET
|
|
import json
|
|
import os
|
|
|
|
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
|
|
|
|
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)
|
|
|
|
# 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": []
|
|
}
|
|
|
|
# Extraer las llamadas a funciones (dentro del elemento Call)
|
|
call_elements = network.findall(".//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 accesos a variables (dentro del elemento Access)
|
|
access_elements = network.findall(".//Access[@Scope='GlobalVariable']")
|
|
for access in access_elements:
|
|
var_name = self.extract_variable_name(access)
|
|
if var_name and var_name not in result["variables"]:
|
|
result["variables"].append(var_name)
|
|
|
|
# Buscar elementos lógicos (dentro del elemento Part)
|
|
part_elements = network.findall(".//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)
|
|
|
|
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 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()
|
|
else:
|
|
return "Unknown output format. Supported formats: markdown, json, call_tree"
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
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)
|
|
|
|
output_file = os.path.splitext(file_path)[0] + "." + ("json" if output_format.lower() == "json" else "md")
|
|
with open(output_file, "w", encoding="utf-8") as f:
|
|
f.write(result)
|
|
|
|
print(f"Documentation generated: {output_file}")
|