ParamManagerScripts/backend/script_groups/ObtainIOFromProjectTia/x4.py

662 lines
26 KiB
Python

"""
export_cross_references_from_tia : Script para exportar las referencias cruzadas
de un proyecto TIA Portal a archivos (probablemente XML).
"""
import tkinter as tk
from tkinter import filedialog
import os
import sys
import traceback
import time
from pathlib import Path
script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
sys.path.append(script_root)
from backend.script_utils import load_configuration
# --- Configuration ---
# Supported TIA Portal versions mapping (extension -> version)
SUPPORTED_TIA_VERSIONS = {".ap18": "18.0", ".ap19": "19.0", ".ap20": "20.0"}
# Filter for cross-references. Based on documentation:
# 1: 'AllObjects', 2: 'ObjectsWithReferences', 3: 'ObjectsWithoutReferences', 4: 'UnusedObjects'
# Using 1 to export all. 0 might also work as a default in some API versions.
CROSS_REF_FILTER = 1
MAX_REOPEN_ATTEMPTS = (
5 # Número máximo de re-aperturas permitidas para evitar bucles infinitos
)
BLOCK_TIMEOUT_SECONDS = 120 # Referencia de tiempo esperado para el procesamiento de cada bloque (para logging)
class PortalDisposedException(Exception):
"""Excepción lanzada cuando TIA Portal se ha cerrado inesperadamente o un objeto ha sido descartado."""
def __init__(self, original_exception, failed_block: str | None = None):
super().__init__(str(original_exception))
self.failed_block = failed_block
def _is_disposed_exception(exc: Exception) -> bool:
"""Devuelve True si la excepción proviene de un objeto/portal ya cerrado o sin mensaje útil."""
msg = str(exc).lower()
return any(
indicator in msg
for indicator in (
"disposed object",
"tia portal has either been disposed",
"unexpected exception - no exception message available",
)
)
# --- TIA Scripting Import Handling ---
if os.getenv("TIA_SCRIPTING"):
sys.path.append(os.getenv("TIA_SCRIPTING"))
else:
pass
try:
import siemens_tia_scripting as ts
except ImportError:
print("ERROR: Error al importar 'siemens_tia_scripting'.")
print("Asegúrese de que:")
print("1. TIA Portal Openness está instalado.")
print(
"2. El módulo 'siemens_tia_scripting' de Python está instalado (pip install ...) o"
)
print(
" la ruta a sus binarios está configurada en la variable de entorno 'TIA_SCRIPTING'."
)
print(
"3. Está usando una versión compatible de Python (ej. 3.12.X según la documentación)."
)
sys.exit(1)
except Exception as e:
print(f"Ocurrió un error inesperado durante la importación: {e}")
traceback.print_exc()
sys.exit(1)
# --- Functions ---
def get_supported_filetypes():
"""Returns the supported file types for TIA Portal projects."""
filetypes = []
for ext, version in SUPPORTED_TIA_VERSIONS.items():
version_major = version.split(".")[0]
filetypes.append((f"TIA Portal V{version_major} Projects", f"*{ext}"))
# Add option to show all supported files
all_extensions = " ".join([f"*{ext}" for ext in SUPPORTED_TIA_VERSIONS.keys()])
filetypes.insert(0, ("All TIA Portal Projects", all_extensions))
return filetypes
def detect_tia_version(project_file_path):
"""Detects TIA Portal version based on file extension."""
file_path = Path(project_file_path)
file_extension = file_path.suffix.lower()
if file_extension in SUPPORTED_TIA_VERSIONS:
detected_version = SUPPORTED_TIA_VERSIONS[file_extension]
print(
f"Versión de TIA Portal detectada: {detected_version} (de la extensión {file_extension})"
)
return detected_version
else:
print(
f"ADVERTENCIA: Extensión de archivo no reconocida '{file_extension}'. Extensiones soportadas: {list(SUPPORTED_TIA_VERSIONS.keys())}"
)
# Default to version 18.0 for backward compatibility
print("Usando por defecto TIA Portal V18.0")
return "18.0"
def select_project_file():
"""Opens a dialog to select a TIA Portal project file."""
root = tk.Tk()
root.withdraw()
file_path = filedialog.askopenfilename(
title="Seleccionar archivo de proyecto TIA Portal",
filetypes=get_supported_filetypes(),
)
root.destroy()
if not file_path:
print("No se seleccionó ningún archivo de proyecto. Saliendo.")
sys.exit(0)
return file_path
def _normalize_name(name: str) -> str:
"""Normaliza un nombre quitando espacios laterales y convirtiendo a minúsculas."""
return name.strip().lower()
def _export_block_with_timeout(
block, blocks_cr_path, block_name, timeout_seconds=BLOCK_TIMEOUT_SECONDS
):
"""
Exporta las referencias cruzadas de un bloque con monitoreo de tiempo.
Note: TIA Portal Openness no permite operaciones multi-hilo, por lo que
implementamos un timeout conceptual que al menos registra cuánto tiempo toma.
Returns:
bool: True si se exportó exitosamente
"""
start_time = time.time()
try:
# Realizar la exportación de forma directa (sin hilos debido a restricciones de TIA)
block.export_cross_references(
target_directorypath=str(blocks_cr_path),
filter=CROSS_REF_FILTER,
)
elapsed_time = time.time() - start_time
# Verificar si excedió el tiempo esperado (aunque ya terminó)
if elapsed_time > timeout_seconds:
print(
f" ADVERTENCIA: El bloque tardó {elapsed_time:.2f}s (>{timeout_seconds}s esperado)"
)
return True
except Exception as e:
elapsed_time = time.time() - start_time
print(f" Tiempo transcurrido antes del error: {elapsed_time:.2f} segundos")
raise e
def export_plc_cross_references(
plc, export_base_dir, exported_blocks=None, problematic_blocks=None
):
"""Exports cross-references for various elements from a given PLC.
Parámetros
----------
plc : objeto PLC actual
export_base_dir : pathlib.Path directorio base para exportar
exported_blocks : set[str] bloques que ya se han exportado
problematic_blocks : set[str] bloques que se deben omitir tras fallos previos
"""
if exported_blocks is None:
exported_blocks = set()
if problematic_blocks is None:
problematic_blocks = set()
plc_name = plc.get_name()
print(f"\n--- Procesando PLC: {plc_name} ---")
# Define base export path for this PLC's cross-references
plc_export_dir = export_base_dir / plc_name
plc_export_dir.mkdir(parents=True, exist_ok=True)
# --- Export Program Block Cross-References ---
blocks_cr_exported = 0
blocks_cr_skipped = 0
current_block_name = None # Track current block being processed
print(
f"\n[PLC: {plc_name}] Exportando referencias cruzadas de bloques de programa..."
)
blocks_cr_path = plc_export_dir / "ProgramBlocks_CR"
blocks_cr_path.mkdir(exist_ok=True)
print(f" Destino: {blocks_cr_path}")
try:
program_blocks = plc.get_program_blocks()
print(f" Se encontraron {len(program_blocks)} bloques de programa.")
# Show which blocks will be skipped from the start
if problematic_blocks:
skipped_names = []
for block in program_blocks:
if _normalize_name(block.get_name()) in problematic_blocks:
skipped_names.append(block.get_name())
if skipped_names:
print(
f" Bloques que serán omitidos (problemáticos previos): {', '.join(skipped_names)}"
)
for block in program_blocks:
block_name = block.get_name()
current_block_name = block_name # Update current block being processed
norm_block = _normalize_name(block_name)
if norm_block in problematic_blocks:
print(
f" Omitiendo bloque problemático previamente detectado: {block_name}"
)
blocks_cr_skipped += 1
continue
if norm_block in exported_blocks:
# Ya exportado en un intento anterior, no repetir
print(f" Omitiendo bloque ya exportado: {block_name}")
continue
print(f" Procesando bloque: {block_name}...")
try:
print(f" Exportando referencias cruzadas para {block_name}...")
start_time = time.time()
# Usar la función con monitoreo de tiempo
_export_block_with_timeout(block, blocks_cr_path, block_name)
elapsed_time = time.time() - start_time
print(f" Exportación completada en {elapsed_time:.2f} segundos")
blocks_cr_exported += 1
exported_blocks.add(norm_block)
except RuntimeError as block_ex:
print(
f" ERROR TIA al exportar referencias cruzadas para el bloque {block_name}: {block_ex}"
)
problematic_blocks.add(norm_block)
blocks_cr_skipped += 1
except Exception as block_ex:
print(
f" ERROR GENERAL al exportar referencias cruzadas para el bloque {block_name}: {block_ex}"
)
traceback.print_exc()
problematic_blocks.add(norm_block) # Always mark as problematic
blocks_cr_skipped += 1
if _is_disposed_exception(block_ex):
# Escalamos para que el script pueda re-abrir el Portal y omitir el bloque
raise PortalDisposedException(block_ex, failed_block=block_name)
print(
f" Resumen de exportación de referencias cruzadas de bloques: Exportados={blocks_cr_exported}, Omitidos/Errores={blocks_cr_skipped}"
)
except AttributeError:
print(
" Error de atributo: No se pudo encontrar 'get_program_blocks' en el objeto PLC. Omitiendo bloques de programa."
)
except Exception as e:
print(
f" ERROR al acceder a los bloques de programa para exportar referencias cruzadas: {e}"
)
traceback.print_exc()
# If we know which block was being processed, mark it as problematic
if current_block_name:
problematic_blocks.add(_normalize_name(current_block_name))
raise PortalDisposedException(e, failed_block=current_block_name)
else:
raise PortalDisposedException(e)
# --- Export PLC Tag Table Cross-References ---
tags_cr_exported = 0
tags_cr_skipped = 0
print(
f"\n[PLC: {plc_name}] Exportando referencias cruzadas de tablas de variables..."
)
tags_cr_path = plc_export_dir / "PlcTags_CR"
tags_cr_path.mkdir(exist_ok=True)
print(f" Destino: {tags_cr_path}")
try:
tag_tables = plc.get_plc_tag_tables()
print(f" Se encontraron {len(tag_tables)} tablas de variables.")
for table in tag_tables:
table_name = table.get_name()
print(f" Procesando tabla de variables: {table_name}...")
try:
print(f" Exportando referencias cruzadas para {table_name}...")
table.export_cross_references(
target_directorypath=str(tags_cr_path), filter=CROSS_REF_FILTER
)
tags_cr_exported += 1
except RuntimeError as table_ex:
print(
f" ERROR TIA al exportar referencias cruzadas para la tabla {table_name}: {table_ex}"
)
tags_cr_skipped += 1
except Exception as table_ex:
print(
f" ERROR GENERAL al exportar referencias cruzadas para la tabla {table_name}: {table_ex}"
)
traceback.print_exc()
tags_cr_skipped += 1
print(
f" Resumen de exportación de referencias cruzadas de tablas: Exportados={tags_cr_exported}, Omitidos/Errores={tags_cr_skipped}"
)
except AttributeError:
print(
" Error de atributo: No se pudo encontrar 'get_plc_tag_tables' en el objeto PLC. Omitiendo tablas de variables."
)
except Exception as e:
print(
f" ERROR al acceder a las tablas de variables para exportar referencias cruzadas: {e}"
)
traceback.print_exc()
# --- Export PLC Data Type (UDT) Cross-References ---
udts_cr_exported = 0
udts_cr_skipped = 0
print(
f"\n[PLC: {plc_name}] Exportando referencias cruzadas de tipos de datos PLC (UDTs)..."
)
udts_cr_path = plc_export_dir / "PlcDataTypes_CR"
udts_cr_path.mkdir(exist_ok=True)
print(f" Destino: {udts_cr_path}")
try:
udts = plc.get_user_data_types()
print(f" Se encontraron {len(udts)} UDTs.")
for udt in udts:
udt_name = udt.get_name()
print(f" Procesando UDT: {udt_name}...")
try:
print(f" Exportando referencias cruzadas para {udt_name}...")
udt.export_cross_references(
target_directorypath=str(udts_cr_path), filter=CROSS_REF_FILTER
)
udts_cr_exported += 1
except RuntimeError as udt_ex:
print(
f" ERROR TIA al exportar referencias cruzadas para el UDT {udt_name}: {udt_ex}"
)
udts_cr_skipped += 1
except Exception as udt_ex:
print(
f" ERROR GENERAL al exportar referencias cruzadas para el UDT {udt_name}: {udt_ex}"
)
traceback.print_exc()
udts_cr_skipped += 1
print(
f" Resumen de exportación de referencias cruzadas de UDTs: Exportados={udts_cr_exported}, Omitidos/Errores={udts_cr_skipped}"
)
except AttributeError:
print(
" Error de atributo: No se pudo encontrar 'get_user_data_types' en el objeto PLC. Omitiendo UDTs."
)
except Exception as e:
print(f" ERROR al acceder a los UDTs para exportar referencias cruzadas: {e}")
traceback.print_exc()
# --- Export System Block Cross-References ---
sys_blocks_cr_exported = 0
sys_blocks_cr_skipped = 0
print(
f"\n[PLC: {plc_name}] Intentando exportar referencias cruzadas de bloques de sistema..."
)
sys_blocks_cr_path = plc_export_dir / "SystemBlocks_CR"
sys_blocks_cr_path.mkdir(exist_ok=True)
print(f" Destino: {sys_blocks_cr_path}")
try:
if hasattr(plc, "get_system_blocks"):
system_blocks = plc.get_system_blocks()
print(f" Se encontraron {len(system_blocks)} bloques de sistema.")
for sys_block in system_blocks:
sys_block_name = sys_block.get_name()
print(f" Procesando bloque de sistema: {sys_block_name}...")
try:
print(
f" Exportando referencias cruzadas para {sys_block_name}..."
)
sys_block.export_cross_references(
target_directorypath=str(sys_blocks_cr_path),
filter=CROSS_REF_FILTER,
)
sys_blocks_cr_exported += 1
except RuntimeError as sys_ex:
print(
f" ERROR TIA al exportar referencias cruzadas para el bloque de sistema {sys_block_name}: {sys_ex}"
)
sys_blocks_cr_skipped += 1
except Exception as sys_ex:
print(
f" ERROR GENERAL al exportar referencias cruzadas para el bloque de sistema {sys_block_name}: {sys_ex}"
)
traceback.print_exc()
sys_blocks_cr_skipped += 1
else:
print(
" Método 'get_system_blocks' no encontrado en el objeto PLC. Omitiendo bloques de sistema."
)
print(
f" Resumen de exportación de referencias cruzadas de bloques de sistema: Exportados={sys_blocks_cr_exported}, Omitidos/Errores={sys_blocks_cr_skipped}"
)
except AttributeError:
print(
" Error de atributo durante el procesamiento de bloques de sistema. Omitiendo bloques de sistema restantes."
)
traceback.print_exc()
except Exception as e:
print(
f" ERROR al acceder/procesar bloques de sistema para exportar referencias cruzadas: {e}"
)
traceback.print_exc()
# --- Export Software Unit Cross-References ---
sw_units_cr_exported = 0
sw_units_cr_skipped = 0
print(
f"\n[PLC: {plc_name}] Intentando exportar referencias cruzadas de unidades de software..."
)
sw_units_cr_path = plc_export_dir / "SoftwareUnits_CR"
sw_units_cr_path.mkdir(exist_ok=True)
print(f" Destino: {sw_units_cr_path}")
try:
if hasattr(plc, "get_software_units"):
software_units = plc.get_software_units()
print(f" Se encontraron {len(software_units)} unidades de software.")
for unit in software_units:
unit_name = unit.get_name()
print(f" Procesando unidad de software: {unit_name}...")
try:
print(f" Exportando referencias cruzadas para {unit_name}...")
unit.export_cross_references(
target_directorypath=str(sw_units_cr_path),
filter=CROSS_REF_FILTER,
)
sw_units_cr_exported += 1
except RuntimeError as unit_ex:
print(
f" ERROR TIA al exportar referencias cruzadas para la unidad de software {unit_name}: {unit_ex}"
)
sw_units_cr_skipped += 1
except Exception as unit_ex:
print(
f" ERROR GENERAL al exportar referencias cruzadas para la unidad de software {unit_name}: {unit_ex}"
)
traceback.print_exc()
sw_units_cr_skipped += 1
print(
f" Resumen de exportación de referencias cruzadas de unidades de software: Exportados={sw_units_cr_exported}, Omitidos/Errores={sw_units_cr_skipped}"
)
else:
print(
" Método 'get_software_units' no encontrado en el objeto PLC. Omitiendo unidades de software."
)
except AttributeError:
print(
" Error de atributo durante el procesamiento de unidades de software. Omitiendo unidades restantes."
)
traceback.print_exc()
except Exception as e:
print(
f" ERROR al acceder/procesar unidades de software para exportar referencias cruzadas: {e}"
)
traceback.print_exc()
print(f"\n--- Finalizado el procesamiento del PLC: {plc_name} ---")
def open_portal_and_project(tia_version: str, project_file_path: str):
"""Abre TIA Portal y el proyecto indicado, devolviendo el portal y el objeto proyecto."""
print(f"\nConectando a TIA Portal V{tia_version}...")
portal = ts.open_portal(
version=tia_version,
portal_mode=ts.Enums.PortalMode.WithGraphicalUserInterface,
)
print("Conectado a TIA Portal.")
print(f"ID del proceso del Portal: {portal.get_process_id()}")
project_obj = portal.open_project(project_file_path=str(project_file_path))
if project_obj is None:
project_obj = portal.get_project()
if project_obj is None:
raise Exception(
"No se pudo abrir u obtener el proyecto especificado tras la reapertura."
)
return portal, project_obj
# --- Main Script ---
if __name__ == "__main__":
configs = load_configuration()
working_directory = configs.get("working_directory")
print("--- Exportador de Referencias Cruzadas de TIA Portal ---")
print(f"Configuración:")
print(
f" - Tiempo esperado por bloque: {BLOCK_TIMEOUT_SECONDS} segundos (para logging)"
)
print(f" - Máximo intentos de reapertura: {MAX_REOPEN_ATTEMPTS}")
print(f" - Filtro de referencias cruzadas: {CROSS_REF_FILTER}")
print("")
# Validate working directory
if not working_directory or not os.path.isdir(working_directory):
print("ERROR: Directorio de trabajo no configurado o inválido.")
print(
"Por favor configure el directorio de trabajo usando la aplicación principal."
)
sys.exit(1)
# 1. Select Project File
project_file = select_project_file()
# 2. Detect TIA Portal version from project file
tia_version = detect_tia_version(project_file)
# 3. Define Export Directory using working_directory and subfolder
export_base_dir = Path(working_directory)
try:
export_base_dir.mkdir(parents=True, exist_ok=True)
print(f"\nProyecto seleccionado: {project_file}")
print(f"Usando directorio base de exportación: {export_base_dir.resolve()}")
except Exception as e:
print(
f"ERROR: No se pudo crear el directorio de exportación '{export_base_dir}'. Error: {e}"
)
sys.exit(1)
portal_instance = None
project_object = None
try:
# 4. Connect to TIA Portal with detected version
portal_instance, project_object = open_portal_and_project(
tia_version, project_file
)
# 5. Get PLCs
plcs = project_object.get_plcs()
if not plcs:
print("No se encontraron dispositivos PLC en el proyecto.")
else:
print(
f"Se encontraron {len(plcs)} PLC(s). Iniciando proceso de exportación de referencias cruzadas..."
)
# 7. Iterate and Export Cross-References for each PLC con lógica de re-apertura
for plc_device in plcs:
plc_name = plc_device.get_name()
exported_blocks = set()
problematic_blocks = set()
skipped_blocks_report = []
reopen_attempts = 0
while True:
try:
export_plc_cross_references(
plc=plc_device,
export_base_dir=export_base_dir,
exported_blocks=exported_blocks,
problematic_blocks=problematic_blocks,
)
break # Éxito
except PortalDisposedException as pd_ex:
reopen_attempts += 1
failed_block = pd_ex.failed_block
if failed_block:
norm_failed_block = _normalize_name(failed_block)
problematic_blocks.add(norm_failed_block)
skipped_blocks_report.append(failed_block)
print(f"Marcando bloque problemático: {failed_block}")
else:
print(
"Error general detectado sin bloque específico identificado"
)
if reopen_attempts > MAX_REOPEN_ATTEMPTS:
print(
f"Se alcanzó el máximo de re-aperturas permitidas ({MAX_REOPEN_ATTEMPTS}) para el PLC '{plc_name}'. Abortando."
)
break
# Intentamos cerrar el portal actual (si existe)
try:
print("Cerrando instancia actual de TIA Portal...")
portal_instance.close_portal()
except Exception:
pass
# Re-abrir portal y proyecto
print(
f"Re-abriendo TIA Portal (intento {reopen_attempts}/{MAX_REOPEN_ATTEMPTS})..."
)
portal_instance, project_object = open_portal_and_project(
tia_version, project_file
)
# Buscar de nuevo el PLC por nombre
plc_device = None
for _plc in project_object.get_plcs():
if _plc.get_name() == plc_name:
plc_device = _plc
break
if plc_device is None:
print(
f"No se encontró el PLC '{plc_name}' tras la re-apertura. Se aborta su procesamiento."
)
break
# Continuar con el while
continue
if skipped_blocks_report:
print(
f"\nBloques problemáticos para el PLC '{plc_name}': {', '.join(set(skipped_blocks_report))}"
)
print(
f"Total de bloques problemáticos registrados: {len(problematic_blocks)}"
)
print("\nProceso de exportación de referencias cruzadas completado.")
except RuntimeError as tia_ex:
print(f"\nError de TIA Portal Openness: {tia_ex}")
traceback.print_exc()
except FileNotFoundError:
print(f"\nERROR: Archivo de proyecto no encontrado en {project_file}")
except Exception as e:
print(f"\nOcurrió un error inesperado: {e}")
traceback.print_exc()
finally:
# 8. Cleanup
if portal_instance:
try:
print("\nCerrando TIA Portal...")
portal_instance.close_portal()
print("TIA Portal cerrado.")
except Exception as close_ex:
print(f"Error durante la limpieza de TIA Portal: {close_ex}")
print("\nScript finalizado.")