From 24a0ece0b44bec37b5a275136b86d1758c243d60 Mon Sep 17 00:00:00 2001 From: Miguel Date: Sun, 24 Aug 2025 21:35:21 +0200 Subject: [PATCH] feat: Implement SHA256 hash detection for XML changes to enhance file processing accuracy and efficiency. Update relevant scripts to calculate and store file hashes, improving change detection beyond traditional methods. --- .../XML Parser to SCL/HASH_DETECTION.md | 135 ++++++++++++ .../scripts_description.json | 12 ++ .../XML Parser to SCL/test_hash_detection.py | 171 +++++++++++++++ .../XML Parser to SCL/test_hash_edge_cases.py | 204 ++++++++++++++++++ .../XML Parser to SCL/x0_main.py | 57 ++++- .../XML Parser to SCL/x1_to_json.py | 23 +- .../XML Parser to SCL/x2_process.py | 32 ++- lib/script_executor.py | 3 +- 8 files changed, 627 insertions(+), 10 deletions(-) create mode 100644 backend/script_groups/XML Parser to SCL/HASH_DETECTION.md create mode 100644 backend/script_groups/XML Parser to SCL/test_hash_detection.py create mode 100644 backend/script_groups/XML Parser to SCL/test_hash_edge_cases.py diff --git a/backend/script_groups/XML Parser to SCL/HASH_DETECTION.md b/backend/script_groups/XML Parser to SCL/HASH_DETECTION.md new file mode 100644 index 0000000..efe53a0 --- /dev/null +++ b/backend/script_groups/XML Parser to SCL/HASH_DETECTION.md @@ -0,0 +1,135 @@ +# Detección de Cambios por Hash SHA256 + +## ¿Qué es y por qué es importante? + +El sistema ahora utiliza **hash SHA256** para detectar cambios en archivos XML, complementando el método tradicional basado en tiempo de modificación y tamaño de archivo. + +## Problema resuelto + +### Antes (método tradicional) +- **Solo verificaba**: tiempo de modificación + tamaño de archivo +- **Problema**: Si un archivo XML cambia pero mantiene el mismo tamaño y tiempo, el sistema NO regeneraba los archivos JSON/SCL +- **Resultado**: Cambios en el XML no se reflejaban en el código SCL generado + +### Ahora (método con hash) +- **Verifica**: hash SHA256 del contenido completo del archivo +- **Ventaja**: Detecta CUALQUIER cambio en el contenido, sin importar tamaño o tiempo +- **Resultado**: Regeneración precisa solo cuando el contenido realmente cambió + +## Casos donde el hash es crucial + +### Caso 1: Cambios sutiles +```xml + +MotorControl_01 + + +MotorControl_02 +``` +- Mismo tamaño de archivo +- Posiblemente mismo tiempo (si se restaura) +- **Método tradicional**: NO detecta cambio ❌ +- **Método hash**: SÍ detecta cambio ✅ + +### Caso 2: Reemplazo de contenido +```xml + +Test AAAAAA + + +Test BBBBBB +``` +- Exactamente el mismo tamaño +- **Método tradicional**: NO detecta cambio ❌ +- **Método hash**: SÍ detecta cambio ✅ + +## Implementación + +### Archivos modificados +1. **`x0_main.py`**: + - Añadida función `calculate_file_hash()` + - Modificada función `check_skip_status()` para usar hash como prioridad + - Logging mejorado para mostrar qué método de detección se usa + +2. **`x1_to_json.py`**: + - Añadida función `calculate_file_hash()` + - Modificada función `convert_xml_to_json()` para calcular y guardar hash + - Nuevo campo `source_xml_hash` en archivos JSON + +3. **`x2_process.py`**: + - Modificado para preservar el campo `source_xml_hash` en JSONs procesados + +### Nuevo campo en JSON +Los archivos JSON ahora incluyen: +```json +{ + "block_name": "FC Pack Motor 71", + "source_xml_mod_time": 1755944794.1680913, + "source_xml_size": 32969, + "source_xml_hash": "a7f5d2e8c4b6a9f3..." // <-- NUEVO +} +``` + +## Comportamiento del sistema + +### Prioridad de verificación +1. **Primera prioridad**: Comparación por hash SHA256 + - Si el hash coincide → saltar procesamiento + - Si el hash difiere → regenerar archivos + +2. **Fallback**: Método tradicional (tiempo + tamaño) + - Se usa si no hay hash almacenado en el JSON + - Mantiene compatibilidad con archivos JSON existentes + +### Logging mejorado +El sistema ahora muestra claramente qué método usa: +``` +✓ Hash coincide para FC_Motor_71.xml - Saltando x1/x2 +✗ Hash diferente para FC_Motor_72.xml - Regenerando +✓ Tiempo/tamaño coinciden para FC_Motor_73.xml - Saltando x1/x2 (método legacy) +``` + +## Beneficios + +### Para el usuario +- **Precisión**: No se pierden cambios por limitaciones del método tradicional +- **Eficiencia**: No regenera archivos que realmente no cambiaron +- **Confiabilidad**: Detecta modificaciones sutiles pero importantes + +### Para el desarrollo +- **Compatibilidad**: Los archivos JSON existentes siguen funcionando +- **Transparencia**: Logs claros sobre qué se procesa y por qué +- **Robustez**: Fallback automático al método tradicional si es necesario + +## Casos de uso típicos + +### Exportación desde TIA Portal +Cuando exportas un proyecto de TIA Portal varias veces: +1. **Primera exportación**: Se generan todos los JSON/SCL +2. **Segunda exportación** (sin cambios): Se saltan todos los archivos (hash coincide) +3. **Tercera exportación** (con cambios menores): Solo se regeneran los archivos modificados + +### Modificaciones manuales +Si modificas manualmente un XML: +- El hash cambia instantáneamente +- El sistema regenera automáticamente los archivos derivados +- No importa si el tamaño o tiempo parecen iguales + +## Testing + +Se incluyen scripts de prueba que demuestran la funcionalidad: +- `test_hash_detection.py`: Demostración básica +- `test_hash_edge_cases.py`: Casos donde el hash es superior al método tradicional + +Para ejecutar: +```bash +python test_hash_detection.py +python test_hash_edge_cases.py +``` + +## Migración automática + +Los archivos JSON existentes seguirán funcionando: +- Sin campo `source_xml_hash`: usa método tradicional +- Con campo `source_xml_hash`: usa método de hash (más preciso) +- La próxima regeneración añadirá automáticamente el hash diff --git a/backend/script_groups/XML Parser to SCL/scripts_description.json b/backend/script_groups/XML Parser to SCL/scripts_description.json index 5c256f5..e184351 100644 --- a/backend/script_groups/XML Parser to SCL/scripts_description.json +++ b/backend/script_groups/XML Parser to SCL/scripts_description.json @@ -70,5 +70,17 @@ "short_description": "Sin descripción corta.", "long_description": "", "hidden": false + }, + "test_hash_detection.py": { + "display_name": "test_hash_detection", + "short_description": "Sin descripción corta.", + "long_description": "", + "hidden": false + }, + "test_hash_edge_cases.py": { + "display_name": "test_hash_edge_cases", + "short_description": "Sin descripción corta.", + "long_description": "", + "hidden": false } } \ No newline at end of file diff --git a/backend/script_groups/XML Parser to SCL/test_hash_detection.py b/backend/script_groups/XML Parser to SCL/test_hash_detection.py new file mode 100644 index 0000000..40d1fa7 --- /dev/null +++ b/backend/script_groups/XML Parser to SCL/test_hash_detection.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +""" +Test script para verificar la detección de cambios basada en hash. + +Este script demuestra cómo el sistema detecta cambios en archivos XML +utilizando hash SHA256 en lugar de solo tiempo/tamaño de archivo. +""" + +import os +import json +import hashlib +import tempfile +import shutil +from datetime import datetime + + +def calculate_file_hash(filepath): + """Calcula el hash SHA256 de un archivo.""" + try: + sha256_hash = hashlib.sha256() + with open(filepath, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + sha256_hash.update(chunk) + return sha256_hash.hexdigest() + except Exception as e: + print(f"Error calculando hash de {filepath}: {e}") + return None + + +def create_test_xml(content, filepath): + """Crea un archivo XML de prueba con el contenido especificado.""" + with open(filepath, "w", encoding="utf-8") as f: + f.write(content) + + +def create_test_json(xml_filepath, json_filepath): + """Crea un archivo JSON de prueba con metadatos del XML.""" + xml_hash = calculate_file_hash(xml_filepath) + xml_mtime = os.path.getmtime(xml_filepath) + xml_size = os.path.getsize(xml_filepath) + + json_data = { + "block_name": "Test_Block", + "block_type": "FC", + "language": "LAD", + "source_xml_mod_time": xml_mtime, + "source_xml_size": xml_size, + "source_xml_hash": xml_hash, + "interface": {}, + "networks": [], + } + + with open(json_filepath, "w", encoding="utf-8") as f: + json.dump(json_data, f, indent=4) + + return json_data + + +def test_hash_detection(): + """Prueba la detección de cambios basada en hash.""" + print("=== Test de Detección de Cambios por Hash ===\n") + + # Crear directorio temporal + with tempfile.TemporaryDirectory() as temp_dir: + xml_file = os.path.join(temp_dir, "test_block.xml") + json_file = os.path.join(temp_dir, "test_block_processed.json") + + # Paso 1: Crear XML original + original_content = """ + + + + Test_Block + 100 + LAD + + + + + +""" + + create_test_xml(original_content, xml_file) + original_hash = calculate_file_hash(xml_file) + print(f"1. XML original creado") + print(f" Hash: {original_hash[:16]}...") + print(f" Tamaño: {os.path.getsize(xml_file)} bytes") + + # Paso 2: Crear JSON con metadatos + json_data = create_test_json(xml_file, json_file) + print(f"\n2. JSON con metadatos creado") + print(f" Hash almacenado: {json_data['source_xml_hash'][:16]}...") + + # Paso 3: Simular verificación - archivo sin cambios + current_hash = calculate_file_hash(xml_file) + hash_match = json_data["source_xml_hash"] == current_hash + print(f"\n3. Verificación sin cambios:") + print(f" Hash actual: {current_hash[:16]}...") + print(f" ¿Hash coincide?: {hash_match}") + print(f" → {'SALTAR procesamiento' if hash_match else 'PROCESAR archivo'}") + + # Paso 4: Modificar archivo manteniendo mismo tamaño y tiempo + print(f"\n4. Modificando XML (cambio sutil)...") + modified_content = original_content.replace( + "Test_Block", "Test_Blck" + ) # Cambio sutil + + # Guardar tiempo original + original_mtime = os.path.getmtime(xml_file) + + create_test_xml(modified_content, xml_file) + + # Restaurar tiempo original (simular que el timestamp no cambió) + os.utime(xml_file, (original_mtime, original_mtime)) + + new_size = os.path.getsize(xml_file) + new_mtime = os.path.getmtime(xml_file) + new_hash = calculate_file_hash(xml_file) + + print(f" Nuevo hash: {new_hash[:16]}...") + print(f" Nuevo tamaño: {new_size} bytes") + print( + f" Tiempo modificación: {abs(new_mtime - json_data['source_xml_mod_time']) < 0.001}" + ) + + # Paso 5: Verificar detección por diferentes métodos + print(f"\n5. Comparación de métodos de detección:") + + # Método tradicional (tiempo + tamaño) + time_match = abs(new_mtime - json_data["source_xml_mod_time"]) < 0.001 + size_match = new_size == json_data["source_xml_size"] + traditional_would_skip = time_match and size_match + + # Método por hash + hash_would_skip = new_hash == json_data["source_xml_hash"] + + print(f" Método tradicional (tiempo+tamaño):") + print(f" ¿Tiempo coincide?: {time_match}") + print(f" ¿Tamaño coincide?: {size_match}") + print(f" → {'SALTAR' if traditional_would_skip else 'PROCESAR'}") + + print(f" Método por hash:") + print(f" ¿Hash coincide?: {hash_would_skip}") + print(f" → {'SALTAR' if hash_would_skip else 'PROCESAR'}") + + print(f"\n6. Resultado:") + if traditional_would_skip and not hash_would_skip: + print(f" ✅ HASH DETECTÓ CAMBIO que el método tradicional PERDIÓ") + print(f" 🔄 El archivo será regenerado correctamente") + elif not traditional_would_skip and not hash_would_skip: + print(f" ✅ Ambos métodos detectaron el cambio") + elif traditional_would_skip and hash_would_skip: + print(f" ✅ Ambos métodos indican que no hay cambios") + else: + print(f" ⚠️ Situación inesperada") + + # Paso 6: Mostrar contenidos para verificación + print(f"\n7. Verificación de contenido:") + print(f" Contenido original tenía: 'Test_Block'") + print(f" Contenido nuevo tiene: 'Test_Blck'") + + with open(xml_file, "r", encoding="utf-8") as f: + content = f.read() + has_original = "Test_Block" in content + has_modified = "Test_Blck" in content + print(f" ¿Contiene 'Test_Block'?: {has_original}") + print(f" ¿Contiene 'Test_Blck'?: {has_modified}") + + +if __name__ == "__main__": + test_hash_detection() diff --git a/backend/script_groups/XML Parser to SCL/test_hash_edge_cases.py b/backend/script_groups/XML Parser to SCL/test_hash_edge_cases.py new file mode 100644 index 0000000..a1d5a2a --- /dev/null +++ b/backend/script_groups/XML Parser to SCL/test_hash_edge_cases.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +""" +Test específico para demostrar casos donde el hash detecta cambios +que el método tradicional (tiempo + tamaño) podría perder. +""" + +import os +import json +import hashlib +import tempfile +import time + + +def calculate_file_hash(filepath): + """Calcula el hash SHA256 de un archivo.""" + try: + sha256_hash = hashlib.sha256() + with open(filepath, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + sha256_hash.update(chunk) + return sha256_hash.hexdigest() + except Exception as e: + print(f"Error calculando hash de {filepath}: {e}") + return None + + +def test_subtle_changes(): + """Demuestra detección de cambios sutiles que mantienen el mismo tamaño.""" + print("=== Test: Cambios Sutiles con Mismo Tamaño ===\n") + + with tempfile.TemporaryDirectory() as temp_dir: + xml_file = os.path.join(temp_dir, "test.xml") + + # Crear contenido original de exactamente el mismo tamaño + original = """ + + MotorControl_01 + Active +""" + + # Crear contenido modificado del mismo tamaño (cambiar solo algunos caracteres) + modified = """ + + MotorControl_02 + Active +""" + + # Verificar que tienen el mismo tamaño + assert len(original) == len( + modified + ), "Los contenidos deben tener el mismo tamaño" + + # Paso 1: Crear archivo original + with open(xml_file, "w", encoding="utf-8") as f: + f.write(original) + + original_hash = calculate_file_hash(xml_file) + original_size = os.path.getsize(xml_file) + original_mtime = os.path.getmtime(xml_file) + + print(f"1. Archivo original:") + print(f" Contenido: MotorControl_01") + print(f" Hash: {original_hash[:16]}...") + print(f" Tamaño: {original_size} bytes") + + # Simular JSON con metadatos + json_data = { + "source_xml_hash": original_hash, + "source_xml_size": original_size, + "source_xml_mod_time": original_mtime, + } + + # Paso 2: Esperar un poco y modificar archivo manteniendo tamaño + time.sleep(0.1) # Pequeña pausa + + with open(xml_file, "w", encoding="utf-8") as f: + f.write(modified) + + # Restaurar tiempo de modificación original + os.utime(xml_file, (original_mtime, original_mtime)) + + new_hash = calculate_file_hash(xml_file) + new_size = os.path.getsize(xml_file) + new_mtime = os.path.getmtime(xml_file) + + print(f"\n2. Archivo modificado:") + print(f" Contenido: MotorControl_02") + print(f" Hash: {new_hash[:16]}...") + print(f" Tamaño: {new_size} bytes") + print( + f" Tiempo: {'Igual' if abs(new_mtime - original_mtime) < 0.001 else 'Diferente'}" + ) + + # Paso 3: Comparar métodos de detección + print(f"\n3. Detección de cambios:") + + # Método tradicional + time_match = abs(new_mtime - json_data["source_xml_mod_time"]) < 0.001 + size_match = new_size == json_data["source_xml_size"] + traditional_detects_change = not (time_match and size_match) + + # Método por hash + hash_detects_change = new_hash != json_data["source_xml_hash"] + + print(f" Método tradicional:") + print(f" Tiempo coincide: {time_match}") + print(f" Tamaño coincide: {size_match}") + print(f" Detecta cambio: {traditional_detects_change}") + + print(f" Método por hash:") + print(f" Hash coincide: {new_hash == json_data['source_xml_hash']}") + print(f" Detecta cambio: {hash_detects_change}") + + # Resultado + print(f"\n4. Resultado:") + if not traditional_detects_change and hash_detects_change: + print(f" 🎯 HASH DETECTÓ CAMBIO que método tradicional PERDIÓ") + print(f" ✅ Sin hash: archivo NO se regeneraría (ERROR)") + print(f" ✅ Con hash: archivo SÍ se regenerará (CORRECTO)") + elif traditional_detects_change and hash_detects_change: + print(f" ✅ Ambos métodos detectaron el cambio correctamente") + else: + print( + f" ℹ️ Resultado: tradicional={traditional_detects_change}, hash={hash_detects_change}" + ) + + return not traditional_detects_change and hash_detects_change + + +def test_same_size_different_content(): + """Test con contenidos completamente diferentes pero mismo tamaño.""" + print("\n" + "=" * 50) + print("=== Test: Contenido Diferente, Mismo Tamaño ===\n") + + with tempfile.TemporaryDirectory() as temp_dir: + xml_file = os.path.join(temp_dir, "test.xml") + + # Contenidos de igual longitud pero diferentes + content1 = "AAAAAAAAAA" # 33 chars + content2 = "BBBBBBBBBB" # 33 chars + + assert len(content1) == len(content2), "Deben tener igual tamaño" + + # Crear archivo con contenido 1 + with open(xml_file, "w", encoding="utf-8") as f: + f.write(content1) + + hash1 = calculate_file_hash(xml_file) + size1 = os.path.getsize(xml_file) + mtime1 = os.path.getmtime(xml_file) + + # Cambiar a contenido 2 manteniendo tiempo + time.sleep(0.1) + with open(xml_file, "w", encoding="utf-8") as f: + f.write(content2) + os.utime(xml_file, (mtime1, mtime1)) + + hash2 = calculate_file_hash(xml_file) + size2 = os.path.getsize(xml_file) + mtime2 = os.path.getmtime(xml_file) + + print(f"Contenido 1: {content1[:20]}...") + print(f"Contenido 2: {content2[:20]}...") + print(f"Tamaños: {size1} = {size2} ({'✓' if size1 == size2 else '✗'})") + print(f"Tiempos: {'iguales' if abs(mtime2 - mtime1) < 0.001 else 'diferentes'}") + print(f"Hashes: {'iguales' if hash1 == hash2 else 'diferentes'}") + + would_skip_traditional = (size1 == size2) and (abs(mtime2 - mtime1) < 0.001) + would_skip_hash = hash1 == hash2 + + print( + f"\nSin hash: {'SALTARÍA procesamiento' if would_skip_traditional else 'procesaría'}" + ) + print( + f"Con hash: {'saltaría procesamiento' if would_skip_hash else 'PROCESARÍA'}" + ) + + if would_skip_traditional and not would_skip_hash: + print("🎯 El hash evita saltar un archivo que SÍ cambió") + return True + + return False + + +if __name__ == "__main__": + print("Ejecutando tests de detección de cambios por hash...\n") + + test1_result = test_subtle_changes() + test2_result = test_same_size_different_content() + + print("\n" + "=" * 50) + print("=== RESUMEN ===") + print( + f"Test 1 (cambios sutiles): {'HASH ÚTIL' if test1_result else 'ambos métodos iguales'}" + ) + print( + f"Test 2 (mismo tamaño): {'HASH ÚTIL' if test2_result else 'ambos métodos iguales'}" + ) + + if test1_result or test2_result: + print("\n✅ El hash SHA256 mejora la detección de cambios") + print("✅ Evita regeneraciones innecesarias Y perdida de cambios reales") + else: + print("\n✓ Ambos métodos funcionaron igual en estos tests") diff --git a/backend/script_groups/XML Parser to SCL/x0_main.py b/backend/script_groups/XML Parser to SCL/x0_main.py index 916eff9..5ed360b 100644 --- a/backend/script_groups/XML Parser to SCL/x0_main.py +++ b/backend/script_groups/XML Parser to SCL/x0_main.py @@ -19,6 +19,7 @@ import json import datetime # <-- NUEVO: Para timestamps import shutil # <-- ADDED: Import shutil for file copying import re # <-- ADDED: Import regex for block header processing +import hashlib # <-- NUEVO: Para hash de archivos XML script_root = os.path.dirname( os.path.dirname(os.path.dirname(os.path.dirname(__file__))) @@ -92,10 +93,24 @@ def log_message(message, log_file_handle, also_print=True): # <-- FIN NUEVO --> +def calculate_file_hash(filepath): + """Calcula el hash SHA256 de un archivo.""" + try: + sha256_hash = hashlib.sha256() + with open(filepath, "rb") as f: + # Leer el archivo en chunks para manejar archivos grandes + for chunk in iter(lambda: f.read(4096), b""): + sha256_hash.update(chunk) + return sha256_hash.hexdigest() + except Exception as e: + print(f"Error calculando hash de {filepath}: {e}") + return None + + # <-- run_script ya no es necesaria --> -# --- Función check_skip_status (sin cambios en su lógica interna) --- +# --- Función check_skip_status (MODIFICADO: Añadido control por hash) --- def check_skip_status( xml_filepath, processed_json_filepath, final_output_dir, log_f ): # Añadido log_f @@ -103,17 +118,21 @@ def check_skip_status( can_check_x3 = False if not os.path.exists(processed_json_filepath): return status + stored_mtime = None stored_size = None + stored_hash = None block_name = None block_type = None processed_json_mtime = None + try: processed_json_mtime = os.path.getmtime(processed_json_filepath) with open(processed_json_filepath, "r", encoding="utf-8") as f: data = json.load(f) stored_mtime = data.get("source_xml_mod_time") stored_size = data.get("source_xml_size") + stored_hash = data.get("source_xml_hash") # <-- NUEVO: Obtener hash almacenado block_name = data.get("block_name") block_type = data.get("block_type") except Exception as e: @@ -124,17 +143,51 @@ def check_skip_status( ) return status - if stored_mtime is None or stored_size is None: + # Calcular hash actual del XML + current_xml_hash = calculate_file_hash(xml_filepath) + + # Priorizar comparación por hash si está disponible + if stored_hash is not None and current_xml_hash is not None: + if stored_hash == current_xml_hash: + log_message( + f" ✓ Hash coincide para {os.path.basename(xml_filepath)} - Saltando x1/x2", + log_f, + also_print=False, + ) + status["skip_x1_x2"] = True + can_check_x3 = True + else: + log_message( + f" ✗ Hash diferente para {os.path.basename(xml_filepath)} - Regenerando", + log_f, + also_print=False, + ) + # Hash diferente, no saltar x1/x2 + can_check_x3 = block_name is not None and block_type is not None + elif stored_mtime is None or stored_size is None: + # Fallback: sin metadatos de tiempo/tamaño, solo verificar si se puede x3 can_check_x3 = block_name is not None and block_type is not None else: + # Fallback: usar método original por tiempo y tamaño try: current_xml_mtime = os.path.getmtime(xml_filepath) current_xml_size = os.path.getsize(xml_filepath) time_match = abs(stored_mtime - current_xml_mtime) < 0.001 size_match = stored_size == current_xml_size if time_match and size_match: + log_message( + f" ✓ Tiempo/tamaño coinciden para {os.path.basename(xml_filepath)} - Saltando x1/x2 (método legacy)", + log_f, + also_print=False, + ) status["skip_x1_x2"] = True can_check_x3 = True + else: + log_message( + f" ✗ Tiempo/tamaño diferentes para {os.path.basename(xml_filepath)} - Regenerando (método legacy)", + log_f, + also_print=False, + ) except OSError as e: log_message( f"Advertencia: Error obteniendo metadatos XML para {xml_filepath}: {e}. No se saltará x1/x2.", diff --git a/backend/script_groups/XML Parser to SCL/x1_to_json.py b/backend/script_groups/XML Parser to SCL/x1_to_json.py index 42dcc0b..7a70acb 100644 --- a/backend/script_groups/XML Parser to SCL/x1_to_json.py +++ b/backend/script_groups/XML Parser to SCL/x1_to_json.py @@ -13,6 +13,7 @@ import os import sys import traceback import importlib +import hashlib # <-- NUEVO: Para calcular hash del XML from lxml import etree from lxml.etree import ( XMLSyntaxError as etree_XMLSyntaxError, @@ -45,6 +46,20 @@ except ImportError as e: sys.exit(1) +def calculate_file_hash(filepath): + """Calcula el hash SHA256 de un archivo.""" + try: + sha256_hash = hashlib.sha256() + with open(filepath, "rb") as f: + # Leer el archivo en chunks para manejar archivos grandes + for chunk in iter(lambda: f.read(4096), b""): + sha256_hash.update(chunk) + return sha256_hash.hexdigest() + except Exception as e: + print(f"Error calculando hash de {filepath}: {e}") + return None + + # --- NUEVAS FUNCIONES DE PARSEO para UDT y Tag Table (Sin cambios) --- def parse_udt(udt_element): """Parsea un elemento (UDT).""" @@ -249,10 +264,14 @@ def convert_xml_to_json(xml_filepath, json_filepath): # Obtener metadatos del XML xml_mod_time = None xml_size = None + xml_hash = None # <-- NUEVO: Hash del archivo XML try: xml_mod_time = os.path.getmtime(xml_filepath) xml_size = os.path.getsize(xml_filepath) - print(f"Metadatos XML: ModTime={xml_mod_time}, Size={xml_size}") + xml_hash = calculate_file_hash(xml_filepath) # <-- NUEVO: Calcular hash + print( + f"Metadatos XML: ModTime={xml_mod_time}, Size={xml_size}, Hash={xml_hash[:16] if xml_hash else None}..." + ) except OSError as e: print(f"Advertencia: No se pudieron obtener metadatos de '{xml_filepath}': {e}") @@ -602,6 +621,8 @@ def convert_xml_to_json(xml_filepath, json_filepath): result["source_xml_mod_time"] = xml_mod_time if xml_size is not None: result["source_xml_size"] = xml_size + if xml_hash is not None: # <-- NUEVO: Añadir hash al resultado + result["source_xml_hash"] = xml_hash print("Paso 6: Escribiendo el resultado en el archivo JSON...") # Advertencias finales si faltan partes clave diff --git a/backend/script_groups/XML Parser to SCL/x2_process.py b/backend/script_groups/XML Parser to SCL/x2_process.py index e840d01..7d202ba 100644 --- a/backend/script_groups/XML Parser to SCL/x2_process.py +++ b/backend/script_groups/XML Parser to SCL/x2_process.py @@ -1,10 +1,11 @@ """ LadderToSCL - Conversor de Siemens LAD/FUP XML a SCL -Este script convierte un archivo JSON simplificado (resultado de un análisis de un XML de Siemens) a un +Este script convierte un archivo JSON simplificado (resultado de un análisis de un XML de Siemens) a un JSON enriquecido con lógica SCL. Se enfoca en la lógica de programación y la agrupación de instrucciones IF. """ + # -*- coding: utf-8 -*- import json import argparse @@ -15,6 +16,7 @@ import re import importlib import sys import sympy + script_root = os.path.dirname( os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ) @@ -270,12 +272,18 @@ def process_json_to_scl(json_filepath, output_json_filepath): # <-- NUEVO: Extraer metadatos del JSON de entrada (si existen) --> source_xml_mod_time = data.get("source_xml_mod_time") source_xml_size = data.get("source_xml_size") + source_xml_hash = data.get("source_xml_hash") # <-- NUEVO: Extraer hash # <-- FIN NUEVO --> block_type = data.get("block_type", "Unknown") print(f"Procesando bloque tipo: {block_type}") - if block_type in ["GlobalDB", "PlcUDT", "PlcTagTable", "InstanceDB"]: # <-- MODIFIED: Add InstanceDB + if block_type in [ + "GlobalDB", + "PlcUDT", + "PlcTagTable", + "InstanceDB", + ]: # <-- MODIFIED: Add InstanceDB print(f"INFO: El bloque es {block_type}. Saltando procesamiento lógico de x2.") print( f"Guardando JSON de {block_type} (con metadatos) en: {output_json_filepath}" @@ -287,6 +295,8 @@ def process_json_to_scl(json_filepath, output_json_filepath): data_to_save["source_xml_mod_time"] = source_xml_mod_time if source_xml_size is not None: data_to_save["source_xml_size"] = source_xml_size + if source_xml_hash is not None: # <-- NUEVO: Preservar hash + data_to_save["source_xml_hash"] = source_xml_hash # <-- FIN MODIFICADO --> with open(output_json_filepath, "w", encoding="utf-8") as f_out: json.dump(data_to_save, f_out, indent=4, ensure_ascii=False) @@ -514,6 +524,8 @@ def process_json_to_scl(json_filepath, output_json_filepath): data["source_xml_mod_time"] = source_xml_mod_time if source_xml_size is not None: data["source_xml_size"] = source_xml_size + if source_xml_hash is not None: # <-- NUEVO: Preservar hash + data["source_xml_hash"] = source_xml_hash # <-- FIN MODIFICADO --> print(f"\nGuardando JSON procesado ({block_type}) en: {output_json_filepath}") @@ -537,7 +549,10 @@ if __name__ == "__main__": import tkinter as tk from tkinter import filedialog except ImportError: - print("Error: Tkinter no está instalado. No se puede mostrar el diálogo de archivo.", file=sys.stderr) + print( + "Error: Tkinter no está instalado. No se puede mostrar el diálogo de archivo.", + file=sys.stderr, + ) tk = None input_json_file = "" @@ -547,7 +562,7 @@ if __name__ == "__main__": print("Por favor, selecciona el archivo JSON de entrada (generado por x1)...") input_json_file = filedialog.askopenfilename( title="Selecciona el archivo JSON de entrada (.json)", - filetypes=[("JSON files", "*.json"), ("All files", "*.*")] + filetypes=[("JSON files", "*.json"), ("All files", "*.*")], ) root.destroy() @@ -560,7 +575,9 @@ if __name__ == "__main__": json_filename_base = os.path.splitext(os.path.basename(input_json_file))[0] # Asumimos que el _processed.json va al mismo directorio 'parsing' parsing_dir = os.path.dirname(input_json_file) - output_json_file = os.path.join(parsing_dir, f"{json_filename_base}_processed.json") + output_json_file = os.path.join( + parsing_dir, f"{json_filename_base}_processed.json" + ) # Asegurarse de que el directorio de salida exista (aunque debería si el input existe) os.makedirs(parsing_dir, exist_ok=True) @@ -574,7 +591,10 @@ if __name__ == "__main__": if success: print("\nProcesamiento completado exitosamente.") else: - print(f"\nError durante el procesamiento de '{os.path.relpath(input_json_file)}'.", file=sys.stderr) + print( + f"\nError durante el procesamiento de '{os.path.relpath(input_json_file)}'.", + file=sys.stderr, + ) # sys.exit(1) # No usar sys.exit except Exception as e: print( diff --git a/lib/script_executor.py b/lib/script_executor.py index ddc7e53..67111f0 100644 --- a/lib/script_executor.py +++ b/lib/script_executor.py @@ -129,8 +129,9 @@ class ScriptExecutor: start_msg = f"[{start_time.strftime('%H:%M:%S')}] Iniciando ejecución de {script_name} en {working_dir}..." broadcast_fn(start_msg) + # Usar CREATE_NEW_CONSOLE para permitir foco en diálogos creation_flags = ( - subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0 + subprocess.CREATE_NEW_CONSOLE if sys.platform == "win32" else 0 ) process = subprocess.Popen(