Add path validation and sanitization tests

- Implemented `test_path_validation.py` to test filename sanitization, path sanitization, and export path validation functions.
- Added comprehensive test cases for various problematic block names and paths to ensure proper handling of invalid characters and whitespace.
- Created `test_sanitization.py` to specifically address problematic block names with updated sanitization logic, including special cases for "I/O access error" and "Time error interrupt".
- Enhanced filename sanitization to replace specific problematic characters and patterns, ensuring consistent output for known issues.
This commit is contained in:
Miguel 2025-08-23 16:24:58 +02:00
parent 586e3cc9b3
commit 48e25282d6
11 changed files with 522 additions and 29939 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,49 +0,0 @@
--- Log de Ejecución: x3.py ---
Grupo: ObtainIOFromProjectTia
Directorio de Trabajo: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport
Inicio: 2025-05-12 14:24:35
Fin: 2025-05-12 14:24:39
Duración: 0:00:04.165462
Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) ---
--- AML (CAx Export) to Hierarchical JSON and Obsidian MD Converter (v31.1 - Corrected IO Summary Table Initialization) ---
Using Working Directory for Output: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport
Input AML: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.aml
Output Directory: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport
Output JSON: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.hierarchical.json
Output IO Debug Tree MD: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export_IO_Upward_Debug.md
Processing AML file: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.aml
Pass 1: Found 203 InternalElement(s). Populating device dictionary...
Pass 2: Identifying PLCs and Networks (Refined v2)...
Identified Network: PROFIBUS_1 (d645659a-3704-4cd6-b2c8-6165ceeed6ee) Type: Profibus
Identified Network: ETHERNET_1 (f0b1c852-7dc9-4748-888e-34c60b519a75) Type: Ethernet/Profinet
Identified PLC: PLC (a48e038f-0bcc-4b48-8373-033da316c62b) - Type: CPU 1516F-3 PN/DP OrderNo: 6ES7 516-3FP03-0AB0
Pass 3: Processing InternalLinks (Robust Network Mapping & IO)...
Found 116 InternalLink(s).
Mapping Device/Node 'E1' (NodeID:439930b8-1bbc-4cb2-a93b-2eed931f4b12, Addr:10.1.33.11) to Network 'ETHERNET_1'
--> Associating Network 'ETHERNET_1' with PLC 'PLC' (via Node 'E1' Addr: 10.1.33.11)
Mapping Device/Node 'P1' (NodeID:904bb0f7-df2d-4c1d-ab65-f45480449db1, Addr:1) to Network 'PROFIBUS_1'
--> Associating Network 'PROFIBUS_1' with PLC 'PLC' (via Node 'P1' Addr: 1)
Mapping Device/Node 'PB1' (NodeID:2784bae8-9807-475f-89bd-bcf44282f5f4, Addr:12) to Network 'PROFIBUS_1'
Mapping Device/Node 'PB1' (NodeID:e9c5f60a-1da2-4c9b-979e-7d03a5b58a44, Addr:20) to Network 'PROFIBUS_1'
Mapping Device/Node 'PB1' (NodeID:dd7201c2-e127-4a9d-b6ae-7a74a4ffe418, Addr:21) to Network 'PROFIBUS_1'
Mapping Device/Node 'PB1' (NodeID:d8825919-3a6c-4f95-aef0-62c782cfdb51, Addr:22) to Network 'PROFIBUS_1'
Mapping Device/Node 'PB1' (NodeID:27d0e31d-46dc-4fdd-ab82-cfb91899a27c, Addr:10) to Network 'PROFIBUS_1'
Mapping Device/Node 'PB1' (NodeID:d91d5905-aa1a-485e-b4eb-8333cc2133c2, Addr:8) to Network 'PROFIBUS_1'
Mapping Device/Node 'PB1' (NodeID:0c5dfe06-786d-4ab6-b57c-8dfede56c2aa, Addr:40) to Network 'PROFIBUS_1'
Data extraction and structuring complete.
Generating JSON output: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.hierarchical.json
JSON data written successfully.
IO upward debug tree written to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export_IO_Upward_Debug.md
Found 1 PLC(s). Generating individual hardware trees...
Generating Hardware Tree for PLC 'PLC' (ID: a48e038f-0bcc-4b48-8373-033da316c62b) at: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\PLC\Documentation\SAE196_c0.2_CAx_Export_Hardware_Tree.md
Markdown summary (including table) written to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\PLC\Documentation\SAE196_c0.2_CAx_Export_Hardware_Tree.md
Script finished.
--- ERRORES (STDERR) ---
Ninguno
--- FIN DEL LOG ---

View File

