import re import os import pandas as pd import tkinter as tk from tkinter import filedialog import subprocess from openpyxl import Workbook, load_workbook from openpyxl.utils import get_column_letter from openpyxl.styles import Alignment, Font, Border, Side, PatternFill from copy import deepcopy # Constantes para los tamaños de tipos de datos TYPE_SIZES = { "Byte": 1, "Char": 1, "Int": 2, "DInt": 4, "Word": 2, "DWord": 4, "Real": 4, "Date": 2, "Time": 4, "Time_Of_Day": 4, "S5Time": 2, "Bool": 0.125, # 1 bit, normalmente agrupado en 8 bits = 1 Byte "String": 256, # String[256] serían 258 bytes (256 caracteres + 2 para longitud) "WString": 512, "LReal": 8, # Punto flotante de doble precisión "UDInt": 4, # Entero sin signo de 32 bits "USInt": 1, # Entero sin signo de 8 bits (Byte) "UInt": 2, # Entero sin signo de 16 bits (Word) "ULInt": 8, # Entero sin signo de 64 bits (Doble DWord) "LWord": 8, # Entero sin signo de 64 bits (Doble DWord) "LInt": 8, # Entero con signo de 64 bits "Date_And_Time": 8, # Fecha y hora combinadas, 8 bytes "DTL": 12, # Date and time long (fecha, hora y precisión a microsegundos, 12 bytes) } #================================= # FUNCIONES DE PARSEO DE ARCHIVOS #================================= def clean_line(line): """Limpia la línea de BOM y espacios o comillas extra.""" # Elimina UTF-8 BOM si existe y elimina espacios iniciales/finales line = line.replace("\ufeff", "").strip() # Estandariza las definiciones TYPE y DATA_BLOCK line = re.sub(r'\s*TYPE\s+"?', 'TYPE "', line) line = re.sub(r'\s*DATA_BLOCK\s+"?', 'DATA_BLOCK "', line) line = remove_text_inside_brackets(line) return line def remove_text_inside_brackets(text): """Elimina texto dentro de corchetes.""" pattern = r"\{.*?\}" cleaned_text = re.sub(pattern, '', text) return cleaned_text def extract_name(line): """Extrae el nombre de una línea de definición TYPE o DATA_BLOCK.""" # Intenta encontrar un nombre entrecomillado primero match = re.search(r'(TYPE|DATA_BLOCK)\s+"([^"]+)"', line) if match: return match.group(2).strip() # El nombre está entre comillas # Si no hay nombre entrecomillado, busca un nombre sin comillas match = re.search(r"(TYPE|DATA_BLOCK)\s+(\S+)", line) if match: return match.group(2).strip() # El nombre está sin comillas def parse_udts(lines): """Parsea User Defined Types de las líneas de código.""" udt_json = {} udt_name = None nested_structs = [] current_struct = None is_within_struct = False for line in lines: line = clean_line(line) if "TYPE" in line and "END_TYPE" not in line: udt_name = extract_name(line) udt_json[udt_name] = {} current_struct = udt_json[udt_name] print(f"Creado UDT: {udt_name}") elif "END_TYPE" in line: print(f"Completado UDT: {udt_name}") udt_name = None nested_structs = [] current_struct = None is_within_struct = False elif "STRUCT" in line and "END_STRUCT" not in line and udt_name is not None: struct_name = ( "Struct" if "STRUCT" == line.strip() else line.split(":")[0].strip() ) new_struct = {} current_struct[struct_name] = new_struct nested_structs.append(current_struct) current_struct = new_struct is_within_struct = True print(f"Creado STRUCT: {struct_name}") elif "END_STRUCT" in line and udt_name is not None: current_struct = nested_structs.pop() if nested_structs else None is_within_struct = bool(nested_structs) print(f"Cerrado STRUCT en UDT '{udt_name}'") elif udt_name and ":" in line and is_within_struct: parts = line.split(":") field_name = parts[0].strip() field_details = parts[1].strip().split("//") field_type = ( field_details[0].replace(";", "").strip() ) # Eliminando ';' del tipo de campo field_comment = parts[1].split("//")[1].strip() if "//" in parts[1] else "" if "Struct" in field_type: new_struct = {} current_struct[field_name] = new_struct nested_structs.append(current_struct) current_struct = new_struct print(f"Abierto STRUCT en línea en el campo '{field_name}'") else: current_struct[field_name] = { "type": field_type, "comment": field_comment, } print( f"Añadido campo '{field_name}' a STRUCT: Tipo={field_type}, Comentario={field_comment}" ) return udt_json def parse_dbs(lines, udts): """Parsea Data Blocks de las líneas de código.""" db_json = {} db_name = None nested_structs = [] current_struct = None is_within_struct = False for line in lines: line = clean_line(line) if "DATA_BLOCK" in line and "END_DATA_BLOCK" not in line: db_name = extract_name(line) db_json[db_name] = {} current_struct = db_json[db_name] print(f"Creado DATA_BLOCK: {db_name}") elif "END_DATA_BLOCK" in line: print(f"Completado DATA_BLOCK: {db_name}") db_name = None nested_structs = [] current_struct = None is_within_struct = False elif "STRUCT" in line and "END_STRUCT" not in line and db_name is not None: struct_name = ( "Struct" if "STRUCT" == line.strip() else line.split(":")[0].strip() ) new_struct = {} current_struct[struct_name] = new_struct nested_structs.append(current_struct) current_struct = new_struct is_within_struct = True print(f"Creado STRUCT en DB '{db_name}': {struct_name}") elif "END_STRUCT" in line and db_name is not None: current_struct = nested_structs.pop() if nested_structs else None is_within_struct = bool(nested_structs) print(f"Cerrado STRUCT en DB '{db_name}'") elif db_name and ":" in line and is_within_struct: parts = line.split(":") field_name = parts[0].strip() field_details = parts[1].strip().split("//") field_type = ( field_details[0].replace(";", "").strip() ) # Eliminando ';' del tipo de campo field_comment = parts[1].split("//")[1].strip() if "//" in parts[1] else "" if "Struct" in field_type: new_struct = {} current_struct[field_name] = new_struct nested_structs.append(current_struct) current_struct = new_struct print(f"Abierto STRUCT en línea en el campo '{field_name}' en DB '{db_name}'") else: current_struct[field_name] = { "type": field_type, "comment": field_comment, } print( f"Añadido campo '{field_name}' a STRUCT en DB '{db_name}': Tipo={field_type}, Comentario={field_comment}" ) return db_json #================================= # FUNCIONES DE EXPANSIÓN DE UDT #================================= def expand_udt_references(db_struct, udts): """ Expande recursivamente las referencias UDT en la estructura DB utilizando las definiciones UDT. """ if isinstance(db_struct, dict): for key, value in list(db_struct.items()): if isinstance(value, dict): # Recursión en diccionarios expand_udt_references(value, udts) elif isinstance(value, str) and key == "type": # Solo expande campos 'type' type_name = value.strip( '"' ) # Elimina comillas que pueden envolver nombres UDT con espacios if type_name in udts: # Reemplaza la referencia UDT con su definición copiada en profundidad db_struct["is_udt_definition"] = True db_struct["fields"] = deepcopy(udts[type_name]) print(f"Expandido UDT '{type_name}' en el campo '{key}'") elif isinstance(db_struct, list): for item in db_struct: expand_udt_references(item, udts) def handle_array_types(db_struct): """ Maneja tipos de arrays para expandirlos en múltiples campos como sub-elementos. Esta función procesa completamente los arrays expandiéndolos en elementos individuales. """ if isinstance(db_struct, dict): # Lista para almacenar nuevas entradas de array a agregar new_entries = {} # Lista de claves a eliminar después de procesar keys_to_remove = [] for key, value in list(db_struct.items()): if isinstance(value, dict): # Procesa recursivamente diccionarios anidados handle_array_types(value) # Verificar si es un tipo array if "type" in value and isinstance(value["type"], str): array_match = re.match(r"ARRAY\s*\[(\d+)\s*\.\.\s*(\d+)\]\s*OF\s*(\w+)", value["type"], re.IGNORECASE) if array_match: lower_bound = int(array_match.group(1)) upper_bound = int(array_match.group(2)) base_type = array_match.group(3).strip() comment = value.get("comment", "") print(f"Expandiendo array '{key}': {lower_bound}..{upper_bound} of {base_type}") # Marcar para eliminar la definición original después keys_to_remove.append(key) # Crear entrada para la definición del array new_entries[key] = { "type": f"Array[{lower_bound}..{upper_bound}] of {base_type}", "comment": comment, "is_array_definition": True } # Crear elementos individuales del array for i in range(lower_bound, upper_bound + 1): array_key = f"{key}[{i}]" new_entries[array_key] = { "type": base_type, "comment": comment, "is_array_element": True } # Eliminar los originales y agregar los nuevos for key in keys_to_remove: if key in db_struct: del db_struct[key] # Agregar las nuevas entradas db_struct.update(new_entries) def expand_dbs(udts, dbs): """ Expande todas las referencias UDT en todos los DBs y luego maneja tipos de arrays. """ for db_name, db_content in dbs.items(): print(f"Expandiendo DB: {db_name}") # Primero expandir las referencias UDT expand_udt_references(db_content, udts) # Luego, manejar y expandir los tipos de arrays print(f"Procesando arrays en DB: {db_name}") handle_array_types(db_content) print(f"Completada expansión para DB: {db_name}") # Registrar el resultado de la expansión para depuración print("\nEstructura DB después de la expansión:") for db_name, db_content in dbs.items(): print(f"DB: {db_name} - Número de campos: {count_fields(db_content)}") def count_fields(struct): """Función auxiliar para contar campos en una estructura.""" count = 0 if isinstance(struct, dict): for key, value in struct.items(): if isinstance(value, dict): if "type" in value: count += 1 else: count += count_fields(value) return count #================================= # FUNCIONES DE CÁLCULO DE OFFSET #================================= def calculate_plc_address(type_name, byte_offset): """ Calcula la notación de dirección PLC basada en el tipo y offset. """ byte_size = TYPE_SIZES.get(type_name, 0) bit_offset = int((byte_offset - int(byte_offset)) * 8) byte_offset = int(byte_offset) if type_name == "Bool": return f"DBX{byte_offset}.{bit_offset}" # Dirección para bits individuales elif type_name == "Byte": return f"DBB{byte_offset}" # Dirección para bytes individuales elif byte_size == 2: return f"DBW{byte_offset}" # Dirección para words de dos bytes elif byte_size == 4: return f"DBD{byte_offset}" # Dirección para double words de cuatro bytes else: return f"DBX{byte_offset}.0" # Por defecto a dirección de bit para tipos de más de 4 bytes def calculate_plc_size(size): """Calcula la representación del tamaño PLC.""" byte_size = size bit_offset = int((size - int(size)) * 8) size = int(size) if bit_offset > 0: return f"{size}.{bit_offset}" else: return f"{size}" class OffsetState: """Clase para mantener el estado durante el cálculo de offset.""" def __init__(self): self.last_key_was_bool = False self.last_bit_offset = 0 # Para rastrear offsets de bit dentro de un byte self.current_offset = 0 def calculate_offsets(value, state, field_name="unknown"): """Calcula offsets de memoria para elementos DB.""" type_name = value["type"].strip() # Eliminar espacios en blanco is_array_element = value.get("is_array_element", False) is_array_definition = value.get("array_definition", False) is_udt_definition = value.get("is_udt_definition", False) # No calculamos offsets para definiciones de array, solo para sus elementos if is_array_definition: print(f"→ Definición de array '{field_name}': no calculando offset") return state print(f"Calculando offset para '{field_name}' (Tipo: {type_name}, Offset actual: {state.current_offset})") if state.last_key_was_bool: is_array_element = True size = 0 # Alineación a boundaries de datos if not is_array_element: if state.current_offset % 2 != 0: old_offset = state.current_offset state.current_offset += 1 # Alinea al siguiente offset par si no es elemento de array print(f" → Alineación: Offset ajustado de {old_offset} a {state.current_offset}") # Ajustando tamaños Bool basados en agrupación if type_name.upper() == "BOOL": state.last_key_was_bool = True size += 1 / 8 print(f" → Tipo Bool detectado: usando {size} bytes") else: if state.last_key_was_bool: # Después de bools state.last_key_was_bool = False # No es Bool if ( state.last_bit_offset > 0 or int(state.current_offset) != state.current_offset ): state.last_bit_offset = 0 old_offset = state.current_offset state.current_offset = int(state.current_offset) + 1 print(f" → Post-Bool: Ajustando offset de {old_offset} a {state.current_offset}") if state.current_offset % 2 != 0: old_offset = state.current_offset state.current_offset += 1 # Alinea al siguiente offset par print(f" → Post-Bool: Alineación a par: {old_offset} → {state.current_offset}") # Manejo especial para tipos String if type_name.upper().startswith("STRING"): match = re.match(r"String\[(\d+)\]", type_name, re.IGNORECASE) state.last_bit_offset = 0 if match: length = int(match.group(1)) size = length + 2 # Cuenta para terminación nula y prefijo de longitud de cadena print(f" → String[{length}] detectado: usando {size} bytes") else: size = TYPE_SIZES.get("String", 0) # Tamaño estándar para strings print(f" → String genérico detectado: usando {size} bytes") else: # Otros Tipos de Datos # Buscar el tipo ignorando mayúsculas/minúsculas type_upper = type_name.upper() type_size = None for key, value_size in TYPE_SIZES.items(): if key.upper() == type_upper: type_size = value_size break if type_size is not None: size = type_size print(f" → Tipo {type_name} encontrado: usando {size} bytes") else: print(f" → ADVERTENCIA: Tipo {type_name} no reconocido directamente") # Para arrays, manejo especial if "ARRAY" in type_upper: print(f" → Array detectado pero no se procesa directamente aquí") size = 0 # Los arrays se procesarán elemento por elemento else: size = 2 # Asumimos INT por defecto print(f" → Asumiendo tamaño de 2 bytes como valor predeterminado para {type_name}") if size == 0 and not is_array_definition and not is_udt_definition: print(f"⚠️ ADVERTENCIA: Tipo '{type_name}' tiene tamaño cero. Asumiendo 2 bytes.") size = 2 # Tamaño mínimo # Calcular dirección PLC plc_address = calculate_plc_address(type_name, state.current_offset) value["offset"] = state.current_offset value["plc_address"] = plc_address # Almacena la dirección PLC calculada value["size"] = calculate_plc_size(size) # Actualizar offset y mostrar resultado old_offset = state.current_offset state.current_offset += size print(f" → Resultado: Dirección={plc_address}, Tamaño={size}, Nuevo offset={state.current_offset}") return state def collect_data_for_table(db_struct, offset_state, level=0, parent_prefix="", collected_data=None, relative_offset=0): """ Recoge datos recursivamente de la estructura DB para mostrar en formato tabular. Añade soporte para offsets relativos dentro de estructuras. """ if collected_data is None: collected_data = [] is_array_element = False increase_level = 0 current_struct_base = offset_state.current_offset if isinstance(db_struct, dict): for key, value in db_struct.items(): # Omite claves 'fields' y 'Struct' en la ruta de nombre if key == "fields" or key == "Struct": next_prefix = parent_prefix # Continúa con el prefijo actual collect_data_for_table(value, offset_state, level, next_prefix, collected_data, relative_offset) continue # Determinar el prefijo de nombre para este elemento if isinstance(value, dict): is_array_element = value.get("is_array_element", False) is_array_definition = value.get("array_definition", False) # Construir el nombre del campo if not is_array_element: next_prefix = f"{parent_prefix}.{key}" if parent_prefix else key else: next_prefix = f"{parent_prefix}{key}" if parent_prefix else key # Si es una definición de array, añadirla a la tabla sin calcular offset if isinstance(value, dict) and value.get("array_definition", False): field_data = { "Nombre": next_prefix, "Tipo": value.get("type", "N/A"), "Offset": relative_offset, "Dirección PLC": "N/A", "Comentario": value.get("comment", ""), } collected_data.append(field_data) print(f"✓ Añadida definición de array: {next_prefix} - (sin dirección)") # No incrementar offset para definiciones de array, continuar con siguiente elemento continue # Procesar campo normal con tipo if isinstance(value, dict) and "type" in value: # Calcular offset si no es una definición de array if not value.get("array_definition", False): # Pasar el nombre del campo para mejorar los logs offset_state = calculate_offsets(value, offset_state, field_name=next_prefix) # Calcular offset relativo si es necesario element_relative_offset = value.get("offset", 0) - current_struct_base if is_array_element: element_relative_offset = value.get("offset", 0) - offset_state.current_offset + relative_offset field_data = { "Nombre": next_prefix, "Tipo": value.get("type", "N/A"), "Offset": element_relative_offset if is_array_element else value.get("offset", 0), "Dirección PLC": value.get("plc_address", "N/A"), "Comentario": value.get("comment", ""), } collected_data.append(field_data) increase_level = 1 print(f"✓ Añadido a tabla: {next_prefix} - {value.get('plc_address', 'N/A')}") # Maneja recursivamente diccionarios y listas anidados if isinstance(value, dict) and not "type" in value: new_relative = offset_state.current_offset if not is_array_element else relative_offset collect_data_for_table( value, offset_state, level + increase_level, next_prefix, collected_data, new_relative ) elif isinstance(db_struct, list): for index, item in enumerate(db_struct): item_prefix = f"{parent_prefix}[{index}]" if parent_prefix else f"[{index}]" collect_data_for_table( item, offset_state, level + increase_level, item_prefix, collected_data, relative_offset ) return collected_data def initiate_conversion_to_table(db_struct): """Inicia el proceso de conversión con un estado de offset nuevo.""" offset_state = OffsetState() return collect_data_for_table(db_struct, offset_state) def convert_to_table(dbs): """ Convierte los datos DB recogidos en un DataFrame de pandas. """ all_data = [] for db_name, db_content in dbs.items(): print(f"Procesando DB: {db_name}") db_data = initiate_conversion_to_table(db_content) all_data.extend(db_data) df = pd.DataFrame(all_data) # Reordenar las columnas al formato deseado if not df.empty and all(col in df.columns for col in ["Nombre", "Tipo", "Offset", "Dirección PLC", "Comentario"]): df = df[["Nombre", "Tipo", "Offset", "Dirección PLC", "Comentario"]] return df #================================= # FUNCIONES DE EXCEL #================================= def format_excel_worksheet(worksheet): """ Formatea la hoja de cálculo de Excel con estilos profesionales. """ # Definir estilos header_font = Font(name='Arial', size=11, bold=True, color="FFFFFF") header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") array_fill = PatternFill(start_color="E2EFDA", end_color="E2EFDA", fill_type="solid") struct_fill = PatternFill(start_color="DEEBF7", end_color="DEEBF7", fill_type="solid") thin_border = Border( left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin') ) # Aplicar estilos a la fila de encabezado for cell in worksheet[1]: cell.font = header_font cell.fill = header_fill cell.alignment = Alignment(horizontal='center', vertical='center') cell.border = thin_border # Obtener el número de filas y columnas max_row = worksheet.max_row max_col = worksheet.max_column # Aplicar estilos a las filas de datos for row in range(2, max_row + 1): # Verificar si es una estructura o array cell_name = worksheet.cell(row=row, column=1).value if worksheet.cell(row=row, column=1).value else "" cell_type = worksheet.cell(row=row, column=2).value if worksheet.cell(row=row, column=2).value else "" # Aplicar fondos especiales para estructuras y arrays is_struct = "Struct" in str(cell_type) is_array = "Array" in str(cell_type) # Aplicar bordes y alineación a todas las celdas for col in range(1, max_col + 1): cell = worksheet.cell(row=row, column=col) cell.border = thin_border # Aplicar color de fondo según el tipo if is_struct: cell.fill = struct_fill elif is_array: cell.fill = array_fill # Centrar columnas numéricas if col in [3, 4]: cell.alignment = Alignment(horizontal='center') # Ajustar ancho de columnas column_widths = { 1: 30, # Nombre 2: 15, # Tipo 3: 10, # Offset 4: 15, # Dirección PLC 5: 30 # Comentario } # Importamos get_column_letter directamente de openpyxl.utils en las importaciones del script for col_num, width in column_widths.items(): if col_num <= max_col: worksheet.column_dimensions[get_column_letter(col_num)].width = width # Congelar la fila del encabezado worksheet.freeze_panes = "A2" def save_dataframe_to_excel(df, filename, sheet_name): """ Guarda el DataFrame proporcionado en un archivo Excel y lo formatea. """ # Guardar el DataFrame en Excel df.to_excel(filename, index=False, sheet_name=sheet_name) print(f"Datos guardados en {filename}") # Abrir el archivo Excel guardado para aplicar formato workbook = load_workbook(filename) worksheet = workbook[sheet_name] # Aplicar formato a la hoja de cálculo format_excel_worksheet(worksheet) # Guardar el libro de trabajo formateado workbook.save(filename) print(f"Formato aplicado a {filename}") return workbook, worksheet #================================= # FUNCIONES DE UTILIDAD DE ARCHIVOS #================================= def select_file(): """ Abre un diálogo de archivo para seleccionar un archivo .db y devuelve la ruta del archivo seleccionado. """ root = tk.Tk() root.withdraw() # Oculta la ventana raíz de tkinter # Abre el diálogo de archivo y devuelve la ruta del archivo seleccionado file_path = filedialog.askopenfilename( title="Selecciona un archivo .db", filetypes=(("Archivos DB", "*.db"), ("Todos los archivos", "*.*")) ) return file_path def extract_file_details(file_path): """ Extrae y devuelve el nombre del archivo sin extensión, la extensión del archivo y la ruta del archivo. """ # Extrae la ruta completa del directorio path_only = os.path.dirname(file_path) # Extrae el nombre completo del archivo con extensión full_file_name = os.path.basename(file_path) # Separa la extensión del nombre del archivo file_name_without_extension, file_extension = os.path.splitext(full_file_name) return (file_name_without_extension, file_extension, path_only) def build_file_path(base_path, file_name, extension): """ Construye una ruta de archivo completa dada una ruta base, un nombre de archivo y una extensión. """ # Asegúrese de que la extensión esté en el formato correcto (es decir, comience con un punto) if not extension.startswith('.'): extension = '.' + extension # Separe el nombre base del archivo de su extensión si está presente file_name_without_extension, _ = os.path.splitext(file_name) # Reconstruir el nombre del archivo con la extensión correcta file_name_corrected = file_name_without_extension + extension # Construir la ruta completa del archivo full_path = os.path.join(base_path, file_name_corrected) return full_path def open_file_explorer(path): """ Abre el explorador de archivos en la ruta dada. """ # Normaliza la ruta para asegurarse de que esté en el formato correcto normalized_path = os.path.normpath(path) # Comprueba si la ruta es un directorio o un archivo y formatea el comando en consecuencia if os.path.isdir(normalized_path): # Si es un directorio, usa el comando 'explorer' directamente command = f'explorer "{normalized_path}"' else: # Si es un archivo, usa el comando 'explorer /select,' para resaltar el archivo en su carpeta command = f'explorer /select,"{normalized_path}"' # Ejecuta el comando usando subprocess.run subprocess.run(command, shell=True) #================================= # FUNCIÓN PRINCIPAL #================================= def main(): """ Función principal para ejecutar la conversión de DB a Excel. """ print("==================================================") print(" Convertidor de DB a Excel para Siemens S7 PLC") print("==================================================\n") # Seleccionar archivo print("Por favor, seleccione un archivo .db para procesar:") file_path = select_file() if not file_path: # No se seleccionó ningún archivo print("❌ No se seleccionó ningún archivo. Operación cancelada.") return print(f"✓ Archivo seleccionado: {file_path}") try: # Leer el contenido del archivo with open(file_path, "r", encoding="utf-8-sig") as file: lines = file.readlines() # Extraer detalles del archivo file_name, extension, dest_path = extract_file_details(file_path) # Crear ruta de salida excel_path = build_file_path(dest_path, file_name, "xlsx") log_path = build_file_path(dest_path, f"{file_name}_log", "txt") print("\n▶ Iniciando procesamiento del archivo DB...") print(f" Nombre del archivo: {file_name}") print(f" Ruta de destino: {excel_path}") # Configurar logging a archivo import sys import datetime original_stdout = sys.stdout log_file = open(log_path, 'w', encoding='utf-8') sys.stdout = log_file print("=== LOG DE PROCESAMIENTO ===") print(f"Archivo procesado: {file_path}") print(f"Fecha de procesamiento: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") print("============================\n") # Parsear UDTs y DBs print("\n=== PARSEANDO UDTs Y DBs ===") udt_json = parse_udts(lines) db_json = parse_dbs(lines, udt_json) # Expandir DBs con definiciones UDT print("\n=== EXPANDIENDO ESTRUCTURAS ===") expand_dbs(udt_json, db_json) # Convertir a tabla print("\n=== CALCULANDO OFFSETS Y DIRECCIONES ===") df = convert_to_table(db_json) # Restaurar stdout para mostrar mensajes en la consola sys.stdout = original_stdout log_file.close() # Guardar en Excel print("\n▶ Generando archivo Excel...") workbook, worksheet = save_dataframe_to_excel(df, excel_path, file_name) # Mostrar resumen print("\n=== RESUMEN DE CONVERSIÓN ===") print(f"✓ Total de variables procesadas: {len(df)}") print(f"✓ Tamaño total del DB: {df['Offset'].max() if not df.empty else 0} bytes") print(f"✓ Archivo Excel generado: {excel_path}") print(f"✓ Archivo de log generado: {log_path}") print("\n✅ ¡Conversión completada con éxito!") # Abrir el archivo de salida en el Explorador print("\n▶ Abriendo el archivo Excel en el Explorador...") open_file_explorer(excel_path) except Exception as e: print(f"\n❌ ERROR: Se produjo un error durante la conversión:") print(f" {str(e)}") import traceback traceback.print_exc() print("\nPor favor, revise el archivo de entrada y vuelva a intentarlo.") # Ejecutar la función principal cuando se ejecuta el script if __name__ == "__main__": main()