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(