@ -1,65 +0,0 @@
--- Log de Ejecución: x4.py ---
Grupo: ObtainIOFromProjectTia
Directorio de Trabajo: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\Source
Inicio: 2025-06-19 19:05:36
Fin: 2025-06-19 19:06:33
Duración: 0:00:57.281042
Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) ---
--- Exportador de Referencias Cruzadas de TIA Portal ---
Versión de TIA Portal detectada: 19.0 (de la extensión .ap19)
Proyecto seleccionado: D:/Trabajo/VM/44 - 98050 - Fiera/InLavoro/PLC/98050_PLC_11/98050_PLC_11.ap19
Usando directorio base de exportación: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\Source
Conectando a TIA Portal V19.0...
2025-06-19 19:05:42,182 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Global OpenPortal - Start TIA Portal, please acknowledge the security dialog.
2025-06-19 19:05:42,202 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Global OpenPortal - With user interface
Conectado a TIA Portal.
2025-06-19 19:05:52,371 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Portal GetProcessId - Process id: 24972
ID del proceso del Portal: 24972
2025-06-19 19:05:52,710 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Portal OpenProject - Open project... D:/Trabajo/VM/44 - 98050 - Fiera/InLavoro/PLC/98050_PLC_11/98050_PLC_11.ap19
Ocurrió un error inesperado: OpennessAccessException: Error when calling method 'OpenWithUpgrade' of type 'Siemens.Engineering.ProjectComposition'.
Unable to open the project under path 'D:\Trabajo\VM\44 - 98050 - Fiera\InLavoro\PLC\98050_PLC_11\98050_PLC_11.ap19'.
An error occurred while opening the project
The project/library D:\Trabajo\VM\44 - 98050 - Fiera\InLavoro\PLC\98050_PLC_11\98050_PLC_11.ap19 cannot be accessed. It has already been opened by user Miguel on computer CSANUC. Note: If the application was not correctly closed, the open projects and libraries can only be opened again after a 2 minute delay.
Script finalizado.
--- ERRORES (STDERR) ---
2025-06-19 19:05:53,136 [1] ERROR Siemens.TiaPortal.OpennessApi19.Implementations.Portal OpenProject -
Siemens.TiaPortal.OpennessContracts.OpennessAccessException: Error when calling method 'OpenWithUpgrade' of type 'Siemens.Engineering.ProjectComposition'.
Unable to open the project under path 'D:\Trabajo\VM\44 - 98050 - Fiera\InLavoro\PLC\98050_PLC_11\98050_PLC_11.ap19'.
An error occurred while opening the project
The project/library D:\Trabajo\VM\44 - 98050 - Fiera\InLavoro\PLC\98050_PLC_11\98050_PLC_11.ap19 cannot be accessed. It has already been opened by user Miguel on computer CSANUC. Note: If the application was not correctly closed, the open projects and libraries can only be opened again after a 2 minute delay.
Traceback (most recent call last):
File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 455, in <module>
portal_instance, project_object = open_portal_and_project(tia_version, project_file)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 413, in open_portal_and_project
project_obj = portal.open_project(project_file_path=str(project_file_path))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: OpennessAccessException: Error when calling method 'OpenWithUpgrade' of type 'Siemens.Engineering.ProjectComposition'.
Unable to open the project under path 'D:\Trabajo\VM\44 - 98050 - Fiera\InLavoro\PLC\98050_PLC_11\98050_PLC_11.ap19'.
An error occurred while opening the project
The project/library D:\Trabajo\VM\44 - 98050 - Fiera\InLavoro\PLC\98050_PLC_11\98050_PLC_11.ap19 cannot be accessed. It has already been opened by user Miguel on computer CSANUC. Note: If the application was not correctly closed, the open projects and libraries can only be opened again after a 2 minute delay.
--- FIN DEL LOG ---

View File

