1685 lines
66 KiB
Python
1685 lines
66 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Convertidor simplificado de LAD TwinCAT a pseudocódigo estructurado
|
|
Versión mejorada con SymPy para optimización de expresiones lógicas
|
|
"""
|
|
|
|
import re
|
|
import sympy
|
|
import os
|
|
import sys
|
|
import glob
|
|
from sympy import symbols, And, Or, Not, simplify
|
|
from sympy.logic.boolalg import to_dnf
|
|
|
|
# Configurar el path al directorio raíz del proyecto
|
|
script_root = os.path.dirname(
|
|
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
|
)
|
|
sys.path.append(script_root)
|
|
|
|
# Importar la función de configuración
|
|
from backend.script_utils import load_configuration
|
|
|
|
|
|
class SymbolManager:
|
|
"""Gestor de símbolos para SymPy"""
|
|
|
|
def __init__(self):
|
|
self._symbol_cache = {}
|
|
|
|
def get_symbol(self, name):
|
|
"""Obtener o crear símbolo SymPy"""
|
|
if name not in self._symbol_cache:
|
|
# Limpiar nombre para SymPy (sin espacios, caracteres especiales)
|
|
clean_name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
|
|
self._symbol_cache[name] = symbols(clean_name)
|
|
return self._symbol_cache[name]
|
|
|
|
|
|
class SimpleLadConverter:
|
|
"""Convertidor simplificado de LAD a código estructurado"""
|
|
|
|
def __init__(self):
|
|
self.networks = []
|
|
self.current_network_id = 0
|
|
self.symbol_manager = SymbolManager()
|
|
self.sympy_expressions = {} # Mapeo de network_id -> sympy expression
|
|
|
|
# Nuevas propiedades para estructura SCL completa
|
|
self.program_name = ""
|
|
self.program_type = (
|
|
"PROGRAM" # PROGRAM, FUNCTION, FUNCTION_BLOCK, TYPE, VAR_GLOBAL_LIST
|
|
)
|
|
self.program_path = ""
|
|
self.var_sections = {} # VAR, VAR_INPUT, VAR_OUTPUT, etc.
|
|
self.actions = {} # Diccionario de ACTIONs
|
|
self.st_main_code = None # Código ST del programa principal
|
|
self.type_content = None # Contenido completo de TYPE
|
|
|
|
def parse_file(self, file_path):
|
|
"""Parse el archivo LAD completo incluyendo variables y ACTIONs"""
|
|
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
content = f.read()
|
|
|
|
# Guardar contenido original para extraer información adicional
|
|
self._original_content = content
|
|
|
|
# Extraer información del header
|
|
self._parse_header_info(content)
|
|
|
|
# Extraer declaraciones de variables
|
|
self._parse_variable_declarations(content)
|
|
|
|
# Verificar si es un TYPE (solo datos) o VAR_GLOBAL_LIST
|
|
if hasattr(self, "program_type") and self.program_type == "TYPE":
|
|
print("Archivo TYPE detectado - Extrayendo declaraciones")
|
|
self._extract_type_content(content)
|
|
elif hasattr(self, "program_type") and self.program_type == "VAR_GLOBAL_LIST":
|
|
print(
|
|
"Archivo de variables globales detectado - Extrayendo todas las secciones"
|
|
)
|
|
self._extract_global_variables(content)
|
|
else:
|
|
# Encontrar sección LAD
|
|
lad_start = content.find("_LD_BODY")
|
|
if lad_start != -1:
|
|
# Extraer contenido LAD hasta ACTION o END_PROGRAM
|
|
action_start = content.find("\nACTION", lad_start)
|
|
end_program = content.find("\nEND_PROGRAM", lad_start)
|
|
|
|
lad_end = action_start if action_start != -1 else end_program
|
|
if lad_end == -1:
|
|
lad_end = len(content)
|
|
|
|
lad_content = content[lad_start:lad_end]
|
|
lines = lad_content.split("\n")
|
|
self._parse_networks(lines)
|
|
else:
|
|
print("No se encontró _LD_BODY - Asumiendo código ST")
|
|
# Extraer código ST del programa principal
|
|
self._extract_st_code(content)
|
|
|
|
# Extraer ACTIONs
|
|
self._parse_actions(content)
|
|
|
|
def _parse_header_info(self, content):
|
|
"""Extraer información del header del programa"""
|
|
# Buscar PATH y nombre del programa
|
|
path_match = re.search(r"\(\* @PATH := \'([^\']+)\' \*\)", content)
|
|
if path_match:
|
|
self.program_path = path_match.group(1)
|
|
|
|
# Verificar si es un archivo de variables globales
|
|
global_var_match = re.search(
|
|
r"\(\*\s*@GLOBAL_VARIABLE_LIST\s*:=\s*(\w+)\s*\*\)", content
|
|
)
|
|
if global_var_match:
|
|
self.program_type = "VAR_GLOBAL_LIST"
|
|
self.program_name = global_var_match.group(1)
|
|
print(f"Archivo de variables globales detectado: {self.program_name}")
|
|
else:
|
|
# Buscar nombre del programa/function_block/function/type
|
|
program_match = re.search(
|
|
r"(PROGRAM|FUNCTION_BLOCK|FUNCTION|TYPE)\s+(\w+)", content
|
|
)
|
|
if program_match:
|
|
self.program_type = program_match.group(1)
|
|
self.program_name = program_match.group(2)
|
|
|
|
print(f"Programa encontrado: {self.program_name}")
|
|
if self.program_path:
|
|
print(f"Path: {self.program_path}")
|
|
|
|
def _parse_variable_declarations(self, content):
|
|
"""Extraer todas las declaraciones de variables"""
|
|
# Buscar todas las secciones VAR
|
|
var_patterns = [
|
|
(r"VAR_INPUT(.*?)END_VAR", "VAR_INPUT"),
|
|
(r"VAR_OUTPUT(.*?)END_VAR", "VAR_OUTPUT"),
|
|
(r"VAR_IN_OUT(.*?)END_VAR", "VAR_IN_OUT"),
|
|
(r"VAR\s+(.*?)END_VAR", "VAR"),
|
|
]
|
|
|
|
for pattern, var_type in var_patterns:
|
|
matches = re.findall(pattern, content, re.DOTALL | re.IGNORECASE)
|
|
if matches:
|
|
variables = []
|
|
for match in matches:
|
|
# Parsear cada línea de variable
|
|
for line in match.split("\n"):
|
|
line = line.strip()
|
|
if line and not line.startswith("(*") and ":" in line:
|
|
# Limpiar comentarios inline
|
|
if "(*" in line:
|
|
line = line[: line.find("(*")]
|
|
variables.append(line.strip())
|
|
|
|
if variables:
|
|
self.var_sections[var_type] = variables
|
|
print(f"Variables {var_type}: {len(variables)} encontradas")
|
|
|
|
def _parse_networks(self, lines):
|
|
"""Parse todas las redes"""
|
|
i = 0
|
|
expected_networks = 0
|
|
|
|
# Buscar cuántas redes se esperan
|
|
for line in lines:
|
|
if line.strip().startswith("_NETWORKS :"):
|
|
expected_networks = int(line.strip().split(":")[1].strip())
|
|
print(f"Se esperan {expected_networks} redes según el archivo")
|
|
break
|
|
|
|
while i < len(lines):
|
|
if lines[i].strip() == "_NETWORK":
|
|
self.current_network_id += 1
|
|
print(f"Procesando red {self.current_network_id}...")
|
|
i = self._parse_network(lines, i)
|
|
else:
|
|
i += 1
|
|
|
|
if len(self.networks) != expected_networks:
|
|
print(
|
|
f"ADVERTENCIA: Se esperaban {expected_networks} redes pero solo se parsearon {len(self.networks)}"
|
|
)
|
|
print("Esto puede indicar redes con _EMPTY o estructuras no reconocidas")
|
|
|
|
def _parse_network(self, lines, start_idx):
|
|
"""Parse una red individual con soporte mejorado para operadores LAD"""
|
|
network = {
|
|
"id": self.current_network_id,
|
|
"comment": "",
|
|
"logic": None,
|
|
"target": "",
|
|
"function_blocks": [],
|
|
}
|
|
|
|
i = start_idx + 1
|
|
|
|
# Parse comentario
|
|
if i < len(lines) and lines[i].strip() == "_COMMENT":
|
|
i, comment = self._parse_comment(lines, i)
|
|
network["comment"] = comment
|
|
|
|
# Parse contenido de la red
|
|
while i < len(lines):
|
|
line = lines[i].strip()
|
|
|
|
if line == "_NETWORK":
|
|
break
|
|
elif line == "_LD_ASSIGN":
|
|
i += 1
|
|
# Parsear la lógica LAD después de _LD_ASSIGN
|
|
i, logic = self._parse_lad_expression(lines, i)
|
|
network["logic"] = logic
|
|
elif line.startswith("_OUTPUT"):
|
|
# Buscar variable de salida
|
|
i += 1
|
|
while i < len(lines) and lines[i].strip().startswith("_"):
|
|
i += 1
|
|
if i < len(lines) and lines[i].strip() and "ENABLELIST" not in lines[i]:
|
|
network["target"] = lines[i].strip()
|
|
i += 1
|
|
else:
|
|
i += 1
|
|
|
|
self.networks.append(network)
|
|
print(f" Red {network['id']} agregada. Total redes: {len(self.networks)}")
|
|
if network["logic"]:
|
|
print(
|
|
f" Con lógica: {network['logic']['type']} - {network['logic'].get('name', 'Sin nombre')}"
|
|
)
|
|
print(f" Target: '{network['target']}'")
|
|
return i
|
|
|
|
def _parse_lad_expression(self, lines, start_idx):
|
|
"""Parse una expresión LAD recursivamente"""
|
|
i = start_idx
|
|
|
|
while i < len(lines):
|
|
line = lines[i].strip()
|
|
|
|
if line == "_LD_AND":
|
|
return self._parse_and_expression(lines, i + 1)
|
|
elif line == "_LD_OR":
|
|
return self._parse_or_expression(lines, i + 1)
|
|
elif line == "_LD_CONTACT":
|
|
return self._parse_contact(lines, i + 1)
|
|
elif line.startswith("_FUNCTIONBLOCK"):
|
|
return self._parse_function_block(lines, i)
|
|
elif line == "_EMPTY":
|
|
# Red vacía que puede contener funciones después
|
|
print(f" Encontrada _EMPTY dentro de _LD_ASSIGN en línea {i}")
|
|
i += 1
|
|
# Buscar funciones en las siguientes líneas
|
|
i, empty_logic = self._parse_empty_network(lines, i)
|
|
return i, empty_logic
|
|
elif line.startswith("_OUTPUT") or line == "ENABLELIST : 0":
|
|
break
|
|
else:
|
|
i += 1
|
|
|
|
return i, None
|
|
|
|
def _parse_and_expression(self, lines, start_idx):
|
|
"""Parse una expresión AND"""
|
|
i = start_idx
|
|
operands = []
|
|
|
|
# Buscar operadores
|
|
if i < len(lines) and lines[i].strip().startswith("_LD_OPERATOR"):
|
|
# Extraer número de operandos
|
|
operator_line = lines[i].strip()
|
|
num_operands = (
|
|
int(operator_line.split(":")[-1].strip()) if ":" in operator_line else 2
|
|
)
|
|
i += 1
|
|
|
|
# Parse cada operando
|
|
for _ in range(num_operands):
|
|
i, operand = self._parse_lad_expression(lines, i)
|
|
if operand:
|
|
operands.append(operand)
|
|
|
|
return i, {"type": "AND", "operands": operands}
|
|
|
|
def _parse_or_expression(self, lines, start_idx):
|
|
"""Parse una expresión OR"""
|
|
i = start_idx
|
|
operands = []
|
|
|
|
# Buscar operadores
|
|
if i < len(lines) and lines[i].strip().startswith("_LD_OPERATOR"):
|
|
# Extraer número de operandos
|
|
operator_line = lines[i].strip()
|
|
num_operands = (
|
|
int(operator_line.split(":")[-1].strip()) if ":" in operator_line else 2
|
|
)
|
|
i += 1
|
|
|
|
# Parse cada operando
|
|
for _ in range(num_operands):
|
|
i, operand = self._parse_lad_expression(lines, i)
|
|
if operand:
|
|
operands.append(operand)
|
|
|
|
return i, {"type": "OR", "operands": operands}
|
|
|
|
def _parse_contact(self, lines, start_idx):
|
|
"""Parse un contacto LAD"""
|
|
i = start_idx
|
|
contact_name = ""
|
|
negated = False
|
|
|
|
# Obtener nombre del contacto
|
|
if i < len(lines):
|
|
contact_name = lines[i].strip()
|
|
i += 1
|
|
|
|
# Verificar si hay expresión
|
|
if i < len(lines) and lines[i].strip() == "_EXPRESSION":
|
|
i += 1
|
|
# Verificar si está negado
|
|
if i < len(lines):
|
|
if lines[i].strip() == "_NEGATIV":
|
|
negated = True
|
|
i += 1
|
|
elif lines[i].strip() == "_POSITIV":
|
|
i += 1
|
|
|
|
return i, {"type": "CONTACT", "name": contact_name, "negated": negated}
|
|
|
|
def _parse_function_block(self, lines, start_idx):
|
|
"""Parse un bloque de función o llamada a ACTION"""
|
|
i = start_idx + 1
|
|
fb_name = ""
|
|
inputs = []
|
|
action_call = None
|
|
max_iterations = 50 # Protección contra bucles infinitos
|
|
iterations = 0
|
|
|
|
if i < len(lines):
|
|
fb_name = lines[i].strip()
|
|
i += 1
|
|
|
|
# PATRÓN IDENTIFICADO: ??? indica llamada a ACTION
|
|
if fb_name == "???":
|
|
print(
|
|
f" Detectado patrón ??? en línea {i-1} - Buscando ACTION call..."
|
|
)
|
|
# Buscar el nombre completo de la ACTION más adelante
|
|
action_call = self._find_action_call_name(lines, i)
|
|
if action_call:
|
|
print(f" ✓ ACTION call encontrada: {action_call}")
|
|
return i, {"type": "ACTION_CALL", "name": action_call, "inputs": []}
|
|
else:
|
|
print(f" ⚠ Patrón ??? pero no se encontró nombre de ACTION")
|
|
else:
|
|
print(f" Detectado Function Block directo: {fb_name}")
|
|
|
|
# Parse inputs del function block normal
|
|
while (
|
|
i < len(lines)
|
|
and not lines[i].strip().startswith("_OUTPUT")
|
|
and iterations < max_iterations
|
|
):
|
|
|
|
line = lines[i].strip()
|
|
iterations += 1
|
|
|
|
if line.startswith("_OPERAND"):
|
|
i += 2 # Saltar _EXPRESSION
|
|
if i < len(lines):
|
|
inputs.append(lines[i].strip())
|
|
i += 1
|
|
elif line and not line.startswith("_"):
|
|
# Para function blocks normales, recopilar inputs
|
|
# Para ACTION calls perdidas, último intento
|
|
if "." in line and fb_name == "???" and action_call is None:
|
|
action_call = line
|
|
print(f" ✓ ACTION call encontrada (fallback): {action_call}")
|
|
return i, {
|
|
"type": "ACTION_CALL",
|
|
"name": action_call,
|
|
"inputs": inputs,
|
|
}
|
|
elif line.startswith("ENABLELIST"):
|
|
# Fin del bloque
|
|
break
|
|
else:
|
|
i += 1
|
|
|
|
# Salida de emergencia del bucle
|
|
if iterations >= max_iterations:
|
|
print(
|
|
f" ADVERTENCIA: Bucle infinito evitado en function block en línea {start_idx}"
|
|
)
|
|
break
|
|
|
|
return i, {"type": "FUNCTION_BLOCK", "name": fb_name, "inputs": inputs}
|
|
|
|
def _find_action_call_name(self, lines, start_idx):
|
|
"""Buscar el nombre completo de la ACTION después de ???"""
|
|
i = start_idx
|
|
print(f" Buscando ACTION name desde línea {start_idx}...")
|
|
|
|
while i < len(lines) and i < start_idx + 15: # Buscar en las próximas 15 líneas
|
|
line = lines[i].strip()
|
|
print(f" Línea {i}: '{line}'")
|
|
|
|
# Buscar patrón namespace.actionname (ej: _Filling_Head_PID_Ctrl.Read_Analog)
|
|
if (
|
|
"." in line
|
|
and "ENABLELIST" not in line
|
|
and "EXPRESSION" not in line
|
|
and "BOX_EXPR" not in line
|
|
and "ENABLED" not in line
|
|
and "OUTPUTS" not in line
|
|
and "NO_SET" not in line
|
|
and "OUTPUT" not in line
|
|
and line != "_POSITIV"
|
|
and line != "_NEGATIV"
|
|
and line != "_NO_SET"
|
|
):
|
|
print(f" ✓ ACTION name encontrado: {line}")
|
|
return line
|
|
i += 1
|
|
|
|
print(
|
|
f" ⚠ No se encontró ACTION name en {start_idx + 15 - start_idx} líneas"
|
|
)
|
|
return None
|
|
|
|
def _parse_comment(self, lines, start_idx):
|
|
"""Parse comentario"""
|
|
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_empty_network(self, lines, start_idx):
|
|
"""Parse una red que empieza con _EMPTY y puede contener funciones"""
|
|
i = start_idx
|
|
max_iterations = 20
|
|
iterations = 0
|
|
function_found = None
|
|
target_found = None
|
|
|
|
print(f" Entrando a _parse_empty_network desde línea {start_idx}")
|
|
|
|
while (
|
|
i < len(lines)
|
|
and iterations < max_iterations
|
|
and not lines[i].strip().startswith("_OUTPUT")
|
|
and not lines[i].strip() == "_NETWORK"
|
|
):
|
|
|
|
line = lines[i].strip()
|
|
iterations += 1
|
|
print(f" Línea {i}: '{line}'")
|
|
|
|
if line.startswith("_FUNCTIONBLOCK"):
|
|
# Encontrada llamada a función/ACTION
|
|
print(f" ENCONTRADO _FUNCTIONBLOCK en línea {i}")
|
|
i, logic = self._parse_function_block(lines, i)
|
|
function_found = logic
|
|
elif line.startswith("_FUNCTION"):
|
|
# Otra variante de función
|
|
print(f" ENCONTRADO _FUNCTION en línea {i}")
|
|
i += 1
|
|
# Buscar el nombre de la función
|
|
while i < len(lines) and i < start_idx + 10:
|
|
func_line = lines[i].strip()
|
|
print(f" Buscando nombre función línea {i}: '{func_line}'")
|
|
if (
|
|
func_line
|
|
and not func_line.startswith("_")
|
|
and "ENABLELIST" not in func_line
|
|
):
|
|
print(f" ENCONTRADO nombre función: {func_line}")
|
|
function_found = {
|
|
"type": "FUNCTION_CALL",
|
|
"name": func_line,
|
|
"inputs": [],
|
|
}
|
|
break
|
|
i += 1
|
|
elif line.startswith("ENABLELIST"):
|
|
print(f" Encontrado ENABLELIST, continuando búsqueda...")
|
|
# No terminar aquí, continuar buscando _ASSIGN
|
|
i += 1
|
|
elif line.startswith("_ASSIGN"):
|
|
print(f" ENCONTRADO _ASSIGN en línea {i}")
|
|
# Buscar función dentro de _ASSIGN
|
|
i += 1
|
|
i, assign_logic = self._parse_assign_section(lines, i)
|
|
if assign_logic:
|
|
function_found = assign_logic
|
|
elif line.startswith("_OUTPUT"):
|
|
# Buscar el target después de _OUTPUT
|
|
print(f" ENCONTRADO _OUTPUT, buscando target...")
|
|
i += 1
|
|
while i < len(lines) and lines[i].strip().startswith("_"):
|
|
i += 1
|
|
if i < len(lines) and lines[i].strip() and "ENABLELIST" not in lines[i]:
|
|
target_found = lines[i].strip()
|
|
print(f" ENCONTRADO target: {target_found}")
|
|
break
|
|
else:
|
|
i += 1
|
|
|
|
# Si encontramos una función, crear red independiente
|
|
if function_found and target_found:
|
|
print(
|
|
f" Creando red independiente para función con target: {target_found}"
|
|
)
|
|
self._create_function_network(function_found, target_found)
|
|
return i, function_found
|
|
elif function_found:
|
|
print(f" Función encontrada pero sin target específico")
|
|
# Crear target por defecto basado en el tipo de función
|
|
if function_found["type"] == "ACTION_CALL":
|
|
default_target = "mDummy" # Variable dummy típica para ACTION calls
|
|
elif function_found["type"] == "FUNCTION_CALL":
|
|
# Para funciones como ProductLiterInTank, usar la variable global correspondiente
|
|
if function_found["name"] == "gProductTankLevel":
|
|
default_target = (
|
|
"gTankProdAmount" # Variable que se asigna según el contexto
|
|
)
|
|
else:
|
|
default_target = "mDummy"
|
|
else:
|
|
# Caso por defecto para otros tipos de función (FUNCTION_BLOCK, etc.)
|
|
default_target = "mDummy"
|
|
|
|
print(f" Usando target por defecto: {default_target}")
|
|
self._create_function_network(function_found, default_target)
|
|
return i, function_found
|
|
|
|
print(f" _parse_empty_network terminó sin encontrar función")
|
|
return i, None
|
|
|
|
def _parse_assign_section(self, lines, start_idx):
|
|
"""Parse una sección _ASSIGN que puede contener funciones"""
|
|
i = start_idx
|
|
max_iterations = 15
|
|
iterations = 0
|
|
|
|
print(f" Entrando a _parse_assign_section desde línea {start_idx}")
|
|
|
|
while (
|
|
i < len(lines)
|
|
and iterations < max_iterations
|
|
and not lines[i].strip() == "_NETWORK"
|
|
and not lines[i].strip() == "ENABLELIST_END"
|
|
):
|
|
|
|
line = lines[i].strip()
|
|
iterations += 1
|
|
print(f" Línea {i}: '{line}'")
|
|
|
|
if line.startswith("_FUNCTIONBLOCK"):
|
|
print(f" ENCONTRADO _FUNCTIONBLOCK en _ASSIGN: línea {i}")
|
|
i, logic = self._parse_function_block(lines, i)
|
|
return i, logic
|
|
elif line.startswith("_FUNCTION"):
|
|
print(f" ENCONTRADO _FUNCTION en _ASSIGN: línea {i}")
|
|
# Buscar el nombre de la función en las siguientes líneas
|
|
func_name = None
|
|
j = i + 1
|
|
while j < len(lines) and j < i + 8:
|
|
func_line = lines[j].strip()
|
|
print(f" Buscando nombre función línea {j}: '{func_line}'")
|
|
if (
|
|
func_line
|
|
and not func_line.startswith("_")
|
|
and "ENABLELIST" not in func_line
|
|
and ":" not in func_line
|
|
):
|
|
func_name = func_line
|
|
print(f" ENCONTRADO nombre función: {func_name}")
|
|
break
|
|
j += 1
|
|
|
|
if func_name:
|
|
return j, {"type": "FUNCTION_CALL", "name": func_name, "inputs": []}
|
|
else:
|
|
i += 1
|
|
elif line == "ENABLELIST_END":
|
|
print(f" Encontrado ENABLELIST_END, terminando")
|
|
break
|
|
else:
|
|
i += 1
|
|
|
|
print(f" _parse_assign_section terminó sin encontrar función")
|
|
return i, None
|
|
|
|
def _create_function_network(self, function_logic, target_name):
|
|
"""Crear una red independiente para una función o ACTION call"""
|
|
self.current_network_id += 1
|
|
|
|
function_network = {
|
|
"id": self.current_network_id,
|
|
"comment": f'Llamada a función: {function_logic.get("name", "unknown")}',
|
|
"logic": function_logic,
|
|
"target": target_name,
|
|
"function_blocks": [],
|
|
}
|
|
|
|
self.networks.append(function_network)
|
|
print(
|
|
f" Red de función {function_network['id']} creada para {function_logic['type']}: {function_logic.get('name', 'Sin nombre')}"
|
|
)
|
|
print(f" Target: '{target_name}'")
|
|
return function_network
|
|
|
|
def convert_to_structured(self):
|
|
"""Convertir a código SCL completo con variables y ACTIONs"""
|
|
output = []
|
|
|
|
# Header del archivo
|
|
output.append("(* Código SCL generado desde LAD TwinCAT *)")
|
|
output.append("(* Convertidor mejorado con SymPy - Estructura DNF preferida *)")
|
|
if self.program_path:
|
|
output.append(f"(* Path original: {self.program_path} *)")
|
|
output.append("")
|
|
|
|
# Verificar si es un TYPE o VAR_GLOBAL_LIST - manejarlos diferente
|
|
program_type = self.program_type if hasattr(self, "program_type") else "PROGRAM"
|
|
if program_type == "TYPE":
|
|
return self._convert_type_to_scl(output)
|
|
elif program_type == "VAR_GLOBAL_LIST":
|
|
return self._convert_global_vars_to_scl(output)
|
|
|
|
# Declaración del programa/función
|
|
program_name = self.program_name if self.program_name else "ConvertedProgram"
|
|
|
|
# Para FUNCTIONs, necesitamos detectar el tipo de retorno
|
|
if program_type == "FUNCTION":
|
|
# Buscar tipo de retorno en el código original (ej: FUNCTION ArrayToReal : REAL)
|
|
return_type = self._extract_function_return_type()
|
|
if return_type:
|
|
output.append(f"FUNCTION {program_name} : {return_type}")
|
|
else:
|
|
output.append(f"FUNCTION {program_name} : BOOL") # Tipo por defecto
|
|
else:
|
|
output.append(f"{program_type} {program_name}")
|
|
|
|
# Declaraciones de variables
|
|
self._add_variable_sections(output)
|
|
|
|
# Inicio del código principal
|
|
output.append("")
|
|
output.append("(* === CÓDIGO PRINCIPAL === *)")
|
|
output.append("")
|
|
|
|
# Código principal - LAD o ST
|
|
if self.networks:
|
|
# Hay redes LAD - generar código desde redes
|
|
output.append(" (* Código LAD convertido *)")
|
|
for network in self.networks:
|
|
output.append(f" // Red {network['id']}")
|
|
if network["comment"]:
|
|
output.append(f" // {network['comment']}")
|
|
|
|
if network["logic"] and network["target"]:
|
|
# Verificar si es una llamada a ACTION sin condiciones
|
|
if network["logic"]["type"] == "ACTION_CALL" and network[
|
|
"target"
|
|
] in [
|
|
"mDummy",
|
|
"EN_Out",
|
|
]: # Variables dummy típicas
|
|
# Es una llamada a ACTION incondicional
|
|
action_name = network["logic"]["name"]
|
|
output.append(f" CALL {action_name}();")
|
|
output.append(
|
|
f" {network['target']} := TRUE; // ACTION ejecutada"
|
|
)
|
|
elif network["logic"]["type"] == "FUNCTION_CALL" and network[
|
|
"target"
|
|
] in [
|
|
"mDummy",
|
|
"EN_Out",
|
|
"gTankProdAmount",
|
|
]: # Variables típicas
|
|
# Es una llamada a FUNCTION incondicional
|
|
function_call = self._convert_logic_to_string(network["logic"])
|
|
output.append(f" {network['target']} := {function_call};")
|
|
else:
|
|
# Usar expresión DNF optimizada si está disponible
|
|
if network["id"] in self.sympy_expressions:
|
|
sympy_expr = self.sympy_expressions[network["id"]]
|
|
condition_str = self._format_dnf_for_lad(sympy_expr)
|
|
# output.append(f" // Optimizada con SymPy DNF")
|
|
else:
|
|
# Fallback al método original
|
|
condition_str = self._convert_logic_to_string(
|
|
network["logic"]
|
|
)
|
|
output.append(f" // Sin optimización SymPy")
|
|
|
|
if condition_str:
|
|
# Si hay saltos de línea en la condición (múltiples términos OR)
|
|
if "\n" in condition_str:
|
|
output.append(f" IF {condition_str} THEN")
|
|
else:
|
|
output.append(f" IF {condition_str} THEN")
|
|
|
|
output.append(f" {network['target']} := TRUE;")
|
|
output.append(" ELSE")
|
|
output.append(f" {network['target']} := FALSE;")
|
|
output.append(" END_IF;")
|
|
else:
|
|
output.append(
|
|
f" {network['target']} := TRUE; // Logic no reconocida"
|
|
)
|
|
|
|
output.append("")
|
|
|
|
elif self.st_main_code:
|
|
# Hay código ST - copiarlo directamente preservando indentación
|
|
output.append(" (* Código ST original *)")
|
|
for line in self.st_main_code.split("\n"):
|
|
if line.strip():
|
|
# Preservar indentación original pero agregar indentación base SCL
|
|
output.append(f" {line}")
|
|
else:
|
|
output.append("")
|
|
|
|
else:
|
|
# No hay código principal
|
|
output.append(" (* Sin código principal detectado *)")
|
|
output.append("")
|
|
|
|
# Agregar ACTIONs como subfunciones
|
|
self._add_actions_as_procedures(output, program_name)
|
|
|
|
# Fin del programa principal
|
|
program_type = self.program_type if hasattr(self, "program_type") else "PROGRAM"
|
|
output.append(f"END_{program_type}")
|
|
|
|
return "\n".join(output)
|
|
|
|
def _add_variable_sections(self, output):
|
|
"""Agregar todas las secciones de variables al código"""
|
|
# Orden preferido de las secciones
|
|
section_order = ["VAR_INPUT", "VAR_OUTPUT", "VAR_IN_OUT", "VAR"]
|
|
|
|
for section_type in section_order:
|
|
if section_type in self.var_sections:
|
|
output.append(f"{section_type}")
|
|
for var_line in self.var_sections[section_type]:
|
|
# Asegurar que termine con punto y coma
|
|
if not var_line.endswith(";"):
|
|
var_line += " ;"
|
|
output.append(f" {var_line}")
|
|
output.append("END_VAR")
|
|
output.append("")
|
|
|
|
def _add_actions_as_procedures(self, output, program_name):
|
|
"""Agregar ACTIONs como procedimientos independientes"""
|
|
if not self.actions:
|
|
return
|
|
|
|
output.append("")
|
|
output.append("(* === SUBFUNCIONES (ACTIONs convertidas) === *)")
|
|
output.append("")
|
|
|
|
for action_name, action_data in self.actions.items():
|
|
# Nombre completo como se usa en TwinCAT (sin doble underscore)
|
|
full_name = f"{program_name}_{action_name}"
|
|
|
|
output.append(f"PROCEDURE {full_name}")
|
|
output.append("(* Convertida desde ACTION *)")
|
|
output.append("")
|
|
|
|
if isinstance(action_data, dict) and action_data["type"] == "LAD":
|
|
# ACTION con código LAD - convertir a SCL
|
|
output.append(" (* Código LAD convertido a SCL *)")
|
|
|
|
for network in action_data["networks"]:
|
|
output.append(f" // Red {network['id']}")
|
|
if network["comment"]:
|
|
output.append(f" // {network['comment']}")
|
|
|
|
if network["logic"] and network["target"]:
|
|
condition_str = self._convert_logic_to_string(network["logic"])
|
|
if condition_str:
|
|
output.append(f" IF {condition_str} THEN")
|
|
output.append(f" {network['target']} := TRUE;")
|
|
output.append(" ELSE")
|
|
output.append(f" {network['target']} := FALSE;")
|
|
output.append(" END_IF;")
|
|
else:
|
|
output.append(
|
|
f" {network['target']} := TRUE; // Logic no reconocida"
|
|
)
|
|
|
|
output.append("")
|
|
|
|
elif isinstance(action_data, dict) and action_data["type"] == "ST":
|
|
# ACTION con código ST - copiar directamente preservando indentación
|
|
output.append(" (* Código ST original *)")
|
|
for line in action_data["code"].split("\n"):
|
|
if line.strip():
|
|
# Preservar indentación original pero agregar indentación base SCL
|
|
output.append(f" {line}")
|
|
else:
|
|
output.append("")
|
|
|
|
else:
|
|
# Compatibilidad hacia atrás - código directo (legacy)
|
|
output.append(" (* Código original - modo compatibilidad *)")
|
|
for line in str(action_data).split("\n"):
|
|
if line.strip():
|
|
# Preservar indentación original pero agregar indentación base SCL
|
|
output.append(f" {line}")
|
|
else:
|
|
output.append("")
|
|
|
|
output.append("")
|
|
output.append("END_PROCEDURE")
|
|
output.append("")
|
|
|
|
def _convert_logic_to_string(self, logic):
|
|
"""Convertir lógica LAD a string estructurado"""
|
|
if not logic:
|
|
return ""
|
|
|
|
if logic["type"] == "CONTACT":
|
|
if logic["negated"]:
|
|
return f"NOT {logic['name']}"
|
|
else:
|
|
return logic["name"]
|
|
|
|
elif logic["type"] == "AND":
|
|
operand_strings = []
|
|
for operand in logic["operands"]:
|
|
operand_str = self._convert_logic_to_string(operand)
|
|
if operand_str:
|
|
operand_strings.append(operand_str)
|
|
|
|
if len(operand_strings) > 1:
|
|
return "(" + " AND ".join(operand_strings) + ")"
|
|
elif len(operand_strings) == 1:
|
|
return operand_strings[0]
|
|
else:
|
|
return ""
|
|
|
|
elif logic["type"] == "OR":
|
|
operand_strings = []
|
|
for operand in logic["operands"]:
|
|
operand_str = self._convert_logic_to_string(operand)
|
|
if operand_str:
|
|
operand_strings.append(operand_str)
|
|
|
|
if len(operand_strings) > 1:
|
|
return "(" + " OR ".join(operand_strings) + ")"
|
|
elif len(operand_strings) == 1:
|
|
return operand_strings[0]
|
|
else:
|
|
return ""
|
|
|
|
elif logic["type"] == "FUNCTION_BLOCK":
|
|
inputs_str = ", ".join(logic["inputs"]) if logic["inputs"] else ""
|
|
return f"{logic['name']}({inputs_str})"
|
|
|
|
elif logic["type"] == "ACTION_CALL":
|
|
return logic["name"] # Solo devolver el nombre, no CALL aquí
|
|
|
|
elif logic["type"] == "FUNCTION_CALL":
|
|
return f"{logic['name']}()"
|
|
|
|
return ""
|
|
|
|
def save_to_file(self, output_path):
|
|
"""Guardar código estructurado"""
|
|
structured_code = self.convert_to_structured()
|
|
|
|
with open(output_path, "w", encoding="utf-8") as f:
|
|
f.write(structured_code)
|
|
|
|
return structured_code
|
|
|
|
def print_debug_info(self):
|
|
"""Mostrar información de debug sobre los networks parseados"""
|
|
print(f"\n=== DEBUG INFO - {len(self.networks)} networks encontrados ===")
|
|
|
|
for network in self.networks:
|
|
print(f"\nRed {network['id']}:")
|
|
if network["comment"]:
|
|
print(f" Comentario: {network['comment']}")
|
|
print(f" Target: {network['target']}")
|
|
|
|
if network["logic"]:
|
|
print(f" Lógica: {self._debug_logic_string(network['logic'])}")
|
|
condition_str = self._convert_logic_to_string(network["logic"])
|
|
print(f" Condición: {condition_str}")
|
|
else:
|
|
print(" Sin lógica")
|
|
|
|
def _debug_logic_string(self, logic, indent=0):
|
|
"""Crear string de debug para la lógica"""
|
|
if not logic:
|
|
return "None"
|
|
|
|
prefix = " " * indent
|
|
|
|
if logic["type"] == "CONTACT":
|
|
neg_str = " (NEGADO)" if logic["negated"] else ""
|
|
return f"{prefix}CONTACT: {logic['name']}{neg_str}"
|
|
|
|
elif logic["type"] == "AND":
|
|
result = f"{prefix}AND:\n"
|
|
for operand in logic["operands"]:
|
|
result += self._debug_logic_string(operand, indent + 1) + "\n"
|
|
return result.rstrip()
|
|
|
|
elif logic["type"] == "OR":
|
|
result = f"{prefix}OR:\n"
|
|
for operand in logic["operands"]:
|
|
result += self._debug_logic_string(operand, indent + 1) + "\n"
|
|
return result.rstrip()
|
|
|
|
elif logic["type"] == "FUNCTION_BLOCK":
|
|
return f"{prefix}FUNCTION_BLOCK: {logic['name']} inputs: {logic['inputs']}"
|
|
|
|
elif logic["type"] == "ACTION_CALL":
|
|
return f"{prefix}ACTION_CALL: {logic['name']}"
|
|
|
|
elif logic["type"] == "FUNCTION_CALL":
|
|
return f"{prefix}FUNCTION_CALL: {logic['name']}"
|
|
|
|
return f"{prefix}UNKNOWN: {logic}"
|
|
|
|
def _logic_to_sympy(self, logic):
|
|
"""Convertir lógica LAD a expresión SymPy"""
|
|
if not logic:
|
|
return None
|
|
|
|
if logic["type"] == "CONTACT":
|
|
symbol = self.symbol_manager.get_symbol(logic["name"])
|
|
if logic["negated"]:
|
|
return Not(symbol)
|
|
else:
|
|
return symbol
|
|
|
|
elif logic["type"] == "AND":
|
|
sympy_operands = []
|
|
for operand in logic["operands"]:
|
|
operand_sympy = self._logic_to_sympy(operand)
|
|
if operand_sympy is not None:
|
|
sympy_operands.append(operand_sympy)
|
|
|
|
if len(sympy_operands) > 1:
|
|
return And(*sympy_operands)
|
|
elif len(sympy_operands) == 1:
|
|
return sympy_operands[0]
|
|
else:
|
|
return None
|
|
|
|
elif logic["type"] == "OR":
|
|
sympy_operands = []
|
|
for operand in logic["operands"]:
|
|
operand_sympy = self._logic_to_sympy(operand)
|
|
if operand_sympy is not None:
|
|
sympy_operands.append(operand_sympy)
|
|
|
|
if len(sympy_operands) > 1:
|
|
return Or(*sympy_operands)
|
|
elif len(sympy_operands) == 1:
|
|
return sympy_operands[0]
|
|
else:
|
|
return None
|
|
|
|
elif logic["type"] == "FUNCTION_BLOCK":
|
|
# Para function blocks, creamos un símbolo especial
|
|
fb_name = f"{logic['name']}({', '.join(logic['inputs'])})"
|
|
return self.symbol_manager.get_symbol(fb_name)
|
|
|
|
elif logic["type"] == "ACTION_CALL":
|
|
# Para llamadas a ACTION, creamos un símbolo especial
|
|
action_name = f"CALL_{logic['name'].replace('.', '_')}"
|
|
return self.symbol_manager.get_symbol(action_name)
|
|
|
|
return None
|
|
|
|
def _sympy_to_structured_string(self, sympy_expr):
|
|
"""Convertir expresión SymPy a string estructurado"""
|
|
if sympy_expr is None:
|
|
return ""
|
|
|
|
# Crear un mapeo inverso de símbolos a nombres originales
|
|
symbol_to_name = {}
|
|
for original_name, symbol in self.symbol_manager._symbol_cache.items():
|
|
symbol_to_name[str(symbol)] = original_name
|
|
|
|
# Convertir la expresión a string
|
|
str_expr = str(sympy_expr)
|
|
|
|
# Reemplazar símbolos por nombres originales
|
|
for symbol_str, original_name in symbol_to_name.items():
|
|
str_expr = str_expr.replace(symbol_str, original_name)
|
|
|
|
# Reemplazar operadores SymPy por operadores IEC61131-3
|
|
str_expr = str_expr.replace("&", " AND ")
|
|
str_expr = str_expr.replace("|", " OR ")
|
|
str_expr = str_expr.replace("~", "NOT ")
|
|
|
|
# Limpiar espacios múltiples
|
|
str_expr = re.sub(r"\s+", " ", str_expr)
|
|
|
|
return str_expr.strip()
|
|
|
|
def optimize_expressions(self):
|
|
"""Optimizar todas las expresiones usando SymPy - Preferir DNF para LAD"""
|
|
print("\n=== Optimizando expresiones con SymPy (forzando DNF para LAD) ===")
|
|
|
|
for network in self.networks:
|
|
if network["logic"]:
|
|
print(f"\nOptimizando Red {network['id']}:")
|
|
|
|
# Convertir a SymPy
|
|
sympy_expr = self._logic_to_sympy(network["logic"])
|
|
if sympy_expr:
|
|
print(f" Expresión original: {sympy_expr}")
|
|
|
|
# Simplificar primero
|
|
try:
|
|
simplified = simplify(sympy_expr)
|
|
print(f" Simplificada: {simplified}")
|
|
|
|
# Verificar complejidad antes de DNF
|
|
num_symbols = len(simplified.free_symbols)
|
|
complexity = self._estimate_expression_complexity(simplified)
|
|
|
|
if num_symbols > 12 or complexity > 800:
|
|
print(
|
|
f" ADVERTENCIA: Expresión muy compleja ({num_symbols} símbolos, complejidad {complexity})"
|
|
)
|
|
print(
|
|
f" Saltando conversión DNF por rendimiento - usando simplificación básica"
|
|
)
|
|
final_expr = simplified
|
|
else:
|
|
# SIEMPRE convertir a DNF para LAD (forma natural: (AND) OR (AND) OR (AND))
|
|
dnf_expr = to_dnf(simplified)
|
|
print(f" DNF (forma LAD preferida): {dnf_expr}")
|
|
|
|
# Post-procesar para eliminar contradicciones
|
|
final_expr = self._post_process_expression(dnf_expr)
|
|
|
|
# Verificar si el post-procesamiento cambió algo
|
|
if str(final_expr) != str(dnf_expr):
|
|
print(f" Post-procesada: {final_expr}")
|
|
|
|
self.sympy_expressions[network["id"]] = final_expr
|
|
|
|
except Exception as e:
|
|
print(f" Error optimizando: {e}")
|
|
self.sympy_expressions[network["id"]] = sympy_expr
|
|
|
|
def group_common_conditions(self):
|
|
"""Agrupar networks con condiciones similares o complementarias"""
|
|
print("\n=== Analizando agrupación de condiciones ===")
|
|
|
|
# Buscar networks que podrían agruparse
|
|
groupable_networks = []
|
|
for network in self.networks:
|
|
if (
|
|
network["logic"]
|
|
and network["target"]
|
|
and network["id"] in self.sympy_expressions
|
|
):
|
|
groupable_networks.append(network)
|
|
|
|
if len(groupable_networks) < 2:
|
|
print("No hay suficientes networks para agrupar")
|
|
return
|
|
|
|
# Analizar condiciones comunes
|
|
print(f"Analizando {len(groupable_networks)} networks para agrupación:")
|
|
|
|
for i, net1 in enumerate(groupable_networks):
|
|
for j, net2 in enumerate(groupable_networks[i + 1 :], i + 1):
|
|
expr1 = self.sympy_expressions[net1["id"]]
|
|
expr2 = self.sympy_expressions[net2["id"]]
|
|
|
|
# Buscar factores comunes
|
|
try:
|
|
# Extraer factores comunes si es posible
|
|
common_factors = self._find_common_factors(expr1, expr2)
|
|
if common_factors:
|
|
print(
|
|
f" Red {net1['id']} y Red {net2['id']} comparten: {common_factors}"
|
|
)
|
|
|
|
# Verificar si son complementarias (útil para SET/RESET)
|
|
if self._are_complementary(expr1, expr2):
|
|
print(
|
|
f" Red {net1['id']} y Red {net2['id']} son complementarias"
|
|
)
|
|
|
|
except Exception as e:
|
|
print(
|
|
f" Error analizando Red {net1['id']} y Red {net2['id']}: {e}"
|
|
)
|
|
|
|
def _find_common_factors(self, expr1, expr2):
|
|
"""Encontrar factores comunes entre dos expresiones"""
|
|
try:
|
|
# Convertir a conjuntos de símbolos para análisis básico
|
|
symbols1 = expr1.free_symbols
|
|
symbols2 = expr2.free_symbols
|
|
common_symbols = symbols1.intersection(symbols2)
|
|
|
|
if len(common_symbols) > 1:
|
|
return f"{len(common_symbols)} símbolos comunes"
|
|
|
|
return None
|
|
except:
|
|
return None
|
|
|
|
def _are_complementary(self, expr1, expr2):
|
|
"""Verificar si dos expresiones son complementarias"""
|
|
try:
|
|
# Verificar si expr1 == NOT(expr2) simplificado
|
|
complement = Not(expr2)
|
|
simplified_complement = simplify(complement)
|
|
simplified_expr1 = simplify(expr1)
|
|
|
|
return simplified_expr1.equals(simplified_complement)
|
|
except:
|
|
return False
|
|
|
|
def _estimate_expression_complexity(self, expr):
|
|
"""Estimar la complejidad de una expresión para evitar explosión exponencial"""
|
|
try:
|
|
# Contar operadores AND/OR anidados
|
|
complexity = 0
|
|
expr_str = str(expr)
|
|
|
|
# Contar número de operadores
|
|
and_count = expr_str.count("&")
|
|
or_count = expr_str.count("|")
|
|
not_count = expr_str.count("~")
|
|
|
|
complexity += and_count * 2 # AND
|
|
complexity += or_count * 4 # OR (más costoso en DNF)
|
|
complexity += not_count * 1 # NOT
|
|
|
|
# Penalizar anidamiento profundo
|
|
depth = expr_str.count("(")
|
|
complexity += depth * 6
|
|
|
|
# Número de símbolos únicos
|
|
num_symbols = len(expr.free_symbols)
|
|
complexity += num_symbols * 15
|
|
|
|
# Detectar patrones problemáticos específicos
|
|
# CNF con muchos términos (patrón típico que causa explosión)
|
|
if and_count > 10 and or_count > 15:
|
|
complexity += 2000 # Penalización severa para CNF complejas
|
|
|
|
# Expresiones con muchos términos negativos (difíciles para DNF)
|
|
if not_count > 8:
|
|
complexity += 500
|
|
|
|
# Longitud total como indicador de complejidad
|
|
if len(expr_str) > 1000:
|
|
complexity += len(expr_str) // 2
|
|
|
|
return complexity
|
|
except:
|
|
return 999999 # Asumir muy complejo si hay error
|
|
|
|
def _post_process_expression(self, expr):
|
|
"""Post-procesar expresión para eliminar contradicciones obvias"""
|
|
try:
|
|
# Detectar contradicciones como X & ~X que deberían ser False
|
|
cleaned_expr = expr
|
|
|
|
# Aplicar simplificaciones adicionales
|
|
cleaned_expr = simplify(cleaned_expr)
|
|
|
|
# Si la expresión contiene contradicciones obvias, intentar limpiar
|
|
free_symbols = cleaned_expr.free_symbols
|
|
for symbol in free_symbols:
|
|
# Verificar si tenemos symbol & ~symbol en alguna parte
|
|
contradiction = And(symbol, Not(symbol))
|
|
if cleaned_expr.has(contradiction):
|
|
print(
|
|
f" Detectada contradicción eliminable: {symbol} AND NOT {symbol}"
|
|
)
|
|
# Reemplazar contradicción por False
|
|
cleaned_expr = cleaned_expr.replace(contradiction, sympy.false)
|
|
cleaned_expr = simplify(cleaned_expr)
|
|
|
|
return cleaned_expr
|
|
except:
|
|
return expr
|
|
|
|
def _format_dnf_for_lad(self, sympy_expr):
|
|
"""Formatear expresión DNF para código LAD más legible"""
|
|
if sympy_expr is None:
|
|
return ""
|
|
|
|
# Crear mapeo de símbolos a nombres originales
|
|
symbol_to_name = {}
|
|
for original_name, symbol in self.symbol_manager._symbol_cache.items():
|
|
symbol_to_name[str(symbol)] = original_name
|
|
|
|
# Convertir a string y analizar la estructura
|
|
str_expr = str(sympy_expr)
|
|
|
|
# Reemplazar símbolos por nombres originales
|
|
for symbol_str, original_name in symbol_to_name.items():
|
|
str_expr = str_expr.replace(symbol_str, original_name)
|
|
|
|
# Reemplazar operadores SymPy por IEC61131-3 primero
|
|
str_expr = str_expr.replace("&", " AND ")
|
|
str_expr = str_expr.replace("|", " OR ")
|
|
str_expr = str_expr.replace("~", "NOT ")
|
|
|
|
# Limpiar espacios múltiples
|
|
str_expr = re.sub(r"\s+", " ", str_expr).strip()
|
|
|
|
# Si es una expresión OR de términos AND principales, formatear cada término
|
|
# Buscar el patrón de OR principales (no anidados en paréntesis)
|
|
if " OR " in str_expr and not str_expr.startswith("("):
|
|
# Dividir por OR de nivel principal
|
|
# Esto es más complejo debido a paréntesis anidados
|
|
parts = self._split_main_or_terms(str_expr)
|
|
|
|
if len(parts) > 1:
|
|
formatted_terms = []
|
|
for part in parts:
|
|
part = part.strip()
|
|
# Asegurar que cada término tenga paréntesis si es complejo
|
|
if " AND " in part and not (
|
|
part.startswith("(") and part.endswith(")")
|
|
):
|
|
part = f"({part})"
|
|
formatted_terms.append(part)
|
|
|
|
# Unir con OR y saltos de línea para mejor legibilidad
|
|
return "\n OR ".join(formatted_terms)
|
|
|
|
return str_expr
|
|
|
|
def _split_main_or_terms(self, expr):
|
|
"""Dividir expresión por OR de nivel principal, respetando paréntesis"""
|
|
parts = []
|
|
current_part = ""
|
|
paren_level = 0
|
|
i = 0
|
|
|
|
while i < len(expr):
|
|
char = expr[i]
|
|
|
|
if char == "(":
|
|
paren_level += 1
|
|
current_part += char
|
|
elif char == ")":
|
|
paren_level -= 1
|
|
current_part += char
|
|
elif paren_level == 0 and expr[i : i + 4] == " OR ":
|
|
# OR de nivel principal encontrado
|
|
parts.append(current_part.strip())
|
|
current_part = ""
|
|
i += 3 # Saltar ' OR '
|
|
else:
|
|
current_part += char
|
|
|
|
i += 1
|
|
|
|
# Agregar la última parte
|
|
if current_part.strip():
|
|
parts.append(current_part.strip())
|
|
|
|
return parts if len(parts) > 1 else [expr]
|
|
|
|
def _parse_actions(self, content):
|
|
"""Extraer todas las ACTIONs del programa"""
|
|
# Buscar patrón ACTION nombre: ... END_ACTION
|
|
action_pattern = r"ACTION\s+(\w+)\s*:(.*?)END_ACTION"
|
|
action_matches = re.findall(action_pattern, content, re.DOTALL | re.IGNORECASE)
|
|
|
|
for action_name, action_code in action_matches:
|
|
# Limpiar el código de la ACTION
|
|
clean_code = action_code.strip()
|
|
|
|
# Verificar si es código LAD o ST
|
|
if "_LD_BODY" in clean_code:
|
|
# Es código LAD - necesita parsing
|
|
print(
|
|
f"ACTION LAD encontrada: {action_name} ({len(clean_code)} caracteres)"
|
|
)
|
|
self.actions[action_name] = self._parse_action_lad(
|
|
action_name, clean_code
|
|
)
|
|
else:
|
|
# Es código ST - copiar directamente
|
|
print(
|
|
f"ACTION ST encontrada: {action_name} ({len(clean_code)} caracteres)"
|
|
)
|
|
self.actions[action_name] = {"type": "ST", "code": clean_code}
|
|
|
|
print(f"Total ACTIONs: {len(self.actions)}")
|
|
|
|
def _parse_action_lad(self, action_name, action_content):
|
|
"""Parsear una ACTION que contiene código LAD"""
|
|
# Crear un convertidor temporal para esta ACTION
|
|
action_converter = SimpleLadConverter()
|
|
action_converter.symbol_manager = self.symbol_manager # Compartir símbolos
|
|
|
|
# Encontrar sección LAD
|
|
lad_start = action_content.find("_LD_BODY")
|
|
if lad_start != -1:
|
|
# Extraer contenido LAD hasta el final
|
|
lad_content = action_content[lad_start:]
|
|
lines = lad_content.split("\n")
|
|
action_converter._parse_networks(lines)
|
|
|
|
return {
|
|
"type": "LAD",
|
|
"networks": action_converter.networks,
|
|
"raw_content": action_content,
|
|
}
|
|
|
|
def _extract_st_code(self, content):
|
|
"""Extraer código ST del programa principal"""
|
|
# Buscar desde después de END_VAR hasta ACTION o END_PROGRAM
|
|
var_end = content.rfind("END_VAR")
|
|
if var_end == -1:
|
|
return
|
|
|
|
# Buscar el final del código principal
|
|
action_start = content.find("\nACTION", var_end)
|
|
end_program = content.find("\nEND_PROGRAM", var_end)
|
|
|
|
code_end = action_start if action_start != -1 else end_program
|
|
if code_end == -1:
|
|
code_end = len(content)
|
|
|
|
# Extraer código ST
|
|
st_code = content[var_end + 7 : code_end].strip() # +7 para saltar "END_VAR"
|
|
|
|
if st_code:
|
|
# Almacenar código ST para usar en la generación
|
|
self.st_main_code = st_code
|
|
print(f"Código ST principal extraído: {len(st_code)} caracteres")
|
|
else:
|
|
self.st_main_code = None
|
|
|
|
def _extract_function_return_type(self):
|
|
"""Extraer el tipo de retorno de una FUNCTION desde el header"""
|
|
if not hasattr(self, "_original_content"):
|
|
return None
|
|
|
|
# Buscar patrón FUNCTION nombre : TIPO
|
|
match = re.search(r"FUNCTION\s+\w+\s*:\s*(\w+)", self._original_content)
|
|
if match:
|
|
return match.group(1)
|
|
return None
|
|
|
|
def _extract_type_content(self, content):
|
|
"""Extraer el contenido completo de un TYPE"""
|
|
# Buscar desde TYPE hasta END_TYPE
|
|
type_start = content.find("TYPE")
|
|
if type_start == -1:
|
|
return
|
|
|
|
# Buscar END_TYPE
|
|
type_end = content.find("END_TYPE", type_start)
|
|
if type_end == -1:
|
|
type_end = len(content)
|
|
else:
|
|
type_end += len("END_TYPE")
|
|
|
|
# Extraer contenido del TYPE
|
|
type_content = content[type_start:type_end].strip()
|
|
|
|
if type_content:
|
|
self.type_content = type_content
|
|
print(f"Contenido TYPE extraído: {len(type_content)} caracteres")
|
|
else:
|
|
self.type_content = None
|
|
|
|
def _convert_type_to_scl(self, output):
|
|
"""Convertir TYPE a SCL - simplemente copiar el contenido original"""
|
|
if self.type_content:
|
|
# Copiar directamente el contenido del TYPE preservando formato
|
|
for line in self.type_content.split("\n"):
|
|
if line.strip():
|
|
output.append(line)
|
|
else:
|
|
output.append("")
|
|
else:
|
|
# Si no hay contenido TYPE, generar estructura básica
|
|
program_name = self.program_name if self.program_name else "UnknownType"
|
|
output.append(f"TYPE {program_name}:")
|
|
output.append("STRUCT")
|
|
output.append(" (* Contenido TYPE no detectado *)")
|
|
output.append(" Dummy : INT;")
|
|
output.append("END_STRUCT")
|
|
output.append("END_TYPE")
|
|
|
|
return "\n".join(output)
|
|
|
|
def _extract_global_variables(self, content):
|
|
"""Extraer todas las secciones de variables globales"""
|
|
# Buscar todas las secciones VAR_GLOBAL
|
|
var_global_patterns = [
|
|
(r"VAR_GLOBAL\s+PERSISTENT(.*?)END_VAR", "VAR_GLOBAL PERSISTENT"),
|
|
(r"VAR_GLOBAL((?:(?!PERSISTENT)(?!END_VAR).)*?)END_VAR", "VAR_GLOBAL"),
|
|
]
|
|
|
|
for pattern, var_type in var_global_patterns:
|
|
matches = re.findall(pattern, content, re.DOTALL | re.IGNORECASE)
|
|
if matches:
|
|
variables = []
|
|
for match in matches:
|
|
# Parsear cada línea de variable
|
|
for line in match.split("\n"):
|
|
line = line.strip()
|
|
if (
|
|
line
|
|
and not line.startswith("(*")
|
|
and not line.startswith("(************")
|
|
and ":" in line
|
|
and not line.startswith("*")
|
|
):
|
|
# Limpiar comentarios inline
|
|
if "(*" in line:
|
|
# Mantener el comentario inline
|
|
pass # No limpiar comentarios para preservar información
|
|
variables.append(line.strip())
|
|
|
|
if variables:
|
|
if var_type not in self.var_sections:
|
|
self.var_sections[var_type] = []
|
|
self.var_sections[var_type].extend(variables)
|
|
|
|
# Mostrar resumen
|
|
for var_type, variables in self.var_sections.items():
|
|
print(f"Variables {var_type}: {len(variables)} encontradas")
|
|
|
|
def _convert_global_vars_to_scl(self, output):
|
|
"""Convertir archivo de variables globales a SCL"""
|
|
# Para archivos de variables globales, simplemente copiar las declaraciones
|
|
|
|
# Agregar secciones de variables encontradas
|
|
section_order = ["VAR_GLOBAL", "VAR_GLOBAL PERSISTENT"]
|
|
|
|
for section_type in section_order:
|
|
if section_type in self.var_sections:
|
|
output.append(f"{section_type}")
|
|
for var_line in self.var_sections[section_type]:
|
|
# Asegurar que termine con punto y coma
|
|
if not var_line.endswith(";"):
|
|
var_line += " ;"
|
|
output.append(f" {var_line}")
|
|
output.append("END_VAR")
|
|
output.append("")
|
|
|
|
# Si no se encontraron variables, agregar estructura básica
|
|
if not self.var_sections:
|
|
output.append("VAR_GLOBAL")
|
|
output.append(" (* No se detectaron variables globales *)")
|
|
output.append(" Dummy : INT ;")
|
|
output.append("END_VAR")
|
|
|
|
return "\n".join(output)
|
|
|
|
|
|
def main():
|
|
"""Función principal - Convierte todos los archivos .EXP a .SCL"""
|
|
try:
|
|
# Verificar si se pasó un archivo específico como parámetro para debug
|
|
debug_file = None
|
|
if len(sys.argv) > 1:
|
|
debug_file = sys.argv[1]
|
|
print(f"=== MODO DEBUG: Procesando archivo específico ===")
|
|
print(f"Archivo: {debug_file}")
|
|
else:
|
|
print("=== Convertidor Masivo LAD a SCL con SymPy ===")
|
|
|
|
# Cargar configuración
|
|
configs = load_configuration()
|
|
|
|
# Verificar que se cargó correctamente
|
|
if not configs:
|
|
print(
|
|
"Advertencia: No se pudo cargar la configuración, usando valores por defecto"
|
|
)
|
|
working_directory = "./"
|
|
scl_output_dir = "TwinCat/scl"
|
|
debug_mode = True
|
|
show_optimizations = True
|
|
show_generated_code = False
|
|
max_display_lines = 50
|
|
force_regenerate = False
|
|
else:
|
|
# Obtener configuraciones
|
|
working_directory = configs.get("working_directory", "./")
|
|
level1_config = configs.get("level1", {})
|
|
level2_config = configs.get("level2", {})
|
|
level3_config = configs.get("level3", {})
|
|
|
|
# Parámetros de configuración
|
|
debug_mode = level1_config.get("debug_mode", True)
|
|
show_optimizations = level1_config.get("show_optimizations", True)
|
|
scl_output_dir = level2_config.get("scl_output_dir", "scl")
|
|
backup_existing = level2_config.get("backup_existing", True)
|
|
show_generated_code = level2_config.get("show_generated_code", False)
|
|
max_display_lines = level2_config.get("max_display_lines", 50)
|
|
sympy_optimization = level3_config.get("sympy_optimization", True)
|
|
group_analysis = level3_config.get("group_analysis", True)
|
|
force_regenerate = level2_config.get(
|
|
"force_regenerate", False
|
|
) # Nueva opción
|
|
|
|
# Directorio de entrada para archivos .EXP (de level3 o working_directory como fallback)
|
|
input_directory = level3_config.get("twincat_exp_directory", working_directory)
|
|
|
|
# Verificar directorio de trabajo
|
|
if not os.path.exists(working_directory):
|
|
print(f"Error: El directorio de trabajo no existe: {working_directory}")
|
|
return
|
|
|
|
# Verificar directorio de entrada
|
|
if not os.path.exists(input_directory):
|
|
print(f"Error: El directorio de entrada no existe: {input_directory}")
|
|
return
|
|
|
|
# Crear directorio de salida SCL
|
|
full_scl_path = os.path.join(working_directory, scl_output_dir)
|
|
if not os.path.exists(full_scl_path):
|
|
os.makedirs(full_scl_path)
|
|
print(f"Directorio creado: {full_scl_path}")
|
|
|
|
# Determinar archivos a procesar
|
|
if debug_file:
|
|
# Modo debug - archivo específico
|
|
if not debug_file.endswith(".EXP"):
|
|
debug_file += ".EXP"
|
|
|
|
debug_file_path = os.path.join(input_directory, debug_file)
|
|
if not os.path.exists(debug_file_path):
|
|
print(f"Error: No se encontró el archivo {debug_file_path}")
|
|
return
|
|
|
|
exp_files = [debug_file_path]
|
|
print(f"Procesando archivo específico: {debug_file}")
|
|
print(f"Directorio de entrada: {input_directory}")
|
|
print(f"Directorio de salida SCL: {full_scl_path}")
|
|
|
|
# En modo debug, forzar regeneración y mostrar más información
|
|
force_regenerate = True
|
|
debug_mode = True
|
|
show_generated_code = True
|
|
max_display_lines = 100
|
|
else:
|
|
# Modo normal - todos los archivos
|
|
exp_pattern = os.path.join(input_directory, "*.EXP")
|
|
exp_files = glob.glob(exp_pattern)
|
|
|
|
if not exp_files:
|
|
print(f"No se encontraron archivos .EXP en: {input_directory}")
|
|
return
|
|
|
|
print(f"Encontrados {len(exp_files)} archivos .EXP en: {input_directory}")
|
|
print(f"Directorio de salida SCL: {full_scl_path}")
|
|
|
|
print()
|
|
|
|
# Procesar cada archivo
|
|
successful_conversions = 0
|
|
failed_conversions = 0
|
|
|
|
for exp_file in exp_files:
|
|
filename = os.path.basename(exp_file)
|
|
base_name = os.path.splitext(filename)[0]
|
|
scl_filename = f"{base_name}.scl"
|
|
scl_output_path = os.path.join(full_scl_path, scl_filename)
|
|
|
|
# Verificar si ya existe el archivo SCL (exportación progresiva)
|
|
if os.path.exists(scl_output_path) and not force_regenerate:
|
|
print(f"{'='*60}")
|
|
print(f"SALTANDO: {filename} - Ya existe {scl_filename}")
|
|
print(
|
|
f" (usa force_regenerate: true en configuración para forzar regeneración)"
|
|
)
|
|
successful_conversions += 1 # Contar como exitoso
|
|
continue
|
|
|
|
print(f"{'='*60}")
|
|
print(f"Procesando: {filename}")
|
|
print(f"Salida: {scl_filename}")
|
|
|
|
try:
|
|
# Crear nuevo convertidor para cada archivo
|
|
converter = SimpleLadConverter()
|
|
|
|
# Parsear archivo
|
|
converter.parse_file(exp_file)
|
|
|
|
print(f" ✓ Redes encontradas: {len(converter.networks)}")
|
|
print(
|
|
f" ✓ Secciones de variables: {list(converter.var_sections.keys())}"
|
|
)
|
|
print(f" ✓ ACTIONs encontradas: {list(converter.actions.keys())}")
|
|
|
|
# Mostrar información de debug si está habilitado
|
|
if debug_mode:
|
|
converter.print_debug_info()
|
|
|
|
# Optimizar expresiones con SymPy si está habilitado
|
|
if sympy_optimization and show_optimizations:
|
|
converter.optimize_expressions()
|
|
|
|
# Analizar agrupación de condiciones si está habilitado
|
|
if group_analysis and show_optimizations:
|
|
converter.group_common_conditions()
|
|
|
|
# Convertir y guardar
|
|
print(f" Generando código SCL...")
|
|
structured_code = converter.save_to_file(scl_output_path)
|
|
|
|
# Mostrar parte del código generado si está habilitado
|
|
if show_generated_code:
|
|
lines = structured_code.split("\n")
|
|
display_lines = min(max_display_lines, len(lines))
|
|
print(
|
|
f" Código SCL generado ({len(lines)} líneas, mostrando {display_lines}):"
|
|
)
|
|
for i in range(display_lines):
|
|
print(f" {i+1:3d}: {lines[i]}")
|
|
if len(lines) > display_lines:
|
|
print(f" ... ({len(lines) - display_lines} líneas más)")
|
|
|
|
print(f" ✓ Guardado en: {scl_output_path}")
|
|
successful_conversions += 1
|
|
|
|
except Exception as e:
|
|
print(f" ✗ Error procesando {filename}: {e}")
|
|
if debug_mode:
|
|
import traceback
|
|
|
|
traceback.print_exc()
|
|
failed_conversions += 1
|
|
|
|
print()
|
|
|
|
# Resumen final
|
|
print(f"{'='*60}")
|
|
print(f"RESUMEN DE CONVERSIÓN:")
|
|
print(f" ✓ Exitosas: {successful_conversions}")
|
|
print(f" ✗ Fallidas: {failed_conversions}")
|
|
print(f" 📁 Directorio salida: {full_scl_path}")
|
|
|
|
if successful_conversions > 0:
|
|
print(f"\n✓ Conversión masiva completada!")
|
|
|
|
except Exception as e:
|
|
print(f"Error general: {e}")
|
|
import traceback
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|