278 lines
16 KiB
Python
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 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 |