273 lines
15 KiB
Python
273 lines
15 KiB
Python
# x3_generate_scl.py
|
|
# -*- coding: utf-8 -*-
|
|
import json
|
|
import os
|
|
import re
|
|
import argparse
|
|
import sys
|
|
import traceback
|
|
|
|
# --- Importar Utilidades (mantener como estaba) ---
|
|
try:
|
|
from processors.processor_utils import format_variable_name
|
|
SCL_SUFFIX = "_sympy_processed"
|
|
GROUPED_COMMENT = "// Logic included in grouped IF"
|
|
except ImportError:
|
|
print("Advertencia: No se pudo importar 'format_variable_name'. Usando fallback.")
|
|
def format_variable_name(name): # Fallback BÁSICO
|
|
if not name: return "_INVALID_NAME_"
|
|
if name.startswith('"') and name.endswith('"'): return name
|
|
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"
|
|
|
|
# --- NUEVA FUNCIÓN para formatear valores iniciales SCL ---
|
|
def format_scl_start_value(value, datatype):
|
|
"""Formatea un valor para la inicialización SCL según el tipo."""
|
|
if value is None: return None
|
|
datatype_lower = datatype.lower() if datatype else ""
|
|
value_str = str(value)
|
|
|
|
if "bool" in datatype_lower:
|
|
return "TRUE" if value_str.lower() == 'true' else "FALSE"
|
|
elif "string" in datatype_lower:
|
|
escaped_value = value_str.replace("'", "''")
|
|
if escaped_value.startswith("'") and escaped_value.endswith("'"): escaped_value = escaped_value[1:-1]
|
|
return f"'{escaped_value}'"
|
|
elif "char" in datatype_lower: # Añadido Char
|
|
escaped_value = value_str.replace("'", "''")
|
|
if escaped_value.startswith("'") and escaped_value.endswith("'"): escaped_value = escaped_value[1:-1]
|
|
return f"'{escaped_value}'"
|
|
elif any(t in datatype_lower for t in ["int", "byte", "word", "dint", "dword", "lint", "lword", "sint", "usint", "uint", "udint", "ulint"]): # Ampliado
|
|
try: return str(int(value_str))
|
|
except ValueError:
|
|
if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str): return value_str
|
|
return f"'{value_str}'" # O como string si no es entero ni símbolo
|
|
elif "real" in datatype_lower or "lreal" in datatype_lower:
|
|
try:
|
|
f_val = float(value_str); s_val = str(f_val)
|
|
if '.' not in s_val and 'e' not in s_val.lower(): s_val += ".0"
|
|
return s_val
|
|
except ValueError:
|
|
if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str): return value_str
|
|
return f"'{value_str}'"
|
|
elif "time" in datatype_lower: # Añadido Time, S5Time, LTime
|
|
# Quitar T#, LT#, S5T# si existen
|
|
prefix = ""
|
|
if value_str.upper().startswith("T#"): prefix="T#"; value_str = value_str[2:]
|
|
elif value_str.upper().startswith("LT#"): prefix="LT#"; value_str = value_str[3:]
|
|
elif value_str.upper().startswith("S5T#"): prefix="S5T#"; value_str = value_str[4:]
|
|
# Devolver con el prefijo correcto o T# por defecto si no había
|
|
if prefix: return f"{prefix}{value_str}"
|
|
elif "s5time" in datatype_lower: return f"S5T#{value_str}"
|
|
elif "ltime" in datatype_lower: return f"LT#{value_str}"
|
|
else: return f"T#{value_str}" # Default a TIME
|
|
elif "date" in datatype_lower: # Añadido Date, DT, TOD
|
|
if value_str.upper().startswith("D#"): return value_str
|
|
elif "dt" in datatype_lower or "date_and_time" in datatype_lower:
|
|
if value_str.upper().startswith("DT#"): return value_str
|
|
else: return f"DT#{value_str}" # Añadir prefijo DT#
|
|
elif "tod" in datatype_lower or "time_of_day" in datatype_lower:
|
|
if value_str.upper().startswith("TOD#"): return value_str
|
|
else: return f"TOD#{value_str}" # Añadir prefijo TOD#
|
|
else: return f"D#{value_str}" # Default a Date
|
|
# Fallback genérico
|
|
else:
|
|
if re.match(r'^[a-zA-Z_][a-zA-Z0-9_."#\[\]]+$', value_str): # Permitir más caracteres en símbolos/tipos
|
|
# Si es un UDT o Struct complejo, podría venir con comillas, quitarlas
|
|
if value_str.startswith('"') and value_str.endswith('"'):
|
|
return value_str[1:-1]
|
|
return value_str
|
|
else:
|
|
escaped_value = value_str.replace("'", "''")
|
|
return f"'{escaped_value}'"
|
|
|
|
# --- NUEVA FUNCIÓN RECURSIVA para generar declaraciones SCL (VAR/STRUCT/ARRAY) ---
|
|
|
|
# --- Función Principal de Generación SCL (MODIFICADA) ---
|
|
def generate_scl(processed_json_filepath, output_scl_filepath):
|
|
"""Genera un archivo SCL a partir del JSON procesado (FC/FB o DB)."""
|
|
|
|
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') # Será "DB" para Data Blocks
|
|
block_type = data.get('block_type', 'Unknown') # FC, FB, GlobalDB
|
|
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}, Lang: {block_lang_original})")
|
|
scl_output = []
|
|
|
|
# --- GENERACIÓN PARA DATA BLOCK (DB) ---
|
|
if block_lang_original == "DB":
|
|
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: scl_output.append(f"// Block Comment: {block_comment}")
|
|
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', {})
|
|
static_vars = interface_data.get('Static', [])
|
|
if static_vars:
|
|
scl_output.append("VAR")
|
|
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.")
|
|
scl_output.append("VAR"); scl_output.append("END_VAR"); scl_output.append("")
|
|
scl_output.append("BEGIN"); scl_output.append(""); scl_output.append("END_DATA_BLOCK")
|
|
|
|
# --- GENERACIÓN PARA FUNCTION BLOCK / FUNCTION (FC/FB) ---
|
|
else:
|
|
print("Modo de generación: FUNCTION_BLOCK / FUNCTION")
|
|
scl_block_keyword = "FUNCTION_BLOCK" if block_type == "FB" else "FUNCTION"
|
|
# 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}")
|
|
scl_output.append(f"// Original Language: {block_lang_original}")
|
|
if block_comment: scl_output.append(f"// Block Comment: {block_comment}")
|
|
scl_output.append("")
|
|
# Manejar tipo de retorno para FUNCTION
|
|
return_type = "Void" # Default
|
|
interface_data = data.get('interface', {})
|
|
if scl_block_keyword == "FUNCTION" and interface_data.get('Return'):
|
|
return_member = interface_data['Return'][0] # Asumir un solo valor de retorno
|
|
return_type_raw = return_member.get('datatype', 'Void')
|
|
return_type = return_type_raw.strip('"') if return_type_raw.startswith('"') and return_type_raw.endswith('"') else return_type_raw
|
|
# Añadir comillas si es UDT
|
|
if return_type != return_type_raw: return_type = f'"{return_type}"'
|
|
|
|
scl_output.append(f"{scl_block_keyword} \"{scl_block_name}\" : {return_type}" if scl_block_keyword == "FUNCTION" else f"{scl_block_keyword} \"{scl_block_name}\"")
|
|
scl_output.append("{ S7_Optimized_Access := 'TRUE' }")
|
|
scl_output.append("VERSION : 0.1"); scl_output.append("")
|
|
|
|
# Declaraciones de Interfaz FC/FB
|
|
section_order = ["Input", "Output", "InOut", "Static", "Temp", "Constant"] # Return ya está en cabecera
|
|
declared_temps = set()
|
|
for section_name in section_order:
|
|
vars_in_section = interface_data.get(section_name, [])
|
|
if vars_in_section:
|
|
scl_section_keyword = f"VAR_{section_name.upper()}"
|
|
if section_name == "Static": scl_section_keyword = "VAR_STAT"
|
|
if section_name == "Temp": scl_section_keyword = "VAR_TEMP"
|
|
if section_name == "Constant": scl_section_keyword = "CONSTANT"
|
|
scl_output.append(scl_section_keyword)
|
|
scl_output.extend(generate_scl_declarations(vars_in_section, indent_level=1))
|
|
if section_name == "Temp": declared_temps.update(format_variable_name(v.get("name")) for v in vars_in_section if v.get("name"))
|
|
scl_output.append("END_VAR"); scl_output.append("")
|
|
|
|
# Declaraciones VAR_TEMP adicionales detectadas
|
|
temp_vars = set()
|
|
temp_pattern = re.compile(r'"?#(_temp_[a-zA-Z0-9_]+)"?|"?(_temp_[a-zA-Z0-9_]+)"?')
|
|
for network in data.get('networks', []):
|
|
for instruction in network.get('logic', []):
|
|
scl_code = instruction.get('scl', ''); 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:
|
|
found_temps = temp_pattern.findall(code_to_scan)
|
|
for temp_tuple in found_temps:
|
|
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)
|
|
additional_temps = sorted(list(temp_vars - declared_temps))
|
|
if additional_temps:
|
|
if not interface_data.get("Temp"): scl_output.append("VAR_TEMP")
|
|
for var_name in additional_temps:
|
|
scl_name = format_variable_name(var_name); inferred_type = "Bool" # Asumir Bool
|
|
scl_output.append(f" {scl_name} : {inferred_type}; // Auto-generated temporary")
|
|
if not interface_data.get("Temp"): scl_output.append("END_VAR"); scl_output.append("")
|
|
|
|
# Cuerpo del Bloque FC/FB
|
|
scl_output.append("BEGIN"); scl_output.append("")
|
|
# Iterar por redes y lógica (como antes, incluyendo manejo STL Markdown)
|
|
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')
|
|
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
|
|
if network_lang == "STL":
|
|
network_has_code = True
|
|
if network.get('logic') and network['logic'][0].get("type") == "RAW_STL_CHUNK":
|
|
raw_stl_code = network['logic'][0].get("stl", "// ERROR: STL code missing")
|
|
scl_output.append(f" {'//'} ```STL")
|
|
for stl_line in raw_stl_code.splitlines(): scl_output.append(f" {stl_line}")
|
|
scl_output.append(f" {'//'} ```")
|
|
else: scl_output.append(" // ERROR: Contenido STL inesperado.")
|
|
else: # LAD, FBD, SCL, etc.
|
|
for instruction in network.get('logic', []):
|
|
instruction_type = instruction.get("type", ""); scl_code = instruction.get('scl', ''); is_grouped = instruction.get("grouped", False)
|
|
if is_grouped: continue
|
|
if (instruction_type.endswith(SCL_SUFFIX) or instruction_type in ["RAW_SCL_CHUNK", "UNSUPPORTED_LANG"]) and scl_code:
|
|
is_only_comment = all(line.strip().startswith("//") for line in scl_code.splitlines() if line.strip())
|
|
is_if_block = scl_code.strip().startswith("IF")
|
|
if not is_only_comment or is_if_block:
|
|
network_has_code = True
|
|
for line in scl_code.splitlines(): scl_output.append(f" {line}")
|
|
if network_has_code: scl_output.append("")
|
|
else: scl_output.append(f" // Network did not produce printable SCL code."); scl_output.append("")
|
|
# Fin del bloque FC/FB
|
|
scl_output.append(f"END_{scl_block_keyword}")
|
|
|
|
# --- 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 (sin cambios) ---
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description="Generate final SCL file from processed JSON (FC/FB/DB).")
|
|
parser.add_argument("source_xml_filepath", help="Path to the original source XML file.")
|
|
args = parser.parse_args()
|
|
source_xml_file = args.source_xml_filepath
|
|
|
|
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.")
|
|
# Continuar, pero verificar existencia del JSON procesado
|
|
|
|
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
|
|
base_dir = os.path.dirname(source_xml_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")
|
|
|
|
print(f"(x3) Generando SCL: '{os.path.relpath(input_json_file)}' -> '{os.path.relpath(output_scl_file)}'")
|
|
|
|
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 'x1_to_json.py' y 'x2_process.py' se ejecutaron correctamente para '{os.path.relpath(source_xml_file)}'.")
|
|
sys.exit(1)
|
|
else:
|
|
try:
|
|
generate_scl(input_json_file, output_scl_file)
|
|
except Exception as e:
|
|
print(f"Error Crítico (x3) durante la generación de SCL desde '{input_json_file}': {e}")
|
|
traceback.print_exc()
|
|
sys.exit(1) |