Se modificó el script `x1_lad_converter.py` para cambiar el manejo de los objetivos de red, pasando de un solo objetivo a una lista de objetivos. Se implementaron mejoras en la lógica de análisis de redes, permitiendo la recopilación de múltiples salidas y optimizando la generación de código SCL. Además, se actualizaron los mensajes de depuración y se mejoró la estructura del código para una mayor claridad y mantenimiento.

This commit is contained in:
Miguel 2025-07-10 12:20:54 +02:00
parent ffc686e140
commit 164667bc2f
2 changed files with 572 additions and 5389 deletions

View File

@ -366,7 +366,7 @@ class SimpleLadConverter:
"id": self.current_network_id, "id": self.current_network_id,
"comment": "", "comment": "",
"logic": None, "logic": None,
"target": "", "targets": [],
"calls": [], # NUEVO: para almacenar llamadas a FB/FUN "calls": [], # NUEVO: para almacenar llamadas a FB/FUN
"function_blocks": [], "function_blocks": [],
} }
@ -417,13 +417,31 @@ class SimpleLadConverter:
# Continuar buscando # Continuar buscando
i += 1 i += 1
elif line.startswith("_OUTPUTS"):
num_outputs = int(line.split(":")[1].strip())
i += 1
for _ in range(num_outputs):
while i < len(lines) and not lines[i].strip().startswith("_OUTPUT"):
i += 1
if i >= len(lines):
break
i += 1 # past _OUTPUT
while i < len(lines) and lines[i].strip().startswith("_"): # flags
i += 1
if i < len(lines):
var_name = lines[i].strip()
if var_name:
network["targets"].append(var_name)
i += 1
elif line.startswith("_OUTPUT"): elif line.startswith("_OUTPUT"):
# Buscar variable de salida # Buscar variable de salida
i += 1 i += 1
while i < len(lines) and lines[i].strip().startswith("_"): while i < len(lines) and lines[i].strip().startswith("_"):
i += 1 i += 1
if i < len(lines) and lines[i].strip() and "ENABLELIST" not in lines[i]: if i < len(lines) and lines[i].strip() and "ENABLELIST" not in lines[i]:
network["target"] = lines[i].strip() network["targets"].append(lines[i].strip())
i += 1 i += 1
else: else:
i += 1 i += 1
@ -442,7 +460,7 @@ class SimpleLadConverter:
print(f" 🎯 Función reconocida: {call['name']}") print(f" 🎯 Función reconocida: {call['name']}")
else: else:
print(f" 📋 Llamada: {call['type']} - {call['name']}") print(f" 📋 Llamada: {call['type']} - {call['name']}")
print(f" Target: '{network['target']}'") print(f" Targets: '{network['targets']}'")
return i return i
def _parse_lad_expression(self, lines, start_idx): def _parse_lad_expression(self, lines, start_idx):
@ -1030,7 +1048,7 @@ class SimpleLadConverter:
"id": self.current_network_id, "id": self.current_network_id,
"comment": f'Llamada a función: {function_logic.get("name", "unknown")}', "comment": f'Llamada a función: {function_logic.get("name", "unknown")}',
"logic": function_logic, "logic": function_logic,
"target": target_name, "targets": [target_name],
"function_blocks": [], "function_blocks": [],
"calls": [function_logic], # ✅ Añadir la llamada al array calls "calls": [function_logic], # ✅ Añadir la llamada al array calls
} }
@ -1134,18 +1152,19 @@ class SimpleLadConverter:
if condition_str: if condition_str:
output.append(" END_IF;") output.append(" END_IF;")
elif network["target"]: elif network["targets"]:
# Red sin llamadas pero con target # Red sin llamadas pero con target(s)
if condition_str: if condition_str:
output.append(f" IF {condition_str} THEN") output.append(f" IF {condition_str} THEN")
output.append(f" {network['target']} := TRUE;") for target in network["targets"]:
output.append(f" {target} := TRUE;")
output.append(" ELSE") output.append(" ELSE")
output.append(f" {network['target']} := FALSE;") for target in network["targets"]:
output.append(f" {target} := FALSE;")
output.append(" END_IF;") output.append(" END_IF;")
else: else:
output.append( for target in network["targets"]:
f" {network['target']} := TRUE; // Sin condición" output.append(f" {target} := TRUE; // Sin condición")
)
output.append("") output.append("")
@ -1301,6 +1320,8 @@ class SimpleLadConverter:
# Asegurar que el campo 'calls' existe # Asegurar que el campo 'calls' existe
if "calls" not in network: if "calls" not in network:
network["calls"] = [] network["calls"] = []
if "targets" not in network:
network["targets"] = []
output.append(f" // Red {network['id']}") output.append(f" // Red {network['id']}")
if network["comment"]: if network["comment"]:
@ -1325,18 +1346,21 @@ class SimpleLadConverter:
call_str = self._convert_call_to_string(call, indent) call_str = self._convert_call_to_string(call, indent)
output.append(call_str) output.append(call_str)
elif network["target"]: elif network["targets"]:
output.append(f"{indent}{network['target']} := TRUE;") for target in network["targets"]:
output.append(f"{indent}{target} := TRUE;")
if condition_str: if condition_str:
if not network_calls and network["target"]: if not network_calls and network["targets"]:
output.append(" ELSE") output.append(" ELSE")
output.append(f" {network['target']} := FALSE;") for target in network["targets"]:
output.append(f" {target} := FALSE;")
output.append(" END_IF;") output.append(" END_IF;")
elif network["target"]: elif network["targets"]:
for target in network["targets"]:
output.append( output.append(
f" {network['target']} := TRUE; // Sin condición, solo target" f" {target} := TRUE; // Sin condición, solo target"
) )
output.append("") output.append("")
@ -1461,7 +1485,7 @@ class SimpleLadConverter:
print(f"\nRed {network['id']}:") print(f"\nRed {network['id']}:")
if network["comment"]: if network["comment"]:
print(f" Comentario: {network['comment']}") print(f" Comentario: {network['comment']}")
print(f" Target: {network['target']}") print(f" Targets: {network['targets']}")
if network["logic"]: if network["logic"]:
print(f" Lógica: {self._debug_logic_string(network['logic'])}") print(f" Lógica: {self._debug_logic_string(network['logic'])}")
@ -1652,7 +1676,7 @@ class SimpleLadConverter:
for network in self.networks: for network in self.networks:
if ( if (
network["logic"] network["logic"]
and network["target"] and network["targets"]
and network["id"] in self.sympy_expressions and network["id"] in self.sympy_expressions
): ):
groupable_networks.append(network) groupable_networks.append(network)
@ -1892,45 +1916,57 @@ class SimpleLadConverter:
print(f"Total ACTIONs: {len(self.actions)}") print(f"Total ACTIONs: {len(self.actions)}")
def _parse_action_lad(self, action_name, action_content): def _parse_action_lad(self, action_name, action_content):
"""Parsear una ACTION que contiene código LAD""" # We create a completely new, isolated instance of the converter to parse the action's LAD code.
# Crear un convertidor temporal para esta ACTION # This prevents any state (like network counts) from leaking between the main program and the action parsing.
action_converter = SimpleLadConverter() action_converter = SimpleLadConverter(self.function_registry)
action_converter.symbol_manager = self.symbol_manager # Compartir símbolos
# Encontrar sección LAD # Extract just the LAD body from the action content
lad_start = action_content.find("_LD_BODY") lad_body_match = re.search(r"_LD_BODY(.*)", action_content, re.DOTALL)
if lad_start != -1: if lad_body_match:
# Extraer contenido LAD hasta el final lad_content = lad_body_match.group(1)
lad_content = action_content[lad_start:] lines = lad_content.strip().split("\n")
lines = lad_content.split("\n")
# The parse_networks method will process all networks it finds in the provided lines.
action_converter._parse_networks(lines) action_converter._parse_networks(lines)
return { # The parsed networks are stored in the temporary converter's `networks` list.
"type": "LAD", # We return this list as the logic for the action.
"networks": action_converter.networks, return {"type": "LAD", "networks": action_converter.networks}
"raw_content": action_content,
} # If no LAD body is found, return an empty structure.
return {"type": "LAD", "networks": []}
def _extract_st_code(self, content): def _extract_st_code(self, content):
"""Extraer código ST del programa principal""" """Extraer código ST del programa principal"""
# Buscar desde después de END_VAR hasta ACTION o END_PROGRAM # Buscar desde después de END_VAR hasta ACTION o END_PROGRAM
var_end = content.rfind("END_VAR") var_end_match = re.search(r"END_VAR", content)
if var_end == -1: if not var_end_match:
self.st_main_code = None
return return
# Buscar el final del código principal start_index = var_end_match.end()
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 # Buscar el final del código principal
if code_end == -1: action_start_match = re.search(r"\nACTION", content[start_index:])
code_end = len(content) end_program_match = re.search(r"\nEND_PROGRAM", content[start_index:])
end_index = -1
if action_start_match:
end_index = start_index + action_start_match.start()
if end_program_match:
program_end = start_index + end_program_match.start()
if end_index == -1 or program_end < end_index:
end_index = program_end
if end_index == -1:
end_index = len(content)
# Extraer código ST # Extraer código ST
st_code = content[var_end + 7 : code_end].strip() # +7 para saltar "END_VAR" st_code = content[start_index:end_index].strip()
if st_code: if st_code:
# Almacenar código ST para usar en la generación
self.st_main_code = st_code self.st_main_code = st_code
print(f"Código ST principal extraído: {len(st_code)} caracteres") print(f"Código ST principal extraído: {len(st_code)} caracteres")
else: else:
@ -2121,144 +2157,132 @@ def collect_function_interfaces(input_directory, debug_mode=False):
return function_registry, function_files return function_registry, function_files
def main(): def run_debug_mode(debug_file_arg):
"""Función principal - Convierte todos los archivos .EXP a .SCL con dos pasadas""" """Ejecuta el convertidor en modo debug para un solo archivo."""
try:
import time
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
print("=== SCRIPT VERIFICADO Y EJECUTÁNDOSE CORRECTAMENTE ===")
print(f"🕒 Timestamp: {timestamp}")
print("=== INICIANDO CONVERTIDOR SCRIPT (2 PASADAS) ===")
# 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"=== MODO DEBUG: Procesando archivo específico ===")
print(f"Archivo: {debug_file}") print(f"Archivo: {debug_file_arg}")
print("🔧 VERIFICACIÓN: Script modificado y ejecutándose en tiempo real") print("🔧 VERIFICACIÓN: Script modificado y ejecutándose en tiempo real")
# Determinar el path absoluto del archivo de debug.
# Esto permite que el script se ejecute desde cualquier lugar.
debug_file_path = os.path.abspath(debug_file_arg)
if not debug_file_path.endswith(".EXP"):
debug_file_path += ".EXP"
if not os.path.exists(debug_file_path):
print(f"Error: No se encontró el archivo de debug: {debug_file_path}")
# Intenta buscarlo en el directorio relativo ExportTwinCat por si acaso
script_dir = os.path.dirname(os.path.abspath(__file__))
export_dir_path = os.path.join(
script_dir, "..", "ExportTwinCat", os.path.basename(debug_file_path)
)
if os.path.exists(export_dir_path):
debug_file_path = os.path.abspath(export_dir_path)
print(f"Archivo encontrado en path alternativo: {debug_file_path}")
else: else:
return
# El directorio de entrada y salida es el del archivo de debug
input_directory = os.path.dirname(debug_file_path)
scl_output_dir = input_directory
print(f"Directorio de análisis para interfaces: {input_directory}")
print(f"Directorio de salida para .SCL: {scl_output_dir}")
# PRIMERA PASADA: Recopilar interfaces de TODOS los archivos .EXP en el directorio
function_registry, _ = collect_function_interfaces(input_directory, debug_mode=True)
if function_registry.functions or function_registry.function_blocks:
print("\n🔧 REGISTRY DE FUNCIONES PARA EL DEBUG:")
function_registry.print_summary()
# SEGUNDA PASADA: Procesar solo el archivo de debug
print("\n=== SEGUNDA PASADA (DEBUG): CONVIRTIENDO ARCHIVO ÚNICO ===")
filename = os.path.basename(debug_file_path)
base_name = os.path.splitext(filename)[0]
scl_filename = f"{base_name}.scl"
scl_output_path = os.path.join(scl_output_dir, scl_filename)
print(f"{'='*60}")
print(f"Procesando: {filename}")
print(f"Salida: {scl_output_path}")
try:
converter = SimpleLadConverter(function_registry)
converter.parse_file(debug_file_path)
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())}")
converter.print_debug_info()
converter.optimize_expressions()
converter.group_common_conditions()
print(f" Generando código SCL...")
structured_code = converter.save_to_file(scl_output_path)
# Mostrar código por stdout como fue solicitado
print("\n\n=== CÓDIGO SCL GENERADO (STDOUT) ===")
print(structured_code)
print("====================================")
print(f"\n ✓ Guardado en: {scl_output_path}")
except Exception as e:
print(f" ✗ Error procesando {filename}: {e}")
import traceback
traceback.print_exc()
print(f"\n✓ Proceso de debug finalizado.")
def run_mass_conversion():
"""Ejecuta el convertidor en modo masivo usando la configuración global."""
print("=== Convertidor Masivo LAD a SCL con SymPy (2 Pasadas) ===") print("=== Convertidor Masivo LAD a SCL con SymPy (2 Pasadas) ===")
# Cargar configuración # Cargar configuración
configs = load_configuration() configs = load_configuration()
# Verificar que se cargó correctamente
if not configs: if not configs:
print( print("Error: No se pudo cargar la configuración. Abortando.")
"Advertencia: No se pudo cargar la configuración, usando valores por defecto" return
)
working_directory = "./"
scl_output_dir = "scl"
debug_mode = True
show_optimizations = True
show_generated_code = False
max_display_lines = 50
force_regenerate = False
level1_config = {}
level2_config = {}
level3_config = {}
else:
# Obtener configuraciones # Obtener configuraciones
working_directory = configs.get("working_directory", "./") working_directory = configs.get("working_directory", "./")
level1_config = configs.get("level1", {}) level1_config = configs.get("level1", {})
level2_config = configs.get("level2", {}) level2_config = configs.get("level2", {})
level3_config = configs.get("level3", {}) level3_config = configs.get("level3", {})
# Parámetros de configuración
debug_mode = level1_config.get("debug_mode", True) debug_mode = level1_config.get("debug_mode", True)
show_optimizations = level1_config.get("show_optimizations", True) show_optimizations = level1_config.get("show_optimizations", True)
scl_output_dir = level2_config.get("scl_output_dir", "scl") 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) show_generated_code = level2_config.get("show_generated_code", False)
max_display_lines = level2_config.get("max_display_lines", 50) max_display_lines = level2_config.get("max_display_lines", 50)
force_regenerate = level2_config.get("force_regenerate", False)
input_directory = level3_config.get("twincat_exp_directory", working_directory)
sympy_optimization = level3_config.get("sympy_optimization", True) sympy_optimization = level3_config.get("sympy_optimization", True)
group_analysis = level3_config.get("group_analysis", 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
# En modo debug, usar el directorio actual si no hay configuración específica
if debug_file:
# Modo debug: usar directorio actual donde están los archivos EXP
input_directory = os.path.dirname(os.path.abspath(__file__))
# Buscar directorio ExportTwinCat si existe
export_dir = os.path.join(input_directory, "../ExportTwinCat")
if os.path.exists(export_dir):
input_directory = os.path.abspath(export_dir)
print(f"Detectado directorio ExportTwinCat: {input_directory}")
else:
print(f"Usando directorio actual: {input_directory}")
else:
# Modo normal: usar configuración
input_directory = level3_config.get(
"twincat_exp_directory", working_directory
)
# Verificar directorio de trabajo
if not os.path.exists(working_directory): if not os.path.exists(working_directory):
print(f"Error: El directorio de trabajo no existe: {working_directory}") print(f"Error: El directorio de trabajo no existe: {working_directory}")
return return
# Verificar directorio de entrada
if not os.path.exists(input_directory): if not os.path.exists(input_directory):
print(f"Error: El directorio de entrada no existe: {input_directory}") print(f"Error: El directorio de entrada no existe: {input_directory}")
return return
# Crear directorio de salida SCL
full_scl_path = os.path.join(working_directory, scl_output_dir) full_scl_path = os.path.join(working_directory, scl_output_dir)
if not os.path.exists(full_scl_path): if not os.path.exists(full_scl_path):
os.makedirs(full_scl_path) os.makedirs(full_scl_path)
print(f"Directorio creado: {full_scl_path}") print(f"Directorio creado: {full_scl_path}")
# PRIMERA PASADA: Recopilar interfaces de funciones # PRIMERA PASADA: Recopilar interfaces de funciones
# IMPORTANTE: Siempre hacer primera pasada, incluso en modo debug function_registry, _ = collect_function_interfaces(input_directory, debug_mode)
print(f"Directorio para primera pasada: {input_directory}")
function_registry, function_files = collect_function_interfaces(
input_directory, debug_mode
)
# Determinar archivos a procesar # SEGUNDA PASADA: Procesar todos los archivos
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"🔍 MODO DEBUG ACTIVADO")
print(f"Procesando archivo específico: {debug_file}")
print(f"Directorio de entrada: {input_directory}")
print(f"Directorio de salida SCL: {full_scl_path}")
print(f"Funciones en registry: {len(function_registry.functions)}")
print(
f"Function Blocks en registry: {len(function_registry.function_blocks)}"
)
# 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
# Mostrar información detallada del registry en modo debug
if function_registry.functions:
print("\n🔧 FUNCIONES DETECTADAS:")
for name, func_info in function_registry.functions.items():
print(f"{name}: {func_info.return_type}")
if func_info.inputs:
print(f" IN: {[f'{n}:{t}' for n, t in func_info.inputs]}")
if func_info.outputs:
print(f" OUT: {[f'{n}:{t}' for n, t in func_info.outputs]}")
print()
else:
# Modo normal - todos los archivos
exp_pattern = os.path.join(input_directory, "*.EXP") exp_pattern = os.path.join(input_directory, "*.EXP")
exp_files = glob.glob(exp_pattern) exp_files = glob.glob(exp_pattern)
@ -2266,15 +2290,11 @@ def main():
print(f"No se encontraron archivos .EXP en: {input_directory}") print(f"No se encontraron archivos .EXP en: {input_directory}")
return return
print(f"Encontrados {len(exp_files)} archivos .EXP en: {input_directory}") print(f"\nEncontrados {len(exp_files)} archivos .EXP en: {input_directory}")
print(f"Directorio de salida SCL: {full_scl_path}") print(f"Directorio de salida SCL: {full_scl_path}")
print("\n=== SEGUNDA PASADA: CONVERSIÓN CON INTERFACES CONOCIDAS ===")
print() successful_conversions, failed_conversions = 0, 0
print("=== SEGUNDA PASADA: CONVERSIÓN CON INTERFACES CONOCIDAS ===")
# Procesar cada archivo
successful_conversions = 0
failed_conversions = 0
for exp_file in exp_files: for exp_file in exp_files:
filename = os.path.basename(exp_file) filename = os.path.basename(exp_file)
@ -2282,50 +2302,29 @@ def main():
scl_filename = f"{base_name}.scl" scl_filename = f"{base_name}.scl"
scl_output_path = os.path.join(full_scl_path, scl_filename) 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: if os.path.exists(scl_output_path) and not force_regenerate:
print(f"{'='*60}")
print(f"SALTANDO: {filename} - Ya existe {scl_filename}")
print( print(
f" (usa force_regenerate: true en configuración para forzar regeneración)" f"SALTANDO: {filename} - Ya existe. (Usar 'force_regenerate: true' para sobreescribir)"
) )
successful_conversions += 1 # Contar como exitoso successful_conversions += 1
continue continue
print(f"{'='*60}") print(f"\n{'='*60}")
print(f"Procesando: {filename}") print(f"Procesando: {filename} -> {scl_filename}")
print(f"Salida: {scl_filename}")
try: try:
# Crear nuevo convertidor para cada archivo CON el registry de funciones
converter = SimpleLadConverter(function_registry) converter = SimpleLadConverter(function_registry)
# Parsear archivo
converter.parse_file(exp_file) 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: if debug_mode:
converter.print_debug_info() converter.print_debug_info()
# Optimizar expresiones con SymPy si está habilitado
if sympy_optimization and show_optimizations: if sympy_optimization and show_optimizations:
converter.optimize_expressions() converter.optimize_expressions()
# Analizar agrupación de condiciones si está habilitado
if group_analysis and show_optimizations: if group_analysis and show_optimizations:
converter.group_common_conditions() converter.group_common_conditions()
# Convertir y guardar
print(f" Generando código SCL...")
structured_code = converter.save_to_file(scl_output_path) structured_code = converter.save_to_file(scl_output_path)
# Mostrar parte del código generado si está habilitado
if show_generated_code: if show_generated_code:
lines = structured_code.split("\n") lines = structured_code.split("\n")
display_lines = min(max_display_lines, len(lines)) display_lines = min(max_display_lines, len(lines))
@ -2348,24 +2347,31 @@ def main():
traceback.print_exc() traceback.print_exc()
failed_conversions += 1 failed_conversions += 1
print() print(f"\n{'='*60}")
print(f"RESUMEN DE CONVERSIÓN MASIVA:")
# Resumen final
print(f"{'='*60}")
print(f"RESUMEN DE CONVERSIÓN:")
print(f" 📋 Funciones registradas: {len(function_registry.functions)}")
print(
f" 📋 Function Blocks registrados: {len(function_registry.function_blocks)}"
)
print(f" ✓ Exitosas: {successful_conversions}") print(f" ✓ Exitosas: {successful_conversions}")
print(f" ✗ Fallidas: {failed_conversions}") print(f" ✗ Fallidas: {failed_conversions}")
print(f" 📁 Directorio salida: {full_scl_path}") print(f" 📁 Directorio salida: {full_scl_path}")
if successful_conversions > 0:
print(f"\n✓ Conversión masiva completada!") def main():
"""Función principal - Despachador para modo debug o masivo."""
try:
import time
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
print("=== SCRIPT VERIFICADO Y EJECUTÁNDOSE CORRECTAMENTE ===")
print(f"🕒 Timestamp: {timestamp}")
if len(sys.argv) > 1:
# Modo debug si hay argumentos en la línea de comandos
run_debug_mode(sys.argv[1])
else:
# Modo masivo por defecto
run_mass_conversion()
except Exception as e: except Exception as e:
print(f"Error general: {e}") print(f"Error general en main: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()

File diff suppressed because one or more lines are too long