@ -1,81 +0,0 @@
--- Log de Ejecución: xTest.py ---
Grupo: ObtainIOFromProjectTia
Directorio de Trabajo: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\SourceDoc\SourcdSD
Inicio: 2025-05-22 11:17:27
Fin: 2025-05-22 11:18:44
Duración: 0:01:16.758340
Estado: ERROR (Código de Salida: 1)
--- SALIDA ESTÁNDAR (STDOUT) ---
============================================================
PRUEBA DE EXPORTACIÓN SIMATIC SD - TIA PORTAL V20
============================================================
Project: C:/Trabajo/SIDEL/09 - SAE452 - Diet as Regular - San Giovanni in Bosco/Reporte/SourceDoc/Migration/SAE452_V20/SAE452_V20.ap20
Export Directory: C:/Users/migue/Downloads/Nueva carpeta (18)\SIMATIC_SD_Test
Connecting to TIA Portal V20...
2025-05-22 11:17:49,266 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Global OpenPortal - Start TIA Portal, please acknowledge the security dialog.
2025-05-22 11:17:49,283 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Global OpenPortal - With user interface
Connected successfully.
Opening project...
2025-05-22 11:18:05,562 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Portal OpenProject - Open project... C:/Trabajo/SIDEL/09 - SAE452 - Diet as Regular - San Giovanni in Bosco/Reporte/SourceDoc/Migration/SAE452_V20/SAE452_V20.ap20
Project opened successfully.
2025-05-22 11:18:20,088 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Project GetPlcs - Found plc CPU 315F-2 PN/DP with parent name _SSAE0452
Found 1 PLC(s)
Testing with PLC: CPU 315F-2 PN/DP
Found 410 program blocks
--- Testing Block 1/3: ISOonTCP_or_TCP_Protocol ---
Programming Language: STL
Available methods on block:
- export
- export_cross_references
✗ ExportAsDocuments method NOT found
Available methods containing 'export':
- export
- export_cross_references
--- Testing Block 2/3: PIDControl ---
Compiling block...
2025-05-22 11:18:24,970 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.ProgramBlock Compile - Compile the PLC program block PIDControl. Result:
2025-05-22 11:18:31,184 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.ProgramBlock Compile - Warning: CPU 315F-2 PN/DP > General warnings > Inputs or outputs are used that do not exist in the configured hardware.
2025-05-22 11:18:31,185 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.ProgramBlock Compile - Warning: CPU 315F-2 PN/DP > Compiling finished (errors: 0; warnings: 1)
Programming Language: LAD
Available methods on block:
- export
- export_cross_references
✗ ExportAsDocuments method NOT found
Available methods containing 'export':
- export
- export_cross_references
--- Testing Block 3/3: DETAIL_DP_DIAG ---
Programming Language: STL
Available methods on block:
- export
- export_cross_references
✗ ExportAsDocuments method NOT found
Available methods containing 'export':
- export
- export_cross_references
============================================================
PRUEBA COMPLETADA
============================================================
No se crearon archivos en C:/Users/migue/Downloads/Nueva carpeta (18)\SIMATIC_SD_Test
Closing TIA Portal...
2025-05-22 11:18:31,209 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Portal ClosePortal - Close TIA Portal
Press Enter to exit...
--- ERRORES (STDERR) ---
Traceback (most recent call last):
File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\xTest.py", line 215, in <module>
input("\nPress Enter to exit...")
EOFError: EOF when reading a line
--- FIN DEL LOG ---

View File

@ -22,5 +22,11 @@
"short_description": "Test específico para exportación SIMATIC SD usando ExportAsDocuments()", "short_description": "Test específico para exportación SIMATIC SD usando ExportAsDocuments()",
"long_description": "Script de prueba experimental para validar la funcionalidad de exportación SIMATIC SD utilizando el método ExportAsDocuments() de la API de TIA Portal Openness.\n***\n**Propósito:**\n\n1. **Validación de API:** Prueba diferentes métodos de exportación SIMATIC SD\n2. **Comparación de métodos:** Evalúa ExportAsDocuments() vs Export() estándar\n3. **Debugging:** Identifica problemas y limitaciones en la exportación SD\n4. **Desarrollo:** Base para mejoras en scripts de producción\n\n**Estado:** Script experimental - usar solo para pruebas y desarrollo\n\n**Nota:** Este script es parte del proceso de desarrollo y optimización de los métodos de exportación SIMATIC SD.", "long_description": "Script de prueba experimental para validar la funcionalidad de exportación SIMATIC SD utilizando el método ExportAsDocuments() de la API de TIA Portal Openness.\n***\n**Propósito:**\n\n1. **Validación de API:** Prueba diferentes métodos de exportación SIMATIC SD\n2. **Comparación de métodos:** Evalúa ExportAsDocuments() vs Export() estándar\n3. **Debugging:** Identifica problemas y limitaciones en la exportación SD\n4. **Desarrollo:** Base para mejoras en scripts de producción\n\n**Estado:** Script experimental - usar solo para pruebas y desarrollo\n\n**Nota:** Este script es parte del proceso de desarrollo y optimización de los métodos de exportación SIMATIC SD.",
"hidden": false "hidden": false
},
"test_simatic_sd_compatibility.py": {
"display_name": "test_simatic_sd_compatibility",
"short_description": "Test script to verify SIMATIC SD compatibility detection",
"long_description": "",
"hidden": false
} }
} }

View File

