Simatic_XML_Parser_to_SCL/ToUpload/x3_generate_scl.py

808 lines
36 KiB
Python

# x3_generate_scl.py
# -*- coding: utf-8 -*-
import json
import os
import re
import argparse
import sys
import traceback # Importar traceback para errores
# --- Importar Utilidades y Constantes (Asumiendo ubicación) ---
try:
# Intenta importar desde el paquete de procesadores si está estructurado así
from processors.processor_utils import format_variable_name
# Definir SCL_SUFFIX aquí o importarlo si está centralizado
SCL_SUFFIX = "_sympy_processed" # Asegúrate que coincida con x2_process.py
GROUPED_COMMENT = (
"// Logic included in grouped IF" # Opcional, si se usa para filtrar
)
except ImportError:
print(
"Advertencia: No se pudo importar 'format_variable_name' desde processors.processor_utils."
)
print(
"Usando una implementación local básica (¡PUEDE FALLAR CON NOMBRES COMPLEJOS!)."
)
# Implementación local BÁSICA como fallback (MENOS RECOMENDADA)
def format_variable_name(name):
if not name:
return "_INVALID_NAME_"
if name.startswith('"') and name.endswith('"'):
return name # Mantener comillas
prefix = "#" if name.startswith("#") else ""
if prefix:
name = name[1:]
if name and name[0].isdigit():
name = "_" + name
name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
return prefix + name
SCL_SUFFIX = "_sympy_processed"
GROUPED_COMMENT = "// Logic included in grouped IF"
# para formatear valores iniciales
def format_scl_start_value(value, datatype):
"""Formatea un valor para la inicialización SCL según el tipo."""
# Add initial debug print
# print(f"DEBUG format_scl_start_value: value='{value}', datatype='{datatype}'")
if value is None:
return None # Retornar None si no hay valor
datatype_lower = datatype.lower() if datatype else ""
value_str = str(value)
# Intentar quitar comillas si existen (para manejar "TRUE" vs TRUE)
if value_str.startswith('"') and value_str.endswith('"') and len(value_str) > 1:
value_str_unquoted = value_str[1:-1]
elif value_str.startswith("'") and value_str.endswith("'") and len(value_str) > 1:
value_str_unquoted = value_str[1:-1]
else:
value_str_unquoted = value_str
# --- Integer-like types ---
if any(
t in datatype_lower
for t in [
"int",
"byte",
"word",
"dint",
"dword",
"lint",
"lword",
"sint",
"usint",
"uint",
"udint",
"ulint",
]
):
try:
# Intentar convertir el valor (sin comillas) a entero
return str(int(value_str_unquoted))
except ValueError:
# Si no es un entero válido, podría ser una constante simbólica
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str_unquoted):
return value_str_unquoted # Devolver como símbolo
# --- Fallback for non-integer, non-symbol ---
print(
f"DEBUG format_scl_start_value: Fallback for int-like. value_str_unquoted='{repr(value_str_unquoted)}', datatype='{datatype}'"
) # More debug
# MODIFIED FALLBACK: Escape newlines and use repr() for safety before formatting
try:
# Escape backslashes and single quotes properly for SCL string literal
escaped_for_scl = value_str_unquoted.replace("\\", "\\\\").replace(
"'", "''"
)
# Remove potential newlines that break Python f-string; SCL strings usually don't span lines implicitly
escaped_for_scl = escaped_for_scl.replace("\n", "").replace("\r", "")
# Format as SCL string literal
formatted_scl_string = f"'{escaped_for_scl}'"
print(
f"DEBUG format_scl_start_value: Fallback result='{formatted_scl_string}'"
)
return formatted_scl_string
except Exception as format_exc:
print(
f"ERROR format_scl_start_value: Exception during fallback formatting: {format_exc}"
)
return f"'ERROR_FORMATTING_{value_str_unquoted[:20]}'" # Return an error string
# --- Other types (Bool, Real, String, Char, Time, Date, etc.) ---
elif "bool" in datatype_lower:
# Comparar sin importar mayúsculas/minúsculas y sin comillas
return "TRUE" if value_str_unquoted.lower() == "true" else "FALSE"
elif "string" in datatype_lower:
# Usar el valor sin comillas originales y escapar las internas
escaped_value = value_str_unquoted.replace("'", "''")
return f"'{escaped_value}'"
elif "char" in datatype_lower:
# Usar el valor sin comillas originales y escapar las internas
escaped_value = value_str_unquoted.replace("'", "''")
# SCL usa comillas simples para Char. Asegurar que sea un solo caracter si es posible?
# Por ahora, solo formatear. Longitud se verifica en TIA.
return f"'{escaped_value}'"
elif "real" in datatype_lower or "lreal" in datatype_lower:
try:
# Intentar convertir a float
f_val = float(value_str_unquoted)
s_val = str(f_val)
# Asegurar que tenga punto decimal si es entero
if "." not in s_val and "e" not in s_val.lower():
s_val += ".0"
return s_val
except ValueError:
# Podría ser constante simbólica
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str_unquoted):
return value_str_unquoted
print(
f"Advertencia: Valor '{value_str}' no reconocido como real o símbolo para tipo {datatype}. Devolviendo como string."
)
# Use the robust fallback formatting here too
escaped_for_scl = (
value_str_unquoted.replace("\\", "\\\\")
.replace("'", "''")
.replace("\n", "")
.replace("\r", "")
)
return f"'{escaped_for_scl}'"
elif "time" in datatype_lower:
# Quitar prefijos y añadir el correcto según el tipo específico
prefix = ""
val_to_use = value_str_unquoted # Usar valor sin comillas
if val_to_use.upper().startswith("T#"):
prefix = "T#"
val_to_use = val_to_use[2:]
elif val_to_use.upper().startswith("LT#"):
prefix = "LT#"
val_to_use = val_to_use[3:]
elif val_to_use.upper().startswith("S5T#"):
prefix = "S5T#"
val_to_use = val_to_use[4:]
if "s5time" in datatype_lower:
return f"S5T#{val_to_use}"
elif "ltime" in datatype_lower:
return f"LT#{val_to_use}"
else:
return f"T#{val_to_use}" # Default a TIME
elif "date" in datatype_lower:
val_to_use = value_str_unquoted
# Handle DTL first as it's longer
if "dtl" in datatype_lower or "date_and_time" in datatype_lower:
prefix = "DTL#" if val_to_use.upper().startswith("DTL#") else "DTL#"
val_to_use = (
val_to_use[4:] if val_to_use.upper().startswith("DTL#") else val_to_use
)
return f"{prefix}{val_to_use}"
elif "dt" in datatype_lower:
prefix = "DT#" if val_to_use.upper().startswith("DT#") else "DT#"
val_to_use = (
val_to_use[3:] if val_to_use.upper().startswith("DT#") else val_to_use
)
return f"{prefix}{val_to_use}"
elif "tod" in datatype_lower or "time_of_day" in datatype_lower:
prefix = "TOD#" if val_to_use.upper().startswith("TOD#") else "TOD#"
val_to_use = (
val_to_use[4:] if val_to_use.upper().startswith("TOD#") else val_to_use
)
return f"{prefix}{val_to_use}"
else: # Default a Date D#
prefix = "D#" if val_to_use.upper().startswith("D#") else "D#"
val_to_use = (
val_to_use[2:] if val_to_use.upper().startswith("D#") else val_to_use
)
return f"{prefix}{val_to_use}"
# --- Fallback for completely unknown types or complex structures ---
else:
# Si es un nombre válido (posiblemente UDT, constante global, etc.), devolverlo tal cual
# Ajustar regex para permitir más caracteres si es necesario
if re.match(
r'^[a-zA-Z_#"][a-zA-Z0-9_."#\[\]%]+$', value_str
): # Permitir % para accesos tipo %DB1.DBD0
# Quitar comillas externas si es un UDT o struct complejo
if (
value_str.startswith('"')
and value_str.endswith('"')
and len(value_str) > 1
):
return value_str[1:-1]
# Mantener comillas si es acceso a DB ("DB_Name".Var)
if '"' in value_str and "." in value_str and value_str.count('"') == 2:
return value_str
# Si no tiene comillas y es un nombre simple o acceso #temp o %I0.0 etc
if not value_str.startswith('"') and not value_str.startswith("'"):
# Formatear nombres simples, pero dejar accesos % y # tal cual
if value_str.startswith("#") or value_str.startswith("%"):
return value_str
else:
# return format_variable_name(value_str) # Evitar formatear aquí, puede ser una constante
return value_str # Return as is if it looks symbolic
# Devolver el valor original si tiene comillas internas o estructura compleja no manejada arriba
return value_str
else:
# Si no parece un nombre/símbolo/acceso, tratarlo como string (último recurso)
print(
f"DEBUG format_scl_start_value: Fallback final. value_str_unquoted='{repr(value_str_unquoted)}', datatype='{datatype}'"
)
# Use the robust fallback formatting
escaped_for_scl = (
value_str_unquoted.replace("\\", "\\\\")
.replace("'", "''")
.replace("\n", "")
.replace("\r", "")
)
return f"'{escaped_for_scl}'"
# ... (generate_scl_declarations and generate_scl function remain the same as the previous version) ...
# --- (Incluye aquí las funciones generate_scl_declarations y generate_scl SIN CAMBIOS respecto a la respuesta anterior) ---
# --- NUEVA FUNCIÓN RECURSIVA para generar declaraciones SCL (VAR/STRUCT/ARRAY) ---
def generate_scl_declarations(variables, indent_level=1):
"""Genera las líneas SCL para declarar variables, structs y arrays."""
scl_lines = []
indent = " " * indent_level
for var in variables:
var_name_scl = format_variable_name(var.get("name"))
var_dtype_raw = var.get("datatype", "VARIANT")
var_comment = var.get("comment")
start_value = var.get("start_value")
children = var.get("children") # Para structs
array_elements = var.get("array_elements") # Para arrays
# Limpiar comillas del tipo de dato si es UDT/String/etc.
var_dtype_cleaned = var_dtype_raw
if isinstance(var_dtype_raw, str):
if var_dtype_raw.startswith('"') and var_dtype_raw.endswith('"'):
var_dtype_cleaned = var_dtype_raw[1:-1]
# Manejar caso 'Array [...] of "MyUDT"'
array_match = re.match(
r'(Array\[.*\]\s+of\s+)"(.*)"', var_dtype_raw, re.IGNORECASE
)
if array_match:
var_dtype_cleaned = f"{array_match.group(1)}{array_match.group(2)}" # Quitar comillas del tipo base
# Determinar tipo base para inicialización (importante para arrays)
base_type_for_init = var_dtype_cleaned
array_prefix_for_decl = ""
if var_dtype_cleaned.lower().startswith("array["):
match = re.match(
r"(Array\[.*\]\s+of\s+)(.*)", var_dtype_cleaned, re.IGNORECASE
)
if match:
array_prefix_for_decl = match.group(1)
base_type_for_init = match.group(2).strip()
# Construir tipo de dato para la declaración SCL
declaration_dtype = var_dtype_raw # Usar el raw por defecto
# Si es UDT o tipo complejo que requiere comillas y no es array simple
if base_type_for_init != var_dtype_cleaned and not array_prefix_for_decl:
# Poner comillas si no las tiene ya el tipo base
if not base_type_for_init.startswith('"'):
declaration_dtype = f'"{base_type_for_init}"'
else:
declaration_dtype = base_type_for_init # Ya tiene comillas
# Si es array de UDT/complejo, reconstruir con comillas en el tipo base
elif array_prefix_for_decl and base_type_for_init != var_dtype_cleaned:
if not base_type_for_init.startswith('"'):
declaration_dtype = f'{array_prefix_for_decl}"{base_type_for_init}"'
else:
declaration_dtype = f"{array_prefix_for_decl}{base_type_for_init}"
declaration_line = f"{indent}{var_name_scl} : {declaration_dtype}"
init_value_scl = None
# ---- Arrays ----
if array_elements:
# Ordenar índices (asumiendo que son numéricos '0', '1', ...)
try:
# Extraer números de los índices string
indices_numeric = {int(k): v for k, v in array_elements.items()}
sorted_indices = sorted(indices_numeric.keys())
# Mapear de nuevo a string para buscar valor
sorted_indices_str = [str(k) for k in sorted_indices]
except ValueError:
# Fallback a orden alfabético si los índices no son números
print(
f"Advertencia: Índices de array no numéricos para '{var_name_scl}'. Usando orden alfabético."
)
sorted_indices_str = sorted(array_elements.keys())
init_values = []
for idx_str in sorted_indices_str:
try:
formatted_val = format_scl_start_value(
array_elements[idx_str], base_type_for_init
)
init_values.append(formatted_val)
except Exception as e_fmt:
print(
f"ERROR: Falló formateo para índice {idx_str} de array '{var_name_scl}'. Valor: {array_elements[idx_str]}. Error: {e_fmt}"
)
init_values.append(f"/*ERR_FMT_{idx_str}*/") # Placeholder de error
# Filtrar Nones que pueden venir de format_scl_start_value si el valor era None
valid_inits = [v for v in init_values if v is not None]
if valid_inits:
# Si todos los valores son iguales y es un array grande, podríamos usar notación x(value)
# Simplificación: por ahora, listar todos
init_value_scl = f"[{', '.join(valid_inits)}]"
elif array_elements: # Si había elementos pero todos formatearon a None
print(
f"Advertencia: Todos los valores iniciales para array '{var_name_scl}' son None o inválidos."
)
# ---- Structs ----
elif children:
# El valor inicial de un struct se maneja recursivamente dentro
# Añadir comentario? Puede ser redundante.
scl_lines.append(
declaration_line
) # Añadir línea de declaración base STRUCT
scl_lines.append(f"{indent}STRUCT")
# Llamada recursiva para los miembros internos
scl_lines.extend(generate_scl_declarations(children, indent_level + 1))
scl_lines.append(f"{indent}END_STRUCT;")
if var_comment: # Comentario después de END_STRUCT
scl_lines.append(f"{indent}// {var_comment}")
scl_lines.append("") # Línea extra para legibilidad
continue # Saltar el resto de la lógica para este struct
# ---- Tipos Simples ----
else:
if start_value is not None:
try:
init_value_scl = format_scl_start_value(
start_value, base_type_for_init
) # Usar tipo base
except Exception as e_fmt_simple:
print(
f"ERROR: Falló formateo para valor simple de '{var_name_scl}'. Valor: {start_value}. Error: {e_fmt_simple}"
)
init_value_scl = f"/*ERR_FMT_SIMPLE*/" # Placeholder
# Añadir inicialización si existe y no es None
if init_value_scl is not None:
declaration_line += f" := {init_value_scl}"
declaration_line += ";"
# Añadir comentario si existe
if var_comment:
declaration_line += f" // {var_comment}"
scl_lines.append(declaration_line)
return scl_lines
# --- Función Principal de Generación SCL ---
def generate_scl(processed_json_filepath, output_scl_filepath):
"""Genera un archivo SCL a partir del JSON procesado (FC/FB/OB o DB).""" # Actualizado
if not os.path.exists(processed_json_filepath):
print(
f"Error: Archivo JSON procesado no encontrado en '{processed_json_filepath}'"
)
return
print(f"Cargando JSON procesado desde: {processed_json_filepath}")
try:
with open(processed_json_filepath, "r", encoding="utf-8") as f:
data = json.load(f)
except Exception as e:
print(f"Error al cargar o parsear JSON: {e}")
traceback.print_exc()
return
# --- Extracción de Información del Bloque (Común) ---
block_name = data.get("block_name", "UnknownBlock")
block_number = data.get("block_number")
# block_lang_original = data.get("language", "Unknown") # Lenguaje original (SCL, LAD, DB...)
block_type = data.get(
"block_type", "Unknown"
) # Tipo de bloque (FC, FB, GlobalDB, OB) <-- Usar este
block_comment = data.get("block_comment", "")
scl_block_name = format_variable_name(block_name) # Nombre SCL seguro
print(
f"Generando SCL para: {block_type} '{scl_block_name}' (Original: {block_name})" # Quitado lenguaje original del log
)
scl_output = []
# --- MODIFICADO: GENERACIÓN PARA DATA BLOCK (GlobalDB) ---
if block_type == "GlobalDB": # <-- Comprobar tipo de bloque
print("Modo de generación: DATA_BLOCK")
scl_output.append(f"// Block Type: {block_type}")
scl_output.append(f"// Block Name (Original): {block_name}")
if block_number:
scl_output.append(f"// Block Number: {block_number}")
if block_comment:
# Dividir comentarios largos en múltiples líneas
comment_lines = block_comment.splitlines()
scl_output.append(f"// Block Comment:")
for line in comment_lines:
scl_output.append(f"// {line}")
scl_output.append("")
scl_output.append(f'DATA_BLOCK "{scl_block_name}"')
scl_output.append("{ S7_Optimized_Access := 'TRUE' }") # Asumir optimizado
scl_output.append("VERSION : 0.1")
scl_output.append("")
interface_data = data.get("interface", {})
# En DBs, la sección relevante suele ser 'Static'
static_vars = interface_data.get("Static", [])
if static_vars:
scl_output.append("VAR")
# Usar la función recursiva para generar declaraciones
scl_output.extend(generate_scl_declarations(static_vars, indent_level=1))
scl_output.append("END_VAR")
scl_output.append("")
else:
print(
"Advertencia: No se encontró sección 'Static' o está vacía en la interfaz del DB."
)
# Añadir bloque VAR vacío si no hay variables
scl_output.append("VAR")
scl_output.append("END_VAR")
scl_output.append("")
scl_output.append("BEGIN")
scl_output.append(
" // Los Data Blocks no tienen código ejecutable en BEGIN/END"
)
scl_output.append("END_DATA_BLOCK")
# --- MODIFICADO: GENERACIÓN PARA FC/FB/OB ---
else:
# Determinar palabra clave SCL
scl_block_keyword = "FUNCTION_BLOCK" # Default
if block_type == "FC":
scl_block_keyword = "FUNCTION"
elif block_type == "OB":
scl_block_keyword = "ORGANIZATION_BLOCK"
elif block_type == "FB":
scl_block_keyword = "FUNCTION_BLOCK"
else: # Fallback
print(
f"Advertencia: Tipo de bloque desconocido '{block_type}', usando FUNCTION_BLOCK."
)
scl_block_keyword = "FUNCTION_BLOCK" # O quizás lanzar error?
print(f"Modo de generación: {scl_block_keyword}")
# Cabecera del Bloque
scl_output.append(f"// Block Type: {block_type}")
scl_output.append(f"// Block Name (Original): {block_name}")
if block_number:
scl_output.append(f"// Block Number: {block_number}")
# Indicar lenguaje original de las redes si es relevante
original_net_langs = set(
n.get("language", "Unknown") for n in data.get("networks", [])
)
scl_output.append(
f"// Original Network Languages: {', '.join(l for l in original_net_langs if l != 'Unknown')}"
)
if block_comment:
comment_lines = block_comment.splitlines()
scl_output.append(f"// Block Comment:")
for line in comment_lines:
scl_output.append(f"// {line}")
scl_output.append("")
# Manejar tipo de retorno para FUNCTION (FC)
return_type = "Void" # Default
interface_data = data.get("interface", {})
if scl_block_keyword == "FUNCTION" and interface_data.get("Return"):
# Asumir un solo valor de retorno
return_member = interface_data["Return"][0]
return_type_raw = return_member.get("datatype", "Void")
# Limpiar comillas si es UDT/String
return_type = (
return_type_raw[1:-1]
if isinstance(return_type_raw, str)
and return_type_raw.startswith('"')
and return_type_raw.endswith('"')
else return_type_raw
)
# Añadir comillas si es UDT y no las tenía
if (
return_type != return_type_raw
and not return_type_raw.lower().startswith("array")
):
return_type = f'"{return_type}"'
else: # Mantener raw si es tipo básico o ya tenía comillas
return_type = return_type_raw
# Línea de declaración del bloque
if scl_block_keyword == "FUNCTION":
scl_output.append(f'{scl_block_keyword} "{scl_block_name}" : {return_type}')
else: # FB y OB
scl_output.append(f'{scl_block_keyword} "{scl_block_name}"')
# Atributos y versión
scl_output.append("{ S7_Optimized_Access := 'TRUE' }") # Asumir optimizado
scl_output.append("VERSION : 0.1")
scl_output.append("")
# Declaraciones de Interfaz (Input, Output, InOut, Static, Temp, Constant)
# Orden estándar SCL
section_order = ["Input", "Output", "InOut", "Static", "Temp", "Constant"]
declared_temps = set() # Para rastrear temps ya declaradas
has_declarations = False
for section_name in section_order:
vars_in_section = interface_data.get(section_name, [])
if vars_in_section:
has_declarations = True
# Mapeo de nombres de sección JSON a palabras clave SCL VAR_
scl_section_keyword = f"VAR_{section_name.upper()}"
if section_name == "Static":
scl_section_keyword = "VAR_STAT" # Para FBs
if section_name == "Temp":
scl_section_keyword = "VAR_TEMP"
if section_name == "Constant":
scl_section_keyword = "CONSTANT" # CONSTANT no usa VAR_
scl_output.append(scl_section_keyword)
# Usar la función recursiva para generar declaraciones
scl_output.extend(
generate_scl_declarations(vars_in_section, indent_level=1)
)
# Añadir END_VAR (o END_CONSTANT)
scl_output.append(
"END_VAR" if section_name != "Constant" else "END_CONSTANT"
)
scl_output.append("") # Línea en blanco
# Guardar nombres de Temp declarados explícitamente
if section_name == "Temp":
declared_temps.update(
format_variable_name(v.get("name"))
for v in vars_in_section
if v.get("name")
)
# Declaraciones VAR_TEMP adicionales (auto-detectadas)
# Buscar variables que empiecen con #_temp_ en el SCL generado
temp_vars_detected = set()
# Patrón para encontrar #variable o "#variable"
temp_pattern = re.compile(
r'"?(#\w+)"?'
) # Busca # seguido de caracteres alfanuméricos
for network in data.get("networks", []):
for instruction in network.get("logic", []):
# Revisar el SCL final y el SCL de actualización de memoria si existe
scl_code = instruction.get("scl", "")
edge_update_code = instruction.get(
"_edge_mem_update_scl", ""
) # Para flancos
code_to_scan = (
(scl_code if scl_code else "")
+ "\n"
+ (edge_update_code if edge_update_code else "")
)
if code_to_scan:
# Usar findall para encontrar todas las ocurrencias
found_temps = temp_pattern.findall(code_to_scan)
for temp_name in found_temps:
# findall devuelve el grupo capturado (#...)
if temp_name:
temp_vars_detected.add(temp_name)
# Filtrar las que ya estaban declaradas
additional_temps = sorted(list(temp_vars_detected - declared_temps))
if additional_temps:
print(f"INFO: Detectadas {len(additional_temps)} VAR_TEMP adicionales.")
# Si no se declaró la sección Temp antes, añadirla ahora
if "Temp" not in interface_data or not interface_data["Temp"]:
scl_output.append("VAR_TEMP")
for temp_name in additional_temps:
# Formatear por si acaso, aunque el patrón ya debería dar #nombre
scl_name = format_variable_name(temp_name)
# Inferir tipo (Bool es lo más común para temporales internos)
# Se podría mejorar si el nombre da pistas (ej. _temp_r para Real)
inferred_type = "Bool" # Asumir Bool por defecto
scl_output.append(
f" {scl_name} : {inferred_type}; // Auto-generated temporary"
)
# Si abrimos la sección aquí, cerrarla
if "Temp" not in interface_data or not interface_data["Temp"]:
scl_output.append("END_VAR")
scl_output.append("")
# --- Cuerpo del Bloque (BEGIN...END) ---
scl_output.append("BEGIN")
scl_output.append("")
# Iterar por redes y lógica (incluyendo manejo STL/SCL crudo)
for i, network in enumerate(data.get("networks", [])):
network_title = network.get(
"title", f'Network {network.get("id", i+1)}'
) # Usar i+1 si falta ID
network_comment = network.get("comment", "")
network_lang = network.get("language", "LAD") # Lenguaje original de la red
scl_output.append(
f" // Network {i+1}: {network_title} (Original Language: {network_lang})"
)
if network_comment:
# Indentar comentarios de red
for line in network_comment.splitlines():
scl_output.append(f" // {line}")
scl_output.append("") # Línea en blanco antes del código de red
network_has_code = False
logic_in_network = network.get("logic", [])
if not logic_in_network:
scl_output.append(f" // Network {i+1} has no logic elements.")
scl_output.append("")
continue
# --- Manejo Especial Redes STL ---
if network_lang == "STL":
# Asumir que la lógica STL está en el primer elemento como RAW_STL_CHUNK
if logic_in_network[0].get("type") == "RAW_STL_CHUNK":
network_has_code = True
raw_stl_code = logic_in_network[0].get(
"stl", "// ERROR: STL code missing"
)
# Incrustar STL como comentario multi-línea o delimitado
scl_output.append(f" // --- BEGIN STL Network {i+1} ---")
# Comentar cada línea STL
for stl_line in raw_stl_code.splitlines():
scl_output.append(f" // {stl_line}")
scl_output.append(f" // --- END STL Network {i+1} ---")
scl_output.append("") # Línea en blanco después
else:
scl_output.append(
f" // ERROR: Contenido STL inesperado en Network {i+1}."
)
scl_output.append("")
# --- Manejo Redes SCL/LAD/FBD procesadas ---
else:
# Iterar por las instrucciones procesadas
for instruction in logic_in_network:
instruction_type = instruction.get("type", "")
scl_code = instruction.get("scl", "")
is_grouped = instruction.get("grouped", False)
# Saltar instrucciones agrupadas (su lógica está en el IF)
if is_grouped:
continue
# Incluir SCL si la instrucción fue procesada o es un chunk crudo/error/placeholder
if (
instruction_type.endswith(SCL_SUFFIX)
or instruction_type
in [
"RAW_SCL_CHUNK",
"UNSUPPORTED_LANG",
"UNSUPPORTED_CONTENT",
"PARSING_ERROR",
]
or "_error" in instruction_type # Incluir errores comentados
) and scl_code:
# Comprobar si el SCL es solo un comentario (a menos que sea un bloque IF)
is_only_comment = all(
line.strip().startswith("//")
for line in scl_code.splitlines()
if line.strip()
)
is_if_block = scl_code.strip().startswith("IF")
# Añadir el SCL indentado si no es solo un comentario (o si es un IF/Error)
if (
not is_only_comment
or is_if_block
or "_error" in instruction_type
or instruction_type
in [
"UNSUPPORTED_LANG",
"UNSUPPORTED_CONTENT",
"PARSING_ERROR",
]
):
network_has_code = True
for line in scl_code.splitlines():
scl_output.append(f" {line}") # Indentar código
# Añadir línea en blanco después de cada bloque SCL para legibilidad
scl_output.append("")
# Si la red no produjo código SCL imprimible (ej. solo lógica interna)
if (
not network_has_code and network_lang != "STL"
): # No añadir para STL ya comentado
scl_output.append(
f" // Network {i+1} did not produce printable SCL code."
)
scl_output.append("")
# Fin del bloque FC/FB/OB
scl_output.append(f"END_{scl_block_keyword}") # <-- Usar keyword determinada
# --- Escritura del Archivo SCL (Común) ---
print(f"Escribiendo archivo SCL en: {output_scl_filepath}")
try:
with open(output_scl_filepath, "w", encoding="utf-8") as f:
for line in scl_output:
f.write(line + "\n")
print("Generación de SCL completada.")
except Exception as e:
print(f"Error al escribir el archivo SCL: {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
import traceback # Asegurarse que traceback está importado
# Configurar ArgumentParser para recibir la ruta del XML original obligatoria
parser = argparse.ArgumentParser(
description="Generate final SCL file from processed JSON (_simplified_processed.json). Expects original XML filepath as argument."
)
parser.add_argument(
"source_xml_filepath", # Argumento posicional obligatorio
help="Path to the original source XML file (passed from x0_main.py, used to derive input/output names).",
)
args = parser.parse_args() # Parsea los argumentos de sys.argv
source_xml_file = args.source_xml_filepath # Obtiene la ruta del XML original
# Verificar si el archivo XML original existe (como referencia)
if not os.path.exists(source_xml_file):
print(
f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON procesado."
)
# Derivar nombres de archivos de entrada (JSON procesado) y salida (SCL)
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
# Asumir que los archivos están en el mismo directorio que el XML original
base_dir = os.path.dirname(source_xml_file) # Directorio del XML original
input_json_file = os.path.join(
base_dir, f"{xml_filename_base}_simplified_processed.json"
)
# Cambiar extensión de salida a .scl
output_scl_file = os.path.join(
base_dir, f"{xml_filename_base}_generated.scl" # Cambiado nombre de salida
)
print(
f"(x3) Generando SCL: '{os.path.relpath(input_json_file)}' -> '{os.path.relpath(output_scl_file)}'"
)
# Verificar si el archivo JSON procesado de entrada EXISTE
if not os.path.exists(input_json_file):
print(
f"Error Fatal (x3): Archivo JSON procesado no encontrado: '{input_json_file}'"
)
print(
f"Asegúrate de que 'x2_process.py' se ejecutó correctamente para '{os.path.relpath(source_xml_file)}'."
)
sys.exit(1) # Salir si el archivo necesario no está
else:
# Llamar a la función principal de generación SCL del script
try:
generate_scl(input_json_file, output_scl_file)
sys.exit(0) # Salir con éxito explícitamente
except Exception as e:
print(
f"Error Crítico (x3) durante la generación de SCL desde '{input_json_file}': {e}"
)
# traceback ya debería estar importado
traceback.print_exc()
sys.exit(1) # Salir con error si la función principal falla