ParamManagerScripts/backend/script_groups/S7_DB_Utils/DB_Parser.py

808 lines
32 KiB
Python

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()