Simatic_XML_Parser_to_SCL/x2_process.py

2019 lines
86 KiB
Python

# -*- coding: utf-8 -*-
import json
import argparse
import os
import copy
import traceback
import re
# --- Constantes y Configuración ---
SCL_SUFFIX = "_scl"
GROUPED_COMMENT = "// Logic included in grouped IF"
# Global data variable
data = {}
# --- Helper Functions ---
# (get_scl_representation, format_variable_name, generate_temp_var_name, get_target_scl_name - sin cambios)
def get_scl_representation(source_info, network_id, scl_map, access_map):
if not source_info:
return None
if isinstance(source_info, list):
scl_parts = []
all_resolved = True
for sub_source in source_info:
sub_scl = get_scl_representation(
sub_source, network_id, scl_map, access_map
)
if sub_scl is None:
all_resolved = False
break
if (
sub_scl in ["TRUE", "FALSE"]
or (sub_scl.startswith('"') and sub_scl.endswith('"'))
or sub_scl.isdigit()
or (sub_scl.startswith("(") and sub_scl.endswith(")"))
):
scl_parts.append(sub_scl)
else:
scl_parts.append(f"({sub_scl})")
return (
" OR ".join(scl_parts)
if len(scl_parts) > 1
else (scl_parts[0] if scl_parts else "FALSE") if all_resolved else None
)
source_type = source_info.get("type")
if source_type == "powerrail":
return "TRUE"
elif source_type == "variable":
name = source_info.get("name")
# Asegurar que los nombres de variables se formatean correctamente aquí también
return (
format_variable_name(name)
if name
else f"_ERR_VAR_NO_NAME_{source_info.get('uid')}_"
)
elif source_type == "constant":
dtype = str(source_info.get("datatype", "")).upper()
value = source_info.get("value")
try:
if dtype == "BOOL":
return str(value).upper()
elif dtype in [
"INT",
"DINT",
"SINT",
"USINT",
"UINT",
"UDINT",
"LINT",
"ULINT",
"WORD",
"DWORD",
"LWORD",
"BYTE",
]:
return str(value)
elif dtype in ["REAL", "LREAL"]:
s_val = str(value)
return s_val if "." in s_val or "e" in s_val.lower() else s_val + ".0"
elif dtype == "STRING":
# Escapar comillas simples dentro del string si es necesario
str_val = str(value).replace("'", "''")
return f"'{str_val}'"
elif dtype == "TYPEDCONSTANT":
# Podría necesitar formateo específico basado en el tipo real
return str(value)
else:
# Otros tipos (TIME, DATE, etc.) - devolver como string por ahora
str_val = str(value).replace("'", "''")
return f"'{str_val}'"
except Exception as e:
print(f"Advertencia: Error formateando constante {source_info}: {e}")
return f"_ERR_CONST_FORMAT_{source_info.get('uid')}_"
elif source_type == "connection":
map_key = (
network_id,
source_info.get("source_instruction_uid"),
source_info.get("source_pin"),
)
return scl_map.get(map_key)
elif source_type == "unknown_source":
print(
f"Advertencia: Refiriendo a fuente desconocida UID: {source_info.get('uid')}"
)
return f"_ERR_UNKNOWN_SRC_{source_info.get('uid')}_"
else:
print(f"Advertencia: Tipo de fuente desconocido: {source_info}")
return f"_ERR_INVALID_SRC_TYPE_"
def format_variable_name(name):
"""Limpia el nombre de la variable para SCL."""
if not name:
return "_INVALID_NAME_"
# Si ya está entre comillas dobles, asumimos que es un nombre complejo (ej. "DB"."Variable")
# y lo devolvemos tal cual para SCL.
if name.startswith('"') and name.endswith('"'):
# Podríamos añadir validación extra aquí si fuera necesario
return name
# Si no tiene comillas, es un nombre simple (ej. Tag_1, #tempVar)
# Reemplazar caracteres no válidos (excepto '_') por '_'
# Permitir '#' al inicio para variables temporales
prefix = ""
if name.startswith("#"):
prefix = "#"
name = name[1:]
# Permitir letras, números y guiones bajos. Reemplazar el resto.
# Asegurarse de que no empiece con número (después del # si existe)
if name and name[0].isdigit():
name = "_" + name
# Reemplazar caracteres no válidos
name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
return prefix + name
def generate_temp_var_name(network_id, instr_uid, pin_name):
net_id_clean = str(network_id).replace("-", "_")
instr_uid_clean = str(instr_uid).replace("-", "_")
pin_name_clean = str(pin_name).replace("-", "_").lower()
# Usar # para variables temporales SCL estándar
return f"#_temp_{net_id_clean}_{instr_uid_clean}_{pin_name_clean}"
def get_target_scl_name(instruction, output_pin_name, network_id, default_to_temp=True):
instr_uid = instruction["instruction_uid"]
output_pin_data = instruction["outputs"].get(output_pin_name)
target_scl = None
if (
output_pin_data
and isinstance(output_pin_data, list)
and len(output_pin_data) == 1
):
dest_access = output_pin_data[0]
if dest_access.get("type") == "variable":
target_scl = dest_access.get("name")
if target_scl:
target_scl = format_variable_name(target_scl) # Formatear nombre
else:
print(
f"Error: Var destino {instr_uid}.{output_pin_name} sin nombre (UID: {dest_access.get('uid')}). {'Usando temp.' if default_to_temp else 'Ignorando.'}"
)
target_scl = (
generate_temp_var_name(network_id, instr_uid, output_pin_name)
if default_to_temp
else None
)
elif dest_access.get("type") == "constant":
print(
f"Advertencia: Instr {instr_uid} escribe en const UID {dest_access.get('uid')}. {'Usando temp.' if default_to_temp else 'Ignorando.'}"
)
target_scl = (
generate_temp_var_name(network_id, instr_uid, output_pin_name)
if default_to_temp
else None
)
else:
print(
f"Advertencia: Destino {instr_uid}.{output_pin_name} no es var/const: {dest_access.get('type')}. {'Usando temp.' if default_to_temp else 'Ignorando.'}"
)
target_scl = (
generate_temp_var_name(network_id, instr_uid, output_pin_name)
if default_to_temp
else None
)
elif default_to_temp:
target_scl = generate_temp_var_name(network_id, instr_uid, output_pin_name)
# Si target_scl sigue siendo None y no se debe usar temp, devolver None
if target_scl is None and not default_to_temp:
return None
# Si target_scl es None pero sí se permite temp, generar uno ahora
if target_scl is None and default_to_temp:
target_scl = generate_temp_var_name(network_id, instr_uid, output_pin_name)
return target_scl
# --- Procesadores de Instrucciones ---
# (process_contact, process_eq, process_coil, process_convert, process_mod,
# process_add, process_move, process_pbox, process_o, process_call - sin cambios significativos,
# solo asegurar que usan format_variable_name donde sea necesario para operandos/destinos)
# ... (código de los procesadores aquí, asumiendo que ya usan format_variable_name) ...
def process_contact(instruction, network_id, scl_map, access_map):
"""Traduce Contact (normal o negado) a una expresión booleana SCL."""
instr_uid = instruction["instruction_uid"]
instr_type = instruction["type"]
if instr_type.endswith(SCL_SUFFIX) or "_error" in instr_type:
return False
# --- INICIO LEER NEGACIÓN ---
# Verificar si el pin 'operand' está marcado como negado en el JSON
is_negated = instruction.get("negated_pins", {}).get("operand", False)
# --- FIN LEER NEGACIÓN ---
# print(f"DEBUG: Intentando procesar CONTACT{' (N)' if is_negated else ''} - UID: {instr_uid} en Red: {network_id}")
in_input = instruction["inputs"].get("in")
in_rlo_scl = (
"TRUE"
if in_input is None
else get_scl_representation(in_input, network_id, scl_map, access_map)
)
operand_scl = get_scl_representation(
instruction["inputs"].get("operand"), network_id, scl_map, access_map
)
if in_rlo_scl is None or operand_scl is None:
return False
# Usar is_negated para aplicar NOT
term = f"NOT {operand_scl}" if is_negated else operand_scl
if not (term.startswith('"') and term.endswith('"')):
# Añadir paréntesis si es NOT o si contiene espacios/operadores
if is_negated or (
" " in term and not (term.startswith("(") and term.endswith(")"))
):
term = f"({term})"
new_rlo_scl = (
term
if in_rlo_scl == "TRUE"
else (
f"({in_rlo_scl}) AND {term}"
if ("AND" in in_rlo_scl or "OR" in in_rlo_scl)
and not (in_rlo_scl.startswith("(") and in_rlo_scl.endswith(")"))
else f"{in_rlo_scl} AND {term}"
)
)
map_key = (network_id, instr_uid, "out")
scl_map[map_key] = new_rlo_scl
instruction["scl"] = f"// RLO: {new_rlo_scl}"
instruction["type"] = instr_type + SCL_SUFFIX
return True
# --- process_edge_detector MODIFICADA ---
def process_edge_detector(instruction, network_id, scl_map, access_map):
"""Genera SCL para PBox (P_TRIG) o NBox (N_TRIG).
Guarda la expresión del pulso en scl_map['out'] y la actualización
de memoria en un campo temporal '_edge_mem_update_scl'.
El campo 'scl' principal se deja casi vacío/comentario.
Usa el nombre de memoria original sin renombrar.
"""
instr_uid = instruction["instruction_uid"]
instr_type_original = instruction["type"] # PBox o NBox
if instr_type_original.endswith(SCL_SUFFIX) or "_error" in instr_type_original:
return False # Ya procesado o error
# 1. Obtener CLK y MemBit original
clk_input = instruction["inputs"].get("in")
mem_bit_input = instruction["inputs"].get("bit")
clk_scl = get_scl_representation(clk_input, network_id, scl_map, access_map)
mem_bit_scl_original = get_scl_representation(mem_bit_input, network_id, scl_map, access_map) # Ej: "M19001"
# 2. Verificar dependencias y tipo de MemBit
if clk_scl is None: return False
if mem_bit_scl_original is None:
instruction["scl"] = f"// ERROR: {instr_type_original} {instr_uid} MemBit no resuelto."
instruction["type"] = instr_type_original + "_error"
return True
if not (mem_bit_input and mem_bit_input.get("type") == "variable"):
instruction["scl"] = f"// ERROR: {instr_type_original} {instr_uid} 'bit' no es variable."
instruction["type"] = instr_type_original + "_error"
return True
# 3. Formatear CLK (usa memoria original)
clk_scl_formatted = clk_scl
if clk_scl not in ["TRUE", "FALSE"] and \
(' ' in clk_scl or 'AND' in clk_scl or 'OR' in clk_scl or ':=' in clk_scl) and \
not (clk_scl.startswith('(') and clk_scl.endswith(')')):
clk_scl_formatted = f"({clk_scl})"
# 4. Generar Lógica SCL del *pulso*
result_pulse_expression = "FALSE"
scl_comment = ""
if instr_type_original == "PBox": # P_TRIG
result_pulse_expression = f"{clk_scl_formatted} AND NOT {mem_bit_scl_original}"
scl_comment = f"// P_TRIG({clk_scl_formatted})"
elif instr_type_original == "NBox": # N_TRIG
result_pulse_expression = f"NOT {clk_scl_formatted} AND {mem_bit_scl_original}"
scl_comment = f"// N_TRIG({clk_scl_formatted})"
else: # Error
instruction["scl"] = f"// ERROR: Tipo de flanco inesperado {instr_type_original}"
instruction["type"] = instr_type_original + "_error"
return True
# 5. Generar la actualización del bit de memoria
scl_mem_update = f"{mem_bit_scl_original} := {clk_scl_formatted};"
# 6. Almacenar Resultados
map_key_out = (network_id, instr_uid, "out")
scl_map[map_key_out] = result_pulse_expression # Guardar EXPRESIÓN del pulso
instruction['_edge_mem_update_scl'] = f"{scl_mem_update} {scl_comment}" # Guardar UPDATE + Comentario en campo temporal
instruction['scl'] = f"// {instr_type_original} Logic moved to consumer Coil" # Dejar SCL principal vacío/comentario
instruction["type"] = instr_type_original + SCL_SUFFIX
# 7. Propagar ENO
map_key_eno = (network_id, instr_uid, "eno")
scl_map[map_key_eno] = clk_scl
return True
# --- process_coil MODIFICADA (con \n correcto) ---
def process_coil(instruction, network_id, scl_map, access_map):
"""Genera la asignación para Coil. Si la entrada viene de PBox/NBox,
añade la actualización de memoria del flanco DESPUÉS de la asignación."""
instr_uid = instruction["instruction_uid"]
instr_type = instruction["type"]
if instr_type.endswith(SCL_SUFFIX) or "_error" in instr_type:
return False
coil_input_info = instruction["inputs"].get("in")
operand_info = instruction["inputs"].get("operand")
in_rlo_scl = get_scl_representation(coil_input_info, network_id, scl_map, access_map)
operand_scl = get_scl_representation(operand_info, network_id, scl_map, access_map)
if in_rlo_scl is None or operand_scl is None: return False
if not (operand_info and operand_info.get("type") == "variable"):
instruction["scl"] = f"// ERROR: Coil {instr_uid} operando no es variable o falta info"
instruction["type"] = instr_type + "_error"
return True
operand_scl_formatted = format_variable_name(operand_scl)
if in_rlo_scl == "(TRUE)": in_rlo_scl = "TRUE"
elif in_rlo_scl == "(FALSE)": in_rlo_scl = "FALSE"
# Generar la asignación SCL principal de la bobina
scl_assignment = f"{operand_scl_formatted} := {in_rlo_scl};"
scl_final = scl_assignment # Inicializar SCL final
# --- Lógica para añadir actualización de memoria de flancos ---
mem_update_scl_combined = None
if isinstance(coil_input_info, dict) and coil_input_info.get("type") == "connection":
source_uid = coil_input_info.get("source_instruction_uid")
source_pin = coil_input_info.get("source_pin")
# Buscar la instrucción fuente PBox/NBox
source_instruction = None
network_logic = next((net["logic"] for net in data["networks"] if net["id"] == network_id), [])
for instr in network_logic:
if instr.get("instruction_uid") == source_uid:
source_instruction = instr
break
if source_instruction:
source_type = source_instruction.get("type","").replace('_scl','').replace('_error','')
# Si la fuente es PBox o NBox y tiene el campo temporal con la actualización
if source_type in ["PBox", "NBox"] and '_edge_mem_update_scl' in source_instruction:
mem_update_scl_combined = source_instruction.get('_edge_mem_update_scl') # Obtener update+comment
if mem_update_scl_combined:
# Añadir la actualización DESPUÉS de la asignación de la bobina, USANDO \n
scl_final = f"{scl_assignment}\n{mem_update_scl_combined}"
# Marcar la instrucción PBox/NBox para que x3 no escriba su SCL (que ahora está vacío/comentario)
source_instruction['scl'] = f"// Logic moved to Coil {instr_uid}" # Actualizar PBox/NBox SCL
instruction["scl"] = scl_final
instruction["type"] = instr_type + SCL_SUFFIX
return True
# EN x2_process.py, junto a otras funciones process_xxx
def process_scoil(instruction, network_id, scl_map, access_map):
"""Genera SCL para Set Coil (SCoil): IF condition THEN variable := TRUE; END_IF;"""
instr_uid = instruction["instruction_uid"]
instr_type = instruction["type"]
if instr_type.endswith(SCL_SUFFIX) or "_error" in instr_type:
return False # Ya procesado o con error
# Obtener condición de entrada (RLO)
in_info = instruction["inputs"].get("in")
condition_scl = get_scl_representation(in_info, network_id, scl_map, access_map)
# Obtener operando (variable a poner a TRUE)
operand_info = instruction["inputs"].get("operand")
variable_scl = get_scl_representation(operand_info, network_id, scl_map, access_map)
# Verificar dependencias
if condition_scl is None or variable_scl is None:
return False # Dependencias no listas
# Verificar que el operando sea una variable
if not (operand_info and operand_info.get("type") == "variable"):
print(f"Error: SCoil {instr_uid} operando no es variable o falta info (Tipo: {operand_info.get('type')}).")
instruction["scl"] = f"// ERROR: SCoil {instr_uid} operando no es variable."
instruction["type"] += "_error"
return True # Procesado con error
# Formatear nombre de variable
variable_name_formatted = format_variable_name(variable_scl)
# Generar SCL
scl_core = f"{variable_name_formatted} := TRUE;"
scl_final = (
f"IF {condition_scl} THEN\n {scl_core}\nEND_IF;" if condition_scl != "TRUE" else scl_core
)
# Actualizar instrucción
instruction["scl"] = scl_final
instruction["type"] = instr_type + SCL_SUFFIX
# SCoil no genera salida 'out' ni 'eno' significativas para propagar
return True
def process_rcoil(instruction, network_id, scl_map, access_map):
"""Genera SCL para Reset Coil (RCoil): IF condition THEN variable := FALSE; END_IF;"""
instr_uid = instruction["instruction_uid"]
instr_type = instruction["type"]
if instr_type.endswith(SCL_SUFFIX) or "_error" in instr_type:
return False # Ya procesado o con error
# Obtener condición de entrada (RLO)
in_info = instruction["inputs"].get("in")
condition_scl = get_scl_representation(in_info, network_id, scl_map, access_map)
# Obtener operando (variable a poner a FALSE)
operand_info = instruction["inputs"].get("operand")
variable_scl = get_scl_representation(operand_info, network_id, scl_map, access_map)
# Verificar dependencias
if condition_scl is None or variable_scl is None:
return False # Dependencias no listas
# Verificar que el operando sea una variable
if not (operand_info and operand_info.get("type") == "variable"):
print(f"Error: RCoil {instr_uid} operando no es variable o falta info (Tipo: {operand_info.get('type')}).")
instruction["scl"] = f"// ERROR: RCoil {instr_uid} operando no es variable."
instruction["type"] += "_error"
return True # Procesado con error
# Formatear nombre de variable
variable_name_formatted = format_variable_name(variable_scl)
# Generar SCL
scl_core = f"{variable_name_formatted} := FALSE;"
scl_final = (
f"IF {condition_scl} THEN\n {scl_core}\nEND_IF;" if condition_scl != "TRUE" else scl_core
)
# Actualizar instrucción
instruction["scl"] = scl_final
instruction["type"] = instr_type + SCL_SUFFIX
# RCoil no genera salida 'out' ni 'eno' significativas para propagar
return True
def process_eq(instruction, network_id, scl_map, access_map):
instr_uid = instruction["instruction_uid"]
instr_type = instruction["type"]
if instr_type.endswith(SCL_SUFFIX) or "_error" in instr_type:
return False
in1_info = instruction["inputs"].get("in1")
in2_info = instruction["inputs"].get("in2")
in1_scl = get_scl_representation(in1_info, network_id, scl_map, access_map)
in2_scl = get_scl_representation(in2_info, network_id, scl_map, access_map)
if in1_scl is None or in2_scl is None:
return False # Dependencias no listas
# Formatear operandos si son variables
op1 = (
format_variable_name(in1_scl)
if in1_info and in1_info.get("type") == "variable"
else in1_scl
)
op2 = (
format_variable_name(in2_scl)
if in2_info and in2_info.get("type") == "variable"
else in2_scl
)
# Añadir paréntesis si los operandos contienen espacios (poco probable después de formatear)
op1 = f"({op1})" if " " in op1 and not op1.startswith("(") else op1
op2 = f"({op2})" if " " in op2 and not op2.startswith("(") else op2
comparison_scl = f"{op1} = {op2}"
# Guardar el resultado booleano de la comparación en el mapa SCL para la salida 'out'
map_key_out = (network_id, instr_uid, "out")
scl_map[map_key_out] = comparison_scl
# Procesar la entrada 'pre' (RLO anterior) para determinar ENO
pre_input = instruction["inputs"].get("pre")
pre_scl = (
"TRUE"
if pre_input is None
else get_scl_representation(pre_input, network_id, scl_map, access_map)
)
if pre_scl is None:
return False # Dependencia 'pre' no lista
# Guardar el estado de 'pre' como ENO
map_key_eno = (network_id, instr_uid, "eno")
scl_map[map_key_eno] = pre_scl
# El SCL generado es solo un comentario indicando la condición,
# ya que la lógica se propaga a través de scl_map['out']
instruction["scl"] = f"// Comparison Eq {instr_uid}: {comparison_scl}"
instruction["type"] = instr_type + SCL_SUFFIX
return True
def process_convert(instruction, network_id, scl_map, access_map):
instr_uid = instruction["instruction_uid"]
instr_type = instruction["type"]
if instr_type.endswith(SCL_SUFFIX) or "_error" in instr_type:
return False
en_input = instruction["inputs"].get("en")
en_scl = (
get_scl_representation(en_input, network_id, scl_map, access_map)
if en_input
else "TRUE"
)
in_info = instruction["inputs"].get("in")
in_scl = get_scl_representation(in_info, network_id, scl_map, access_map)
if en_scl is None or in_scl is None:
return False # Esperar si dependencias no listas
target_scl = get_target_scl_name(
instruction, "out", network_id, default_to_temp=True
)
if target_scl is None:
print(f"Error: Sin destino claro para CONVERT {instr_uid}")
instruction["scl"] = f"// ERROR: Convert {instr_uid} sin destino"
instruction["type"] += "_error"
return True # Procesado con error
# Formatear entrada si es variable
in_scl_formatted = (
format_variable_name(in_scl)
if in_info and in_info.get("type") == "variable"
else in_scl
)
# Determinar el tipo de destino (simplificado, necesitaría info del XML original)
# Asumimos que el tipo está en TemplateValues o inferirlo del nombre/contexto
target_type = instruction.get("template_values", {}).get(
"destType", "VARIANT"
) # Ejemplo, ajustar según XML real
conversion_func = f"{target_type}_TO_" # Necesita el tipo de origen también
# Esta parte es compleja sin saber los tipos exactos. Usaremos una conversión genérica o MOVE.
# Para una conversión real, necesitaríamos algo como:
# conversion_expr = f"CONVERT(IN := {in_scl_formatted}, OUT => {target_scl})" # Sintaxis inventada
# O usar funciones específicas: INT_TO_REAL, etc.
# Simplificación: Usar asignación directa (MOVE implícito) o función genérica si existe
# Asumiremos asignación directa por ahora.
conversion_expr = in_scl_formatted
scl_core = f"{target_scl} := {conversion_expr};"
# Añadir IF EN si es necesario
scl_final = (
f"IF {en_scl} THEN\n {scl_core}\nEND_IF;" if en_scl != "TRUE" else scl_core
)
instruction["scl"] = scl_final
instruction["type"] = instr_type + SCL_SUFFIX
map_key_out = (network_id, instr_uid, "out")
scl_map[map_key_out] = target_scl # El valor de salida es el contenido del destino
map_key_eno = (network_id, instr_uid, "eno")
scl_map[map_key_eno] = en_scl # ENO sigue a EN
return True
def process_mod(instruction, network_id, scl_map, access_map):
instr_uid = instruction["instruction_uid"]
instr_type = instruction["type"]
if instr_type.endswith(SCL_SUFFIX) or "_error" in instr_type:
return False
en_input = instruction["inputs"].get("en")
en_scl = (
get_scl_representation(en_input, network_id, scl_map, access_map)
if en_input
else "TRUE"
)
in1_info = instruction["inputs"].get("in1")
in2_info = instruction["inputs"].get("in2")
in1_scl = get_scl_representation(in1_info, network_id, scl_map, access_map)
in2_scl = get_scl_representation(in2_info, network_id, scl_map, access_map)
if en_scl is None or in1_scl is None or in2_scl is None:
return False
target_scl = get_target_scl_name(
instruction, "out", network_id, default_to_temp=True
)
if target_scl is None:
print(f"Error: Sin destino MOD {instr_uid}")
instruction["scl"] = f"// ERROR: Mod {instr_uid} sin destino"
instruction["type"] += "_error"
return True
# Formatear operandos si son variables
op1 = (
format_variable_name(in1_scl)
if in1_info and in1_info.get("type") == "variable"
else in1_scl
)
op2 = (
format_variable_name(in2_scl)
if in2_info and in2_info.get("type") == "variable"
else in2_scl
)
# Añadir paréntesis si es necesario (poco probable tras formatear)
op1 = f"({op1})" if " " in op1 and not op1.startswith("(") else op1
op2 = f"({op2})" if " " in op2 and not op2.startswith("(") else op2
scl_core = f"{target_scl} := {op1} MOD {op2};"
scl_final = (
f"IF {en_scl} THEN\n {scl_core}\nEND_IF;" if en_scl != "TRUE" else scl_core
)
instruction["scl"] = scl_final
instruction["type"] = instr_type + SCL_SUFFIX
map_key_out = (network_id, instr_uid, "out")
scl_map[map_key_out] = target_scl
map_key_eno = (network_id, instr_uid, "eno")
scl_map[map_key_eno] = en_scl
return True
def process_add(instruction, network_id, scl_map, access_map):
instr_uid = instruction["instruction_uid"]
instr_type = instruction["type"]
if instr_type.endswith(SCL_SUFFIX) or "_error" in instr_type:
return False
en_input = instruction["inputs"].get("en")
en_scl = (
get_scl_representation(en_input, network_id, scl_map, access_map)
if en_input
else "TRUE"
)
in1_info = instruction["inputs"].get("in1")
in2_info = instruction["inputs"].get("in2")
in1_scl = get_scl_representation(in1_info, network_id, scl_map, access_map)
in2_scl = get_scl_representation(in2_info, network_id, scl_map, access_map)
if en_scl is None or in1_scl is None or in2_scl is None:
return False
target_scl = get_target_scl_name(
instruction, "out", network_id, default_to_temp=True
)
if target_scl is None:
print(f"Error: Sin destino ADD {instr_uid}")
instruction["scl"] = f"// ERROR: Add {instr_uid} sin destino"
instruction["type"] += "_error"
return True
# Formatear operandos si son variables
op1 = (
format_variable_name(in1_scl)
if in1_info and in1_info.get("type") == "variable"
else in1_scl
)
op2 = (
format_variable_name(in2_scl)
if in2_info and in2_info.get("type") == "variable"
else in2_scl
)
# Añadir paréntesis si es necesario
op1 = f"({op1})" if " " in op1 and not op1.startswith("(") else op1
op2 = f"({op2})" if " " in op2 and not op2.startswith("(") else op2
scl_core = f"{target_scl} := {op1} + {op2};"
scl_final = (
f"IF {en_scl} THEN\n {scl_core}\nEND_IF;" if en_scl != "TRUE" else scl_core
)
instruction["scl"] = scl_final
instruction["type"] = instr_type + SCL_SUFFIX
map_key_out = (network_id, instr_uid, "out")
scl_map[map_key_out] = target_scl
map_key_eno = (network_id, instr_uid, "eno")
scl_map[map_key_eno] = en_scl
return True
def process_move(instruction, network_id, scl_map, access_map):
instr_uid = instruction["instruction_uid"]
instr_type = instruction["type"]
if instr_type.endswith(SCL_SUFFIX) or "_error" in instr_type:
return False
en_input = instruction["inputs"].get("en")
en_scl = (
get_scl_representation(en_input, network_id, scl_map, access_map)
if en_input
else "TRUE"
)
in_info = instruction["inputs"].get("in")
in_scl = get_scl_representation(in_info, network_id, scl_map, access_map)
if en_scl is None or in_scl is None:
return False
# Intentar obtener el destino de 'out1' (o 'out' si es el estándar)
target_scl = get_target_scl_name(
instruction, "out1", network_id, default_to_temp=False
)
if target_scl is None:
target_scl = get_target_scl_name(
instruction, "out", network_id, default_to_temp=False
)
if target_scl is None:
# Si no hay destino explícito, podríamos usar una temp si la lógica lo requiere,
# pero MOVE generalmente necesita un destino claro. Marcar como advertencia/error.
print(
f"Advertencia/Error: MOVE {instr_uid} sin destino claro en 'out' o 'out1'. Se requiere destino explícito."
)
# Decidir si generar error o simplemente no hacer nada. No hacer nada es más seguro.
# instruction["scl"] = f"// ERROR: MOVE {instr_uid} sin destino"
# instruction["type"] += "_error"
# return True
return False # No procesar si no hay destino claro
# Formatear entrada si es variable
in_scl_formatted = (
format_variable_name(in_scl)
if in_info and in_info.get("type") == "variable"
else in_scl
)
scl_core = f"{target_scl} := {in_scl_formatted};"
scl_final = (
f"IF {en_scl} THEN\n {scl_core}\nEND_IF;" if en_scl != "TRUE" else scl_core
)
instruction["scl"] = scl_final
instruction["type"] = instr_type + SCL_SUFFIX
# Propagar el valor movido a través de scl_map si se usa 'out' o 'out1' como fuente
map_key_out = (network_id, instr_uid, "out") # Asumir 'out' como estándar
scl_map[map_key_out] = target_scl # El valor es lo que está en el destino
map_key_out1 = (network_id, instr_uid, "out1") # Si existe out1
scl_map[map_key_out1] = target_scl
map_key_eno = (network_id, instr_uid, "eno")
scl_map[map_key_eno] = en_scl
return True
# --- FUNCIÓN UNIFICADA CORREGIDA para PBox y NBox ---
"""Genera SCL para PBox (P_TRIG) o NBox (N_TRIG)."""
instr_uid = instruction["instruction_uid"]
instr_type_original = instruction["type"] # PBox o NBox
# print(f"DEBUG Edge: Intentando procesar {instr_type_original} UID {instr_uid} en Red {network_id}") # Debug
if instr_type_original.endswith(SCL_SUFFIX) or "_error" in instr_type_original:
return False # Ya procesado o error
# 1. Obtener representaciones SCL de las entradas CLK y MemBit
clk_input = instruction["inputs"].get("in")
mem_bit_input = instruction["inputs"].get("bit")
clk_scl = get_scl_representation(clk_input, network_id, scl_map, access_map)
mem_bit_scl_original = get_scl_representation(mem_bit_input, network_id, scl_map, access_map)
# 2. Verificar si las dependencias están listas
if clk_scl is None:
# print(f"DEBUG Edge: CLK no resuelto para {instr_type_original} UID {instr_uid}. Esperando.")
return False # Dependencia CLK no lista
if mem_bit_scl_original is None:
# Esto es menos probable, pero por seguridad
print(f"Error: No se pudo resolver MemBit para {instr_type_original} UID {instr_uid}.")
instruction["scl"] = f"// ERROR: {instr_type_original} {instr_uid} MemBit no resuelto."
instruction["type"] = instr_type_original + "_error"
return True # Marcar como error
# 3. Validar que el bit de memoria sea una variable
if not (mem_bit_input and mem_bit_input.get("type") == "variable"):
print(f"Error: {instr_type_original} {instr_uid} 'bit' no es variable o falta información.")
instruction["scl"] = f"// ERROR: {instr_type_original} {instr_uid} 'bit' no es variable."
instruction["type"] = instr_type_original + "_error"
return True # Procesado con error
# 4. Renombrar bit de memoria para VAR_STAT y formatear CLK
mem_bit_name_clean = mem_bit_scl_original.strip('"')
stat_mem_bit_scl = f'"stat_{mem_bit_name_clean}"' if not mem_bit_name_clean.startswith("stat_") else mem_bit_scl_original
clk_scl_formatted = clk_scl
# Añadir paréntesis si es necesario (expresión compleja) - No a TRUE/FALSE
if clk_scl not in ["TRUE", "FALSE"] and \
(' ' in clk_scl or 'AND' in clk_scl or 'OR' in clk_scl or ':=' in clk_scl) and \
not (clk_scl.startswith('(') and clk_scl.endswith(')')):
clk_scl_formatted = f"({clk_scl})"
# 5. Generar Lógica SCL específica para PBox o NBox
result_pulse_scl = "FALSE" # SCL para la salida del flanco (pin 'out')
scl_comment = ""
if instr_type_original == "PBox": # Flanco Positivo (P_TRIG)
# Pulso = CLK actual Y NO Memoria anterior
result_pulse_scl = f"{clk_scl_formatted} AND NOT {stat_mem_bit_scl}"
scl_comment = f"// P_TRIG({clk_scl_formatted})"
elif instr_type_original == "NBox": # Flanco Negativo (N_TRIG)
# Pulso = NO CLK actual Y Memoria anterior
result_pulse_scl = f"NOT {clk_scl_formatted} AND {stat_mem_bit_scl}"
scl_comment = f"// N_TRIG({clk_scl_formatted})"
else:
print(f"Error interno: process_edge_detector llamado para tipo inesperado {instr_type_original}")
instruction["scl"] = f"// ERROR: Tipo de flanco inesperado {instr_type_original}"
instruction["type"] = instr_type_original + "_error"
return True
# 6. Generar la actualización del bit de memoria (siempre se actualiza con el estado actual de CLK)
scl_mem_update = f"{stat_mem_bit_scl} := {clk_scl_formatted};"
# 7. Almacenar Resultados
# - El *pulso* resultante se almacena en el mapa para la salida 'out'.
# - La *actualización de memoria* se almacena en el campo 'scl' de la instrucción.
map_key_out = (network_id, instr_uid, "out")
scl_map[map_key_out] = result_pulse_scl
# print(f"DEBUG Edge: {instr_type_original} UID {instr_uid} -> map['out'] = {result_pulse_scl}") # Debug
instruction["scl"] = f"{scl_mem_update} {scl_comment}" # Incluye la acción principal y un comentario
instruction["type"] = instr_type_original + SCL_SUFFIX
# print(f"DEBUG Edge: {instr_type_original} UID {instr_uid} -> instruction['scl'] = {instruction['scl']}") # Debug
# 8. Propagar ENO (generalmente sigue al CLK)
map_key_eno = (network_id, instr_uid, "eno")
scl_map[map_key_eno] = clk_scl # Usar el clk_scl original sin formato extra
# print(f"DEBUG Edge: {instr_type_original} UID {instr_uid} procesado exitosamente.") # Debug
return True # Indicar que se procesó
# EN x2_process.py
def process_blkmov(instruction, network_id, scl_map, access_map):
"""
Genera SCL usando BLKMOV directamente como nombre de función,
sin COUNT y con formato específico, según solicitud del usuario.
ADVERTENCIA: Es MUY PROBABLE que esto NO compile en TIA Portal estándar,
ya que BLKMOV no es una función SCL y MOVE_BLK requiere COUNT.
"""
instr_uid = instruction["instruction_uid"]
instr_type = instruction["type"]
if instr_type.endswith(SCL_SUFFIX) or "_error" in instr_type:
return False # Ya procesado o con error
# --- Obtener Entradas ---
en_input = instruction["inputs"].get("en")
en_scl = (
get_scl_representation(en_input, network_id, scl_map, access_map)
if en_input
else "TRUE"
)
srcblk_info = instruction["inputs"].get("SRCBLK")
# ¡IMPORTANTE! Obtenemos el nombre RAW antes de formatearlo para usarlo como pide el usuario
raw_srcblk_name = srcblk_info.get("name") if srcblk_info else None
# Verificar dependencias de entrada (solo necesitamos que EN esté resuelto)
if en_scl is None:
return False # Dependencia EN no lista
if raw_srcblk_name is None:
print(f"Error: BLKMOV {instr_uid} sin información válida para SRCBLK.")
instruction["scl"] = f"// ERROR: BLKMOV {instr_uid} sin SRCBLK válido."
instruction["type"] += "_error"
return True
# --- Obtener Destinos (Salidas) ---
# RET_VAL (Usamos get_target_scl_name para manejar variables temporales si es necesario)
retval_target_scl = get_target_scl_name(
instruction, "RET_VAL", network_id, default_to_temp=True
)
if retval_target_scl is None:
print(f"Error: BLKMOV {instr_uid} sin destino claro para RET_VAL.")
instruction["scl"] = f"// ERROR: BLKMOV {instr_uid} sin destino RET_VAL"
instruction["type"] += "_error"
return True
# DSTBLK (Obtenemos el nombre RAW para usarlo como pide el usuario)
raw_dstblk_name = None
dstblk_output_list = instruction.get("outputs", {}).get("DSTBLK", [])
if dstblk_output_list and isinstance(dstblk_output_list, list) and len(dstblk_output_list) == 1:
dest_access = dstblk_output_list[0]
if dest_access.get("type") == "variable":
raw_dstblk_name = dest_access.get("name") # Nombre raw del JSON
else:
print(f"Advertencia: Destino DSTBLK de BLKMOV {instr_uid} no es una variable (Tipo: {dest_access.get('type')}).")
else:
print(f"Error: No se encontró un destino único y válido para DSTBLK en BLKMOV {instr_uid}.")
if raw_dstblk_name is None:
instruction["scl"] = f"// ERROR: BLKMOV {instr_uid} sin destino DSTBLK válido."
instruction["type"] += "_error"
return True
# --- Formateo especial para SRCBLK/DSTBLK como pidió el usuario ---
# Asume formato "DB".Variable o "Struct".Variable del JSON y lo mantiene
# (Esto anula la limpieza normal de format_variable_name para estos parámetros)
srcblk_final_str = raw_srcblk_name if raw_srcblk_name else "_ERROR_SRC_"
dstblk_final_str = raw_dstblk_name if raw_dstblk_name else "_ERROR_DST_"
# --- Generar SCL Exacto Solicitado ---
scl_core = (
f"{retval_target_scl} := BLKMOV(SRCBLK := {srcblk_final_str}, "
f"DSTBLK => {dstblk_final_str}); "
f"// ADVERTENCIA: BLKMOV usado directamente, probablemente no compile!"
)
# Añadir condición EN (usando la representación SCL obtenida para EN)
scl_final = (
f"IF {en_scl} THEN\n {scl_core}\nEND_IF;" if en_scl != "TRUE" else scl_core
)
# --- Actualizar Instrucción y Mapa SCL ---
instruction["scl"] = scl_final
instruction["type"] = instr_type + SCL_SUFFIX
# Propagar ENO (igual que EN)
map_key_eno = (network_id, instr_uid, "eno")
scl_map[map_key_eno] = en_scl
# Propagar el valor de retorno (el contenido de la variable asignada a RET_VAL)
map_key_ret_val = (network_id, instr_uid, "RET_VAL")
scl_map[map_key_ret_val] = retval_target_scl # El valor es lo que sea que se asigne
return True
# ... (Asegúrate de que esta función está registrada en processor_map como antes) ...
def process_o(instruction, network_id, scl_map, access_map):
instr_uid = instruction["instruction_uid"]
instr_type = instruction["type"]
if instr_type.endswith(SCL_SUFFIX) or "_error" in instr_type:
return False
# Buscar todas las entradas 'in', 'in1', 'in2', ...
input_pins = sorted([pin for pin in instruction["inputs"] if pin.startswith("in")])
if not input_pins:
print(f"Error: O {instr_uid} sin pines de entrada (inX).")
instruction["scl"] = f"// ERROR: O {instr_uid} sin pines inX"
instruction["type"] += "_error"
return True # Procesado con error
scl_parts = []
all_resolved = True
for pin in input_pins:
in_scl = get_scl_representation(
instruction["inputs"][pin], network_id, scl_map, access_map
)
if in_scl is None:
all_resolved = False
# print(f"DEBUG: O {instr_uid} esperando pin {pin}")
break # Salir del bucle for si una entrada no está lista
# Formatear término (añadir paréntesis si es necesario)
term = in_scl
if (" " in term or "AND" in term) and not (
term.startswith("(") and term.endswith(")")
):
term = f"({term})"
scl_parts.append(term)
if not all_resolved:
return False # Esperar a que todas las entradas estén resueltas
# Construir la expresión OR
result_scl = "FALSE" # Valor por defecto si no hay entradas válidas (raro)
if scl_parts:
result_scl = " OR ".join(scl_parts)
# Simplificar si solo hay un término
if len(scl_parts) == 1:
result_scl = scl_parts[0]
# Quitar paréntesis redundantes si solo hay un término y está entre paréntesis
if result_scl.startswith("(") and result_scl.endswith(")"):
# Comprobar si los paréntesis son necesarios (contienen operadores de menor precedencia)
# Simplificación: quitar siempre si solo hay un término. Podría ser incorrecto en casos complejos.
# result_scl = result_scl[1:-1] # Comentado por seguridad
pass
# Actualizar mapa SCL y la instrucción
map_key_out = (network_id, instr_uid, "out")
scl_map[map_key_out] = result_scl
instruction["scl"] = (
f"// Logic O {instr_uid}: {result_scl}" # Comentario informativo
)
instruction["type"] = instr_type + SCL_SUFFIX
# La instrucción 'O' no tiene ENO propio, propaga el resultado por 'out'
return True
def process_call(instruction, network_id, scl_map, access_map):
instr_uid = instruction["instruction_uid"]
instr_type = instruction.get("type", "") # Usar get con default
if instr_type.endswith(SCL_SUFFIX) or "_error" in instr_type:
return False
block_name = instruction.get("block_name", f"UnknownCall_{instr_uid}")
block_type = instruction.get("block_type") # FC, FB
instance_db = instruction.get("instance_db") # Nombre del DB de instancia (para FB)
# Formatear nombres
block_name_scl = format_variable_name(block_name)
instance_db_scl = format_variable_name(instance_db) if instance_db else None
# --- Manejo de EN ---
en_input = instruction["inputs"].get("en")
en_scl = (
get_scl_representation(en_input, network_id, scl_map, access_map)
if en_input
else "TRUE"
)
if en_scl is None:
return False # Dependencia EN no resuelta
# --- Procesar Parámetros de Entrada/Salida ---
# Necesitamos iterar sobre los pines definidos en la interfaz del bloque llamado.
# Esta información no está directamente en la instrucción 'Call' del JSON simplificado.
# ¡Limitación! Sin la interfaz del bloque llamado, solo podemos manejar EN/ENO
# y asumir una llamada sin parámetros o con parámetros conectados implícitamente.
# Solución temporal: Buscar conexiones en 'inputs' y 'outputs' que NO sean 'en'/'eno'
# y construir la llamada basándose en eso. Esto es muy heurístico.
scl_call_params = []
processed_inputs = {"en"} # Marcar 'en' como ya procesado
for pin_name, source_info in instruction.get("inputs", {}).items():
if pin_name not in processed_inputs:
param_scl = get_scl_representation(
source_info, network_id, scl_map, access_map
)
if param_scl is None:
# print(f"DEBUG: Call {instr_uid} esperando parámetro de entrada {pin_name}")
return False # Dependencia de parámetro no resuelta
# Formatear si es variable
param_scl_formatted = (
format_variable_name(param_scl)
if source_info.get("type") == "variable"
else param_scl
)
scl_call_params.append(
f"{format_variable_name(pin_name)} := {param_scl_formatted}"
)
processed_inputs.add(pin_name)
# Procesar parámetros de salida (asignaciones después de la llamada o pasados como VAR_IN_OUT/VAR_OUTPUT)
# Esto es aún más complejo. SCL normalmente asigna salidas después o usa punteros/referencias.
# Simplificación: Asumir que las salidas se manejan por asignación posterior si es necesario,
# o que son VAR_OUTPUT y se acceden como instancia.salida.
# Por ahora, no generamos asignaciones explícitas para las salidas aquí.
# --- Construcción de la Llamada SCL ---
scl_call_body = ""
param_string = ", ".join(scl_call_params)
if block_type == "FB":
if not instance_db_scl:
print(
f"Error: Llamada a FB '{block_name_scl}' (UID {instr_uid}) sin DB de instancia especificado."
)
instruction["scl"] = f"// ERROR: FB Call {block_name_scl} sin instancia"
instruction["type"] = "Call_FB_error"
return True # Procesado con error
# Llamada a FB con DB de instancia
scl_call_body = f"{instance_db_scl}({param_string});"
elif block_type == "FC":
# Llamada a FC
scl_call_body = f"{block_name_scl}({param_string});"
else:
print(
f"Advertencia: Tipo de bloque no soportado para Call UID {instr_uid}: {block_type}"
)
scl_call_body = f"// ERROR: Call a bloque tipo '{block_type}' no soportado: {block_name_scl}"
instruction["type"] = f"Call_{block_type}_error" # Marcar como error parcial
# --- Aplicar Condición EN ---
scl_final = ""
if en_scl != "TRUE":
# Indentar la llamada dentro del IF
indented_call = "\n".join([f" {line}" for line in scl_call_body.splitlines()])
scl_final = f"IF {en_scl} THEN\n{indented_call}\nEND_IF;"
else:
scl_final = scl_call_body
# --- Actualizar JSON y Mapa SCL ---
instruction["scl"] = scl_final
instruction["type"] = (
f"Call_{block_type}_scl"
if "_error" not in instruction["type"]
else instruction["type"]
)
# Actualizar scl_map con el estado ENO (igual a EN para llamadas simples sin manejo explícito de ENO)
map_key_eno = (network_id, instr_uid, "eno")
scl_map[map_key_eno] = en_scl
# Propagar valores de salida (si pudiéramos determinarlos)
# Ejemplo: Si supiéramos que hay una salida 'Out1' de tipo INT
# map_key_out1 = (network_id, instr_uid, "Out1")
# if block_type == "FB" and instance_db_scl:
# scl_map[map_key_out1] = f"{instance_db_scl}.Out1" # Acceso a salida de instancia
# else:
# # Para FCs, necesitaríamos una variable temporal o asignación explícita
# temp_out1 = generate_temp_var_name(network_id, instr_uid, "Out1")
# # Modificar scl_call_body para incluir la asignación: Out1 => temp_out1
# scl_map[map_key_out1] = temp_out1
return True
# --- Procesador de Temporizadores (TON, TOF) ---
def process_timer(instruction, network_id, scl_map, access_map):
"""
Genera SCL para Temporizadores (TON, TOF).
Requiere datos de instancia (DB o STAT).
"""
instr_uid = instruction["instruction_uid"]
instr_type = instruction["type"] # Será "TON" o "TOF"
if instr_type.endswith(SCL_SUFFIX) or "_error" in instr_type:
return False
# 1. Obtener Inputs
in_info = instruction["inputs"].get("IN") # Entrada booleana
pt_info = instruction["inputs"].get("PT") # Preset Time (Tipo TIME)
scl_in = get_scl_representation(in_info, network_id, scl_map, access_map)
scl_pt = get_scl_representation(pt_info, network_id, scl_map, access_map)
if scl_in is None or scl_pt is None:
return False # Dependencias no listas
# 2. Obtener Nombre de Instancia (NECESITA MEJORA EN x1.py)
instance_name = instruction.get("instance_db") # Reutilizar campo si x1 lo llena
if not instance_name:
# Generar placeholder si x1 no extrajo la instancia para Part
instance_name = f"#TIMER_INSTANCE_{instr_uid}" # Placeholder para VAR_TEMP o VAR_STAT
print(f"Advertencia: No se encontró instancia para {instr_type} UID {instr_uid}. Usando placeholder '{instance_name}'. Ajustar x1.py y declarar en x3.py.")
else:
instance_name = format_variable_name(instance_name) # Limpiar si viene de x1
# 3. Formatear entradas si son variables
scl_in_formatted = format_variable_name(scl_in) if in_info and in_info.get("type") == "variable" else scl_in
scl_pt_formatted = format_variable_name(scl_pt) if pt_info and pt_info.get("type") == "variable" else scl_pt
# 4. Generar la llamada SCL
# Nota: Las salidas Q y ET se acceden directamente desde la instancia.
scl_call = f"{instance_name}(IN := {scl_in_formatted}, PT := {scl_pt_formatted}); // TODO: Declarar {instance_name} : {instr_type}; en VAR_STAT o VAR"
instruction["scl"] = scl_call
instruction["type"] = instr_type + SCL_SUFFIX
# 5. Actualizar scl_map para las salidas Q y ET
map_key_q = (network_id, instr_uid, "Q")
scl_map[map_key_q] = f"{instance_name}.Q"
map_key_et = (network_id, instr_uid, "ET")
scl_map[map_key_et] = f"{instance_name}.ET"
# TON/TOF no tienen un pin ENO estándar en LAD/FBD que se mapee directamente
return True
# --- Procesador de Contadores (CTU, CTD, CTUD) ---
def process_counter(instruction, network_id, scl_map, access_map):
"""
Genera SCL para Contadores (CTU, CTD, CTUD).
Requiere datos de instancia (DB o STAT).
"""
instr_uid = instruction["instruction_uid"]
instr_type = instruction["type"] # CTU, CTD, CTUD
if instr_type.endswith(SCL_SUFFIX) or "_error" in instr_type:
return False
# 1. Obtener Inputs (varía según tipo)
params = []
resolved = True
input_pins = []
if instr_type == "CTU": input_pins = ["CU", "R", "PV"]
elif instr_type == "CTD": input_pins = ["CD", "LD", "PV"]
elif instr_type == "CTUD": input_pins = ["CU", "CD", "R", "LD", "PV"]
else:
instruction["scl"] = f"// ERROR: Tipo de contador no soportado: {instr_type}"
instruction["type"] += "_error"
return True # Procesado con error
for pin in input_pins:
pin_info = instruction["inputs"].get(pin)
if pin_info is None and pin not in ["R", "LD"]: # R y LD pueden no estar conectados
print(f"Error: Falta entrada requerida '{pin}' para {instr_type} UID {instr_uid}.")
# Permitir continuar si solo faltan R o LD opcionales? Por ahora no.
instruction["scl"] = f"// ERROR: Falta entrada requerida '{pin}' para {instr_type} UID {instr_uid}."
instruction["type"] += "_error"
return True # Error
elif pin_info: # Si el pin existe en el JSON
scl_pin = get_scl_representation(pin_info, network_id, scl_map, access_map)
if scl_pin is None:
resolved = False
break # Salir si una dependencia no está lista
scl_pin_formatted = format_variable_name(scl_pin) if pin_info.get("type") == "variable" else scl_pin
params.append(f"{pin} := {scl_pin_formatted}")
if not resolved: return False
# 2. Obtener Nombre de Instancia (NECESITA MEJORA EN x1.py)
instance_name = instruction.get("instance_db")
if not instance_name:
instance_name = f"#COUNTER_INSTANCE_{instr_uid}" # Placeholder
print(f"Advertencia: No se encontró instancia para {instr_type} UID {instr_uid}. Usando placeholder '{instance_name}'. Ajustar x1.py y declarar en x3.py.")
else:
instance_name = format_variable_name(instance_name)
# 3. Generar la llamada SCL
param_string = ", ".join(params)
scl_call = f"{instance_name}({param_string}); // TODO: Declarar {instance_name} : {instr_type}; en VAR_STAT o VAR"
instruction["scl"] = scl_call
instruction["type"] = instr_type + SCL_SUFFIX
# 4. Actualizar scl_map para las salidas (QU, QD, CV)
output_pins = []
if instr_type == "CTU": output_pins = ["QU", "CV"]
elif instr_type == "CTD": output_pins = ["QD", "CV"]
elif instr_type == "CTUD": output_pins = ["QU", "QD", "CV"]
for pin in output_pins:
map_key = (network_id, instr_uid, pin)
scl_map[map_key] = f"{instance_name}.{pin}"
# Contadores no tienen ENO estándar en LAD/FBD
return True
# --- Procesador de Comparadores (EQ ya existe, añadir otros) ---
def process_comparison(instruction, network_id, scl_map, access_map):
"""
Genera la expresión SCL para Comparadores (GT, LT, GE, LE, NE).
El resultado se propaga por scl_map['out'].
"""
instr_uid = instruction["instruction_uid"]
instr_type = instruction["type"] # GT, LT, GE, LE, NE
if instr_type.endswith(SCL_SUFFIX) or "_error" in instr_type:
return False
# Mapa de tipos a operadores SCL
op_map = {"GT": ">", "LT": "<", "GE": ">=", "LE": "<=", "NE": "<>"}
scl_operator = op_map.get(instr_type)
if not scl_operator:
instruction["scl"] = f"// ERROR: Tipo de comparación no soportado: {instr_type}"
instruction["type"] += "_error"
return True
# Obtener operandos
in1_info = instruction["inputs"].get("in1")
in2_info = instruction["inputs"].get("in2")
in1_scl = get_scl_representation(in1_info, network_id, scl_map, access_map)
in2_scl = get_scl_representation(in2_info, network_id, scl_map, access_map)
if in1_scl is None or in2_scl is None:
return False # Dependencias no listas
# Formatear operandos si son variables
op1 = format_variable_name(in1_scl) if in1_info and in1_info.get("type") == "variable" else in1_scl
op2 = format_variable_name(in2_scl) if in2_info and in2_info.get("type") == "variable" else in2_scl
# Añadir paréntesis si contienen espacios (poco probable tras formatear)
op1 = f"({op1})" if " " in op1 and not op1.startswith("(") else op1
op2 = f"({op2})" if " " in op2 and not op2.startswith("(") else op2
comparison_scl = f"{op1} {scl_operator} {op2}"
# Guardar resultado en el mapa para 'out'
map_key_out = (network_id, instr_uid, "out")
scl_map[map_key_out] = f"({comparison_scl})" # Poner paréntesis por seguridad
# Manejar entrada 'pre'/RLO -> ENO (como en EQ)
pre_input = instruction["inputs"].get("pre") # Asumir 'pre' como en EQ
en_scl = get_scl_representation(pre_input, network_id, scl_map, access_map) if pre_input else "TRUE"
if en_scl is None:
return False # Dependencia 'pre'/'en' no lista
map_key_eno = (network_id, instr_uid, "eno")
scl_map[map_key_eno] = en_scl
instruction["scl"] = f"// Comparison {instr_type} {instr_uid}: {comparison_scl}"
instruction["type"] = instr_type + SCL_SUFFIX
return True
# --- Procesador de Matemáticas (ADD ya existe, añadir otros) ---
def process_math(instruction, network_id, scl_map, access_map):
"""
Genera SCL para operaciones matemáticas (SUB, MUL, DIV).
"""
instr_uid = instruction["instruction_uid"]
instr_type = instruction["type"] # SUB, MUL, DIV
if instr_type.endswith(SCL_SUFFIX) or "_error" in instr_type:
return False
# Mapa de tipos a operadores SCL
op_map = {"SUB": "-", "MUL": "*", "DIV": "/"}
scl_operator = op_map.get(instr_type)
if not scl_operator:
instruction["scl"] = f"// ERROR: Operación matemática no soportada: {instr_type}"
instruction["type"] += "_error"
return True
# Obtener EN, IN1, IN2
en_input = instruction["inputs"].get("en")
in1_info = instruction["inputs"].get("in1")
in2_info = instruction["inputs"].get("in2")
en_scl = get_scl_representation(en_input, network_id, scl_map, access_map) if en_input else "TRUE"
in1_scl = get_scl_representation(in1_info, network_id, scl_map, access_map)
in2_scl = get_scl_representation(in2_info, network_id, scl_map, access_map)
if en_scl is None or in1_scl is None or in2_scl is None:
return False # Dependencias no listas
# Obtener destino 'out'
target_scl = get_target_scl_name(instruction, "out", network_id, default_to_temp=True)
if target_scl is None:
instruction["scl"] = f"// ERROR: {instr_type} {instr_uid} sin destino 'out'."
instruction["type"] += "_error"
return True
# Formatear operandos si son variables
op1 = format_variable_name(in1_scl) if in1_info and in1_info.get("type") == "variable" else in1_scl
op2 = format_variable_name(in2_scl) if in2_info and in2_info.get("type") == "variable" else in2_scl
# Añadir paréntesis si es necesario (especialmente para expresiones)
op1 = f"({op1})" if (" " in op1 or "+" in op1 or "-" in op1 or "*" in op1 or "/" in op1) and not op1.startswith("(") else op1
op2 = f"({op2})" if (" " in op2 or "+" in op2 or "-" in op2 or "*" in op2 or "/" in op2) and not op2.startswith("(") else op2
# Generar SCL
scl_core = f"{target_scl} := {op1} {scl_operator} {op2};"
scl_final = f"IF {en_scl} THEN\n {scl_core}\nEND_IF;" if en_scl != "TRUE" else scl_core
instruction["scl"] = scl_final
instruction["type"] = instr_type + SCL_SUFFIX
# Actualizar mapa SCL
map_key_out = (network_id, instr_uid, "out")
scl_map[map_key_out] = target_scl
map_key_eno = (network_id, instr_uid, "eno")
scl_map[map_key_eno] = en_scl
return True
# --- Procesador NOT ---
def process_not(instruction, network_id, scl_map, access_map):
"""Genera la expresión SCL para la inversión lógica NOT."""
instr_uid = instruction["instruction_uid"]
instr_type = instruction["type"] # Not
if instr_type.endswith(SCL_SUFFIX) or "_error" in instr_type:
return False
in_info = instruction["inputs"].get("in")
in_scl = get_scl_representation(in_info, network_id, scl_map, access_map)
if in_scl is None:
return False # Dependencia no lista
# Formatear entrada (añadir paréntesis si es complejo)
in_scl_formatted = in_scl
if (" " in in_scl or "AND" in in_scl or "OR" in in_scl) and not (in_scl.startswith("(") and in_scl.endswith(")")):
in_scl_formatted = f"({in_scl})"
result_scl = f"NOT {in_scl_formatted}"
# Guardar resultado en mapa para 'out'
map_key_out = (network_id, instr_uid, "out")
scl_map[map_key_out] = result_scl
instruction["scl"] = f"// Logic NOT {instr_uid}: {result_scl}"
instruction["type"] = instr_type + SCL_SUFFIX
# NOT no tiene EN/ENO explícito en LAD, modifica el RLO
return True
# EN x2_process.py, junto a otros procesadores
# --- Procesador para Se (Timer Pulse -> TP SCL) ---
def process_se(instruction, network_id, scl_map, access_map):
"""
Genera SCL para Temporizador de Pulso (Se -> TP).
Requiere datos de instancia (DB o STAT/TEMP).
"""
instr_uid = instruction["instruction_uid"]
instr_type = "Se" # Tipo original LAD
if instruction["type"].endswith(SCL_SUFFIX) or "_error" in instruction["type"]:
return False
# 1. Obtener Inputs: s (start), tv (time value)
# El pin 'r' (reset) no tiene equivalente directo en TP, se ignora aquí.
s_info = instruction["inputs"].get("s")
tv_info = instruction["inputs"].get("tv")
timer_instance_info = instruction["inputs"].get("timer") # Esperando que x1 lo extraiga
scl_s = get_scl_representation(s_info, network_id, scl_map, access_map)
scl_tv = get_scl_representation(tv_info, network_id, scl_map, access_map)
# Obtenemos el nombre de la variable instancia, crucial!
scl_instance_name = get_scl_representation(timer_instance_info, network_id, scl_map, access_map)
if scl_s is None or scl_tv is None:
return False # Dependencias no listas
# 2. Validar y obtener Nombre de Instancia
instance_name = None
if timer_instance_info and timer_instance_info.get("type") == "variable":
instance_name = scl_instance_name # Ya debería estar formateado por get_scl_repr
elif timer_instance_info: # Si está conectado pero no es variable directa? Raro.
print(f"Advertencia: Pin 'timer' de {instr_type} UID {instr_uid} conectado a algo inesperado: {timer_instance_info.get('type')}")
instance_name = f"#TP_INSTANCE_{instr_uid}" # Usar placeholder
else: # Si no hay pin 'timer' conectado (no debería pasar si x1 funciona)
instance_name = f"#TP_INSTANCE_{instr_uid}" # Usar placeholder
print(f"Advertencia: No se encontró conexión al pin 'timer' para {instr_type} UID {instr_uid}. Usando placeholder '{instance_name}'. ¡Revisar x1.py y XML!")
# 3. Formatear entradas si son variables (aunque get_scl_representation ya debería hacerlo)
scl_s_formatted = format_variable_name(scl_s) if s_info and s_info.get("type") == "variable" else scl_s
scl_tv_formatted = format_variable_name(scl_tv) if tv_info and tv_info.get("type") == "variable" else scl_tv
# 4. Generar la llamada SCL (TP usa IN, PT, Q, ET)
scl_call = f"{instance_name}(IN := {scl_s_formatted}, PT := {scl_tv_formatted}); // TODO: Declarar {instance_name} : TP; en VAR_STAT o VAR"
instruction["scl"] = scl_call
instruction["type"] = instr_type + SCL_SUFFIX
# 5. Actualizar scl_map usando los nombres de pin ORIGINALES mapeados si existen
output_pin_mapping_reverse = {v: k for k, v in instruction.get("_output_pin_mapping", {}).items()} # Necesitaríamos guardar el mapeo en x1
q_original_pin = "q" # Default
rt_original_pin = "rt" # Default
# Intentar encontrar los pines originales si x1 guardó el mapeo (MEJORA NECESARIA en x1)
# Por ahora, para SdCoil que mapeaba out->q, usaremos 'out' directamente
if instruction.get("type") == "Se_scl" and instruction.get("original_type") == "SdCoil": # Necesitamos guardar original_type en x1
q_original_pin = "out"
# rt no existe en SdCoil
map_key_q = (network_id, instr_uid, q_original_pin)
scl_map[map_key_q] = f"{instance_name}.Q"
if rt_original_pin: # Solo añadir rt si corresponde
map_key_rt = (network_id, instr_uid, rt_original_pin)
scl_map[map_key_rt] = f"{instance_name}.ET"
return True
# --- Procesador para Sd (On-Delay Timer -> TON SCL) ---
def process_sd(instruction, network_id, scl_map, access_map):
"""
Genera SCL para Temporizador On-Delay (Sd -> TON).
Requiere datos de instancia (DB o STAT/TEMP).
"""
instr_uid = instruction["instruction_uid"]
instr_type = "Sd" # Tipo original LAD
if instruction["type"].endswith(SCL_SUFFIX) or "_error" in instruction["type"]:
return False
# 1. Obtener Inputs: s (start), tv (time value)
# El pin 'r' (reset) no tiene equivalente directo en TON, se ignora aquí.
s_info = instruction["inputs"].get("s")
tv_info = instruction["inputs"].get("tv")
timer_instance_info = instruction["inputs"].get("timer") # Esperando que x1 lo extraiga
scl_s = get_scl_representation(s_info, network_id, scl_map, access_map)
scl_tv = get_scl_representation(tv_info, network_id, scl_map, access_map)
scl_instance_name = get_scl_representation(timer_instance_info, network_id, scl_map, access_map)
if scl_s is None or scl_tv is None:
return False # Dependencias no listas
# 2. Validar y obtener Nombre de Instancia
instance_name = None
if timer_instance_info and timer_instance_info.get("type") == "variable":
instance_name = scl_instance_name
elif timer_instance_info:
print(f"Advertencia: Pin 'timer' de {instr_type} UID {instr_uid} conectado a algo inesperado: {timer_instance_info.get('type')}")
instance_name = f"#TON_INSTANCE_{instr_uid}"
else:
instance_name = f"#TON_INSTANCE_{instr_uid}"
print(f"Advertencia: No se encontró conexión al pin 'timer' para {instr_type} UID {instr_uid}. Usando placeholder '{instance_name}'. ¡Revisar x1.py y XML!")
# 3. Formatear entradas si son variables
scl_s_formatted = format_variable_name(scl_s) if s_info and s_info.get("type") == "variable" else scl_s
scl_tv_formatted = format_variable_name(scl_tv) if tv_info and tv_info.get("type") == "variable" else scl_tv
# 4. Generar la llamada SCL (TON usa IN, PT, Q, ET)
scl_call = f"{instance_name}(IN := {scl_s_formatted}, PT := {scl_tv_formatted}); // TODO: Declarar {instance_name} : TON; en VAR_STAT o VAR"
instruction["scl"] = scl_call
instruction["type"] = instr_type + SCL_SUFFIX
# 5. Actualizar scl_map para las salidas Q y RT (mapeado a ET de TON)
map_key_q = (network_id, instr_uid, "q")
scl_map[map_key_q] = f"{instance_name}.Q"
map_key_rt = (network_id, instr_uid, "rt")
scl_map[map_key_rt] = f"{instance_name}.ET"
return True
# --- NUEVO: Procesador de Agrupación (Refinado) ---
def process_group_ifs(instruction, network_id, scl_map, access_map):
"""
Busca instrucciones que generan condiciones (Contact, O, Eq, PBox, etc.) ya procesadas
y, si habilitan un grupo (>1) de bloques funcionales (Move, Add, Call, etc.),
construye el bloque IF agrupado.
Modifica el campo 'scl' de la instrucción generadora de condición.
"""
instr_uid = instruction["instruction_uid"]
instr_type = instruction["type"]
instr_type_original = instr_type.replace("_scl", "").replace("_error", "")
made_change = False
# Solo actuar sobre generadores de condición ya procesados (_scl)
# y que no sean ellos mismos errores o ya agrupados por otro IF
if (
not instr_type.endswith("_scl")
or "_error" in instr_type
or instruction.get("grouped", False)
or instr_type_original
not in [
"Contact",
"O",
"Eq",
"Ne",
"Gt",
"Lt",
"Ge",
"Le",
"PBox",
"And",
"Xor",
]
): # Añadir más si es necesario
return False
# Evitar reagrupar si ya se hizo (comprobando si SCL ya es un IF complejo)
current_scl = instruction.get("scl", "")
if current_scl.strip().startswith("IF") and "END_IF;" in current_scl:
# print(f"DEBUG Group: {instr_uid} ya tiene IF complejo, saltando agrupación.")
return False
# Ignorar comentarios simples que empiezan por IF
if current_scl.strip().startswith("//") and "IF" in current_scl:
return False
# Obtener la condición generada por esta instrucción (debería estar en scl_map['out'])
map_key_out = (network_id, instr_uid, "out")
condition_scl = scl_map.get(map_key_out)
# No agrupar para condiciones triviales, no encontradas o ya agrupadas
if condition_scl is None or condition_scl in ["TRUE", "FALSE"]:
return False
# Encontrar todos los bloques funcionales habilitados DIRECTAMENTE por esta condición
grouped_instructions_cores = [] # Lista de SCL 'core' de los consumidores
consumer_instr_list = [] # Lista de instrucciones consumidoras
network_logic = next(
(net["logic"] for net in data["networks"] if net["id"] == network_id), []
)
if not network_logic:
return False
# Identificar los tipos de instrucciones que queremos agrupar (bloques funcionales)
groupable_types = [
"Move",
"Add",
"Sub",
"Mul",
"Div",
"Mod",
"Convert",
"Call_FC",
"Call_FB",
] # Añadir más si es necesario
for consumer_instr in network_logic:
consumer_uid = consumer_instr["instruction_uid"]
# Saltar si ya está agrupado por otra condición o es él mismo
if consumer_instr.get("grouped", False) or consumer_uid == instr_uid:
continue
consumer_en = consumer_instr.get("inputs", {}).get("en")
consumer_type = consumer_instr.get("type", "") # Tipo actual (_scl o no)
consumer_type_original = consumer_type.replace("_scl", "").replace("_error", "")
# ¿Está la entrada 'en' del consumidor conectada a nuestra salida 'out'?
is_enabled_by_us = False
if (
isinstance(consumer_en, dict)
and consumer_en.get("type") == "connection"
and consumer_en.get("source_instruction_uid") == instr_uid
and consumer_en.get("source_pin")
== "out" # Condición viene de 'out' del generador
):
is_enabled_by_us = True
# ¿Es un tipo de instrucción agrupable y ya procesado (tiene SCL)?
if (
is_enabled_by_us
and consumer_type.endswith("_scl")
and consumer_type_original in groupable_types
):
consumer_scl = consumer_instr.get("scl", "")
# Extraer el SCL core (la parte DENTRO del IF o la línea única si no había IF)
core_scl = None
if consumer_scl.strip().startswith("IF"):
# Extraer todo entre THEN y END_IF;
match = re.search(
r"IF\s+.*\s+THEN\s*(.*?)\s*END_IF;",
consumer_scl,
re.DOTALL | re.IGNORECASE,
)
if match:
core_scl = match.group(1).strip()
elif consumer_scl and not consumer_scl.strip().startswith("//"):
# Si no es IF y no es solo comentario, es el core
core_scl = consumer_scl.strip()
if core_scl:
grouped_instructions_cores.append(core_scl)
consumer_instr_list.append(consumer_instr) # Guardar referencia
# else: # Debug
# print(f"DEBUG Group: Consumidor {consumer_uid} ({consumer_type}) habilitado por {instr_uid} no tenía SCL core extraíble. SCL: '{consumer_scl}'")
# Si encontramos más de un consumidor agrupable
if len(grouped_instructions_cores) > 1:
print(
f"INFO: Agrupando {len(grouped_instructions_cores)} instrucciones bajo condición de {instr_type_original} UID {instr_uid} (Cond: {condition_scl})"
)
# Construir el bloque IF agrupado
scl_grouped = [f"IF {condition_scl} THEN"]
for core_line in grouped_instructions_cores:
# Añadir indentación adecuada (2 espacios)
indented_core = "\n".join(
[f" {line.strip()}" for line in core_line.splitlines()]
)
scl_grouped.append(indented_core)
scl_grouped.append("END_IF;")
final_grouped_scl = "\n".join(scl_grouped)
# Sobrescribir 'scl' de la instrucción generadora de condición
instruction["scl"] = final_grouped_scl
# Marcar los consumidores como agrupados y limpiar su SCL original
for consumer_instr in consumer_instr_list:
consumer_instr["scl"] = f"{GROUPED_COMMENT} (by UID {instr_uid})"
consumer_instr["grouped"] = True # Marcar como agrupado
made_change = True
# else: # Debug
# if len(grouped_instructions_cores) == 1:
# print(f"DEBUG Group: Solo 1 consumidor ({consumer_instr_list[0]['instruction_uid']}) para {instr_uid}. No se agrupa.")
# elif len(grouped_instructions_cores) == 0 and condition_scl not in ['TRUE', 'FALSE']:
# # Solo mostrar si la condición no era trivial
# pass # print(f"DEBUG Group: Ningún consumidor agrupable encontrado para {instr_uid} (Cond: {condition_scl}).")
return made_change
# --- Bucle Principal de Procesamiento ---
def process_json_to_scl(json_filepath):
"""Lee el JSON, aplica los procesadores iterativamente y guarda el resultado."""
if not os.path.exists(json_filepath):
print(f"Error: JSON no encontrado: {json_filepath}")
return
print(f"Cargando JSON desde: {json_filepath}")
try:
with open(json_filepath, "r", encoding="utf-8") as f:
global data # Modificar la variable global
data = json.load(f)
except Exception as e:
print(f"Error al cargar JSON: {e}")
traceback.print_exc()
return
# Crear mapas de acceso por red (para resolver UIDs de variables/constantes rápidamente)
network_access_maps = {}
# print("Creando mapas de acceso por red...") # Comentado para brevedad
for network in data.get("networks", []):
net_id = network["id"]
current_access_map = {}
# Extraer todos los 'Access' (variables/constantes) usados en esta red
# Esta información ya está dentro de inputs/outputs en el JSON simplificado
for instr in network.get("logic", []):
# Revisar Inputs
for _, source in instr.get("inputs", {}).items():
sources_to_check = (
source
if isinstance(source, list)
else ([source] if isinstance(source, dict) else [])
)
for src in sources_to_check:
if (
isinstance(src, dict)
and src.get("uid")
and src.get("type") in ["variable", "constant"]
):
current_access_map[src["uid"]] = (
src # Guardar info del Access por UID
)
# Revisar Outputs
for _, dest_list in instr.get("outputs", {}).items():
if isinstance(dest_list, list):
for dest in dest_list:
if (
isinstance(dest, dict)
and dest.get("uid")
and dest.get("type") in ["variable", "constant"]
):
current_access_map[dest["uid"]] = (
dest # Guardar info del Access por UID
)
network_access_maps[net_id] = current_access_map
# Mapa global para almacenar el SCL generado para cada salida de instrucción (pin)
# Clave: (network_id, instruction_uid, pin_name) -> Valor: SCL string
scl_map = {}
# --- Bucle Iterativo ---
max_passes = 30 # Aumentar por si hay dependencias largas o agrupaciones complejas
passes = 0
processing_complete = False
# Lista y mapa de procesadores base
base_processors = [
process_convert,
process_mod,
process_eq,
process_contact,
process_o,
process_not, # <-- Nuevo
process_edge_detector,
process_comparison, # <-- Nuevo (para GT, LT, etc.)
process_add,
process_math, # <-- Nuevo (para SUB, MUL, DIV)
process_move,
process_timer, # <-- Nuevo
process_se, # <-- Añadido
process_sd, # <-- Añadido
process_counter, # <-- Nuevo
process_call,
process_coil,
process_scoil,
process_rcoil,
process_blkmov,
]
# Crear mapa por nombre de tipo original (en minúsculas)
processor_map = {}
for func in base_processors:
match = re.match(r"process_(\w+)", func.__name__)
if match:
type_name = match.group(1).lower()
if type_name == "call":
processor_map["call_fc"] = func
processor_map["call_fb"] = func
processor_map["call"] = func
elif type_name == "edge_detector": # Mapear PBox y NBox a la nueva función
processor_map["pbox"] = func
processor_map["nbox"] = func
elif type_name == "blkmov":
processor_map[type_name] = process_blkmov # Usar la nueva función BLKMOV
elif type_name == "scoil":
processor_map[type_name] = func
elif type_name == "rcoil":
processor_map[type_name] = func
elif type_name == "se":
processor_map["se"] = func
processor_map["sdcoil"] = func # Mapear SdCoil a process_se basado en análisis
elif type_name == "sd":
processor_map["sd"] = func
elif type_name == "timer":
processor_map["ton"] = func # Mapear TON al procesador de timer
processor_map["tof"] = func # Mapear TOF al procesador de timer
elif type_name == "counter":
processor_map["ctu"] = func
processor_map["ctd"] = func
processor_map["ctud"] = func
elif type_name == "comparison":
processor_map["gt"] = func
processor_map["lt"] = func
processor_map["ge"] = func
processor_map["le"] = func
processor_map["ne"] = func
# EQ ya tiene su propio procesador (process_eq), así que no lo añadimos aquí.
elif type_name == "math":
processor_map["sub"] = func
processor_map["mul"] = func
processor_map["div"] = func
# ADD ya tiene su propio procesador (process_add)
elif type_name == "not":
processor_map["not"] = func # Mapear 'not'
elif type_name not in processor_map:
processor_map[type_name] = func
print("\n--- Iniciando Bucle de Procesamiento Iterativo ---")
while passes < max_passes and not processing_complete:
passes += 1
made_change_in_base_pass = False
made_change_in_group_pass = False
print(f"\n--- Pase {passes} ---")
num_processed_this_pass = 0
num_grouped_this_pass = 0
# --- FASE 1: Procesadores Base ---
for network in data.get("networks", []):
network_id = network["id"]
access_map = network_access_maps.get(network_id, {})
network_logic = network.get(
"logic", []
) # Obtener referencia a la lista de lógica
# Crear una copia de la lista de instrucciones para iterar,
# ya que modificaremos la original (al añadir _scl al tipo)
# logic_to_process = list(network_logic) # No necesario si solo modificamos in-place
for instruction in network_logic: # Iterar sobre la lista original
instr_uid = instruction.get("instruction_uid")
instr_type_original = instruction.get("type", "Unknown")
# Saltar si ya procesado, es un error, o está agrupado
if (
instr_type_original.endswith(SCL_SUFFIX)
or "_error" in instr_type_original
or instruction.get("grouped", False)
):
continue
# Buscar el procesador adecuado
# Para 'Call', necesitamos distinguir FC/FB si es posible
lookup_key = instr_type_original.lower()
if instr_type_original == "Call":
block_type = instruction.get("block_type", "").upper()
if block_type == "FC":
lookup_key = "call_fc"
elif block_type == "FB":
lookup_key = "call_fb"
# else: se usará 'call' genérico si existe
func_to_call = processor_map.get(lookup_key)
if func_to_call:
try:
# Pasar la lista de lógica completa solo si es necesario (PBox)
changed = func_to_call(
instruction, network_id, scl_map, access_map
)
if changed:
made_change_in_base_pass = True
num_processed_this_pass += 1
# print(f"DEBUG: Procesado {instr_type_original} UID {instr_uid}") # Debug detallado
except Exception as e:
print(
f"ERROR(Base) al procesar {instr_type_original} UID {instr_uid} con {func_to_call.__name__}: {e}"
)
traceback.print_exc()
# Marcar como error para no reintentar
instruction["scl"] = f"// ERROR en procesador base: {e}"
instruction["type"] = instr_type_original + "_error"
made_change_in_base_pass = True # Considerar error como cambio para evitar bucles infinitos
# else: # Debug para tipos no encontrados
# if lookup_key not in ['unknown', 'unknown_structure', 'error_parsing_symbol', 'error_parsing_constant', 'error_no_name']:
# print(f"DEBUG: No se encontró procesador base para el tipo '{instr_type_original}' (lookup: '{lookup_key}') UID {instr_uid}")
# pass
# --- FASE 2: Procesador de Agrupación ---
# Ejecutar solo si hubo cambios en la fase base (o en el primer pase) para optimizar
if made_change_in_base_pass or passes == 1:
# print(f"DEBUG: Iniciando Fase 2 (Agrupación IF) - Pase {passes}") # Debug
for network in data.get("networks", []):
network_id = network["id"]
access_map = network_access_maps.get(network_id, {})
network_logic = network.get("logic", [])
# Iterar sobre generadores de condición ya procesados
for instruction in network_logic:
# Solo intentar agrupar si es un generador de condición procesado y no agrupado
if instruction["type"].endswith("_scl") and not instruction.get(
"grouped", False
):
try:
group_changed = process_group_ifs(
instruction, network_id, scl_map, access_map
)
if group_changed:
made_change_in_group_pass = True
num_grouped_this_pass += 1
except Exception as e:
print(
f"ERROR(Group) al intentar agrupar desde UID {instruction.get('instruction_uid')}: {e}"
)
traceback.print_exc()
# No marcar la instrucción generadora como error, solo falló la agrupación
# --- Comprobar si se completó el procesamiento ---
if not made_change_in_base_pass and not made_change_in_group_pass:
print(
f"\n--- No se hicieron más cambios en el pase {passes}. Proceso iterativo completado. ---"
)
processing_complete = True
else:
print(
f"--- Fin Pase {passes}: {num_processed_this_pass} procesados, {num_grouped_this_pass} agrupados. Continuando..."
)
if passes == max_passes and not processing_complete:
print(
f"\n--- ADVERTENCIA: Límite de {max_passes} pases alcanzado. Puede haber dependencias no resueltas. ---"
)
# --- FIN BUCLE ITERATIVO ---
# --- NUEVO: Verificación de Instrucciones No Procesadas ---
print("\n--- Verificación Final de Instrucciones No Procesadas ---")
unprocessed_count = 0
unprocessed_details = [] # Lista para guardar detalles
for network in data.get("networks", []):
network_id = network.get("id", "Unknown ID")
network_title = network.get("title", f"Network {network_id}")
for instruction in network.get("logic", []):
instr_uid = instruction.get("instruction_uid", "Unknown UID")
instr_type = instruction.get("type", "Unknown Type")
is_grouped = instruction.get("grouped", False)
# Comprobar si NO está procesada (sin _scl), NO es error, y NO está agrupada
if (
not instr_type.endswith(SCL_SUFFIX)
and "_error" not in instr_type
and not is_grouped
):
unprocessed_count += 1
# Añadir detalle formateado a la lista
unprocessed_details.append(
f" - Red '{network_title}' (ID: {network_id}), "
f"Instrucción UID: {instr_uid}, Tipo Original: '{instr_type}'"
)
# Imprimir el resumen de no procesadas
if unprocessed_count > 0:
print(
f"ADVERTENCIA: Se encontraron {unprocessed_count} instrucciones que no pudieron ser procesadas a SCL:"
)
# Imprimir cada detalle de la lista
for detail in unprocessed_details:
print(detail)
print(
">>> Estos tipos de instrucción podrían necesitar un procesador específico en 'x2_process.py'."
)
else:
print(
"INFO: Todas las instrucciones fueron procesadas a SCL, marcadas como error o agrupadas exitosamente."
)
# --- FIN Verificación ---
# --- Guardar JSON Final ---
output_filename = json_filepath.replace(
"_simplified.json", "_simplified_processed.json"
)
print(f"\nGuardando JSON procesado en: {output_filename}")
try:
with open(output_filename, "w", encoding="utf-8") as f:
json.dump(data, f, indent=4, ensure_ascii=False)
print("Guardado completado.")
except Exception as e:
print(f"Error Crítico al guardar JSON procesado: {e}")
traceback.print_exc()
# --- Ejecución ---
if __name__ == "__main__":
# Imports necesarios solo para la ejecución como script principal
import argparse
import os
import sys
parser = argparse.ArgumentParser(description="Process simplified JSON to embed SCL logic.")
# Acepta el nombre del XML original como referencia para derivar nombres
parser.add_argument(
"source_xml_filepath",
nargs="?",
default="TestLAD.xml",
help="Path to the original source XML file (used to derive JSON input name, default: TestLAD.xml)"
)
args = parser.parse_args()
# Derivar el nombre del archivo JSON de entrada esperado
xml_filename_base = os.path.splitext(os.path.basename(args.source_xml_filepath))[0]
input_dir = os.path.dirname(args.source_xml_filepath) # Directorio del XML original
input_json_file = os.path.join(input_dir, f"{xml_filename_base}_simplified.json")
if not os.path.exists(input_json_file):
print(
f"Error Fatal: El archivo de entrada JSON simplificado no existe: '{input_json_file}'"
)
print(
f"Asegúrate de haber ejecutado 'x1_to_json.py' primero sobre '{args.source_xml_filepath}'."
)
sys.exit(1) # Salir si el archivo de entrada no existe
else:
# Llamar a la función principal con el nombre del JSON de entrada
# La función process_json_to_scl ya deriva el nombre de salida (_processed.json)
process_json_to_scl(input_json_file)