808 lines
36 KiB
Python
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
|