ParamManagerScripts/backend/script_groups/XML Parser to SCL/generators/generator_utils.py

278 lines
16 KiB
Python

# ToUpload/generators/generator_utils.py
# -*- coding: utf-8 -*-
import re
import os
import json
import traceback # Para depuración si es necesario
import sys
# --- Importar format_variable_name desde processors ---
try:
# Asumiendo que este script está en 'generators' y 'processors' está al mismo nivel
current_dir = os.path.dirname(os.path.abspath(__file__))
project_base_dir = os.path.dirname(current_dir)
processors_dir = os.path.join(project_base_dir, 'processors')
if processors_dir not in sys.path:
sys.path.insert(0, processors_dir) # Añadir al path si no está
from processors.processor_utils import format_variable_name
except ImportError:
print("Advertencia: No se pudo importar 'format_variable_name' desde processors.processor_utils.")
print("Usando una implementación local básica.")
def format_variable_name(name): # Fallback
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
# --- Fin Fallback ---
# --- format_scl_start_value (Sin cambios respecto a la versión anterior) ---
def format_scl_start_value(value, datatype):
if value is None: return None
# Convertir complex dict a string para procesar
if isinstance(value, dict):
# Si tiene 'value', usar ese. Si no, representar el dict como comentario
value_to_process = value.get('value')
if value_to_process is None:
return f"/* Init: {json.dumps(value)} */" # Representar dict como comentario
value = value_to_process # Usar el valor interno
datatype_lower = datatype.lower() if isinstance(datatype, str) else ""
value_str = str(value)
# Determinar si es tipo complejo (no estrictamente básico)
is_complex_type = (
('"' in datatype_lower) or ('array' in datatype_lower) or ('struct' in datatype_lower) or
datatype_lower not in {
"bool", "int", "dint", "sint", "usint", "uint", "udint", "lint", "ulint",
"byte", "word", "dword", "lword", "real", "lreal", "time", "ltime",
"s5time", "date", "dt", "dtl", "tod", "string", "char", "wstring", "wchar", "variant",
"timer", "counter", "iec_timer", "iec_counter", "iec_sfc", "iec_ld_timer" # Añadir otros tipos IEC comunes
}
)
if is_complex_type:
# Para tipos complejos, solo permitir constantes simbólicas o inicializadores básicos (0, FALSE, '')
if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str): return value_str # Constante simbólica
if value_str == '0': return '0' # Cero numérico
if value_str.lower() == 'false': return 'FALSE' # Booleano Falso
if value_str == "''" or value_str == "": return "''" # String vacío
# Ignorar otros valores iniciales para tipos complejos (incluye JSON de arrays)
# print(f"INFO: Start value '{value_str}' for complex type '{datatype}' skipped.")
return None
# Quitar comillas simples/dobles externas si las hay
value_str_unquoted = value_str
if len(value_str) > 1:
if value_str.startswith('"') and value_str.endswith('"'): value_str_unquoted = value_str[1:-1]
elif value_str.startswith("'") and value_str.endswith("'"): value_str_unquoted = value_str[1:-1]
# Formateo por tipo básico
if any(t in datatype_lower for t in ["int","byte","word","dint","dword","lint","lword","sint","usint","uint","udint","ulint"]):
try: return str(int(value_str_unquoted))
except ValueError: return value_str_unquoted if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str_unquoted) else None # Permitir constante simbólica
elif "bool" in datatype_lower:
val_low = value_str_unquoted.lower();
if val_low in ['true', '1']: return "TRUE"
elif val_low in ['false', '0']: return "FALSE"
else: return value_str_unquoted if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str_unquoted) else "FALSE" # Default FALSE
elif "string" in datatype_lower or "char" in datatype_lower:
escaped_value = value_str_unquoted.replace("'", "''") # Escapar comillas simples
prefix = "WSTRING#" if "wstring" in datatype_lower else ("WCHAR#" if "wchar" in datatype_lower else "")
return f"{prefix}'{escaped_value}'" # Usar comillas simples SCL
elif "real" in datatype_lower or "lreal" in datatype_lower:
try:
f_val = float(value_str_unquoted)
s_val = "{:.7g}".format(f_val) # Notación científica si es necesario, precisión limitada
return s_val + (".0" if "." not in s_val and "e" not in s_val.lower() else "") # Añadir .0 si es entero
except ValueError: return value_str_unquoted if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str_unquoted) else None # Permitir constante simbólica
elif "time" in datatype_lower: # Incluye TIME, LTIME, S5TIME
prefix, val_to_use = "", value_str_unquoted
# Extraer prefijo si ya existe (T#, LT#, S5T#)
match_prefix = re.match(r"^(T#|LT#|S5T#)(.*)", val_to_use, re.IGNORECASE)
if match_prefix: prefix, val_to_use = match_prefix.groups()
# Validar formato del valor de tiempo (simplificado)
if re.match(r'^-?(\d+d_)?(\d+h_)?(\d+m_)?(\d+s_)?(\d+ms)?$', val_to_use, re.IGNORECASE):
target_prefix = "S5T#" if "s5time" in datatype_lower else ("LT#" if "ltime" in datatype_lower else "T#")
return f"{target_prefix}{val_to_use}"
elif re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str_unquoted): return value_str_unquoted # Constante simbólica
else: return None # Formato inválido
elif any(t in datatype_lower for t in ["date", "dtl", "dt", "tod", "time_of_day"]):
val_to_use = value_str_unquoted; prefix = ""
# Extraer prefijo si ya existe (DTL#, D#, DT#, TOD#)
match_prefix = re.match(r"^(DTL#|D#|DT#|TOD#)(.*)", val_to_use, re.IGNORECASE)
if match_prefix: prefix, val_to_use = match_prefix.groups()
# Determinar prefijo SCL correcto
target_prefix="DTL#" if "dtl" in datatype_lower or "date_and_time" in datatype_lower else ("DT#" if "dt" in datatype_lower else ("TOD#" if "tod" in datatype_lower or "time_of_day" in datatype_lower else "D#"))
# Validar formato (simplificado)
if re.match(r'^\d{4}-\d{2}-\d{2}(-\d{2}:\d{2}:\d{2}(\.\d+)?)?$', val_to_use) or re.match(r'^\d{2}:\d{2}:\d{2}(\.\d+)?$', val_to_use):
return f"{target_prefix}{val_to_use}"
elif re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str_unquoted): return value_str_unquoted # Constante simbólica
else: return None # Formato inválido
else: # Otros tipos o desconocidos
return value_str if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str) else None # Solo permitir constantes simbólicas
# <-- MODIFICADO: generate_scl_declarations -->
def generate_scl_declarations(variables, indent_level=1, project_root_dir=None):
"""
Genera líneas SCL para declarar variables, manejando UDTs, FBs (InstanceOfName),
Arrays y Structs.
"""
scl_lines = []
indent = " " * indent_level
# Lista de tipos básicos simples (en minúsculas) - ampliada
basic_types = {
"bool", "int", "dint", "sint", "usint", "uint", "udint", "lint", "ulint",
"byte", "word", "dword", "lword", "real", "lreal", "time", "ltime",
"s5time", "date", "dt", "dtl", "tod", "time_of_day", # TOD sinónimos
"char", "wchar", "variant",
# Tipos IEC comunes
"timer", "counter", "iec_timer", "iec_counter", "iec_sfc", "iec_ld_timer"
}
# Patrones para tipos básicos parametrizados (ignorando mayúsculas/minúsculas)
string_pattern = re.compile(r"^(W?STRING)(\[\s*\d+\s*\])?$", re.IGNORECASE)
array_pattern = re.compile(r'^(Array\[.*\]\s+of\s+)(.*)', re.IGNORECASE)
for var in variables:
var_name_scl = format_variable_name(var.get("name"))
var_dtype_raw = var.get("datatype", "VARIANT")
# <-- NUEVO: Obtener instance_of_name -->
instance_of_name = var.get("instance_of_name") # Puede ser None
# <-- FIN NUEVO -->
var_comment = var.get("comment")
start_value_raw = var.get("start_value")
children = var.get("children") # Para STRUCT anidados
array_elements = var.get("array_elements") # Para inicialización de ARRAY
declaration_dtype = var_dtype_raw # Tipo a usar en la declaración SCL
base_type_for_init = var_dtype_raw # Tipo base para formatear valor inicial
is_array = False
is_struct_inline = bool(children) # Es un STRUCT definido inline
is_potential_udt_or_fb = False # Flag para comprobar si buscar archivo .json
type_to_check = None # Nombre limpio del tipo a buscar (UDT o FB)
# --- Lógica Principal de Determinación de Tipo ---
if is_struct_inline:
# Si tiene hijos, se declara como STRUCT ... END_STRUCT
declaration_dtype = "STRUCT"
base_type_for_init = "STRUCT" # Valor inicial no aplica a STRUCT directamente
elif isinstance(var_dtype_raw, str):
# 1. Comprobar si es FB Instance usando InstanceOfName
if instance_of_name:
# Si InstanceOfName existe, usarlo como tipo (entre comillas)
declaration_dtype = f'"{instance_of_name}"'
base_type_for_init = instance_of_name # Usar nombre limpio para init/check
is_potential_udt_or_fb = True # Marcar para buscar archivo FB
type_to_check = instance_of_name
else:
# 2. No es FB Instance directo, comprobar si es Array
array_match = array_pattern.match(var_dtype_raw)
if array_match:
is_array = True
array_prefix_for_decl = array_match.group(1)
base_type_raw = array_match.group(2).strip()
base_type_for_init = base_type_raw # Tipo base para init/check
# Limpiar tipo base para comprobar si es básico/UDT/String
base_type_clean = base_type_raw[1:-1] if base_type_raw.startswith('"') and base_type_raw.endswith('"') else base_type_raw
base_type_lower = base_type_clean.lower()
# ¿El tipo base es UDT/FB conocido o un tipo básico/paramétrico?
if (base_type_lower not in basic_types and
not string_pattern.match(base_type_clean)):
# Asumir UDT/FB si no es básico ni String[N]/Char
declaration_dtype = f'{array_prefix_for_decl}"{base_type_clean}"' # Poner comillas
is_potential_udt_or_fb = True # Marcar para buscar archivo UDT/FB
type_to_check = base_type_clean
else:
# Es básico o String[N]/Char
declaration_dtype = f'{array_prefix_for_decl}{base_type_raw}' # Usar como viene (puede tener comillas si era así)
else:
# 3. No es FB ni Array, ¿es UDT, String, Char o Básico?
base_type_clean = var_dtype_raw[1:-1] if var_dtype_raw.startswith('"') and var_dtype_raw.endswith('"') else var_dtype_raw
base_type_lower = base_type_clean.lower()
base_type_for_init = base_type_clean # Tipo base para init/check
if (base_type_lower not in basic_types and
not string_pattern.match(base_type_clean)):
# Asumir UDT/FB si no es básico ni String[N]/Char
declaration_dtype = f'"{base_type_clean}"' # Poner comillas
is_potential_udt_or_fb = True # Marcar para buscar archivo UDT/FB
type_to_check = base_type_clean
else:
# Es básico o String[N]/Char
declaration_dtype = var_dtype_raw # Usar como viene
# --- Búsqueda Opcional de Archivo de Definición (UDT o FB) ---
if is_potential_udt_or_fb and type_to_check and project_root_dir:
# Buscar tanto en 'PLC data types' como en 'Program blocks'
found_path = None
type_scl_name = format_variable_name(type_to_check)
possible_paths = [
os.path.join(project_root_dir, 'PLC data types', 'parsing', f'{type_scl_name}_processed.json'),
os.path.join(project_root_dir, 'Program blocks', 'parsing', f'{type_scl_name}_processed.json')
# Añadir más rutas si la estructura del proyecto varía
]
for path in possible_paths:
if os.path.exists(path):
found_path = path
break
if found_path:
print(f" INFO: Definición '{type_to_check}' localizada en: '{os.path.relpath(found_path, project_root_dir)}'")
else:
print(f" WARNING: No se encontró definición para '{type_to_check}'. Se buscó en directorios estándar.")
# --- Construir Línea de Declaración SCL ---
declaration_line = f"{indent}{var_name_scl} : {declaration_dtype}"
init_value_scl_part = ""
if is_struct_inline:
# Generar STRUCT anidado
scl_lines.append(declaration_line) # Añade "VarName : STRUCT"
# Llamada recursiva para los hijos
scl_lines.extend(generate_scl_declarations(children, indent_level + 1, project_root_dir))
scl_lines.append(f"{indent}END_STRUCT;")
# Añadir comentario al END_STRUCT si existe
if var_comment: scl_lines[-1] += f" // {var_comment}"
scl_lines.append("") # Línea en blanco después del struct
continue # Pasar a la siguiente variable del nivel actual
# --- Manejo de Valor Inicial (para no-STRUCTs) ---
init_value_scl = None
if is_array and array_elements:
# Inicialización de Array
init_values = []
try: # Intentar ordenar índices numéricamente
indices_numeric = {int(k): v for k, v in array_elements.items()}
sorted_indices_str = [str(k) for k in sorted(indices_numeric.keys())]
except ValueError: # Ordenar como strings si no son numéricos
print(f"Advertencia: Índices array no numéricos para '{var_name_scl}', ordenando como strings.")
sorted_indices_str = sorted(array_elements.keys())
for idx_str in sorted_indices_str:
val_info = array_elements[idx_str] # val_info puede ser dict o valor directo
# Formatear valor usando el tipo base del array
formatted_val = format_scl_start_value(val_info, base_type_for_init)
# Usar 'NULL' o comentario si el formateo falla o es complejo
init_values.append(formatted_val if formatted_val is not None else f"/* Array[{idx_str}] unsupported init */")
if init_values: init_value_scl = f"[{', '.join(init_values)}]"
elif not is_array and not is_struct_inline and start_value_raw is not None:
# Inicialización de variable simple
init_value_scl = format_scl_start_value(start_value_raw, base_type_for_init)
# Añadir parte del valor inicial si existe
if init_value_scl is not None:
init_value_scl_part = f" := {init_value_scl}"
# Combinar todo para la línea final
declaration_line += f"{init_value_scl_part};"
if var_comment: declaration_line += f" // {var_comment}"
scl_lines.append(declaration_line)
return scl_lines