# 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