@ -0,0 +1,103 @@
"""
Test script for path validation and sanitization functions
"""
import os
import re
def sanitize_filename(name):
"""Sanitizes a filename by removing/replacing invalid characters and whitespace."""
# Replace spaces and other problematic characters with underscores
sanitized = re.sub(r'[<>:"/\\|?*\s]+', "_", name)
# Remove leading/trailing underscores and dots
sanitized = sanitized.strip("_.")
# Ensure it's not empty
if not sanitized:
sanitized = "unknown"
return sanitized
def sanitize_path(path):
"""Sanitizes a path by ensuring it doesn't contain problematic whitespace."""
# Normalize the path and remove any trailing/leading whitespace
normalized = os.path.normpath(path.strip())
return normalized
def validate_export_path(path):
"""Validates that an export path is suitable for TIA Portal."""
if not path:
return False, "La ruta está vacía"
# Check for problematic characters or patterns
if any(char in path for char in '<>"|?*'):
return False, f"La ruta contiene caracteres no válidos: {path}"
# Check for excessive whitespace
if path != path.strip():
return False, f"La ruta contiene espacios al inicio o final: '{path}'"
# Check for multiple consecutive spaces
if " " in path:
return False, f"La ruta contiene espacios múltiples consecutivos: '{path}'"
# Check path length (Windows limitation)
if len(path) > 250:
return False, f"La ruta es demasiado larga ({len(path)} caracteres): {path}"
return True, "OK"
# Test cases
test_names = [
"IO access error",
"CreatesAnyPointer",
"WriteMemArea_G",
"Block with spaces",
"Block<>with:invalid|chars",
" Block with leading and trailing spaces ",
"Block with multiple spaces",
"",
]
test_paths = [
"C:\\normal\\path",
"C:\\path with spaces\\subdir",
" C:\\path with leading space",
"C:\\path with trailing space ",
"C:\\path with multiple spaces\\subdir",
"C:\\path<with>invalid:chars|in\\subdir",
"",
]
print("=== Testing sanitize_filename ===")
for name in test_names:
sanitized = sanitize_filename(name)
print(f"'{name}' -> '{sanitized}'")
print("\n=== Testing sanitize_path ===")
for path in test_paths:
sanitized = sanitize_path(path)
print(f"'{path}' -> '{sanitized}'")
print("\n=== Testing validate_export_path ===")
for path in test_paths:
sanitized = sanitize_path(path)
is_valid, msg = validate_export_path(sanitized)
print(f"'{sanitized}' -> Valid: {is_valid}, Message: {msg}")
print("\n=== Test specific problematic block names ===")
problematic_blocks = ["IO access error", "CreatesAnyPointer", "WriteMemArea_G"]
base_path = "D:\\Export\\Test"
for block_name in problematic_blocks:
sanitized_name = sanitize_filename(block_name)
full_path = sanitize_path(
os.path.join(base_path, sanitized_name, "ProgramBlocks_XML")
)
is_valid, msg = validate_export_path(full_path)
print(f"Block: '{block_name}' -> '{sanitized_name}'")
print(f" Full path: '{full_path}'")
print(f" Valid: {is_valid}, Message: {msg}")
print()

View File

@ -0,0 +1,49 @@
"""
Test script for updated sanitization function
"""
import re
def sanitize_filename(name):
"""Sanitizes a filename by removing/replacing invalid characters and whitespace."""
# Handle specific problematic cases first
if name == "I/O access error":
return "IO_access_error"
elif name == "Time error interrupt":
return "Time_error_interrupt"
elif name.startswith("I/O_"):
return name.replace("I/O_", "IO_").replace("/", "_")
# Replace spaces and other problematic characters with underscores
sanitized = re.sub(r'[<>:"/\\|?*\s]+', "_", name)
# Remove leading/trailing underscores and dots
sanitized = sanitized.strip("_.")
# Ensure it's not empty
if not sanitized:
sanitized = "unknown"
return sanitized
# Test the problematic block names from the log
problematic_blocks = [
"IO access error",
"Time error interrupt",
"I/O_FLT1",
"I/O_FLT2",
"CreatesAnyPointer",
"WriteMemArea_G",
"ComGetPut_G",
"Sys_Plc_G",
"Sys_PLC_D",
"RACK_FLT",
"Startup",
"PROG_ERR",
]
print("=== Testing updated sanitize_filename function ===")
for block_name in problematic_blocks:
sanitized = sanitize_filename(block_name)
has_spaces = " " in block_name
has_slash = "/" in block_name
print(f"'{block_name}' -> '{sanitized}' [Spaces: {has_spaces}, Slash: {has_slash}]")

View File

@ -1,70 +0,0 @@
"""
Test script to verify SIMATIC SD compatibility detection
"""
import os
import sys
# --- TIA Scripting Import Handling ---
if os.getenv("TIA_SCRIPTING"):
sys.path.append(os.getenv("TIA_SCRIPTING"))
try:
import siemens_tia_scripting as ts
print("✓ TIA Scripting import successful")
print(
f"Available programming languages: {[lang for lang in dir(ts.Enums.ProgrammingLanguage) if not lang.startswith('_')]}"
)
print(
f"Available export formats: {[fmt for fmt in dir(ts.Enums.ExportFormats) if not fmt.startswith('_')]}"
)
print(
f"Available block types: {[bt for bt in dir(ts.Enums.BlockType) if not bt.startswith('_')]}"
)
# Check if SIMATIC SD is available
try:
simatic_sd_format = ts.Enums.ExportFormats.SimaticSD
print(f"✓ SIMATIC SD format available: {simatic_sd_format}")
except AttributeError:
print("✗ SIMATIC SD format NOT available in this TIA Scripting version")
except ImportError as e:
print(f"✗ Failed to import TIA Scripting: {e}")
print(
"This is expected if TIA Portal is not installed or TIA_SCRIPTING env var not set"
)
def analyze_simatic_sd_requirements():
"""Analyze and display SIMATIC SD requirements"""
print("\n=== SIMATIC SD FORMAT REQUIREMENTS ===")
print("Based on official Siemens documentation:")
print()
print("✓ SUPPORTED:")
print(" • Programming Language: LAD (Ladder) ONLY")
print(
" • Block Types: FB (Function Block), FC (Function), OB (Organization Block)"
)
print(" • TIA Portal Version: V20 or later")
print(" • Target PLCs: S7-1200, S7-1500")
print()
print("✗ NOT SUPPORTED:")
print(" • SCL (Structured Control Language)")
print(" • STL (Statement List)")
print(" • FBD (Function Block Diagram)")
print(" • Graph programming")
print(" • CFC (Continuous Function Chart)")
print(" • Complex LAD elements (some advanced functions)")
print()
print("📋 COMMON CAUSES FOR XML-ONLY EXPORT:")
print(" 1. Block programmed in SCL/STL/FBD instead of LAD")
print(" 2. Block contains unsupported LAD elements")
print(" 3. Block is not compiled/consistent")
print(" 4. TIA Portal version < V20")
print(" 5. Wrong block type (not FB/FC/OB)")
if __name__ == "__main__":
analyze_simatic_sd_requirements()

