refactor: Simplify configuration and improve code readability in x4.py

This commit is contained in:
Miguel 2025-08-23 10:52:02 +02:00
parent 5ed4d9391e
commit 5da864abe0
1 changed files with 109 additions and 55 deletions

View File

@ -19,20 +19,19 @@ from backend.script_utils import load_configuration
# --- Configuration --- # --- Configuration ---
# Supported TIA Portal versions mapping (extension -> version) # Supported TIA Portal versions mapping (extension -> version)
SUPPORTED_TIA_VERSIONS = { SUPPORTED_TIA_VERSIONS = {".ap18": "18.0", ".ap19": "19.0", ".ap20": "20.0"}
".ap18": "18.0",
".ap19": "19.0",
".ap20": "20.0"
}
# Filter for cross-references. Based on documentation: # Filter for cross-references. Based on documentation:
# 1: 'AllObjects', 2: 'ObjectsWithReferences', 3: 'ObjectsWithoutReferences', 4: 'UnusedObjects' # 1: 'AllObjects', 2: 'ObjectsWithReferences', 3: 'ObjectsWithoutReferences', 4: 'UnusedObjects'
# Using 1 to export all. 0 might also work as a default in some API versions. # Using 1 to export all. 0 might also work as a default in some API versions.
CROSS_REF_FILTER = 1 CROSS_REF_FILTER = 1
MAX_REOPEN_ATTEMPTS = 5 # Número máximo de re-aperturas permitidas para evitar bucles infinitos 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) BLOCK_TIMEOUT_SECONDS = 120 # Referencia de tiempo esperado para el procesamiento de cada bloque (para logging)
class PortalDisposedException(Exception): class PortalDisposedException(Exception):
"""Excepción lanzada cuando TIA Portal se ha cerrado inesperadamente o un objeto ha sido descartado.""" """Excepción lanzada cuando TIA Portal se ha cerrado inesperadamente o un objeto ha sido descartado."""
@ -53,6 +52,7 @@ def _is_disposed_exception(exc: Exception) -> bool:
) )
) )
# --- TIA Scripting Import Handling --- # --- TIA Scripting Import Handling ---
if os.getenv("TIA_SCRIPTING"): if os.getenv("TIA_SCRIPTING"):
sys.path.append(os.getenv("TIA_SCRIPTING")) sys.path.append(os.getenv("TIA_SCRIPTING"))
@ -82,11 +82,12 @@ except Exception as e:
# --- Functions --- # --- Functions ---
def get_supported_filetypes(): def get_supported_filetypes():
"""Returns the supported file types for TIA Portal projects.""" """Returns the supported file types for TIA Portal projects."""
filetypes = [] filetypes = []
for ext, version in SUPPORTED_TIA_VERSIONS.items(): for ext, version in SUPPORTED_TIA_VERSIONS.items():
version_major = version.split('.')[0] version_major = version.split(".")[0]
filetypes.append((f"TIA Portal V{version_major} Projects", f"*{ext}")) filetypes.append((f"TIA Portal V{version_major} Projects", f"*{ext}"))
# Add option to show all supported files # Add option to show all supported files
@ -95,6 +96,7 @@ def get_supported_filetypes():
return filetypes return filetypes
def detect_tia_version(project_file_path): def detect_tia_version(project_file_path):
"""Detects TIA Portal version based on file extension.""" """Detects TIA Portal version based on file extension."""
file_path = Path(project_file_path) file_path = Path(project_file_path)
@ -102,21 +104,26 @@ def detect_tia_version(project_file_path):
if file_extension in SUPPORTED_TIA_VERSIONS: if file_extension in SUPPORTED_TIA_VERSIONS:
detected_version = SUPPORTED_TIA_VERSIONS[file_extension] detected_version = SUPPORTED_TIA_VERSIONS[file_extension]
print(f"Versión de TIA Portal detectada: {detected_version} (de la extensión {file_extension})") print(
f"Versión de TIA Portal detectada: {detected_version} (de la extensión {file_extension})"
)
return detected_version return detected_version
else: else:
print(f"ADVERTENCIA: Extensión de archivo no reconocida '{file_extension}'. Extensiones soportadas: {list(SUPPORTED_TIA_VERSIONS.keys())}") 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 # Default to version 18.0 for backward compatibility
print("Usando por defecto TIA Portal V18.0") print("Usando por defecto TIA Portal V18.0")
return "18.0" return "18.0"
def select_project_file(): def select_project_file():
"""Opens a dialog to select a TIA Portal project file.""" """Opens a dialog to select a TIA Portal project file."""
root = tk.Tk() root = tk.Tk()
root.withdraw() root.withdraw()
file_path = filedialog.askopenfilename( file_path = filedialog.askopenfilename(
title="Seleccionar archivo de proyecto TIA Portal", title="Seleccionar archivo de proyecto TIA Portal",
filetypes=get_supported_filetypes() filetypes=get_supported_filetypes(),
) )
root.destroy() root.destroy()
if not file_path: if not file_path:
@ -124,11 +131,15 @@ def select_project_file():
sys.exit(0) sys.exit(0)
return file_path return file_path
def _normalize_name(name: str) -> str: def _normalize_name(name: str) -> str:
"""Normaliza un nombre quitando espacios laterales y convirtiendo a minúsculas.""" """Normaliza un nombre quitando espacios laterales y convirtiendo a minúsculas."""
return name.strip().lower() return name.strip().lower()
def _export_block_with_timeout(block, blocks_cr_path, block_name, timeout_seconds=BLOCK_TIMEOUT_SECONDS):
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. Exporta las referencias cruzadas de un bloque con monitoreo de tiempo.
@ -151,7 +162,9 @@ def _export_block_with_timeout(block, blocks_cr_path, block_name, timeout_second
# Verificar si excedió el tiempo esperado (aunque ya terminó) # Verificar si excedió el tiempo esperado (aunque ya terminó)
if elapsed_time > timeout_seconds: if elapsed_time > timeout_seconds:
print(f" ADVERTENCIA: El bloque tardó {elapsed_time:.2f}s (>{timeout_seconds}s esperado)") print(
f" ADVERTENCIA: El bloque tardó {elapsed_time:.2f}s (>{timeout_seconds}s esperado)"
)
return True return True
@ -160,7 +173,10 @@ def _export_block_with_timeout(block, blocks_cr_path, block_name, timeout_second
print(f" Tiempo transcurrido antes del error: {elapsed_time:.2f} segundos") print(f" Tiempo transcurrido antes del error: {elapsed_time:.2f} segundos")
raise e raise e
def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, problematic_blocks=None):
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. """Exports cross-references for various elements from a given PLC.
Parámetros Parámetros
---------- ----------
@ -185,7 +201,9 @@ def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, prob
blocks_cr_exported = 0 blocks_cr_exported = 0
blocks_cr_skipped = 0 blocks_cr_skipped = 0
current_block_name = None # Track current block being processed current_block_name = None # Track current block being processed
print(f"\n[PLC: {plc_name}] Exportando referencias cruzadas de bloques de programa...") print(
f"\n[PLC: {plc_name}] Exportando referencias cruzadas de bloques de programa..."
)
blocks_cr_path = plc_export_dir / "ProgramBlocks_CR" blocks_cr_path = plc_export_dir / "ProgramBlocks_CR"
blocks_cr_path.mkdir(exist_ok=True) blocks_cr_path.mkdir(exist_ok=True)
print(f" Destino: {blocks_cr_path}") print(f" Destino: {blocks_cr_path}")
@ -201,14 +219,18 @@ def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, prob
if _normalize_name(block.get_name()) in problematic_blocks: if _normalize_name(block.get_name()) in problematic_blocks:
skipped_names.append(block.get_name()) skipped_names.append(block.get_name())
if skipped_names: if skipped_names:
print(f" Bloques que serán omitidos (problemáticos previos): {', '.join(skipped_names)}") print(
f" Bloques que serán omitidos (problemáticos previos): {', '.join(skipped_names)}"
)
for block in program_blocks: for block in program_blocks:
block_name = block.get_name() block_name = block.get_name()
current_block_name = block_name # Update current block being processed current_block_name = block_name # Update current block being processed
norm_block = _normalize_name(block_name) norm_block = _normalize_name(block_name)
if norm_block in problematic_blocks: if norm_block in problematic_blocks:
print(f" Omitiendo bloque problemático previamente detectado: {block_name}") print(
f" Omitiendo bloque problemático previamente detectado: {block_name}"
)
blocks_cr_skipped += 1 blocks_cr_skipped += 1
continue continue
if norm_block in exported_blocks: if norm_block in exported_blocks:
@ -251,7 +273,9 @@ def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, prob
" Error de atributo: No se pudo encontrar 'get_program_blocks' en el objeto PLC. Omitiendo bloques de programa." " Error de atributo: No se pudo encontrar 'get_program_blocks' en el objeto PLC. Omitiendo bloques de programa."
) )
except Exception as e: except Exception as e:
print(f" ERROR al acceder a los bloques de programa para exportar referencias cruzadas: {e}") print(
f" ERROR al acceder a los bloques de programa para exportar referencias cruzadas: {e}"
)
traceback.print_exc() traceback.print_exc()
# If we know which block was being processed, mark it as problematic # If we know which block was being processed, mark it as problematic
if current_block_name: if current_block_name:
@ -263,7 +287,9 @@ def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, prob
# --- Export PLC Tag Table Cross-References --- # --- Export PLC Tag Table Cross-References ---
tags_cr_exported = 0 tags_cr_exported = 0
tags_cr_skipped = 0 tags_cr_skipped = 0
print(f"\n[PLC: {plc_name}] Exportando referencias cruzadas de tablas de variables...") print(
f"\n[PLC: {plc_name}] Exportando referencias cruzadas de tablas de variables..."
)
tags_cr_path = plc_export_dir / "PlcTags_CR" tags_cr_path = plc_export_dir / "PlcTags_CR"
tags_cr_path.mkdir(exist_ok=True) tags_cr_path.mkdir(exist_ok=True)
print(f" Destino: {tags_cr_path}") print(f" Destino: {tags_cr_path}")
@ -277,8 +303,7 @@ def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, prob
try: try:
print(f" Exportando referencias cruzadas para {table_name}...") print(f" Exportando referencias cruzadas para {table_name}...")
table.export_cross_references( table.export_cross_references(
target_directorypath=str(tags_cr_path), target_directorypath=str(tags_cr_path), filter=CROSS_REF_FILTER
filter=CROSS_REF_FILTER
) )
tags_cr_exported += 1 tags_cr_exported += 1
except RuntimeError as table_ex: except RuntimeError as table_ex:
@ -300,13 +325,17 @@ def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, prob
" Error de atributo: No se pudo encontrar 'get_plc_tag_tables' en el objeto PLC. Omitiendo tablas de variables." " Error de atributo: No se pudo encontrar 'get_plc_tag_tables' en el objeto PLC. Omitiendo tablas de variables."
) )
except Exception as e: except Exception as e:
print(f" ERROR al acceder a las tablas de variables para exportar referencias cruzadas: {e}") print(
f" ERROR al acceder a las tablas de variables para exportar referencias cruzadas: {e}"
)
traceback.print_exc() traceback.print_exc()
# --- Export PLC Data Type (UDT) Cross-References --- # --- Export PLC Data Type (UDT) Cross-References ---
udts_cr_exported = 0 udts_cr_exported = 0
udts_cr_skipped = 0 udts_cr_skipped = 0
print(f"\n[PLC: {plc_name}] Exportando referencias cruzadas de tipos de datos PLC (UDTs)...") 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 = plc_export_dir / "PlcDataTypes_CR"
udts_cr_path.mkdir(exist_ok=True) udts_cr_path.mkdir(exist_ok=True)
print(f" Destino: {udts_cr_path}") print(f" Destino: {udts_cr_path}")
@ -320,8 +349,7 @@ def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, prob
try: try:
print(f" Exportando referencias cruzadas para {udt_name}...") print(f" Exportando referencias cruzadas para {udt_name}...")
udt.export_cross_references( udt.export_cross_references(
target_directorypath=str(udts_cr_path), target_directorypath=str(udts_cr_path), filter=CROSS_REF_FILTER
filter=CROSS_REF_FILTER
) )
udts_cr_exported += 1 udts_cr_exported += 1
except RuntimeError as udt_ex: except RuntimeError as udt_ex:
@ -349,7 +377,9 @@ def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, prob
# --- Export System Block Cross-References --- # --- Export System Block Cross-References ---
sys_blocks_cr_exported = 0 sys_blocks_cr_exported = 0
sys_blocks_cr_skipped = 0 sys_blocks_cr_skipped = 0
print(f"\n[PLC: {plc_name}] Intentando exportar referencias cruzadas de bloques de sistema...") 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 = plc_export_dir / "SystemBlocks_CR"
sys_blocks_cr_path.mkdir(exist_ok=True) sys_blocks_cr_path.mkdir(exist_ok=True)
print(f" Destino: {sys_blocks_cr_path}") print(f" Destino: {sys_blocks_cr_path}")
@ -357,14 +387,14 @@ def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, prob
try: try:
if hasattr(plc, "get_system_blocks"): if hasattr(plc, "get_system_blocks"):
system_blocks = plc.get_system_blocks() system_blocks = plc.get_system_blocks()
print( print(f" Se encontraron {len(system_blocks)} bloques de sistema.")
f" Se encontraron {len(system_blocks)} bloques de sistema."
)
for sys_block in system_blocks: for sys_block in system_blocks:
sys_block_name = sys_block.get_name() sys_block_name = sys_block.get_name()
print(f" Procesando bloque de sistema: {sys_block_name}...") print(f" Procesando bloque de sistema: {sys_block_name}...")
try: try:
print(f" Exportando referencias cruzadas para {sys_block_name}...") print(
f" Exportando referencias cruzadas para {sys_block_name}..."
)
sys_block.export_cross_references( sys_block.export_cross_references(
target_directorypath=str(sys_blocks_cr_path), target_directorypath=str(sys_blocks_cr_path),
filter=CROSS_REF_FILTER, filter=CROSS_REF_FILTER,
@ -403,7 +433,9 @@ def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, prob
# --- Export Software Unit Cross-References --- # --- Export Software Unit Cross-References ---
sw_units_cr_exported = 0 sw_units_cr_exported = 0
sw_units_cr_skipped = 0 sw_units_cr_skipped = 0
print(f"\n[PLC: {plc_name}] Intentando exportar referencias cruzadas de unidades de software...") 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 = plc_export_dir / "SoftwareUnits_CR"
sw_units_cr_path.mkdir(exist_ok=True) sw_units_cr_path.mkdir(exist_ok=True)
print(f" Destino: {sw_units_cr_path}") print(f" Destino: {sw_units_cr_path}")
@ -453,6 +485,7 @@ def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, prob
print(f"\n--- Finalizado el procesamiento del PLC: {plc_name} ---") print(f"\n--- Finalizado el procesamiento del PLC: {plc_name} ---")
def open_portal_and_project(tia_version: str, project_file_path: str): 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.""" """Abre TIA Portal y el proyecto indicado, devolviendo el portal y el objeto proyecto."""
print(f"\nConectando a TIA Portal V{tia_version}...") print(f"\nConectando a TIA Portal V{tia_version}...")
@ -467,9 +500,12 @@ def open_portal_and_project(tia_version: str, project_file_path: str):
if project_obj is None: if project_obj is None:
project_obj = portal.get_project() project_obj = portal.get_project()
if project_obj is None: if project_obj is None:
raise Exception("No se pudo abrir u obtener el proyecto especificado tras la reapertura.") raise Exception(
"No se pudo abrir u obtener el proyecto especificado tras la reapertura."
)
return portal, project_obj return portal, project_obj
# --- Main Script --- # --- Main Script ---
if __name__ == "__main__": if __name__ == "__main__":
@ -478,7 +514,9 @@ if __name__ == "__main__":
print("--- Exportador de Referencias Cruzadas de TIA Portal ---") print("--- Exportador de Referencias Cruzadas de TIA Portal ---")
print(f"Configuración:") print(f"Configuración:")
print(f" - Tiempo esperado por bloque: {BLOCK_TIMEOUT_SECONDS} segundos (para logging)") print(
f" - Tiempo esperado por bloque: {BLOCK_TIMEOUT_SECONDS} segundos (para logging)"
)
print(f" - Máximo intentos de reapertura: {MAX_REOPEN_ATTEMPTS}") print(f" - Máximo intentos de reapertura: {MAX_REOPEN_ATTEMPTS}")
print(f" - Filtro de referencias cruzadas: {CROSS_REF_FILTER}") print(f" - Filtro de referencias cruzadas: {CROSS_REF_FILTER}")
print("") print("")
@ -486,7 +524,9 @@ if __name__ == "__main__":
# Validate working directory # Validate working directory
if not working_directory or not os.path.isdir(working_directory): if not working_directory or not os.path.isdir(working_directory):
print("ERROR: Directorio de trabajo no configurado o inválido.") print("ERROR: Directorio de trabajo no configurado o inválido.")
print("Por favor configure el directorio de trabajo usando la aplicación principal.") print(
"Por favor configure el directorio de trabajo usando la aplicación principal."
)
sys.exit(1) sys.exit(1)
# 1. Select Project File # 1. Select Project File
@ -502,7 +542,9 @@ if __name__ == "__main__":
print(f"\nProyecto seleccionado: {project_file}") print(f"\nProyecto seleccionado: {project_file}")
print(f"Usando directorio base de exportación: {export_base_dir.resolve()}") print(f"Usando directorio base de exportación: {export_base_dir.resolve()}")
except Exception as e: except Exception as e:
print(f"ERROR: No se pudo crear el directorio de exportación '{export_base_dir}'. Error: {e}") print(
f"ERROR: No se pudo crear el directorio de exportación '{export_base_dir}'. Error: {e}"
)
sys.exit(1) sys.exit(1)
portal_instance = None portal_instance = None
@ -510,7 +552,9 @@ if __name__ == "__main__":
try: try:
# 4. Connect to TIA Portal with detected version # 4. Connect to TIA Portal with detected version
portal_instance, project_object = open_portal_and_project(tia_version, project_file) portal_instance, project_object = open_portal_and_project(
tia_version, project_file
)
# 5. Get PLCs # 5. Get PLCs
plcs = project_object.get_plcs() plcs = project_object.get_plcs()
@ -547,7 +591,9 @@ if __name__ == "__main__":
skipped_blocks_report.append(failed_block) skipped_blocks_report.append(failed_block)
print(f"Marcando bloque problemático: {failed_block}") print(f"Marcando bloque problemático: {failed_block}")
else: else:
print("Error general detectado sin bloque específico identificado") print(
"Error general detectado sin bloque específico identificado"
)
if reopen_attempts > MAX_REOPEN_ATTEMPTS: if reopen_attempts > MAX_REOPEN_ATTEMPTS:
print( print(
@ -563,8 +609,12 @@ if __name__ == "__main__":
pass pass
# Re-abrir portal y proyecto # Re-abrir portal y proyecto
print(f"Re-abriendo TIA Portal (intento {reopen_attempts}/{MAX_REOPEN_ATTEMPTS})...") print(
portal_instance, project_object = open_portal_and_project(tia_version, project_file) 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 # Buscar de nuevo el PLC por nombre
plc_device = None plc_device = None
@ -581,8 +631,12 @@ if __name__ == "__main__":
continue continue
if skipped_blocks_report: if skipped_blocks_report:
print(f"\nBloques problemáticos para el PLC '{plc_name}': {', '.join(set(skipped_blocks_report))}") print(
print(f"Total de bloques problemáticos registrados: {len(problematic_blocks)}") 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.") print("\nProceso de exportación de referencias cruzadas completado.")