808 lines
32 KiB
Python
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() |