View File

@ -7,6 +7,8 @@ from tkinter import filedialog
import os import os
import sys import sys
import traceback import traceback
import shutil
import tempfile
from pathlib import Path # Import Path from pathlib import Path # Import Path
script_root = os.path.dirname( script_root = os.path.dirname(
@ -17,14 +19,12 @@ 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"
}
EXPORT_OPTIONS = None # Use default export options EXPORT_OPTIONS = None # Use default export options
KEEP_FOLDER_STRUCTURE = True # Replicate TIA project folder structure in export directory KEEP_FOLDER_STRUCTURE = (
True # Replicate TIA project folder structure in export directory
)
# --- TIA Scripting Import Handling --- # --- TIA Scripting Import Handling ---
# Check if the TIA_SCRIPTING environment variable is set # Check if the TIA_SCRIPTING environment variable is set
@ -46,7 +46,7 @@ try:
except ImportError: except ImportError:
print("ERROR: Failed to import 'siemens_tia_scripting'.") print("ERROR: Failed to import 'siemens_tia_scripting'.")
print("Ensure:") print("Ensure:")
print(f"1. TIA Portal Openness for V{TIA_PORTAL_VERSION} is installed.") print("1. TIA Portal Openness is installed.")
print( print(
"2. The 'siemens_tia_scripting' Python module is installed (pip install ...) or" "2. The 'siemens_tia_scripting' Python module is installed (pip install ...) or"
) )
@ -64,11 +64,121 @@ except Exception as e:
# --- Functions --- # --- Functions ---
def sanitize_filename(name):
"""Sanitizes a filename by removing/replacing invalid characters and whitespace."""
import re
# Handle specific problematic cases first
if name == "I/O access error":
return "IO_access_error"
elif name == "Time error interrupt":
return "Time_error_interrupt"
elif name.startswith("I/O_"):
return name.replace("I/O_", "IO_").replace("/", "_")
# Replace spaces and other problematic characters with underscores
sanitized = re.sub(r'[<>:"/\\|?*\s]+', "_", name)
# Remove leading/trailing underscores and dots
sanitized = sanitized.strip("_.")
# Ensure it's not empty
if not sanitized:
sanitized = "unknown"
return sanitized
def sanitize_path(path):
"""Sanitizes a path by ensuring it doesn't contain problematic whitespace."""
# Normalize the path and remove any trailing/leading whitespace
normalized = os.path.normpath(path.strip())
return normalized
def validate_export_path(path):
"""Validates that an export path is suitable for TIA Portal."""
if not path:
return False, "La ruta está vacía"
# Check for problematic characters or patterns
if any(char in path for char in '<>"|?*'):
return False, f"La ruta contiene caracteres no válidos: {path}"
# Check for excessive whitespace
if path != path.strip():
return False, f"La ruta contiene espacios al inicio o final: '{path}'"
# Check for multiple consecutive spaces
if " " in path:
return False, f"La ruta contiene espacios múltiples consecutivos: '{path}'"
# Check path length (Windows limitation)
if len(path) > 250:
return False, f"La ruta es demasiado larga ({len(path)} caracteres): {path}"
return True, "OK"
def create_temp_export_dir():
"""Creates a temporary directory for export that doesn't contain spaces."""
# Create a temporary directory with a safe name
temp_base = tempfile.gettempdir()
temp_export = os.path.join(temp_base, "TIA_Export_Temp")
# Ensure the temp directory exists and is clean
if os.path.exists(temp_export):
shutil.rmtree(temp_export)
os.makedirs(temp_export, exist_ok=True)
return temp_export
def copy_temp_to_final(temp_dir, final_dir):
"""Copies files from temporary directory to final destination."""
try:
print(f"\nCopiando archivos exportados desde directorio temporal...")
print(f" Origen: {temp_dir}")
print(f" Destino: {final_dir}")
# Ensure final directory exists
os.makedirs(final_dir, exist_ok=True)
# Copy all contents from temp to final directory
for item in os.listdir(temp_dir):
src_path = os.path.join(temp_dir, item)
dst_path = os.path.join(final_dir, item)
if os.path.isdir(src_path):
if os.path.exists(dst_path):
shutil.rmtree(dst_path)
shutil.copytree(src_path, dst_path)
print(f" Directorio copiado: {item}")
else:
shutil.copy2(src_path, dst_path)
print(f" Archivo copiado: {item}")
print(" Copia completada exitosamente.")
return True
except Exception as e:
print(f" ERROR durante la copia: {e}")
return False
def cleanup_temp_dir(temp_dir):
"""Cleans up the temporary directory."""
try:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
print(f"Directorio temporal limpiado: {temp_dir}")
except Exception as e:
print(f"ADVERTENCIA: No se pudo limpiar el directorio temporal {temp_dir}: {e}")
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
@ -77,6 +187,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)
@ -84,21 +195,25 @@ 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() # Hide the main tkinter window root.withdraw() # Hide the main tkinter window
file_path = filedialog.askopenfilename( file_path = filedialog.askopenfilename(
title="Select TIA Portal Project File", title="Select TIA Portal Project File", filetypes=get_supported_filetypes()
filetypes=get_supported_filetypes()
) )
root.destroy() root.destroy()
if not file_path: if not file_path:
@ -122,18 +237,40 @@ def select_export_directory():
def export_plc_data(plc, export_base_dir): def export_plc_data(plc, export_base_dir):
"""Exports Blocks, UDTs, and Tag Tables from a given PLC.""" """Exports Blocks, UDTs, and Tag Tables from a given PLC."""
plc_name = plc.get_name() plc_name = plc.get_name()
plc_name_sanitized = sanitize_filename(plc_name)
print(f"\n--- Procesando PLC: {plc_name} ---") print(f"\n--- Procesando PLC: {plc_name} ---")
if plc_name != plc_name_sanitized:
print(f" Nombre sanitizado para directorios: {plc_name_sanitized}")
# Define base export path for this PLC # Define base export path for this PLC
plc_export_dir = os.path.join(export_base_dir, plc_name) plc_export_dir = sanitize_path(os.path.join(export_base_dir, plc_name_sanitized))
# Validate PLC export directory
is_valid, validation_msg = validate_export_path(plc_export_dir)
if not is_valid:
print(f"ERROR: Directorio de exportación del PLC no válido - {validation_msg}")
return
os.makedirs(plc_export_dir, exist_ok=True) os.makedirs(plc_export_dir, exist_ok=True)
# --- Export Program Blocks --- # --- Export Program Blocks ---
blocks_exported = 0 blocks_exported = 0
blocks_skipped = 0 blocks_skipped = 0
print(f"\n[PLC: {plc_name}] Exportando bloques de programa...") print(f"\n[PLC: {plc_name}] Exportando bloques de programa...")
xml_blocks_path = os.path.join(plc_export_dir, "ProgramBlocks_XML") xml_blocks_path = sanitize_path(os.path.join(plc_export_dir, "ProgramBlocks_XML"))
scl_blocks_path = os.path.join(plc_export_dir, "ProgramBlocks_SCL") scl_blocks_path = sanitize_path(os.path.join(plc_export_dir, "ProgramBlocks_SCL"))
# Validate block export paths
xml_valid, xml_msg = validate_export_path(xml_blocks_path)
scl_valid, scl_msg = validate_export_path(scl_blocks_path)
if not xml_valid:
print(f" ERROR: Ruta XML no válida - {xml_msg}")
return
if not scl_valid:
print(f" ERROR: Ruta SCL no válida - {scl_msg}")
return
os.makedirs(xml_blocks_path, exist_ok=True) os.makedirs(xml_blocks_path, exist_ok=True)
os.makedirs(scl_blocks_path, exist_ok=True) os.makedirs(scl_blocks_path, exist_ok=True)
print(f" Destino XML: {xml_blocks_path}") print(f" Destino XML: {xml_blocks_path}")
@ -157,6 +294,63 @@ def export_plc_data(plc, export_base_dir):
continue continue
print(f" Exportando {block_name} como XML...") print(f" Exportando {block_name} como XML...")
try:
print(f" Destino: {xml_blocks_path}")
# Check if this is a system block that might need special handling
is_system_block = any(
keyword in block_name.lower()
for keyword in [
"interrupt",
"error",
"startup",
"i/o",
"rack_flt",
"prog_err",
"time error",
"io access",
"createsan",
]
)
# Try creating a sanitized filename for problematic blocks
if is_system_block or " " in block_name or "/" in block_name:
print(
f" Detectado bloque con nombre problemático: '{block_name}'"
)
# Create a temporary export directory with sanitized name
sanitized_block_name = sanitize_filename(block_name)
temp_block_dir = os.path.join(
xml_blocks_path, sanitized_block_name
)
os.makedirs(temp_block_dir, exist_ok=True)
print(f" Usando directorio sanitizado: {temp_block_dir}")
block.export(
target_directory_path=temp_block_dir,
export_options=EXPORT_OPTIONS,
export_format=ts.Enums.ExportFormats.SimaticML,
keep_folder_structure=False, # Disable folder structure for problematic blocks
)
# Rename files to use original block name in metadata
for file in os.listdir(temp_block_dir):
if file.endswith(".xml"):
original_path = os.path.join(temp_block_dir, file)
# Move file to main directory with original name preserved in content
target_path = os.path.join(xml_blocks_path, file)
if os.path.exists(target_path):
os.remove(target_path)
shutil.move(original_path, target_path)
# Remove temporary directory
if os.path.exists(temp_block_dir):
os.rmdir(temp_block_dir)
else:
# Normal export for regular blocks
block.export( block.export(
target_directory_path=xml_blocks_path, target_directory_path=xml_blocks_path,
export_options=EXPORT_OPTIONS, export_options=EXPORT_OPTIONS,
@ -164,16 +358,88 @@ def export_plc_data(plc, export_base_dir):
keep_folder_structure=KEEP_FOLDER_STRUCTURE, keep_folder_structure=KEEP_FOLDER_STRUCTURE,
) )
except Exception as xml_ex:
print(
f" ERROR en exportación XML para {block_name}: {xml_ex}"
)
print(f" Ruta problemática: '{xml_blocks_path}'")
print(f" Tipo de bloque: {type(block).__name__}")
# Skip this block and continue with others
blocks_skipped += 1
continue
# If we get here, XML export was successful
# Now try SCL export if applicable
try: try:
prog_language = block.get_property(name="ProgrammingLanguage") prog_language = block.get_property(name="ProgrammingLanguage")
if prog_language == "SCL": if prog_language == "SCL":
print(f" Exportando {block_name} como SCL...") print(f" Exportando {block_name} como SCL...")
try:
print(f" Destino: {scl_blocks_path}")
# Use same logic for SCL export
is_system_block = any(
keyword in block_name.lower()
for keyword in [
"interrupt",
"error",
"startup",
"i/o",
"rack_flt",
"prog_err",
"time error",
"io access",
"createsan",
]
)
if (
is_system_block
or " " in block_name
or "/" in block_name
):
sanitized_block_name = sanitize_filename(block_name)
temp_block_dir = os.path.join(
scl_blocks_path, sanitized_block_name
)
os.makedirs(temp_block_dir, exist_ok=True)
block.export(
target_directory_path=temp_block_dir,
export_options=EXPORT_OPTIONS,
export_format=ts.Enums.ExportFormats.ExternalSource,
keep_folder_structure=False,
)
# Move files to main directory
for file in os.listdir(temp_block_dir):
if file.endswith(".scl"):
original_path = os.path.join(
temp_block_dir, file
)
target_path = os.path.join(
scl_blocks_path, file
)
if os.path.exists(target_path):
os.remove(target_path)
shutil.move(original_path, target_path)
if os.path.exists(temp_block_dir):
os.rmdir(temp_block_dir)
else:
block.export( block.export(
target_directory_path=scl_blocks_path, target_directory_path=scl_blocks_path,
export_options=EXPORT_OPTIONS, export_options=EXPORT_OPTIONS,
export_format=ts.Enums.ExportFormats.ExternalSource, export_format=ts.Enums.ExportFormats.ExternalSource,
keep_folder_structure=KEEP_FOLDER_STRUCTURE, keep_folder_structure=KEEP_FOLDER_STRUCTURE,
) )
except Exception as scl_ex:
print(
f" ERROR en exportación SCL para {block_name}: {scl_ex}"
)
print(f" Ruta problemática: '{scl_blocks_path}'")
# Don't raise, just continue
except Exception as prop_ex: except Exception as prop_ex:
print( print(
f" No se pudo obtener el lenguaje de programación para {block_name}. Omitiendo SCL. Error: {prop_ex}" f" No se pudo obtener el lenguaje de programación para {block_name}. Omitiendo SCL. Error: {prop_ex}"
@ -194,7 +460,14 @@ def export_plc_data(plc, export_base_dir):
udts_exported = 0 udts_exported = 0
udts_skipped = 0 udts_skipped = 0
print(f"\n[PLC: {plc_name}] Exportando tipos de datos PLC (UDTs)...") print(f"\n[PLC: {plc_name}] Exportando tipos de datos PLC (UDTs)...")
udt_export_path = os.path.join(plc_export_dir, "PlcDataTypes") udt_export_path = sanitize_path(os.path.join(plc_export_dir, "PlcDataTypes"))
# Validate UDT export path
udt_valid, udt_msg = validate_export_path(udt_export_path)
if not udt_valid:
print(f" ERROR: Ruta UDT no válida - {udt_msg}")
return
os.makedirs(udt_export_path, exist_ok=True) os.makedirs(udt_export_path, exist_ok=True)
print(f" Destino: {udt_export_path}") print(f" Destino: {udt_export_path}")
@ -216,11 +489,19 @@ def export_plc_data(plc, export_base_dir):
continue continue
print(f" Exportando {udt_name}...") print(f" Exportando {udt_name}...")
try:
print(f" Destino: {udt_export_path}")
udt.export( udt.export(
target_directory_path=udt_export_path, target_directory_path=udt_export_path,
export_options=EXPORT_OPTIONS, export_options=EXPORT_OPTIONS,
keep_folder_structure=KEEP_FOLDER_STRUCTURE, keep_folder_structure=KEEP_FOLDER_STRUCTURE,
) )
except Exception as udt_export_ex:
print(
f" ERROR en exportación UDT para {udt_name}: {udt_export_ex}"
)
print(f" Ruta problemática: '{udt_export_path}'")
raise udt_export_ex
udts_exported += 1 udts_exported += 1
except Exception as udt_ex: except Exception as udt_ex:
print(f" ERROR exportando UDT {udt_name}: {udt_ex}") print(f" ERROR exportando UDT {udt_name}: {udt_ex}")
@ -236,7 +517,14 @@ def export_plc_data(plc, export_base_dir):
tags_exported = 0 tags_exported = 0
tags_skipped = 0 tags_skipped = 0
print(f"\n[PLC: {plc_name}] Exportando tablas de variables PLC...") print(f"\n[PLC: {plc_name}] Exportando tablas de variables PLC...")
tags_export_path = os.path.join(plc_export_dir, "PlcTags") tags_export_path = sanitize_path(os.path.join(plc_export_dir, "PlcTags"))
# Validate tags export path
tags_valid, tags_msg = validate_export_path(tags_export_path)
if not tags_valid:
print(f" ERROR: Ruta Tags no válida - {tags_msg}")
return
os.makedirs(tags_export_path, exist_ok=True) os.makedirs(tags_export_path, exist_ok=True)
print(f" Destino: {tags_export_path}") print(f" Destino: {tags_export_path}")
@ -248,14 +536,24 @@ def export_plc_data(plc, export_base_dir):
print(f" Procesando tabla de variables: {table_name}...") print(f" Procesando tabla de variables: {table_name}...")
try: try:
print(f" Exportando {table_name}...") print(f" Exportando {table_name}...")
try:
print(f" Destino: {tags_export_path}")
table.export( table.export(
target_directory_path=tags_export_path, target_directory_path=tags_export_path,
export_options=EXPORT_OPTIONS, export_options=EXPORT_OPTIONS,
keep_folder_structure=KEEP_FOLDER_STRUCTURE, keep_folder_structure=KEEP_FOLDER_STRUCTURE,
) )
except Exception as table_export_ex:
print(
f" ERROR en exportación tabla para {table_name}: {table_export_ex}"
)
print(f" Ruta problemática: '{tags_export_path}'")
raise table_export_ex
tags_exported += 1 tags_exported += 1
except Exception as table_ex: except Exception as table_ex:
print(f" ERROR exportando tabla de variables {table_name}: {table_ex}") print(
f" ERROR exportando tabla de variables {table_name}: {table_ex}"
)
tags_skipped += 1 tags_skipped += 1
print( print(
f" Resumen de exportación de tablas de variables: Exportados={tags_exported}, Omitidos/Errores={tags_skipped}" f" Resumen de exportación de tablas de variables: Exportados={tags_exported}, Omitidos/Errores={tags_skipped}"
@ -279,12 +577,22 @@ 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, Export Directory comes from config # 1. Select Project File, Export Directory comes from config
project_file = select_project_file() project_file = select_project_file()
export_dir = working_directory # Use working directory from config export_dir = sanitize_path(
working_directory
) # Use working directory from config with sanitization
# Validate export directory
is_valid, validation_msg = validate_export_path(export_dir)
if not is_valid:
print(f"ERROR: Directorio de exportación no válido - {validation_msg}")
sys.exit(1)
# 2. Detect TIA Portal version from project file # 2. Detect TIA Portal version from project file
tia_version = detect_tia_version(project_file) tia_version = detect_tia_version(project_file)
@ -309,7 +617,9 @@ if __name__ == "__main__":
print(f"Abriendo proyecto: {os.path.basename(project_file)}...") print(f"Abriendo proyecto: {os.path.basename(project_file)}...")
project_object = portal_instance.open_project(project_file_path=project_file) project_object = portal_instance.open_project(project_file_path=project_file)
if project_object is None: if project_object is None:
print("El proyecto podría estar ya abierto, intentando obtener el manejador...") print(
"El proyecto podría estar ya abierto, intentando obtener el manejador..."
)
project_object = portal_instance.get_project() project_object = portal_instance.get_project()
if project_object is None: if project_object is None:
raise Exception("No se pudo abrir u obtener el proyecto especificado.") raise Exception("No se pudo abrir u obtener el proyecto especificado.")
@ -320,7 +630,9 @@ if __name__ == "__main__":
if not plcs: if not plcs:
print("No se encontraron dispositivos PLC en el proyecto.") print("No se encontraron dispositivos PLC en el proyecto.")
else: else:
print(f"Se encontraron {len(plcs)} PLC(s). Iniciando proceso de exportación...") print(
f"Se encontraron {len(plcs)} PLC(s). Iniciando proceso de exportación..."
)
# 6. Iterate and Export Data for each PLC # 6. Iterate and Export Data for each PLC
for plc_device in plcs: for plc_device in plcs:

23786
data/log.txt

File diff suppressed because it is too large Load Diff