Simatic_XML_Parser_to_SCL/x3_generate_scl.py

322 lines
16 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"
# --- 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 por x2_process (versión SymPy)."""
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 ---
block_name = data.get('block_name', 'UnknownBlock')
block_number = data.get('block_number')
block_lang_original = data.get('language', 'LAD') # Lenguaje original
# Determinar tipo de bloque SCL (Asumir FB si no se especifica)
# Idealmente, x1_to_json.py guardaría esto en data['block_type_scl'] = 'FC' o 'FB'
block_type_scl = data.get('block_type_scl', 'FUNCTION_BLOCK')
block_comment = data.get('block_comment', '')
# Usar format_variable_name para el nombre del bloque en SCL
scl_block_name = format_variable_name(block_name)
print(f"Generando SCL para {block_type_scl}: {scl_block_name} (Original: {block_name})")
# --- Identificación de Variables Temporales y Estáticas ---
# La detección basada en regex sobre el SCL final debería seguir funcionando
temp_vars = set()
stat_vars = set()
# Regex mejorado para capturar variables temporales que empiezan con # o _temp_
# y estáticas (si usas un prefijo como 'stat_' o para bits de memoria de flanco)
temp_pattern = re.compile(r'"?#(_temp_[a-zA-Z0-9_]+)"?|"?(_temp_[a-zA-Z0-9_]+)"?') # Captura con o sin #
stat_pattern = re.compile(r'"?(stat_[a-zA-Z0-9_]+)"?') # Para memorias de flanco si usan prefijo 'stat_'
edge_memory_bits = set() # Para detectar bits de memoria de flanco por nombre
for network in data.get('networks', []):
for instruction in network.get('logic', []):
scl_code = instruction.get('scl', '')
# Buscar también en _edge_mem_update_scl si existe
edge_update_code = instruction.get('_edge_mem_update_scl','')
code_to_scan = (scl_code if scl_code else '') + '\n' + (edge_update_code if edge_update_code else '')
if code_to_scan:
# Buscar #_temp_... o _temp_...
found_temps = temp_pattern.findall(code_to_scan)
for temp_tuple in found_temps:
# findall devuelve tuplas por los grupos de captura, tomar el no vacío
temp_name = next((t for t in temp_tuple if t), None)
if temp_name:
temp_vars.add("#"+temp_name if not temp_name.startswith("#") else temp_name) # Asegurar que empiece con #
# Buscar estáticas (ej: stat_...)
found_stats = stat_pattern.findall(code_to_scan)
stat_vars.update(found_stats)
# Identificar explícitamente bits de memoria usados por PBox/NBox
# Asumiendo que el nombre se guarda en el JSON (requiere ajuste en x1/x2)
# if instruction.get("type","").startswith(("PBox", "NBox")):
# mem_bit_info = instruction.get("inputs", {}).get("bit")
# if mem_bit_info and mem_bit_info.get("type") == "variable":
# edge_memory_bits.add(format_variable_name(mem_bit_info.get("name")))
print(f"Variables temporales (#_temp_...) detectadas: {len(temp_vars)}")
# Si se detectan memorias de flanco, añadirlas a stat_vars si no tienen prefijo 'stat_'
# stat_vars.update(edge_memory_bits - stat_vars) # Añadir solo las nuevas
print(f"Variables estáticas (stat_...) detectadas: {len(stat_vars)}")
# --- Construcción del String SCL ---
scl_output = []
# Cabecera del Bloque
scl_output.append(f"// Block Name (Original): {block_name}")
if block_number: scl_output.append(f"// Block Number: {block_number}")
scl_output.append(f"// Original Language: {block_lang_original}")
if block_comment: scl_output.append(f"// Block Comment: {block_comment}")
scl_output.append("")
scl_output.append(f"{block_type_scl} \"{scl_block_name}\"")
scl_output.append("{ S7_Optimized_Access := 'TRUE' }")
scl_output.append("VERSION : 0.1")
scl_output.append("")
# Declaraciones de Interfaz (Implementación básica)
interface_sections = ["Input", "Output", "InOut", "Static", "Temp", "Constant", "Return"]
interface_data = data.get('interface', {})
for section_name in interface_sections:
scl_section_name = section_name
# Ajustar nombres de sección para SCL (Static -> STAT, Temp -> TEMP)
if section_name == "Static": scl_section_name = "STAT"
if section_name == "Temp": scl_section_name = "TEMP" # Usar VAR_TEMP para variables #temp
vars_in_section = interface_data.get(section_name, [])
# No declarar VAR_TEMP aquí, se hará después con las detectadas/originales
if section_name == "Temp": continue
# No declarar VAR_STAT aquí si ya lo hacemos abajo con las detectadas
if section_name == "Static" and stat_vars: continue
if vars_in_section or (section_name == "Static" and stat_vars): # Incluir STAT si hay detectadas
# Usar VAR para Input/Output/InOut/Constant/Return
var_keyword = "VAR" if section_name != "Static" else "VAR_STAT"
scl_output.append(f"{var_keyword}_{section_name.upper()}")
for var in vars_in_section:
var_name = var.get('name')
var_dtype = var.get('datatype', 'VARIANT') # Default a VARIANT
if var_name:
# Usar format_variable_name CORRECTO
scl_name = format_variable_name(var_name)
scl_output.append(f" {scl_name} : {var_dtype};")
# Declarar stat_vars detectadas si esta es la sección STAT
if section_name == "Static" and stat_vars:
for var_name in sorted(list(stat_vars)):
# Asumir Bool para stat_, podría necesitar inferencia
scl_output.append(f" {format_variable_name(var_name)} : Bool; // Auto-detected STAT")
scl_output.append("END_VAR")
scl_output.append("")
# Declaraciones Estáticas (Si no estaban en la interfaz y se detectaron)
# Esto es redundante si la sección VAR_STAT ya se generó arriba
# if stat_vars and not interface_data.get("Static"):
# scl_output.append("VAR_STAT")
# for var_name in sorted(list(stat_vars)):
# scl_output.append(f" {format_variable_name(var_name)} : Bool; // Auto-detected STAT")
# scl_output.append("END_VAR")
# scl_output.append("")
# Declaraciones Temporales (Interfaz Temp + _temp_ detectadas)
scl_output.append("VAR_TEMP")
declared_temps = set()
interface_temps = interface_data.get('Temp', [])
if interface_temps:
for var in interface_temps:
var_name = var.get('name')
var_dtype = var.get('datatype', 'VARIANT')
if var_name:
scl_name = format_variable_name(var_name)
scl_output.append(f" {scl_name} : {var_dtype};")
declared_temps.add(scl_name) # Marcar como declarada
# Declarar las _temp_ generadas si no estaban ya en la interfaz Temp
if temp_vars:
for var_name in sorted(list(temp_vars)):
scl_name = format_variable_name(var_name) # #_temp_...
if scl_name not in declared_temps:
# Inferencia básica de tipo
inferred_type = "Bool" # Asumir Bool para la mayoría de temps de lógica
# Se podría mejorar si los procesadores añadieran info de tipo
scl_output.append(f" {scl_name} : {inferred_type}; // Auto-generated temporary")
declared_temps.add(scl_name)
scl_output.append("END_VAR")
scl_output.append("")
# Cuerpo del Bloque
scl_output.append("BEGIN")
scl_output.append("")
# Iterar por redes y lógica
for i, network in enumerate(data.get('networks', [])):
network_title = network.get('title', f'Network {network.get("id")}')
network_comment = network.get('comment', '')
network_lang = network.get('language', 'LAD') # O el lenguaje original
scl_output.append(f" // Network {i+1}: {network_title} (Original Language: {network_lang})")
if network_comment:
for line in network_comment.splitlines():
scl_output.append(f" // {line}")
scl_output.append("")
network_has_code = False
# --- NUEVO MANEJO STL con formato Markdown ---
if network_lang == "STL":
network_has_code = True # Marcar que la red tiene contenido
if network.get('logic') and isinstance(network['logic'], list) and len(network['logic']) > 0:
stl_chunk = network['logic'][0]
if stl_chunk.get("type") == "RAW_STL_CHUNK" and "stl" in stl_chunk:
raw_stl_code = stl_chunk["stl"]
# Añadir marcador de inicio (como comentario SCL para evitar errores)
scl_output.append(f" {'//'} ```STL") # Doble '//' para asegurar que sea comentario
# Escribir el código STL crudo, indentado
for stl_line in raw_stl_code.splitlines():
# Añadir indentación estándar de SCL
scl_output.append(f" {stl_line}") # <-- STL sin comentar
# Añadir marcador de fin (como comentario SCL)
scl_output.append(f" {'//'} ```")
else:
scl_output.append(" // ERROR: Contenido STL inesperado en JSON.")
else:
scl_output.append(" // ERROR: No se encontró lógica STL en JSON para esta red.")
scl_output.append("") # Línea en blanco después de la red STL
# --- FIN NUEVO MANEJO STL con formato Markdown ---
else:
# Iterar sobre la 'logica' de la red
for instruction in network.get('logic', []):
instruction_type = instruction.get("type", "")
scl_code = instruction.get('scl', "") # Obtener SCL generado por x2
# Saltar instrucciones agrupadas
if instruction.get("grouped", False):
continue
# Escribir SCL si es un tipo procesado y tiene código relevante
# (Ignorar comentarios de depuración de SymPy)
if instruction_type.endswith(SCL_SUFFIX) and scl_code:
is_internal_sympy_comment_only = scl_code.strip().startswith("// SymPy") or \
scl_code.strip().startswith("// PBox SymPy processed") or \
scl_code.strip().startswith("// NBox SymPy processed")
# O podría ser más genérico: ignorar cualquier línea que solo sea comentario SCL
is_only_comment = all(line.strip().startswith("//") for line in scl_code.splitlines())
# Escribir solo si NO es un comentario interno de SymPy O si es un bloque IF (que sí debe escribirse)
if not is_only_comment or scl_code.strip().startswith("IF"):
network_has_code = True
for line in scl_code.splitlines():
# Añadir indentación estándar
scl_output.append(f" {line}")
# Incluir también tipos especiales directamente
elif instruction_type in ["RAW_SCL_CHUNK", "UNSUPPORTED_LANG"] and scl_code:
network_has_code = True
for line in scl_code.splitlines():
scl_output.append(f" {line}") # Indentar
# Podríamos añadir comentarios para errores si se desea
# elif "_error" in instruction_type:
# network_has_code = True
# scl_output.append(f" // ERROR processing instruction UID {instruction.get('instruction_uid')}: {instruction.get('scl', 'No details')}")
if network_has_code:
scl_output.append("") # Línea en blanco después del código de la red
else:
scl_output.append(f" // Network did not produce printable SCL code.")
scl_output.append("")
# Fin del bloque
scl_output.append("END_FUNCTION_BLOCK") # O END_FUNCTION si es FC
# --- Escritura del Archivo SCL ---
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__":
parser = argparse.ArgumentParser(description="Generate final SCL file from processed JSON (SymPy version).")
parser.add_argument(
"source_xml_filepath",
nargs="?",
default="TestLAD.xml",
help="Path to the original source XML file (used to derive input/output names, default: TestLAD.xml)"
)
args = parser.parse_args()
xml_filename_base = os.path.splitext(os.path.basename(args.source_xml_filepath))[0]
# Usar directorio del script si no hay ruta, o la ruta del XML si la tiene
xml_dir = os.path.dirname(args.source_xml_filepath)
base_dir = xml_dir if xml_dir else os.path.dirname(__file__)
input_json_file = os.path.join(base_dir, f"{xml_filename_base}_simplified_processed.json")
output_scl_file = os.path.join(base_dir, f"{xml_filename_base}_simplified_processed.scl")
if not os.path.exists(input_json_file):
print(f"Error: Processed JSON file not found: '{input_json_file}'")
print(f"Ensure 'x2_process.py' ran successfully for '{args.source_xml_filepath}'.")
sys.exit(1)
else:
generate_scl(input_json_file, output_scl_file)