429 lines
15 KiB
Python
429 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Convertidor mejorado de código LAD (Ladder Diagram) de TwinCAT a pseudocódigo estructurado
|
|
Compatible con IEC61131-3
|
|
|
|
Autor: Asistente AI
|
|
Fecha: 2024
|
|
"""
|
|
|
|
import re
|
|
from typing import List, Dict, Any, Optional, Tuple
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
|
|
|
|
class LadElementType(Enum):
|
|
CONTACT = "CONTACT"
|
|
COIL = "COIL"
|
|
FUNCTION_BLOCK = "FUNCTION_BLOCK"
|
|
OPERATOR = "OPERATOR"
|
|
ASSIGNMENT = "ASSIGNMENT"
|
|
CONDITION = "CONDITION"
|
|
|
|
|
|
@dataclass
|
|
class LadVariable:
|
|
"""Representa una variable en LAD"""
|
|
name: str
|
|
negated: bool = False
|
|
value: str = ""
|
|
|
|
|
|
@dataclass
|
|
class LadExpression:
|
|
"""Representa una expresión en LAD"""
|
|
operator: str = ""
|
|
operands: List[str] = field(default_factory=list)
|
|
result_var: str = ""
|
|
|
|
|
|
@dataclass
|
|
class LadNetwork:
|
|
"""Representa una red LAD completa"""
|
|
id: int
|
|
comment: str = ""
|
|
contacts: List[LadVariable] = field(default_factory=list)
|
|
operators: List[str] = field(default_factory=list)
|
|
function_blocks: List[Dict] = field(default_factory=list)
|
|
expressions: List[LadExpression] = field(default_factory=list)
|
|
outputs: List[LadVariable] = field(default_factory=list)
|
|
assignments: List[Dict] = field(default_factory=list)
|
|
|
|
|
|
class EnhancedLadConverter:
|
|
"""Convertidor mejorado de LAD a código estructurado"""
|
|
|
|
def __init__(self):
|
|
self.networks: List[LadNetwork] = []
|
|
self.output_lines: List[str] = []
|
|
|
|
def parse_file(self, file_path: str) -> None:
|
|
"""Parse el archivo LAD completo"""
|
|
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
content = f.read()
|
|
|
|
# Encontrar la sección LAD
|
|
lad_start = content.find('_LD_BODY')
|
|
if lad_start == -1:
|
|
raise ValueError("No se encontró sección _LD_BODY en el archivo")
|
|
|
|
# Extraer solo la sección LAD hasta END_PROGRAM
|
|
lad_end = content.find('END_PROGRAM', lad_start)
|
|
if lad_end == -1:
|
|
lad_content = content[lad_start:]
|
|
else:
|
|
lad_content = content[lad_start:lad_end]
|
|
|
|
self._parse_networks(lad_content)
|
|
|
|
def _parse_networks(self, content: str) -> None:
|
|
"""Parse todas las redes del contenido LAD"""
|
|
lines = content.split('\n')
|
|
i = 0
|
|
network_id = 0
|
|
|
|
while i < len(lines):
|
|
if lines[i].strip() == '_NETWORK':
|
|
network_id += 1
|
|
i = self._parse_single_network(lines, i, network_id)
|
|
else:
|
|
i += 1
|
|
|
|
def _parse_single_network(self, lines: List[str], start_idx: int, network_id: int) -> int:
|
|
"""Parse una red individual"""
|
|
network = LadNetwork(id=network_id)
|
|
i = start_idx + 1
|
|
|
|
# Parse comment
|
|
if i < len(lines) and lines[i].strip() == '_COMMENT':
|
|
i, comment = self._extract_comment(lines, i)
|
|
network.comment = comment
|
|
|
|
# Parse network content
|
|
while i < len(lines):
|
|
line = lines[i].strip()
|
|
|
|
if line == '_NETWORK':
|
|
# Inicio de nueva red
|
|
break
|
|
elif line == '_LD_ASSIGN':
|
|
i = self._parse_assignment_block(lines, i, network)
|
|
elif line == '_LD_CONTACT':
|
|
i = self._parse_contact(lines, i, network)
|
|
elif line == '_LD_AND' or line == '_LD_OR':
|
|
network.operators.append(line.replace('_LD_', ''))
|
|
i += 1
|
|
elif line.startswith('_FUNCTIONBLOCK'):
|
|
i = self._parse_function_block_detailed(lines, i, network)
|
|
else:
|
|
i += 1
|
|
|
|
self.networks.append(network)
|
|
return i
|
|
|
|
def _extract_comment(self, lines: List[str], start_idx: int) -> Tuple[int, str]:
|
|
"""Extrae comentario de la red"""
|
|
i = start_idx + 1
|
|
comment_lines = []
|
|
|
|
while i < len(lines):
|
|
line = lines[i].strip()
|
|
if line == '_END_COMMENT':
|
|
break
|
|
if line and not line.startswith('_'):
|
|
comment_lines.append(line)
|
|
i += 1
|
|
|
|
return i + 1, ' '.join(comment_lines)
|
|
|
|
def _parse_contact(self, lines: List[str], start_idx: int, network: LadNetwork) -> int:
|
|
"""Parse un contacto"""
|
|
i = start_idx + 1
|
|
|
|
if i < len(lines):
|
|
contact_name = lines[i].strip()
|
|
i += 1
|
|
|
|
# Verificar polaridad
|
|
negated = False
|
|
if i < len(lines) and lines[i].strip() == '_NEGATIV':
|
|
negated = True
|
|
|
|
contact = LadVariable(name=contact_name, negated=negated)
|
|
network.contacts.append(contact)
|
|
|
|
return i + 1
|
|
|
|
def _parse_assignment_block(self, lines: List[str], start_idx: int, network: LadNetwork) -> int:
|
|
"""Parse un bloque de asignación completo"""
|
|
i = start_idx + 1
|
|
assignment_data = {
|
|
'conditions': [],
|
|
'target_var': '',
|
|
'source_expr': '',
|
|
'operator': '',
|
|
'operands': [],
|
|
'function_name': '',
|
|
'fb_params': []
|
|
}
|
|
|
|
while i < len(lines):
|
|
line = lines[i].strip()
|
|
|
|
if line == '_NETWORK' or line == 'ENABLELIST_END':
|
|
break
|
|
elif line == '_LD_CONTACT':
|
|
i += 1
|
|
if i < len(lines):
|
|
contact_name = lines[i].strip()
|
|
i += 1
|
|
negated = False
|
|
if i < len(lines) and lines[i].strip() == '_NEGATIV':
|
|
negated = True
|
|
i += 1
|
|
assignment_data['conditions'].append(LadVariable(contact_name, negated))
|
|
elif line.startswith('_FUNCTIONBLOCK'):
|
|
# Parse function block dentro de asignación
|
|
i, fb_data = self._parse_functionblock_in_assignment(lines, i)
|
|
assignment_data.update(fb_data)
|
|
elif line.startswith('_OPERATOR'):
|
|
i, op_data = self._parse_operator_in_assignment(lines, i)
|
|
assignment_data.update(op_data)
|
|
elif line.startswith('_OUTPUT'):
|
|
# Siguiente información es sobre la salida
|
|
i = self._parse_output_info(lines, i, assignment_data)
|
|
elif not line.startswith('_') and line and 'ENABLELIST' not in line:
|
|
# Variable o expresión
|
|
if not assignment_data['target_var']:
|
|
assignment_data['source_expr'] = line
|
|
else:
|
|
i += 1
|
|
|
|
if assignment_data['target_var'] or assignment_data['function_name']:
|
|
network.assignments.append(assignment_data)
|
|
|
|
return i
|
|
|
|
def _parse_functionblock_in_assignment(self, lines: List[str], start_idx: int) -> Tuple[int, Dict]:
|
|
"""Parse function block dentro de asignación"""
|
|
i = start_idx + 1
|
|
fb_data = {'function_name': '', 'fb_params': []}
|
|
|
|
if i < len(lines):
|
|
fb_data['function_name'] = lines[i].strip()
|
|
i += 1
|
|
|
|
# Buscar parámetros
|
|
while i < len(lines) and not lines[i].strip().startswith('_OUTPUT'):
|
|
line = lines[i].strip()
|
|
if line.startswith('_OPERAND'):
|
|
i += 2 # Saltar _EXPRESSION y _POSITIV/_NEGATIV
|
|
if i < len(lines):
|
|
param = lines[i].strip()
|
|
fb_data['fb_params'].append(param)
|
|
i += 1
|
|
|
|
return i, fb_data
|
|
|
|
def _parse_operator_in_assignment(self, lines: List[str], start_idx: int) -> Tuple[int, Dict]:
|
|
"""Parse operador dentro de asignación"""
|
|
i = start_idx + 1
|
|
op_data = {'operator': '', 'operands': []}
|
|
|
|
# Buscar operandos y operador
|
|
while i < len(lines) and not lines[i].strip().startswith('_OUTPUT'):
|
|
line = lines[i].strip()
|
|
|
|
if line.startswith('_OPERAND'):
|
|
i += 2 # Saltar _EXPRESSION y _POSITIV/_NEGATIV
|
|
if i < len(lines):
|
|
operand = lines[i].strip()
|
|
op_data['operands'].append(operand)
|
|
elif line in ['ADD', 'SUB', 'MUL', 'DIV', 'AND', 'OR', 'LT', 'GT', 'EQ', 'SEL', 'MOVE']:
|
|
op_data['operator'] = line
|
|
i += 1
|
|
|
|
return i, op_data
|
|
|
|
def _parse_output_info(self, lines: List[str], start_idx: int, assignment_data: Dict) -> int:
|
|
"""Parse información de salida"""
|
|
i = start_idx + 1
|
|
|
|
# Saltar información de salida hasta encontrar la variable
|
|
while i < len(lines):
|
|
line = lines[i].strip()
|
|
if not line.startswith('_') and line and 'ENABLELIST' not in line:
|
|
assignment_data['target_var'] = line
|
|
break
|
|
i += 1
|
|
|
|
return i + 1
|
|
|
|
def _parse_function_block_detailed(self, lines: List[str], start_idx: int, network: LadNetwork) -> int:
|
|
"""Parse function block detallado"""
|
|
i = start_idx + 1
|
|
fb_data = {'name': '', 'params': [], 'outputs': []}
|
|
|
|
if i < len(lines):
|
|
fb_data['name'] = lines[i].strip()
|
|
i += 1
|
|
|
|
# Parse parámetros y salidas
|
|
while i < len(lines):
|
|
line = lines[i].strip()
|
|
if line.startswith('_OPERAND'):
|
|
i += 2
|
|
if i < len(lines):
|
|
param = lines[i].strip()
|
|
fb_data['params'].append(param)
|
|
elif line.startswith('_OUTPUT'):
|
|
i += 3 # Saltar información de salida
|
|
if i < len(lines):
|
|
output = lines[i].strip()
|
|
fb_data['outputs'].append(output)
|
|
elif line == '_NETWORK':
|
|
break
|
|
i += 1
|
|
|
|
network.function_blocks.append(fb_data)
|
|
return i
|
|
|
|
def convert_to_structured(self) -> str:
|
|
"""Convierte las redes a código pseudo estructurado"""
|
|
self.output_lines = [
|
|
"// Código pseudo estructurado generado desde LAD TwinCAT",
|
|
"// Compatible con IEC61131-3",
|
|
"PROGRAM Input_Converted",
|
|
""
|
|
]
|
|
|
|
for network in self.networks:
|
|
self._convert_network_to_structured(network)
|
|
|
|
self.output_lines.append("END_PROGRAM")
|
|
return '\n'.join(self.output_lines)
|
|
|
|
def _convert_network_to_structured(self, network: LadNetwork) -> None:
|
|
"""Convierte una red a código estructurado"""
|
|
self.output_lines.append(f" // Red {network.id}")
|
|
|
|
if network.comment:
|
|
self.output_lines.append(f" // {network.comment}")
|
|
|
|
# Procesar asignaciones de la red
|
|
for assignment in network.assignments:
|
|
self._convert_assignment_to_structured(assignment)
|
|
|
|
# Procesar function blocks independientes
|
|
for fb in network.function_blocks:
|
|
self._convert_function_block_to_structured(fb)
|
|
|
|
self.output_lines.append("")
|
|
|
|
def _convert_assignment_to_structured(self, assignment: Dict) -> None:
|
|
"""Convierte una asignación a código estructurado"""
|
|
conditions = assignment.get('conditions', [])
|
|
target_var = assignment.get('target_var', '')
|
|
source_expr = assignment.get('source_expr', '')
|
|
operator = assignment.get('operator', '')
|
|
operands = assignment.get('operands', [])
|
|
function_name = assignment.get('function_name', '')
|
|
fb_params = assignment.get('fb_params', [])
|
|
|
|
# Construir condiciones
|
|
condition_str = ""
|
|
if conditions:
|
|
condition_parts = []
|
|
for cond in conditions:
|
|
if cond.negated:
|
|
condition_parts.append(f"NOT {cond.name}")
|
|
else:
|
|
condition_parts.append(cond.name)
|
|
condition_str = " AND ".join(condition_parts)
|
|
|
|
# Construir la expresión de asignación
|
|
if function_name:
|
|
# Function block call
|
|
params_str = ", ".join(fb_params) if fb_params else ""
|
|
if target_var:
|
|
expr = f"{target_var} := {function_name}({params_str})"
|
|
else:
|
|
expr = f"{function_name}({params_str})"
|
|
elif operator and operands:
|
|
# Operación matemática/lógica
|
|
if len(operands) == 2:
|
|
if operator == "SEL":
|
|
expr = f"{target_var} := SEL({operands[0]}, {operands[1]}, condition)"
|
|
else:
|
|
expr = f"{target_var} := {operands[0]} {operator} {operands[1]}"
|
|
elif len(operands) == 1:
|
|
if operator == "MOVE":
|
|
expr = f"{target_var} := {operands[0]}"
|
|
else:
|
|
expr = f"{target_var} := {operator}({operands[0]})"
|
|
else:
|
|
expr = f"{target_var} := {operator}({', '.join(operands)})"
|
|
elif source_expr and target_var:
|
|
# Asignación simple
|
|
expr = f"{target_var} := {source_expr}"
|
|
else:
|
|
return # Sin información suficiente
|
|
|
|
# Generar código estructurado
|
|
if condition_str:
|
|
self.output_lines.append(f" IF {condition_str} THEN")
|
|
self.output_lines.append(f" {expr};")
|
|
self.output_lines.append(f" END_IF;")
|
|
else:
|
|
self.output_lines.append(f" {expr};")
|
|
|
|
def _convert_function_block_to_structured(self, fb: Dict) -> None:
|
|
"""Convierte un function block a código estructurado"""
|
|
name = fb.get('name', '')
|
|
params = fb.get('params', [])
|
|
|
|
if name:
|
|
params_str = ", ".join(params) if params else ""
|
|
self.output_lines.append(f" {name}({params_str});")
|
|
|
|
def save_to_file(self, output_path: str) -> None:
|
|
"""Guarda el código estructurado a un archivo"""
|
|
structured_code = self.convert_to_structured()
|
|
|
|
with open(output_path, 'w', encoding='utf-8') as f:
|
|
f.write(structured_code)
|
|
|
|
print(f"Código pseudo estructurado guardado en: {output_path}")
|
|
|
|
|
|
def main():
|
|
"""Función principal"""
|
|
import sys
|
|
|
|
if len(sys.argv) != 3:
|
|
print("Uso: python lad_to_pseudocode_converter_enhanced.py <archivo_entrada.EXP> <archivo_salida.txt>")
|
|
return
|
|
|
|
input_file = sys.argv[1]
|
|
output_file = sys.argv[2]
|
|
|
|
try:
|
|
converter = EnhancedLadConverter()
|
|
converter.parse_file(input_file)
|
|
converter.save_to_file(output_file)
|
|
|
|
print(f"Conversión completada exitosamente!")
|
|
print(f"Redes procesadas: {len(converter.networks)}")
|
|
|
|
# Mostrar estadísticas por red
|
|
for network in converter.networks[:5]: # Primeras 5 redes
|
|
print(f"Red {network.id}: {len(network.assignments)} asignaciones, {len(network.contacts)} contactos")
|
|
|
|
except Exception as e:
|
|
print(f"Error durante la conversión: {e}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |