Compare commits

...

4 Commits

Author SHA1 Message Date
Miguel be3b333491 Implementación de nuevas funcionalidades en la API y mejoras en la interfaz de usuario
- Se añadió una nueva ruta API para abrir la carpeta de un grupo en el explorador de archivos, mejorando la accesibilidad.
- Se implementaron botones en la interfaz para abrir la carpeta del grupo y copiar la ruta del grupo al portapapeles.
- Se mejoró la lógica de visualización de botones en la interfaz, permitiendo un mejor manejo de la visibilidad de los mismos.
- Se realizaron ajustes en los logs de ejecución para reflejar las nuevas funcionalidades y cambios en los directorios de trabajo.
2025-06-12 20:06:45 +02:00
Miguel 9ac769e2fc Actualización de scripts de adaptación IO y mejora en la gestión de directorios de trabajo
- Se modificaron los logs de ejecución para reflejar nuevas fechas y duraciones en los scripts x1 y x2.
- Se implementó un sistema de fallback para el directorio de trabajo, creando un directorio `.debug` en caso de que no se pueda cargar la configuración.
- Se añadieron nuevas funcionalidades en el script x2_process_CAx.py para generar informes en Excel con detalles de IOs por nodos del PLC.
- Se mejoró la documentación en el archivo readme.md, incluyendo detalles sobre el uso del directorio de debug y su estructura.
- Se actualizaron las descripciones de los scripts en scripts_description.json para reflejar cambios recientes y nuevas funcionalidades.
2025-06-07 15:40:30 +02:00
Miguel 88806ee4e4 Implementación de nuevas funcionalidades en el Launcher GUI
- Se añadieron nuevas rutas API para gestionar procesos de lanzadores, incluyendo la activación de foco y la terminación de procesos.
- Se mejoró la ejecución de scripts GUI al permitir especificar el directorio de trabajo y el tipo de ejecutable (python o pythonw).
- Se actualizaron los logs de ejecución para reflejar nuevas ubicaciones y detalles de los scripts.
- Se ajustaron los archivos de configuración y se mejoró la interfaz de usuario para soportar las nuevas funcionalidades.
2025-06-06 16:35:11 +02:00
Miguel 71a2a63de4 Implementación de nuevas funcionalidades en el Launcher GUI
- Se añadió un mecanismo para seleccionar el entorno de Miniconda a utilizar para cada grupo de scripts, mejorando la flexibilidad en la ejecución de scripts.
- Se implementaron nuevas rutas API para obtener todos los scripts de un grupo, incluyendo la gestión de metadatos de scripts específicos.
- Se mejoró la interfaz de usuario con un nuevo modal para gestionar scripts, permitiendo editar metadatos y ocultar scripts.
- Se actualizaron los archivos de configuración y se añadieron nuevos elementos en el HTML para soportar las nuevas funcionalidades.
2025-06-03 12:28:43 +02:00
45 changed files with 19877 additions and 28714 deletions

View File

@ -28,6 +28,7 @@ Esta guía detalla la implementación de un nuevo launcher para scripts Python c
- **Reutilización**: Aprovechar componentes existentes (logging, UI base, etc.)
- **Modularidad**: Código separado y mantenible
- **Flexibilidad**: Configuración manual de directorios y categorías
- Mecanismo para seleccionar el entorno miniconda a utlizar para cada grupo de scripts.
### Diferencias con Sistema Actual

358
app.py
View File

@ -8,6 +8,7 @@ from datetime import datetime
import time # Added for shutdown delay
import sys # Added for platform detection
import subprocess # Add this to the imports at the top
import shutil # For shutil.whichimport os
# --- Imports for System Tray Icon ---
import threading
@ -566,6 +567,41 @@ def get_launcher_scripts(group_id):
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/launcher-scripts-all/<group_id>")
def get_all_launcher_scripts(group_id):
"""Obtener TODOS los scripts de un grupo (incluyendo ocultos) para gestión"""
try:
scripts = launcher_manager.get_all_group_scripts(group_id)
return jsonify(scripts)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/launcher-script-metadata/<group_id>/<script_name>", methods=["GET", "POST"])
def handle_launcher_script_metadata(group_id, script_name):
"""Gestionar metadatos de un script específico"""
if request.method == "GET":
try:
metadata = launcher_manager.get_script_metadata(group_id, script_name)
return jsonify(metadata)
except Exception as e:
return jsonify({"error": str(e)}), 500
else: # POST
try:
data = request.json
result = launcher_manager.update_script_metadata(group_id, script_name, data)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-environments")
def get_python_environments():
"""Obtener entornos de Python disponibles"""
try:
envs = launcher_manager.get_available_python_envs()
return jsonify(envs)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/execute-gui-script", methods=["POST"])
def execute_gui_script():
"""Ejecutar script GUI con argumentos opcionales"""
@ -574,9 +610,11 @@ def execute_gui_script():
group_id = data["group_id"]
script_name = data["script_name"]
script_args = data.get("args", [])
working_dir = data.get("working_dir", None)
use_pythonw = data.get("use_pythonw", False) # Por defecto python.exe para logging
result = launcher_manager.execute_gui_script(
group_id, script_name, script_args, broadcast_message
group_id, script_name, script_args, broadcast_message, working_dir, use_pythonw
)
return jsonify(result)
except Exception as e:
@ -655,8 +693,326 @@ def get_group_icon(launcher_type, group_id):
except Exception as e:
return jsonify({"error": str(e)}), 500
# Nuevas APIs para gestión de procesos y Markdown
@app.route("/api/launcher-process-focus/<int:pid>", methods=["POST"])
def focus_launcher_process(pid):
"""Activar foco de un proceso"""
try:
result = launcher_manager.focus_process(pid)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/launcher-process-terminate/<int:pid>", methods=["POST"])
def terminate_launcher_process(pid):
"""Cerrar un proceso"""
try:
result = launcher_manager.terminate_process(pid)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/launcher-running-processes")
def get_launcher_running_processes():
"""Obtener procesos en ejecución"""
try:
processes = launcher_manager.get_running_processes()
return jsonify({"processes": processes})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/launcher-open-vscode/<group_id>", methods=["POST"])
def open_launcher_group_in_vscode(group_id):
"""Abrir grupo del launcher en VS Code"""
try:
group = launcher_manager.get_launcher_group(group_id)
if not group:
return jsonify({"status": "error", "message": "Grupo no encontrado"}), 404
script_group_path = group["directory"]
if not os.path.isdir(script_group_path):
return jsonify({
"status": "error",
"message": f"Directorio del grupo '{group['name']}' no encontrado"
}), 404
# VS Code executable path
vscode_path = r"C:\Users\migue\AppData\Local\Programs\Microsoft VS Code\Code.exe"
if not os.path.isfile(vscode_path):
return jsonify({
"status": "error",
"message": f"VS Code no encontrado en: {vscode_path}"
}), 404
print(f"Launching VS Code for launcher group: {group['name']}")
print(f"Opening directory: {script_group_path}")
process = subprocess.Popen(f'"{vscode_path}" "{script_group_path}"', shell=True)
print(f"Process started with PID: {process.pid}")
return jsonify({
"status": "success",
"message": f"VS Code abierto en: {script_group_path}"
})
except Exception as e:
print(f"Error opening VS Code for launcher group '{group_id}': {str(e)}")
return jsonify({
"status": "error",
"message": f"Error al abrir VS Code: {str(e)}"
}), 500
@app.route("/api/launcher-markdown/<group_id>")
def get_launcher_markdown_files(group_id):
"""Obtener archivos Markdown de un grupo"""
try:
markdown_files = launcher_manager.get_markdown_files(group_id)
return jsonify({"files": markdown_files})
except Exception as e:
print(f"Error getting markdown files for group {group_id}: {e}")
# Devolver lista vacía en lugar de error para no interferir con scripts
return jsonify({"files": []})
@app.route("/api/launcher-markdown-content/<group_id>/<path:relative_path>")
def get_launcher_markdown_content(group_id, relative_path):
"""Obtener contenido de un archivo Markdown"""
try:
result = launcher_manager.read_markdown_file(group_id, relative_path)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
# --- Global Error Handler (for debugging unhandled exceptions) ---
@app.errorhandler(Exception)
def handle_unhandled_exception(e):
# Log the error with traceback
app.logger.error('Unhandled Exception: %s', e, exc_info=True)
# Return a JSON response for API calls, or HTML for others
if request.path.startswith('/api/'):
return jsonify({"status": "error", "message": f"Internal Server Error: {str(e)}"}), 500
else:
return "<h1>Internal Server Error</h1><p>An unhandled error occurred.</p>", 500
# === FIN LAUNCHER GUI APIs ===
# --- Helper function to find VS Code ---
def find_vscode_executable():
"""Intenta encontrar el ejecutable de VS Code en ubicaciones comunes y en el PATH."""
# Comprobar la variable de entorno VSCODE_PATH primero (si la defines)
vscode_env_path = os.getenv("VSCODE_PATH")
if vscode_env_path and os.path.isfile(vscode_env_path):
return vscode_env_path
common_paths = []
local_app_data = os.getenv('LOCALAPPDATA')
if local_app_data:
common_paths.append(os.path.join(local_app_data, r"Programs\Microsoft VS Code\Code.exe"))
common_paths.extend([
r"C:\Program Files\Microsoft VS Code\Code.exe",
r"C:\Program Files (x86)\Microsoft VS Code\Code.exe",
])
for path in common_paths:
if os.path.isfile(path):
return path
return shutil.which("code") # Busca 'code' en el PATH
@app.route("/api/open-editor/<editor>/<group_system>/<group_id>", methods=["POST"])
def open_group_in_editor(editor, group_system, group_id):
"""Ruta unificada para abrir grupos en diferentes editores"""
try:
# Validar editor
if editor not in ['vscode', 'cursor']:
return jsonify({
"status": "error",
"message": f"Editor '{editor}' no soportado. Usar 'vscode' o 'cursor'"
}), 400
# Determinar directorio según el sistema
if group_system == 'config':
script_group_path = os.path.join(config_manager.script_groups_path, group_id)
if not os.path.isdir(script_group_path):
return jsonify({
"status": "error",
"message": f"Directorio del grupo config '{group_id}' no encontrado"
}), 404
elif group_system == 'launcher':
group = launcher_manager.get_launcher_group(group_id)
if not group:
return jsonify({
"status": "error",
"message": f"Grupo launcher '{group_id}' no encontrado"
}), 404
script_group_path = group["directory"]
if not os.path.isdir(script_group_path):
return jsonify({
"status": "error",
"message": f"Directorio del grupo launcher '{group['name']}' no encontrado"
}), 404
else:
return jsonify({
"status": "error",
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config' o 'launcher'"
}), 400
# Definir rutas de ejecutables
if editor == 'vscode':
editor_path = r"C:\Users\migue\AppData\Local\Programs\Microsoft VS Code\Code.exe"
editor_name = "VS Code"
elif editor == 'cursor':
# Rutas comunes donde se instala Cursor
possible_cursor_paths = [
r"C:\Users\migue\AppData\Local\Programs\cursor\Cursor.exe",
r"C:\Program Files\Cursor\Cursor.exe",
r"C:\Program Files (x86)\Cursor\Cursor.exe"
]
editor_path = None
for path in possible_cursor_paths:
if os.path.isfile(path):
editor_path = path
break
if not editor_path:
# Intentar buscar en PATH
editor_path = shutil.which("cursor")
if not editor_path:
return jsonify({
"status": "error",
"message": f"Cursor no encontrado. Intenté en: {', '.join(possible_cursor_paths)}"
}), 404
editor_name = "Cursor"
# Verificar que el ejecutable existe
if not os.path.isfile(editor_path):
return jsonify({
"status": "error",
"message": f"{editor_name} no encontrado en: {editor_path}"
}), 404
print(f"Launching {editor_name} from: {editor_path}")
print(f"Opening directory: {script_group_path}")
# Ejecutar el editor
process = subprocess.Popen(f'"{editor_path}" "{script_group_path}"', shell=True)
print(f"{editor_name} process started with PID: {process.pid}")
return jsonify({
"status": "success",
"message": f"{editor_name} abierto en: {script_group_path}"
})
except Exception as e:
print(f"Error opening {editor} for {group_system} group '{group_id}': {str(e)}")
return jsonify({
"status": "error",
"message": f"Error al abrir {editor}: {str(e)}"
}), 500
@app.route("/api/open-group-folder/<group_system>/<group_id>", methods=["POST"])
def open_group_folder(group_system, group_id):
"""Abrir carpeta de un grupo en el explorador de archivos"""
try:
# Determinar directorio según el sistema
if group_system == 'config':
script_group_path = os.path.join(config_manager.script_groups_path, group_id)
if not os.path.isdir(script_group_path):
return jsonify({
"status": "error",
"message": f"Directorio del grupo config '{group_id}' no encontrado"
}), 404
elif group_system == 'launcher':
group = launcher_manager.get_launcher_group(group_id)
if not group:
return jsonify({
"status": "error",
"message": f"Grupo launcher '{group_id}' no encontrado"
}), 404
script_group_path = group["directory"]
if not os.path.isdir(script_group_path):
return jsonify({
"status": "error",
"message": f"Directorio del grupo launcher '{group['name']}' no encontrado"
}), 404
else:
return jsonify({
"status": "error",
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config' o 'launcher'"
}), 400
# Abrir en el explorador según el sistema operativo
try:
if sys.platform == "win32":
os.startfile(script_group_path)
elif sys.platform == "darwin": # macOS
subprocess.Popen(["open", script_group_path])
else: # linux variants
subprocess.Popen(["xdg-open", script_group_path])
return jsonify({
"status": "success",
"message": f"Abriendo '{script_group_path}' en el explorador",
"path": script_group_path
})
except Exception as e:
return jsonify({
"status": "error",
"message": f"Error al abrir el explorador en '{script_group_path}': {str(e)}"
}), 500
except Exception as e:
print(f"Error opening folder for {group_system} group '{group_id}': {str(e)}")
return jsonify({
"status": "error",
"message": f"Error al abrir carpeta: {str(e)}"
}), 500
@app.route("/api/get-group-path/<group_system>/<group_id>", methods=["GET"])
def get_group_path(group_system, group_id):
"""Obtener el path completo de un grupo de scripts"""
try:
# Determinar directorio según el sistema
if group_system == 'config':
script_group_path = os.path.join(config_manager.script_groups_path, group_id)
if not os.path.isdir(script_group_path):
return jsonify({
"status": "error",
"message": f"Directorio del grupo config '{group_id}' no encontrado"
}), 404
elif group_system == 'launcher':
group = launcher_manager.get_launcher_group(group_id)
if not group:
return jsonify({
"status": "error",
"message": f"Grupo launcher '{group_id}' no encontrado"
}), 404
script_group_path = group["directory"]
if not os.path.isdir(script_group_path):
return jsonify({
"status": "error",
"message": f"Directorio del grupo launcher '{group['name']}' no encontrado"
}), 404
else:
return jsonify({
"status": "error",
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config' o 'launcher'"
}), 400
return jsonify({
"status": "success",
"path": script_group_path
})
except Exception as e:
print(f"Error getting path for {group_system} group '{group_id}': {str(e)}")
return jsonify({
"status": "error",
"message": f"Error al obtener path: {str(e)}"
}), 500
if __name__ == "__main__":
# --- Start Flask in a background thread ---

View File

@ -1,39 +1,35 @@
--- Log de Ejecución: x1.py ---
Grupo: EmailCrono
Directorio de Trabajo: C:\Trabajo\SIDEL\12 - SAE052 - Syrup Update & GSD Update\Reporte\Emails
Inicio: 2025-05-18 16:00:44
Fin: 2025-05-18 16:00:44
Duración: 0:00:00.445734
Directorio de Trabajo: C:\Trabajo\SIDEL\14 - E5.007172 - Modifica O&U - SAE340\Reporte\Email
Inicio: 2025-06-09 17:06:35
Fin: 2025-06-09 17:06:36
Duración: 0:00:00.370858
Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) ---
Working directory: C:\Trabajo\SIDEL\12 - SAE052 - Syrup Update & GSD Update\Reporte\Emails
Input directory: C:\Trabajo\SIDEL\12 - SAE052 - Syrup Update & GSD Update\Reporte\Emails
Output directory: C:/Users/migue/OneDrive/Miguel/Obsidean/Trabajo/VM/04-SIDEL/12 - SAE052 - Syrup Update & GSD Update
Cronologia file: C:/Users/migue/OneDrive/Miguel/Obsidean/Trabajo/VM/04-SIDEL/12 - SAE052 - Syrup Update & GSD Update\cronologia.md
Attachments directory: C:\Trabajo\SIDEL\12 - SAE052 - Syrup Update & GSD Update\Reporte\Emails\adjuntos
Working directory: C:\Trabajo\SIDEL\14 - E5.007172 - Modifica O&U - SAE340\Reporte\Email
Input directory: C:\Trabajo\SIDEL\14 - E5.007172 - Modifica O&U - SAE340\Reporte\Email
Output directory: C:/Users/migue/OneDrive/Miguel/Obsidean/Trabajo/VM/04-SIDEL/14 - E5.007172 - Modifica O&U - SAE340
Cronologia file: C:/Users/migue/OneDrive/Miguel/Obsidean/Trabajo/VM/04-SIDEL/14 - E5.007172 - Modifica O&U - SAE340\cronologia.md
Attachments directory: C:\Trabajo\SIDEL\14 - E5.007172 - Modifica O&U - SAE340\Reporte\Email\adjuntos
Beautify rules file: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\config\beautify_rules.json
Found 2 .eml files
Loaded 0 existing messages
Processing C:\Trabajo\SIDEL\12 - SAE052 - Syrup Update & GSD Update\Reporte\Emails\I_ Backup SAE052.eml
Processing C:\Trabajo\SIDEL\14 - E5.007172 - Modifica O&U - SAE340\Reporte\Email\E5.007172.eml
Aplicando reglas de prioridad 1
Aplicando reglas de prioridad 2
Aplicando reglas de prioridad 3
Aplicando reglas de prioridad 4
Processing C:\Trabajo\SIDEL\12 - SAE052 - Syrup Update & GSD Update\Reporte\Emails\Parametri Modificati SAE052.eml
Aplicando reglas de prioridad 1
Aplicando reglas de prioridad 2
Aplicando reglas de prioridad 3
Aplicando reglas de prioridad 4
Processing C:\Trabajo\SIDEL\14 - E5.007172 - Modifica O&U - SAE340\Reporte\Email\R_ NOTICE OF GOODS READY ASSIGN ORDER 169423 - Won Opportunity - Services- 169423.A.2.2 - AJETHAI CO., LTD. - Thailand - Filling - Service CRM_0037299==CHECK PAYMENT TERM==E5.007172.eml
Estadísticas de procesamiento:
- Total mensajes encontrados: 2
- Mensajes únicos añadidos: 2
- Total mensajes encontrados: 1
- Mensajes únicos añadidos: 1
- Mensajes duplicados ignorados: 0
Writing 2 messages to C:/Users/migue/OneDrive/Miguel/Obsidean/Trabajo/VM/04-SIDEL/12 - SAE052 - Syrup Update & GSD Update\cronologia.md
Writing 1 messages to C:/Users/migue/OneDrive/Miguel/Obsidean/Trabajo/VM/04-SIDEL/14 - E5.007172 - Modifica O&U - SAE340\cronologia.md
--- ERRORES (STDERR) ---
Ninguno

View File

@ -8,7 +8,7 @@
"cronologia_file": "cronologia.md"
},
"level3": {
"output_directory": "C:/Users/migue/OneDrive/Miguel/Obsidean/Trabajo/VM/04-SIDEL/12 - SAE052 - Syrup Update & GSD Update"
"output_directory": "C:/Users/migue/OneDrive/Miguel/Obsidean/Trabajo/VM/04-SIDEL/14 - E5.007172 - Modifica O&U - SAE340"
},
"working_directory": "C:\\Trabajo\\SIDEL\\12 - SAE052 - Syrup Update & GSD Update\\Reporte\\Emails"
"working_directory": "C:\\Trabajo\\SIDEL\\14 - E5.007172 - Modifica O&U - SAE340\\Reporte\\Email"
}

View File

@ -1,6 +1,7 @@
{
"path": "C:\\Trabajo\\SIDEL\\12 - SAE052 - Syrup Update & GSD Update\\Reporte\\Emails",
"path": "C:\\Trabajo\\SIDEL\\14 - E5.007172 - Modifica O&U - SAE340\\Reporte\\Email",
"history": [
"C:\\Trabajo\\SIDEL\\14 - E5.007172 - Modifica O&U - SAE340\\Reporte\\Email",
"C:\\Trabajo\\SIDEL\\12 - SAE052 - Syrup Update & GSD Update\\Reporte\\Emails",
"C:\\Trabajo\\SIDEL\\10 - E5.007095 - Modifica O&U - SAE463\\Reporte\\Email",
"C:\\Trabajo\\SIDEL\\08 - Masselli TEST\\Reporte\\EMAILs",
@ -9,7 +10,6 @@
"C:\\Trabajo\\VM\\40 - 93040 - HENKEL - NEXT2 Problem\\Reporte\\EmailTody",
"C:\\Trabajo\\VM\\30 - 9.3941- Kosme - Portogallo (Modifica + Linea)\\Reporte\\Emails",
"C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM\\30 - 9.3941- Kosme - Portogallo (Modifica + Linea)\\Emails",
"C:\\Trabajo\\VM\\40 - 93040 - HENKEL - NEXT2 Problem\\Reporte\\Emails\\Trial",
"C:\\Trabajo\\VM\\40 - 93040 - HENKEL - NEXT2 Problem\\Reporte\\Emails"
"C:\\Trabajo\\VM\\40 - 93040 - HENKEL - NEXT2 Problem\\Reporte\\Emails\\Trial"
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
# IO Summary Table for PLC: A40510
| Network | Type | Address | Device Name | Sub-Device | OrderNo | Type | IO Type | IO Address | Number of Bits |
|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|
| PLC Local Modules | Local I/O | Local | PLC A40510 | A41110 | 6ES7 131-6BF01-0BA0 | DI 8x24VDC ST | Input | `EW 0..0` | 8 |
| PLC Local Modules | Local I/O | Local | PLC A40510 | A41111 | 6ES7 131-6BF01-0BA0 | DI 8x24VDC ST | Input | `EW 1..1` | 8 |
| PLC Local Modules | Local I/O | Local | PLC A40510 | A41120 | 6ES7 131-6BF01-0BA0 | DI 8x24VDC ST | Input | `EW 2..2` | 8 |
| PLC Local Modules | Local I/O | Local | PLC A40510 | A41121 | 6ES7 131-6BF01-0BA0 | DI 8x24VDC ST | Input | `EW 3..3` | 8 |
| PLC Local Modules | Local I/O | Local | PLC A40510 | A41130 | 6ES7 132-6BF01-0BA0 | DQ 8x24VDC/0.5A ST | Output | `AW 0..0` | 8 |
| PLC Local Modules | Local I/O | Local | PLC A40510 | A41131 | 6ES7 132-6BF01-0BA0 | DQ 8x24VDC/0.5A ST | Output | `AW 1..1` | 8 |
| PLC Local Modules | Local I/O | Local | PLC A40510 | A41140 | 6ES7 132-6BF01-0BA0 | DQ 8x24VDC/0.5A ST | Output | `AW 2..2` | 8 |

View File

@ -1,37 +1,14 @@
--- Log de Ejecución: x1_export_CAx.py ---
Grupo: IO_adaptation
Directorio de Trabajo: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2
Inicio: 2025-05-15 10:41:11
Fin: 2025-05-15 10:43:18
Duración: 0:02:07.367695
Directorio de Trabajo: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia
Inicio: 2025-06-06 18:25:09
Fin: 2025-06-06 18:25:20
Duración: 0:00:11.673530
Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) ---
--- TIA Portal Project CAx Exporter and Analyzer ---
Selected Project: C:/Trabajo/SIDEL/06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)/InLavoro/PLC/_NEW/SAE196_c0.2/SAE196_c0.2.ap18
Using Output Directory (Working Directory): C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2
Will export CAx data to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2\SAE196_c0.2_CAx_Export.aml
Will generate summary to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2\SAE196_c0.2_CAx_Summary.md
Export log file: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2\SAE196_c0.2_CAx_Export.log
Connecting to TIA Portal V18.0...
2025-05-15 10:41:33,504 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Global OpenPortal - Start TIA Portal, please acknowledge the security dialog.
2025-05-15 10:41:33,524 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Global OpenPortal - With user interface
Connected.
Opening project: SAE196_c0.2.ap18...
2025-05-15 10:42:05,513 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Portal OpenProject - Open project... C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\InLavoro\PLC\_NEW\SAE196_c0.2\SAE196_c0.2.ap18
Project opened.
Exporting CAx data for the project to C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2\SAE196_c0.2_CAx_Export.aml...
CAx data exported successfully.
Closing TIA Portal...
2025-05-15 10:43:15,299 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Portal ClosePortal - Close TIA Portal
TIA Portal closed.
Parsing AML file: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2\SAE196_c0.2_CAx_Export.aml
Markdown summary written to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2\SAE196_c0.2_CAx_Summary.md
Script finished.
No project file selected. Exiting.
--- ERRORES (STDERR) ---
Ninguno

View File

@ -1,48 +1,76 @@
--- Log de Ejecución: x2_process_CAx.py ---
Grupo: IO_adaptation
Directorio de Trabajo: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2
Inicio: 2025-05-15 11:01:47
Fin: 2025-05-15 11:01:52
Duración: 0:00:04.917359
Directorio de Trabajo: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia
Inicio: 2025-06-08 00:15:28
Fin: 2025-06-08 00:15:33
Duración: 0:00:04.525461
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\TAGsIO\v2
Input AML: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2\SAE196_c0.2_CAx_Export.aml
Output Directory: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2
Output JSON: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2\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\TAGsIO\v2\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\TAGsIO\v2\SAE196_c0.2_CAx_Export.aml
Pass 1: Found 203 InternalElement(s). Populating device dictionary...
--- AML (CAx Export) to Hierarchical JSON and Obsidian MD Converter (v32.4 - Fixed Excel Integer Format for IO Addresses) ---
Using configured working directory: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia
Using Working Directory for Output: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia
Input AML: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01.aml
Output Directory: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia
Output JSON: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01.hierarchical.json
Output IO Debug Tree MD: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_IO_Upward_Debug.md
Processing AML file: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01.aml
Pass 1: Found 363 InternalElement(s). Populating device dictionary...
Pass 2: Identifying PLCs and Networks (Refined v2)...
Identified Network: PROFIBUS_1 (442e4d1d-7d42-4d59-bd77-ec619f883907) Type: Profibus
Identified Network: ETHERNET_1 (26504433-7319-4b53-8f42-0ae24c9e88a2) Type: Ethernet/Profinet
Identified PLC: PLC (a48e038f-0bcc-4b48-8373-033da316c62b) - Type: CPU 1516F-3 PN/DP OrderNo: 6ES7 516-3FP03-0AB0
Identified Network: PN/IE_1 (f9f048af-beb4-4e14-8a03-dc27e564649c) Type: Ethernet/Profinet
Identified PLC: A40510 (fc0d3bac-267e-488a-8dcf-7dc8599d80e8) - Type: CPU 1514SP T-2 PN OrderNo: 6ES7 514-2VN03-0AB0
Pass 3: Processing InternalLinks (Robust Network Mapping & IO)...
Found 115 InternalLink(s).
Mapping Device/Node 'E1' (NodeID:60d02ba8-54ea-4508-8a3a-986826e276c6, 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:8cf403ec-810d-43de-826a-aa447f887ee3, Addr:1) to Network 'PROFIBUS_1'
--> Associating Network 'PROFIBUS_1' with PLC 'PLC' (via Node 'P1' Addr: 1)
Mapping Device/Node 'PB1' (NodeID:b618b2b1-9ea8-41c1-a87e-fb0a3627a51d, Addr:12) to Network 'PROFIBUS_1'
Mapping Device/Node 'PB1' (NodeID:4e5d84a4-eb22-4d1f-8143-fc4d770eb2e7, Addr:20) to Network 'PROFIBUS_1'
Mapping Device/Node 'PB1' (NodeID:e6f362b4-398b-4854-b15b-7435745e4650, Addr:21) to Network 'PROFIBUS_1'
Mapping Device/Node 'PB1' (NodeID:1a5422d6-c2bf-4a1c-8cf9-7b89fbaf4090, Addr:22) to Network 'PROFIBUS_1'
Mapping Device/Node 'PB1' (NodeID:6b3de492-236f-4894-9bcf-3ee6c851c230, Addr:10) to Network 'PROFIBUS_1'
Mapping Device/Node 'PB1' (NodeID:bf8c18a3-aa60-4106-b779-afff78cdac47, Addr:8) to Network 'PROFIBUS_1'
Mapping Device/Node 'PB1' (NodeID:5e2810b0-4018-4747-bba9-19b8f9b14994, Addr:40) to Network 'PROFIBUS_1'
Found 103 InternalLink(s).
Mapping Device/Node 'E1' (NodeID:49534400-9e59-4c19-996d-7ad00a2957e9, Addr:10.1.30.11) to Network 'PN/IE_1'
--> Found PLC in children: A40510 (ID: fc0d3bac-267e-488a-8dcf-7dc8599d80e8)
--> Associating Network 'PN/IE_1' with PLC 'A40510' (via Node 'E1' Addr: 10.1.30.11)
Mapping Device/Node 'IE1' (NodeID:051911c8-9591-4119-a3ee-d6be687b7f9d, Addr:10.1.30.58) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:83519e1a-0912-4ae5-845b-b82bc743a92c, Addr:10.1.30.59) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:2ba93b89-6b43-4a92-850b-8110b9c2efcc, Addr:10.1.30.60) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:b31787f3-2682-4d09-affc-69db1e2b89d3, Addr:10.1.30.61) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:ef087f66-fd57-4be7-93a7-19497f4f852c, Addr:10.1.30.62) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:6fe53dd5-7093-4245-923f-f4bbce67fabf, Addr:10.1.30.63) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:59f51321-8469-447f-ace9-05e1495bbafd, Addr:10.1.30.64) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:60c861c6-1889-4bde-9d16-1c625116aece, Addr:10.1.30.65) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:4f6ad448-3a75-4e48-8107-83634ba6d192, Addr:10.1.30.66) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:b2153a8e-6526-4831-b2a1-5cf3da07c062, Addr:10.1.30.31) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:08066980-9963-4ca3-9a5d-1d9de9b611a3, Addr:10.1.30.32) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:2acb9ba4-f0f9-4fc6-9fd9-4592c1c8ef5a, Addr:10.1.30.170) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:feda1ddb-816d-40bd-a88c-8b1bc310c424, Addr:10.1.30.33) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:80a42e65-b1b1-4a48-b4ea-f95ca4032021, Addr:10.1.30.34) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:082ca69e-92aa-43c5-982b-2b134e5ca9ea, Addr:10.1.30.35) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:96aef85c-66f1-4792-94fd-f9ad5bb41d62, Addr:10.1.30.36) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:9b791861-b139-4a7d-b7ba-ebe195bbaf2c, Addr:10.1.30.40) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:5085a882-dc3c-4794-9c0a-4821a9ecbf24, Addr:10.1.30.44) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:0ea5739f-aa85-46b8-958f-9b6ce2960d6d, Addr:10.1.30.41) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:1f6cec79-c24c-4b0f-8aec-47d49ea1ce81, Addr:10.1.30.42) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:ab6516fc-5061-437d-bdc1-529d79d3d42a, Addr:10.1.30.43) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:3f7bbecb-2bb9-42e8-8668-4a102c98873b, Addr:10.1.30.37) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:6807d26a-ff0f-4ce7-89b9-8785cf5310ae, Addr:10.1.30.45) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:4c58e397-5fda-4076-bd52-374ba80460cb, Addr:10.1.30.46) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:36da4ca9-6dc1-489a-bbdc-d124bf125dad, Addr:10.1.30.47) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:02213f0f-cc1f-4d97-ba86-eb3d11f7cb5b, Addr:10.1.30.48) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:628bab85-5b77-4335-9444-48f51dec6e66, Addr:10.1.30.49) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:5800b36d-59bb-43f6-8857-5b616730246d, Addr:10.1.30.70) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:b68a427c-e7f6-459a-9e6a-39b76a4497ad, Addr:10.1.30.71) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:d4bad7e1-0461-4dbb-b797-cb45a75225d9, Addr:10.1.30.72) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:145f1526-6b77-4ea7-968b-de89a7bbeda2, Addr:10.1.30.74) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:8cde4014-94bb-47be-aacd-32f1915152d2, Addr:10.1.30.73) to Network 'PN/IE_1'
Data extraction and structuring complete.
Generating JSON output: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2\SAE196_c0.2_CAx_Export.hierarchical.json
Generating JSON output: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01.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\TAGsIO\v2\SAE196_c0.2_CAx_Export_IO_Upward_Debug.md
IO upward debug tree written to: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_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\TAGsIO\v2\PLC\Documentation\SAE196_c0.2_CAx_Export_Hardware_Tree.md
Markdown tree summary written to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2\PLC\Documentation\SAE196_c0.2_CAx_Export_Hardware_Tree.md
IO summary table written to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2\Hardware.md
IO summary table generated in separate file: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2\Hardware.md
Generating Hardware Tree for PLC 'A40510' (ID: fc0d3bac-267e-488a-8dcf-7dc8599d80e8) at: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\A40510\Documentation\98050_PLC_01_Hardware_Tree.md
Markdown tree summary written to: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\A40510\Documentation\98050_PLC_01_Hardware_Tree.md
IO summary table written to: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\Hardware.md
IO summary table generated in separate file: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\Hardware.md
Generating Excel IO Report for PLC 'A40510' (ID: fc0d3bac-267e-488a-8dcf-7dc8599d80e8) at: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\A40510\Documentation\98050_PLC_01_IO_Report.xlsx
Generating Excel IO report for PLC: A40510
Excel IO report saved to: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\A40510\Documentation\98050_PLC_01_IO_Report.xlsx
Total rows in report: 33
Script finished.

View File

@ -1,18 +1,21 @@
--- Log de Ejecución: x3_excel_to_md.py ---
Grupo: IO_adaptation
Directorio de Trabajo: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2
Inicio: 2025-05-15 11:58:03
Fin: 2025-05-15 11:58:05
Duración: 0:00:01.664065
Directorio de Trabajo: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia
Inicio: 2025-06-08 13:21:43
Fin: 2025-06-08 13:22:12
Duración: 0:00:29.516302
Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) ---
Usando directorio de trabajo: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2
Configuración de paths cargada desde: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2\io_paths_config.json
Usando archivo Excel predeterminado: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2\PLCTags.xlsx
Procesando archivo Excel: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2\PLCTags.xlsx...
Usando directorio de trabajo: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia
Configuración de paths cargada desde: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\io_paths_config.json
Archivo PLCTags.xlsx no encontrado. Seleccione el archivo Excel exportado de TIA Portal:
Procesando archivo Excel: D:/Trabajo/VM/44 - 98050 - Fiera/Reporte/ExportsTia/PLCTagsv_02.xlsx...
Paths configurados para procesar: ['Inputs', 'Outputs', 'OutputsFesto', 'IO Not in Hardware\\InputsMaster', 'IO Not in Hardware\\OutputsMaster']
¡Éxito! Archivo Excel convertido a Markdown en: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2\Master IO Tags.md
No se encontraron entradas para el path: OutputsFesto
No se encontraron entradas para el path: IO Not in Hardware\InputsMaster
No se encontraron entradas para el path: IO Not in Hardware\OutputsMaster
¡Éxito! Archivo Excel convertido a Markdown en: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\Master IO Tags.md
--- ERRORES (STDERR) ---
Ninguno

View File

@ -1,19 +1,19 @@
--- Log de Ejecución: x4_prompt_generator.py ---
Grupo: IO_adaptation
Directorio de Trabajo: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2
Inicio: 2025-05-15 14:05:02
Fin: 2025-05-15 14:05:04
Duración: 0:00:01.643930
Directorio de Trabajo: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia
Inicio: 2025-06-08 11:05:58
Fin: 2025-06-08 11:06:03
Duración: 0:00:04.909042
Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) ---
Generador de prompt para adaptación de IO
=========================================
Usando directorio de trabajo: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2
Usando directorio de trabajo: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia
Usando ruta de Obsidian desde configuración: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\00 - MASTER\MIXER\IO
Usando carpeta de equivalencias en Obsidian: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\00 - MASTER\MIXER\IO
¡Prompt generado y copiado al portapapeles con éxito!
Prompt guardado en: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\v2\IO_Adaptation_Prompt.txt
Prompt guardado en: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\IO_Adaptation_Prompt.txt
--- ERRORES (STDERR) ---
Ninguno

View File

@ -0,0 +1,657 @@
--- Log de Ejecución: x7_update_CAx.py ---
Grupo: IO_adaptation
Directorio de Trabajo: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia
Inicio: 2025-06-07 18:57:44
Fin: 2025-06-07 18:57:53
Duración: 0:00:08.646157
Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) ---
--- Actualizador de AML desde Excel Modificado (v1.4 - Enhanced Address Element Search with Debug) ---
Directorio de trabajo: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia
1. Seleccione el archivo AML original:
2. Seleccione el archivo Excel modificado:
Archivo AML original: D:/Trabajo/VM/44 - 98050 - Fiera/Reporte/ExportsTia/98050_PLC_01.aml
Archivo Excel modificado: D:/Trabajo/VM/44 - 98050 - Fiera/Reporte/ExportsTia/A40510/Documentation/98050_PLC_01_IO_Report.xlsx
Usando directorio temporal: C:\Users\migue\AppData\Local\Temp\tmplzmtjbwx
Generando Excel de referencia desde AML original...
Pass 1: Found 363 InternalElement(s). Populating device dictionary...
Pass 2: Identifying PLCs and Networks (Refined v2)...
Identified Network: PN/IE_1 (6ce86626-0043-4a58-b675-cc13ac87121c) Type: Ethernet/Profinet
Identified PLC: A40510 (fc0d3bac-267e-488a-8dcf-7dc8599d80e8) - Type: CPU 1514SP T-2 PN OrderNo: 6ES7 514-2VN03-0AB0
Pass 3: Processing InternalLinks (Robust Network Mapping & IO)...
Found 103 InternalLink(s).
Mapping Device/Node 'E1' (NodeID:ab796923-4471-4a60-98f4-f8ea5920b3b9, Addr:10.1.30.11) to Network 'PN/IE_1'
--> Found PLC in children: A40510 (ID: fc0d3bac-267e-488a-8dcf-7dc8599d80e8)
--> Associating Network 'PN/IE_1' with PLC 'A40510' (via Node 'E1' Addr: 10.1.30.11)
Mapping Device/Node 'IE1' (NodeID:c53ae31d-bee4-47ba-950d-15c31d2599b9, Addr:10.1.30.58) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:1a8366c9-7d4c-4e49-b0a3-77d445eabc8b, Addr:10.1.30.59) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:deeda41b-54d0-4c86-82c3-a311f753021e, Addr:10.1.30.60) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:8a916a9d-895e-4a12-9190-516ef6a8f191, Addr:10.1.30.61) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:c51471e0-9621-4ef7-b050-09bf4d695ea1, Addr:10.1.30.62) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:77ca312c-3dd0-46f6-bd64-5da69a99cf6f, Addr:10.1.30.63) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:011019c6-5925-4544-87aa-27288c3aa70c, Addr:10.1.30.64) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:5ea1e894-b51c-44f9-8aff-80c58c6cb7ef, Addr:10.1.30.65) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:612916a9-7a26-4712-9de2-d3c7894db862, Addr:10.1.30.66) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:7be7cf9f-f7af-418c-8fe4-96b4a97a581d, Addr:10.1.30.31) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:135492a8-02ab-4236-92ce-7a5585538297, Addr:10.1.30.32) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:b7a06147-6428-4d95-ba0d-834fad49a1ae, Addr:10.1.30.170) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:7ce83bdf-dc4d-40f0-85c8-a246dd2c44be, Addr:10.1.30.33) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:e9a07506-2869-4c34-8541-ee021d1623f0, Addr:10.1.30.34) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:3dd0f886-c3d0-4628-804b-ae18cf5931e8, Addr:10.1.30.35) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:67c3fa72-a956-4363-9a4d-e3300d7d1429, Addr:10.1.30.36) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:b35e73e8-a9ad-47e2-8ad5-00442c7a2df7, Addr:10.1.30.40) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:867d0580-06cd-4722-a66f-8c5559b624f5, Addr:10.1.30.44) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:6f1b4062-80ac-4d1a-b351-546c9d0157e2, Addr:10.1.30.41) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:c0016a06-48cc-47af-83aa-d4a0a6cb44f6, Addr:10.1.30.42) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:07fa5e8f-ffcf-4d81-9897-5ff9f73d6125, Addr:10.1.30.43) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:5e8a4449-c958-4397-8b71-877af262333b, Addr:10.1.30.37) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:79096325-dcb8-4650-bc44-2c9735a93f52, Addr:10.1.30.45) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:957e5975-eafe-477c-a682-bebf330a2868, Addr:10.1.30.46) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:379ccd79-27b4-4b53-a552-3da783bc5b25, Addr:10.1.30.47) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:b53dd719-e8af-431f-b837-a0903ffb3a76, Addr:10.1.30.48) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:0f28d88a-5208-4fc4-8ee1-f1bb33a947e8, Addr:10.1.30.49) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:d1f8ea18-50d2-410e-9966-136d8a79471d, Addr:10.1.30.70) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:1c86a627-5646-45e7-9c21-5d18d6544568, Addr:10.1.30.71) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:e80a9939-59d7-44e0-9a46-1fade44e1b78, Addr:10.1.30.72) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:8c51fa26-883a-468c-8c36-c0e1b31852e4, Addr:10.1.30.74) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:17be2ccc-dede-4187-ba77-1ad8499a7349, Addr:10.1.30.73) to Network 'PN/IE_1'
Data extraction and structuring complete.
Generating Excel IO report for PLC: A40510
Excel IO report saved to: C:\Users\migue\AppData\Local\Temp\tmplzmtjbwx\temp_plc_fc0d3bac-267e-488a-8dcf-7dc8599d80e8.xlsx
Total rows in report: 33
Comparando archivos Excel...
Estructura básica validada correctamente.
Detectados 22 dispositivos con cambios.
A40510+U30210-AxisY: ['IO Input Start Address', 'IO Output Start Address']
A40510+E44010-Encoder: ['IO Input Start Address', 'IO Output Start Address']
A40510+U30310: ['IO Input Start Address', 'IO Output Start Address']
A40510+U30410: ['IO Input Start Address', 'IO Output Start Address']
A40510+U30510: ['IO Input Start Address', 'IO Output Start Address']
A40510+U30610: ['IO Input Start Address', 'IO Output Start Address']
A40510+M31010: ['IO Input Start Address', 'IO Output Start Address']
A40510+M31410: ['IO Input Start Address', 'IO Output Start Address']
A40510+M31110: ['IO Input Start Address', 'IO Output Start Address']
A40510+M31210: ['IO Input Start Address', 'IO Output Start Address']
A40510+M31310: ['IO Input Start Address', 'IO Output Start Address']
A40510+M30710: ['IO Input Start Address', 'IO Output Start Address']
A40510+M31510: ['IO Input Start Address', 'IO Output Start Address']
A40510+M31610: ['IO Input Start Address', 'IO Output Start Address']
A40510+M31710: ['IO Input Start Address', 'IO Output Start Address']
A40510+M31810: ['IO Input Start Address', 'IO Output Start Address']
A40510+M31910: ['IO Input Start Address', 'IO Output Start Address']
A40510+M34010: ['IO Input Start Address', 'IO Output Start Address']
A40510+M34110: ['IO Input Start Address', 'IO Output Start Address']
A40510+M34210: ['IO Input Start Address', 'IO Output Start Address']
A40510+M34410: ['IO Input Start Address', 'IO Output Start Address']
A40510+M34310: ['IO Input Start Address', 'IO Output Start Address']
Cargando archivo AML original...
Aplicando cambios al archivo AML...
Procesando cambios para: A40510+U30210-AxisY
Debug: Encontrado dispositivo - node_id: 135492a8-02ab-4236-92ce-7a5585538297, display_id: 0eef2113-ec3f-4c2f-8448-fa7456a6140d
Debug: Usando elemento XML con ID 0eef2113-ec3f-4c2f-8448-fa7456a6140d
Debug: Buscando elementos Address en dispositivo U30210-AxisY
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 120 -> 112
Debug: Buscando elementos Address en dispositivo U30210-AxisY
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 120 -> 112
Procesando cambios para: A40510+E44010-Encoder
Debug: Encontrado dispositivo - node_id: b7a06147-6428-4d95-ba0d-834fad49a1ae, display_id: e59b0b92-6c37-45d5-aa9b-8dae625e5272
Debug: Usando elemento XML con ID e59b0b92-6c37-45d5-aa9b-8dae625e5272
Debug: Buscando elementos Address en dispositivo E44010-Encoder
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 140 -> 574
Debug: Buscando elementos Address en dispositivo E44010-Encoder
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 140 -> 574
Procesando cambios para: A40510+U30310
Debug: Encontrado dispositivo - node_id: 7ce83bdf-dc4d-40f0-85c8-a246dd2c44be, display_id: 98a3e87d-176b-42b9-a606-fd97b54bc54b
Debug: Usando elemento XML con ID 98a3e87d-176b-42b9-a606-fd97b54bc54b
Debug: Buscando elementos Address en dispositivo U30310
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 4 -> 124
Debug: Buscando elementos Address en dispositivo U30310
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 3 -> 124
Procesando cambios para: A40510+U30410
Debug: Encontrado dispositivo - node_id: e9a07506-2869-4c34-8541-ee021d1623f0, display_id: 1fcdf8b4-7c90-48e0-bb72-f29eef363705
Debug: Usando elemento XML con ID 1fcdf8b4-7c90-48e0-bb72-f29eef363705
Debug: Buscando elementos Address en dispositivo U30410
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 52 -> 150
Debug: Buscando elementos Address en dispositivo U30410
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 51 -> 150
Procesando cambios para: A40510+U30510
Debug: Encontrado dispositivo - node_id: 3dd0f886-c3d0-4628-804b-ae18cf5931e8, display_id: 44004215-6e6f-456c-9c97-164ee1f8e7b7
Debug: Usando elemento XML con ID 44004215-6e6f-456c-9c97-164ee1f8e7b7
Debug: Buscando elementos Address en dispositivo U30510
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 156 -> 176
Debug: Buscando elementos Address en dispositivo U30510
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 144 -> 176
Procesando cambios para: A40510+U30610
Debug: Encontrado dispositivo - node_id: 67c3fa72-a956-4363-9a4d-e3300d7d1429, display_id: 2ea5ad51-0411-413d-8bb5-dfd0655592cf
Debug: Usando elemento XML con ID 2ea5ad51-0411-413d-8bb5-dfd0655592cf
Debug: Buscando elementos Address en dispositivo U30610
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 204 -> 202
Debug: Buscando elementos Address en dispositivo U30610
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 192 -> 202
Procesando cambios para: A40510+M31010
Debug: Encontrado dispositivo - node_id: b35e73e8-a9ad-47e2-8ad5-00442c7a2df7, display_id: 4e0722c0-9114-40bf-8452-733e5d275591
Debug: Usando elemento XML con ID 4e0722c0-9114-40bf-8452-733e5d275591
Debug: Buscando elementos Address en dispositivo M31010
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 272 -> 252
Debug: Buscando elementos Address en dispositivo M31010
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 272 -> 252
Procesando cambios para: A40510+M31410
Debug: Encontrado dispositivo - node_id: 867d0580-06cd-4722-a66f-8c5559b624f5, display_id: 6530fefc-0f00-45c5-b33a-9c009bf83e7b
Debug: Usando elemento XML con ID 6530fefc-0f00-45c5-b33a-9c009bf83e7b
Debug: Buscando elementos Address en dispositivo M31410
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 288 -> 334
Debug: Buscando elementos Address en dispositivo M31410
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 288 -> 334
Procesando cambios para: A40510+M31110
Debug: Encontrado dispositivo - node_id: 6f1b4062-80ac-4d1a-b351-546c9d0157e2, display_id: 95cedabf-d2b0-4d17-a36d-668d1b6db6d8
Debug: Usando elemento XML con ID 95cedabf-d2b0-4d17-a36d-668d1b6db6d8
Debug: Buscando elementos Address en dispositivo M31110
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 4000 -> 262
Debug: Buscando elementos Address en dispositivo M31110
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 4000 -> 262
Procesando cambios para: A40510+M31210
Debug: Encontrado dispositivo - node_id: c0016a06-48cc-47af-83aa-d4a0a6cb44f6, display_id: 1739a036-2231-4791-947b-9fa19eac922e
Debug: Usando elemento XML con ID 1739a036-2231-4791-947b-9fa19eac922e
Debug: Buscando elementos Address en dispositivo M31210
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 4022 -> 286
Debug: Buscando elementos Address en dispositivo M31210
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 4044 -> 286
Procesando cambios para: A40510+M31310
Debug: Encontrado dispositivo - node_id: 07fa5e8f-ffcf-4d81-9897-5ff9f73d6125, display_id: 90c971fa-0bdd-4dba-8d04-f6eb4dd940cc
Debug: Usando elemento XML con ID 90c971fa-0bdd-4dba-8d04-f6eb4dd940cc
Debug: Buscando elementos Address en dispositivo M31310
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 4044 -> 310
Debug: Buscando elementos Address en dispositivo M31310
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 4088 -> 310
Procesando cambios para: A40510+M30710
Debug: Encontrado dispositivo - node_id: 5e8a4449-c958-4397-8b71-877af262333b, display_id: b2f6e277-b969-4581-b3ed-4ca0bafe9994
Debug: Usando elemento XML con ID b2f6e277-b969-4581-b3ed-4ca0bafe9994
Debug: Buscando elementos Address en dispositivo M30710
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 4066 -> 228
Debug: Buscando elementos Address en dispositivo M30710
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 4132 -> 228
Procesando cambios para: A40510+M31510
Debug: Encontrado dispositivo - node_id: 79096325-dcb8-4650-bc44-2c9735a93f52, display_id: d6fa2c62-51ab-4f68-a0f9-d86b2d52c1a9
Debug: Usando elemento XML con ID d6fa2c62-51ab-4f68-a0f9-d86b2d52c1a9
Debug: Buscando elementos Address en dispositivo M31510
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 304 -> 344
Debug: Buscando elementos Address en dispositivo M31510
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 304 -> 344
Procesando cambios para: A40510+M31610
Debug: Encontrado dispositivo - node_id: 957e5975-eafe-477c-a682-bebf330a2868, display_id: 0a03a25f-121e-4e6a-ab81-f017d170ba52
Debug: Usando elemento XML con ID 0a03a25f-121e-4e6a-ab81-f017d170ba52
Debug: Buscando elementos Address en dispositivo M31610
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 4088 -> 354
Debug: Buscando elementos Address en dispositivo M31610
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 4176 -> 354
Procesando cambios para: A40510+M31710
Debug: Encontrado dispositivo - node_id: 379ccd79-27b4-4b53-a552-3da783bc5b25, display_id: 8d6214bd-7a77-462f-a2bf-8de1dccc0db2
Debug: Usando elemento XML con ID 8d6214bd-7a77-462f-a2bf-8de1dccc0db2
Debug: Buscando elementos Address en dispositivo M31710
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 4110 -> 378
Debug: Buscando elementos Address en dispositivo M31710
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 4220 -> 378
Procesando cambios para: A40510+M31810
Debug: Encontrado dispositivo - node_id: b53dd719-e8af-431f-b837-a0903ffb3a76, display_id: 9d4ba8bc-a7e7-40a0-99d1-f2fd88a6b00d
Debug: Usando elemento XML con ID 9d4ba8bc-a7e7-40a0-99d1-f2fd88a6b00d
Debug: Buscando elementos Address en dispositivo M31810
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 4132 -> 402
Debug: Buscando elementos Address en dispositivo M31810
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 4264 -> 402
Procesando cambios para: A40510+M31910
Debug: Encontrado dispositivo - node_id: 0f28d88a-5208-4fc4-8ee1-f1bb33a947e8, display_id: 0c0c14f0-df2b-4c5e-8163-f7cb58788dae
Debug: Usando elemento XML con ID 0c0c14f0-df2b-4c5e-8163-f7cb58788dae
Debug: Buscando elementos Address en dispositivo M31910
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 4154 -> 426
Debug: Buscando elementos Address en dispositivo M31910
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 4308 -> 426
Procesando cambios para: A40510+M34010
Debug: Encontrado dispositivo - node_id: d1f8ea18-50d2-410e-9966-136d8a79471d, display_id: a01c051b-9abc-4947-9632-6dcad7da84b8
Debug: Usando elemento XML con ID a01c051b-9abc-4947-9632-6dcad7da84b8
Debug: Buscando elementos Address en dispositivo M34010
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 4176 -> 468
Debug: Buscando elementos Address en dispositivo M34010
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 4352 -> 468
Procesando cambios para: A40510+M34110
Debug: Encontrado dispositivo - node_id: 1c86a627-5646-45e7-9c21-5d18d6544568, display_id: d2440b5d-132e-4d66-986c-fe42a8ff4f74
Debug: Usando elemento XML con ID d2440b5d-132e-4d66-986c-fe42a8ff4f74
Debug: Buscando elementos Address en dispositivo M34110
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 4198 -> 492
Debug: Buscando elementos Address en dispositivo M34110
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 4396 -> 492
Procesando cambios para: A40510+M34210
Debug: Encontrado dispositivo - node_id: e80a9939-59d7-44e0-9a46-1fade44e1b78, display_id: 041fea68-1b2b-4087-be60-b254def81cf6
Debug: Usando elemento XML con ID 041fea68-1b2b-4087-be60-b254def81cf6
Debug: Buscando elementos Address en dispositivo M34210
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 4220 -> 516
Debug: Buscando elementos Address en dispositivo M34210
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 4440 -> 516
Procesando cambios para: A40510+M34410
Debug: Encontrado dispositivo - node_id: 8c51fa26-883a-468c-8c36-c0e1b31852e4, display_id: 28037fc6-dc45-4ec9-bea0-eafb922f6c6e
Debug: Usando elemento XML con ID 28037fc6-dc45-4ec9-bea0-eafb922f6c6e
Debug: Buscando elementos Address en dispositivo M34410
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 362 -> 564
Debug: Buscando elementos Address en dispositivo M34410
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 340 -> 564
Procesando cambios para: A40510+M34310
Debug: Encontrado dispositivo - node_id: 17be2ccc-dede-4187-ba77-1ad8499a7349, display_id: 7a77a815-8c59-4cfd-81fe-d851a8e57aea
Debug: Usando elemento XML con ID 7a77a815-8c59-4cfd-81fe-d851a8e57aea
Debug: Buscando elementos Address en dispositivo M34310
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: ¡Encontrado elemento Input compatible!
✓ IO Input Start Address actualizada: 4242 -> 540
Debug: Buscando elementos Address en dispositivo M34310
Debug: No se encontró Address directo, buscando en elementos hijos...
Debug: No se encontró Address en hijos, buscando en elementos padre...
Debug: Encontrados 1 elemento(s) Address
Debug: Explorando Address element...
Debug: Encontrados 2 sub-elementos en Address
Debug: Revisando sub-elemento: 1
Debug: Encontrado IoType: Input
Debug: Revisando sub-elemento: 2
Debug: Encontrado IoType: Output
Debug: ¡Encontrado elemento Output compatible!
✓ IO Output Start Address actualizada: 4484 -> 540
Total de cambios aplicados: 44
Archivo AML actualizado guardado en: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_updated.aml
¡Proceso completado exitosamente!
Archivo AML actualizado: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_updated.aml
--- ERRORES (STDERR) ---
D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\IO_adaptation\x7_update_CAx.py:532: FutureWarning: Truth-testing of elements was a source of confusion and will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.
if target_io_element:
D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\IO_adaptation\x7_update_CAx.py:535: FutureWarning: Truth-testing of elements was a source of confusion and will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.
if not target_io_element:
--- FIN DEL LOG ---

View File

@ -24,8 +24,29 @@ if __name__ == "__main__":
working_dir = configs.get("working_directory", "")
""""
try:
configs = load_configuration()
working_directory = configs.get("working_directory")
except Exception as e:
print(f"Warning: Could not load configuration (frontend not running): {e}")
configs = {}
working_directory = None
# Validate working directory with .debug fallback for standalone execution
if not working_directory or not os.path.isdir(working_directory):
print("Working directory not set or invalid in configuration.")
print("Using .debug directory as fallback for direct script execution.")
# Fallback to .debug directory under script location
script_dir = os.path.dirname(os.path.abspath(__file__))
debug_dir = os.path.join(script_dir, ".debug")
# Create .debug directory if it doesn't exist
os.makedirs(debug_dir, exist_ok=True)
working_directory = debug_dir
print(f"Using debug directory: {working_directory}")
else:
print(f"Using configured working directory: {working_directory}")
# Acceder a la configuración específica del grupo
group_config = configs.get("level2", {})
@ -38,6 +59,34 @@ if __name__ == "__main__":
### Debug Directory Fallback
Cuando los scripts se ejecutan directamente (no desde el frontend), utilizan un sistema de fallback para determinar el directorio de trabajo:
1. **Con Frontend**: Los scripts utilizan el `working_directory` configurado dinámicamente a través de `load_configuration()`
2. **Ejecución Directa (Debug)**: Si no hay configuración válida, los scripts crean automáticamente un directorio `.debug` en la ubicación del script
**Estructura del directorio de debug:**
```
IO_adaptation/
├── .debug/ # ← Directorio creado automáticamente para debug
│ ├── *.aml # Archivos AML procesados
│ ├── *.hierarchical.json # Datos estructurados extraídos
│ ├── *_IO_Upward_Debug.md # Archivo de debug de conexiones IO
│ ├── Hardware.md # Tabla resumen de IO
│ └── <PLC_Name>/
│ └── Documentation/
│ └── *_Hardware_Tree.md # Árbol de hardware por PLC
├── x2_process_CAx.py # Script principal
└── example/ # Archivos de ejemplo
```
**Ventajas del directorio .debug:**
- ✅ Separación clara entre archivos de debug y archivos de producción
- ✅ Fácil limpieza (se puede eliminar todo el directorio `.debug`)
- ✅ No interfiere con archivos del proyecto principal
- ✅ Creación automática sin configuración manual
### Directory structure for Tia Portal scripts
<working_directory>/

View File

@ -8,5 +8,5 @@
"ObsideanProjectsBase": "\\04-SIDEL"
},
"level3": {},
"working_directory": "C:\\Trabajo\\SIDEL\\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\\Reporte\\TAGsIO\\v2"
"working_directory": "D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia"
}

View File

@ -29,10 +29,64 @@
"long_description": "# Script de Adaptación IO para PLCTags\n\n## Descripción\n\nEste script automatiza la adaptación de tags PLC entre hardware físico Siemens TIA Portal y software master, actualizando las direcciones lógicas según un archivo de mapeo en formato Markdown.\n\n## Funcionamiento\n\n### Entradas\n- **Archivo Excel**: Exportación de tags desde TIA Portal\n- **Archivo Markdown**: Tabla de adaptación IO con mapeo entre hardware y master tags\n\n### Proceso\n1. **Análisis inicial**:\n - Lee la tabla Markdown de adaptación IO\n - Identifica las columnas de IO y Master Tag\n - Crea un diccionario de mapeo entre tags y direcciones IO\n\n2. **Procesamiento de tags**:\n - Filtra tags en los paths \"Inputs\", \"Outputs\", \"IO Not in Hardware\\InputsMaster\" y \"IO Not in Hardware\\OutputsMaster\"\n - Actualiza las direcciones lógicas de los tags encontrados en el mapeo\n - Reasigna paths según el tipo de señal (%E -> \"Inputs\", %A -> \"Outputs\")\n\n3. **Gestión de tags sin hardware**:\n - Asigna direcciones de memoria (%Mxxxx.x) para tags sin correspondencia\n - Tags de entrada: Asignados a \"IO Not in Hardware\\InputsMaster\" desde %M3600.0\n - Tags de salida: Asignados a \"IO Not in Hardware\\OutputsMaster\" desde %M3800.0\n\n4. **Manejo de direcciones**:\n - Convierte formatos (I0.0 → %E0.0, PEW100 → %EW100, etc.)\n - Gestiona bits incrementalmente (0.0, 0.1, ..., 0.7, luego 1.0)\n - Alinea words cada 2 bytes (%MW3600, %MW3602, etc.)\n\n### Salidas\n- **Archivo Excel actualizado**: Con las nuevas direcciones lógicas y paths\n- **Archivo de log**: Registro detallado del proceso con estadísticas\n\n## Uso\n1. Ejecute el script\n2. Seleccione el archivo Excel exportado de TIA Portal\n3. Seleccione el archivo Markdown con la tabla de adaptación\n4. El script generará automáticamente el archivo Excel actualizado\n\n## Detección de tipos\n- **Entradas**: Identificadas por prefijos (DI_, P_AI_, etc.)\n- **Salidas**: Identificadas por prefijos (DO_, P_AO_, etc.)\n- **Bits vs Words**: Determinados por el tipo de dato (Bool vs Word/Int)",
"hidden": false
},
"x7_update_CAx.py": {
"display_name": "7: Actualizar AML desde Excel modificado",
"short_description": "Actualiza un archivo AML original basándose en las modificaciones hechas en el archivo Excel de IOs.",
"long_description": "Este script permite hacer un 'round trip' de datos: extrae información del AML al Excel, permite ediciones manuales en el Excel, y luego aplica esos cambios de vuelta al AML original.\n***\n**Pipeline:**\n\n1. **Entrada:** Solicita el archivo AML original y el archivo Excel modificado (generado por x2_process_CAx.py)\n2. **Validación:** Genera un Excel de referencia desde el AML y compara con el Excel modificado para validar que la estructura base sea la misma (mismos nodos y nombres)\n3. **Detección de cambios:** Identifica modificaciones en:\n - Direcciones IP/nodos\n - Direcciones IO (inputs/outputs)\n - Tipos de dispositivos\n - Números de orden\n - Versiones de firmware\n4. **Aplicación:** Localiza los elementos correspondientes en el XML del AML y aplica los cambios detectados\n5. **Salida:** Genera un nuevo archivo AML con sufijo '_updated' que contiene todas las modificaciones\n\n**ID único:** Utiliza PLC+Device Name como identificador único para mapear cambios entre Excel y AML.\n\n**Restricciones:** Solo procede si el Excel modificado mantiene la misma cantidad de nodos y nombres que el Excel original.",
"hidden": false
},
"x0_documentation.py": {
"display_name": "0:Documentación",
"short_description": "Descripción del flujo de trabajo",
"long_description": "### Flujo de trabajo:\n***\n1. Se usa [x1] para exportar los datos de configuración desde el proyecto de Tia Portal, incluidos los IO configurados en el hardware. Esto genera un archivo AML.\n2. Con [x2] se procesa el archivo AML y se convierte en markdown. Si hay un solo PLC se genera el archivo [Hardware.md]\n3. Se deben procesar los IO **desde el esquema eléctrico y agregarlos** a [Hardware.md]\n4. Se debe exportar todos los tags como un archivo excel desde el Tia Portal.\n5. Con [x3] se convierte y se filtran los tags según los path definidos en [io_paths_config] y se genera un archivo Master IO Tags.md\n6. [x4] Genera el prompt a usar con Claude usando MCP para que pueda acceder a los archivos originales y evitar tener que hacer uploads.\n7. Una vez que se genera el archivo procesado por el LLM se puede usar [x5] que convierte las adaptaciones a un archivo que luego se puede importar en Tia Portal\n8. Importar en Tia Portal el archivo [PLCTags_Updated.xlsx]",
"long_description": "### Flujo de trabajo:\n***\n1. Se usa [x1] para exportar los datos de configuración desde el proyecto de Tia Portal, incluidos los IO configurados en el hardware. Esto genera un archivo AML.\n2. Con [x2] se procesa el archivo AML y se convierte en markdown. Si hay un solo PLC se genera el archivo [Hardware.md]. **Además genera un archivo Excel con los IOs por nodos del PLC.**\n3. Se deben procesar los IO **desde el esquema eléctrico y agregarlos** a [Hardware.md]\n4. Se debe exportar todos los tags como un archivo excel desde el Tia Portal.\n5. Con [x3] se convierte y se filtran los tags según los path definidos en [io_paths_config] y se genera un archivo Master IO Tags.md\n6. [x4] Genera el prompt a usar con Claude usando MCP para que pueda acceder a los archivos originales y evitar tener que hacer uploads.\n7. Una vez que se genera el archivo procesado por el LLM se puede usar [x5] que convierte las adaptaciones a un archivo que luego se puede importar en Tia Portal\n8. Importar en Tia Portal el archivo [PLCTags_Updated.xlsx]\n\n**Nuevo flujo alternativo con Excel:**\nDesp¹és del paso 2, se puede usar [x7] para:\n- Modificar manualmente el archivo Excel de IOs generado\n- Aplicar los cambios de vuelta al archivo AML original\n- Generar un AML actualizado con las modificaciones",
"hidden": false
},
"analyze_u32810_specific.py": {
"display_name": "analyze_u32810_specific",
"short_description": "Sin descripción corta.",
"long_description": "",
"hidden": false
},
"compare_u32810_telegrams.py": {
"display_name": "compare_u32810_telegrams",
"short_description": "Sin descripción corta.",
"long_description": "",
"hidden": false
},
"comprehensive_address_search.py": {
"display_name": "comprehensive_address_search",
"short_description": "Sin descripción corta.",
"long_description": "",
"hidden": false
},
"find_address_differences.py": {
"display_name": "find_address_differences",
"short_description": "Sin descripción corta.",
"long_description": "",
"hidden": false
},
"find_u32810_addresses.py": {
"display_name": "find_u32810_addresses",
"short_description": "Sin descripción corta.",
"long_description": "",
"hidden": false
},
"debug_hierarchy.py": {
"display_name": "debug_hierarchy",
"short_description": "Sin descripción corta.",
"long_description": "",
"hidden": false
},
"debug_io_processing.py": {
"display_name": "debug_io_processing",
"short_description": "Sin descripción corta.",
"long_description": "",
"hidden": false
},
"debug_table_generation.py": {
"display_name": "debug_table_generation",
"short_description": "Sin descripción corta.",
"long_description": "",
"hidden": false
}
}

View File

@ -1,6 +1,8 @@
{
"path": "C:\\Trabajo\\SIDEL\\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\\Reporte\\TAGsIO\\v2",
"path": "D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia",
"history": [
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia",
"D:\\Proyectos\\Scripts\\ParamManagerScripts\\backend\\script_groups\\IO_adaptation\\example",
"C:\\Trabajo\\SIDEL\\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\\Reporte\\TAGsIO\\v2"
]
}

View File

@ -17,7 +17,12 @@ sys.path.append(script_root)
from backend.script_utils import load_configuration
# --- Configuration ---
TIA_PORTAL_VERSION = "18.0" # Target TIA Portal version
# Supported TIA Portal versions mapping (extension -> version)
SUPPORTED_TIA_VERSIONS = {
".ap18": "18.0",
".ap19": "19.0",
".ap20": "20.0"
}
# --- TIA Scripting Import Handling ---
# (Same import handling as the previous script)
@ -40,18 +45,43 @@ except Exception as e:
# --- Functions ---
def get_supported_filetypes():
"""Returns the supported file types for TIA Portal projects."""
filetypes = []
for ext, version in SUPPORTED_TIA_VERSIONS.items():
version_major = version.split('.')[0]
filetypes.append((f"TIA Portal V{version_major} Projects", f"*{ext}"))
# Add option to show all supported files
all_extensions = " ".join([f"*{ext}" for ext in SUPPORTED_TIA_VERSIONS.keys()])
filetypes.insert(0, ("All TIA Portal Projects", all_extensions))
return filetypes
def detect_tia_version(project_file_path):
"""Detects TIA Portal version based on file extension."""
file_path = Path(project_file_path)
file_extension = file_path.suffix.lower()
if file_extension in SUPPORTED_TIA_VERSIONS:
detected_version = SUPPORTED_TIA_VERSIONS[file_extension]
print(f"Detected TIA Portal version: {detected_version} (from extension {file_extension})")
return detected_version
else:
print(f"WARNING: Unrecognized file extension '{file_extension}'. Supported extensions: {list(SUPPORTED_TIA_VERSIONS.keys())}")
# Default to version 18.0 for backward compatibility
print("Defaulting to TIA Portal V18.0")
return "18.0"
def select_project_file():
"""Opens a dialog to select a TIA Portal project file."""
root = tk.Tk()
root.withdraw()
file_path = filedialog.askopenfilename(
title="Select TIA Portal Project File",
filetypes=[
(
f"TIA Portal V{TIA_PORTAL_VERSION} Projects",
f"*.ap{TIA_PORTAL_VERSION.split('.')[0]}",
)
],
filetypes=get_supported_filetypes(),
)
root.destroy()
if not file_path:
@ -242,6 +272,9 @@ if __name__ == "__main__":
print(f"\nSelected Project: {project_file}")
print(f"Using Output Directory (Working Directory): {output_dir}")
# 2. Detect TIA Portal version from project file
tia_version = detect_tia_version(project_file)
# Define output file names using Path object
project_path = Path(project_file)
project_base_name = project_path.stem # Get filename without extension
@ -260,15 +293,15 @@ if __name__ == "__main__":
cax_export_successful = False
try:
# 2. Connect to TIA Portal
print(f"\nConnecting to TIA Portal V{TIA_PORTAL_VERSION}...")
# 3. Connect to TIA Portal with detected version
print(f"\nConnecting to TIA Portal V{tia_version}...")
portal_instance = ts.open_portal(
version=TIA_PORTAL_VERSION,
version=tia_version,
portal_mode=ts.Enums.PortalMode.WithGraphicalUserInterface,
)
print("Connected.")
# 3. Open Project
# 4. Open Project
print(
f"Opening project: {project_path.name}..."
) # Use Path object's name attribute
@ -282,7 +315,7 @@ if __name__ == "__main__":
raise Exception("Failed to open or get the specified project.")
print("Project opened.")
# 4. Export CAx Data (Project Level)
# 5. Export CAx Data (Project Level)
print(f"Exporting CAx data for the project to {aml_file}...")
# Ensure output directory exists (Path.mkdir handles this implicitly if needed later, but good practice)
output_dir.mkdir(parents=True, exist_ok=True)
@ -322,7 +355,7 @@ if __name__ == "__main__":
except Exception as close_ex:
print(f"Error during TIA Portal cleanup: {close_ex}")
# 5. Parse AML and Generate Markdown (only if export was successful)
# 6. Parse AML and Generate Markdown (only if export was successful)
if cax_export_successful:
if aml_file.exists(): # Use Path object's exists() method
parse_aml_to_markdown(aml_file, md_file)

View File

@ -12,6 +12,7 @@ import json
from pathlib import Path
import re
import math # Needed for ceil
import pandas as pd # For Excel generation
script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
@ -221,6 +222,7 @@ def extract_aml_data(root):
is_subnet_by_role = any("SUBNET" in rc.upper() for rc in role_classes)
if is_subnet_by_role:
is_network = True
# Check role classes first
for rc in role_classes:
rc_upper = rc.upper()
if "PROFINET" in rc_upper or "ETHERNET" in rc_upper:
@ -229,12 +231,23 @@ def extract_aml_data(root):
elif "PROFIBUS" in rc_upper:
net_type = "Profibus"
break
# If still unknown, check the Type attribute (crucial for PROFINET)
if net_type == "Unknown":
type_attr = device.get("attributes", {}).get("Type", "")
if type_attr.upper() == "ETHERNET":
net_type = "Ethernet/Profinet"
elif type_attr.upper() == "PROFIBUS":
net_type = "Profibus"
# Finally, check device name as fallback
if net_type == "Unknown":
if "PROFIBUS" in device["name"].upper():
net_type = "Profibus"
elif (
"ETHERNET" in device["name"].upper()
or "PROFINET" in device["name"].upper()
or "PN/IE" in device["name"].upper() # Add common PROFINET naming pattern
):
net_type = "Ethernet/Profinet"
if is_network:
@ -328,6 +341,33 @@ def extract_aml_data(root):
and interface_info["parent_id"] in data["plcs"]
):
potential_plc_id = interface_info["parent_id"]
# Enhanced PLC search: look for PLCs in the entire hierarchy
if not potential_plc_id:
# Search for PLCs in the hierarchy starting from linked_device_id
current_search_id = linked_device_id
search_depth = 0
max_search_depth = 10
while current_search_id and search_depth < max_search_depth:
# Check current device and all its children for PLCs
device_to_check = data["devices"].get(current_search_id)
if device_to_check:
# Check if current device has PLCs as children
for child_id in device_to_check.get("children_ids", []):
if child_id in data["plcs"]:
potential_plc_id = child_id
print(f" --> Found PLC in children: {data['plcs'][child_id].get('name', 'Unknown PLC')} (ID: {child_id})")
break
if potential_plc_id:
break
# Move up to parent
current_search_id = device_to_check.get("parent_id")
search_depth += 1
else:
break
if potential_plc_id:
plc_object = data["plcs"][potential_plc_id]
if "connected_networks" not in plc_object:
@ -497,6 +537,470 @@ def generate_io_summary_file(all_plc_io_for_table, md_file_path, plc_name, proje
return hardware_file_path
# --- generate_io_excel_report function ---
def generate_io_excel_report(project_data, excel_file_path, target_plc_id, output_root_path):
"""
Genera un archivo Excel con información detallada de IOs por nodos del PLC.
"""
plc_info = project_data.get("plcs", {}).get(target_plc_id)
if not plc_info:
print(f"PLC ID '{target_plc_id}' not found in project data.")
return
plc_name = plc_info.get('name', target_plc_id)
print(f"Generating Excel IO report for PLC: {plc_name}")
# Lista para almacenar todas las filas del Excel
excel_rows = []
# v32.5: First, process PLC's local modules (modules in the same rack/structure as the PLC)
plc_parent_id = plc_info.get("parent_id")
if plc_parent_id:
# Find sibling modules in the same parent structure (rack)
for dev_id, dev_info in project_data.get("devices", {}).items():
if dev_info.get("parent_id") == plc_parent_id and dev_id != target_plc_id:
# This is a sibling module of the PLC
module_context = {
"id": dev_id,
"name": dev_info.get("name", dev_id),
"order_number": dev_info.get("order_number", "N/A"),
"type_name": dev_info.get("type_name", "N/A")
}
module_ios = find_io_recursively(dev_id, project_data, module_context)
if module_ios:
# Agrupar IOs por módulo para este módulo local
ios_by_module = {}
for addr_info in module_ios:
module_id = addr_info.get("module_id")
if module_id not in ios_by_module:
ios_by_module[module_id] = {
'module_info': {
'name': addr_info.get('module_name', '?'),
'type': addr_info.get('module_type_name', 'N/A'),
'order': addr_info.get('module_order_number', 'N/A'),
'position': addr_info.get('module_pos', 'N/A')
},
'inputs': [],
'outputs': []
}
# Clasificar IO como input u output
io_type = addr_info.get("type", "").lower()
if io_type == "input":
ios_by_module[module_id]['inputs'].append(addr_info)
elif io_type == "output":
ios_by_module[module_id]['outputs'].append(addr_info)
# Crear una fila por cada módulo local con IOs
for module_id, module_data in ios_by_module.items():
module_info = module_data['module_info']
# Calcular direcciones de entrada - Start + Word Count
input_start_addr = 'N/A'
input_word_count = 0
total_input_bits = 0
for addr_info in module_data['inputs']:
start_str = addr_info.get("start", "?")
length_str = addr_info.get("length", "?")
try:
start_byte = int(start_str)
length_bits = int(length_str)
length_bytes = math.ceil(length_bits / 8.0)
if length_bits > 0 and length_bytes == 0:
length_bytes = 1
# Para múltiples rangos, tomar el primer inicio y sumar words
if input_start_addr == 'N/A':
input_start_addr = start_byte
else:
input_start_addr = min(input_start_addr, start_byte)
# Convertir bytes a words (asumiendo words de 2 bytes)
word_count = math.ceil(length_bytes / 2.0)
input_word_count += word_count
total_input_bits += length_bits
except:
# En caso de error, mantener N/A
pass
# Calcular direcciones de salida - Start + Word Count
output_start_addr = 'N/A'
output_word_count = 0
total_output_bits = 0
for addr_info in module_data['outputs']:
start_str = addr_info.get("start", "?")
length_str = addr_info.get("length", "?")
try:
start_byte = int(start_str)
length_bits = int(length_str)
length_bytes = math.ceil(length_bits / 8.0)
if length_bits > 0 and length_bytes == 0:
length_bytes = 1
# Para múltiples rangos, tomar el primer inicio y sumar words
if output_start_addr == 'N/A':
output_start_addr = start_byte
else:
output_start_addr = min(output_start_addr, start_byte)
# Convertir bytes a words (asumiendo words de 2 bytes)
word_count = math.ceil(length_bytes / 2.0)
output_word_count += word_count
total_output_bits += length_bits
except:
# En caso de error, mantener N/A
pass
excel_rows.append({
'PLC Name': plc_name,
'Network Path': f"Local I/O -> {module_info['name']}",
'Network Type': 'Local I/O',
'Device Address': 'Local',
'Device Name': f"PLC {plc_name}",
'Device Type': module_info['type'],
'Order Number': module_info['order'],
'Firmware Version': 'N/A',
'Position': module_info['position'],
'IO Input Start Address': input_start_addr,
'IO Input Word Count': input_word_count if input_word_count > 0 else 'N/A',
'IO Output Start Address': output_start_addr,
'IO Output Word Count': output_word_count if output_word_count > 0 else 'N/A',
'Total Input Bits': total_input_bits,
'Total Output Bits': total_output_bits,
'Module Name': module_info['name'],
'Module Type': module_info['type'],
'Module Order Number': module_info['order']
})
# Procesar las redes conectadas al PLC
plc_networks = plc_info.get("connected_networks", {})
if not plc_networks and not excel_rows:
# Si no hay redes, crear una fila básica del PLC
excel_rows.append({
'PLC Name': plc_name,
'Network Path': 'No networks connected',
'Network Type': 'N/A',
'Device Address': 'N/A',
'Device Name': plc_name,
'Device Type': plc_info.get("type_name", "N/A"),
'Order Number': plc_info.get("order_number", "N/A"),
'Firmware Version': plc_info.get("firmware_version", "N/A"),
'Position': plc_info.get("position", "N/A"),
'IO Input Start Address': 'N/A',
'IO Input Word Count': 'N/A',
'IO Output Start Address': 'N/A',
'IO Output Word Count': 'N/A',
'Total Input Bits': 0,
'Total Output Bits': 0,
'Module Name': 'N/A',
'Module Type': 'N/A',
'Module Order Number': 'N/A'
})
else:
# Procesar cada red conectada
for net_id, plc_addr_on_net in plc_networks.items():
net_info = project_data.get("networks", {}).get(net_id)
if not net_info:
continue
network_name = net_info.get('name', net_id)
network_type = net_info.get('type', 'Unknown')
devices_on_net = net_info.get("devices_on_net", {})
# Identificar nodos que pertenecen al PLC para excluirlos de la lista de dispositivos
plc_interface_and_node_ids = set()
for node in plc_info.get("network_nodes", []):
plc_interface_and_node_ids.add(node["id"])
interface_id_lookup = project_data["devices"].get(node["id"], {}).get("parent_id")
if interface_id_lookup:
plc_interface_and_node_ids.add(interface_id_lookup)
plc_interface_and_node_ids.add(target_plc_id)
# Filtrar dispositivos que no son interfaces del PLC
other_devices = [
(node_id, node_addr)
for node_id, node_addr in devices_on_net.items()
if node_id not in plc_interface_and_node_ids
]
if not other_devices:
# Si no hay otros dispositivos, crear fila solo para el PLC en esta red
excel_rows.append({
'PLC Name': plc_name,
'Network Path': f"{network_name} -> {plc_name}",
'Network Type': network_type,
'Device Address': plc_addr_on_net,
'Device Name': plc_name,
'Device Type': plc_info.get("type_name", "N/A"),
'Order Number': plc_info.get("order_number", "N/A"),
'Firmware Version': plc_info.get("firmware_version", "N/A"),
'Position': plc_info.get("position", "N/A"),
'IO Input Start Address': 'N/A',
'IO Input Word Count': 'N/A',
'IO Output Start Address': 'N/A',
'IO Output Word Count': 'N/A',
'Total Input Bits': 0,
'Total Output Bits': 0,
'Module Name': 'PLC Main Unit',
'Module Type': plc_info.get("type_name", "N/A"),
'Module Order Number': plc_info.get("order_number", "N/A")
})
else:
# Procesar cada dispositivo en la red
for node_id, node_addr in other_devices:
node_info = project_data.get("devices", {}).get(node_id)
if not node_info:
continue
# Determinar la estructura jerárquica del dispositivo
interface_id = node_info.get("parent_id")
interface_info = None
actual_device_id = None
actual_device_info = None
if interface_id:
interface_info = project_data.get("devices", {}).get(interface_id)
if interface_info:
actual_device_id = interface_info.get("parent_id")
if actual_device_id:
actual_device_info = project_data.get("devices", {}).get(actual_device_id)
# Determinar qué información mostrar
display_info = actual_device_info if actual_device_info else (interface_info if interface_info else node_info)
display_id = actual_device_id if actual_device_info else (interface_id if interface_info else node_id)
device_name = display_info.get("name", display_id)
device_type = display_info.get("type_name", "N/A")
device_order = display_info.get("order_number", "N/A")
device_position = display_info.get("position", "N/A")
firmware_version = display_info.get("firmware_version", "N/A")
# Construir el path de red
network_path = f"{network_name} ({network_type}) -> {device_name} @ {node_addr}"
# Buscar IOs recursivamente
io_search_root_id = display_id
io_search_root_info = project_data.get("devices", {}).get(io_search_root_id)
aggregated_io_addresses = []
# Buscar IOs en la estructura padre si existe
parent_structure_id = io_search_root_info.get("parent_id") if io_search_root_info else None
if parent_structure_id:
# Buscar IOs en dispositivos hermanos bajo la misma estructura padre
for dev_scan_id, dev_scan_info in project_data.get("devices", {}).items():
if dev_scan_info.get("parent_id") == parent_structure_id:
module_context = {
"id": dev_scan_id,
"name": dev_scan_info.get("name", dev_scan_id),
"order_number": dev_scan_info.get("order_number", "N/A"),
"type_name": dev_scan_info.get("type_name", "N/A")
}
io_from_sibling = find_io_recursively(dev_scan_id, project_data, module_context)
aggregated_io_addresses.extend(io_from_sibling)
elif io_search_root_id:
# Buscar IOs directamente en el dispositivo
module_context = {
"id": io_search_root_id,
"name": io_search_root_info.get("name", io_search_root_id),
"order_number": io_search_root_info.get("order_number", "N/A"),
"type_name": io_search_root_info.get("type_name", "N/A")
}
aggregated_io_addresses = find_io_recursively(io_search_root_id, project_data, module_context)
# Procesar IOs por módulo
if aggregated_io_addresses:
# Agrupar IOs por módulo
ios_by_module = {}
for addr_info in aggregated_io_addresses:
module_id = addr_info.get("module_id")
if module_id not in ios_by_module:
ios_by_module[module_id] = {
'module_info': {
'name': addr_info.get('module_name', '?'),
'type': addr_info.get('module_type_name', 'N/A'),
'order': addr_info.get('module_order_number', 'N/A'),
'position': addr_info.get('module_pos', 'N/A')
},
'inputs': [],
'outputs': []
}
# Clasificar IO como input u output
io_type = addr_info.get("type", "").lower()
if io_type == "input":
ios_by_module[module_id]['inputs'].append(addr_info)
elif io_type == "output":
ios_by_module[module_id]['outputs'].append(addr_info)
# Crear una fila por cada módulo con IOs
for module_id, module_data in ios_by_module.items():
module_info = module_data['module_info']
# Calcular direcciones de entrada - Start + Word Count
input_start_addr = 'N/A'
input_word_count = 0
total_input_bits = 0
for addr_info in module_data['inputs']:
start_str = addr_info.get("start", "?")
length_str = addr_info.get("length", "?")
try:
start_byte = int(start_str)
length_bits = int(length_str)
length_bytes = math.ceil(length_bits / 8.0)
if length_bits > 0 and length_bytes == 0:
length_bytes = 1
# Para múltiples rangos, tomar el primer inicio y sumar words
if input_start_addr == 'N/A':
input_start_addr = start_byte
else:
input_start_addr = min(input_start_addr, start_byte)
# Convertir bytes a words (asumiendo words de 2 bytes)
word_count = math.ceil(length_bytes / 2.0)
input_word_count += word_count
total_input_bits += length_bits
except:
# En caso de error, mantener N/A
pass
# Calcular direcciones de salida - Start + Word Count
output_start_addr = 'N/A'
output_word_count = 0
total_output_bits = 0
for addr_info in module_data['outputs']:
start_str = addr_info.get("start", "?")
length_str = addr_info.get("length", "?")
try:
start_byte = int(start_str)
length_bits = int(length_str)
length_bytes = math.ceil(length_bits / 8.0)
if length_bits > 0 and length_bytes == 0:
length_bytes = 1
# Para múltiples rangos, tomar el primer inicio y sumar words
if output_start_addr == 'N/A':
output_start_addr = start_byte
else:
output_start_addr = min(output_start_addr, start_byte)
# Convertir bytes a words (asumiendo words de 2 bytes)
word_count = math.ceil(length_bytes / 2.0)
output_word_count += word_count
total_output_bits += length_bits
except:
# En caso de error, mantener N/A
pass
excel_rows.append({
'PLC Name': plc_name,
'Network Path': network_path,
'Network Type': network_type,
'Device Address': node_addr,
'Device Name': device_name,
'Device Type': device_type,
'Order Number': device_order,
'Firmware Version': firmware_version,
'Position': device_position,
'IO Input Start Address': input_start_addr,
'IO Input Word Count': input_word_count if input_word_count > 0 else 'N/A',
'IO Output Start Address': output_start_addr,
'IO Output Word Count': output_word_count if output_word_count > 0 else 'N/A',
'Total Input Bits': total_input_bits,
'Total Output Bits': total_output_bits,
'Module Name': module_info['name'],
'Module Type': module_info['type'],
'Module Order Number': module_info['order']
})
else:
# Dispositivo sin IOs
excel_rows.append({
'PLC Name': plc_name,
'Network Path': network_path,
'Network Type': network_type,
'Device Address': node_addr,
'Device Name': device_name,
'Device Type': device_type,
'Order Number': device_order,
'Firmware Version': firmware_version,
'Position': device_position,
'IO Input Start Address': 'N/A',
'IO Input Word Count': 'N/A',
'IO Output Start Address': 'N/A',
'IO Output Word Count': 'N/A',
'Total Input Bits': 0,
'Total Output Bits': 0,
'Module Name': 'N/A',
'Module Type': 'N/A',
'Module Order Number': 'N/A'
})
# Crear DataFrame y guardar Excel
if excel_rows:
df = pd.DataFrame(excel_rows)
# Agregar columna de ID único para compatibilidad con x7_update_CAx
df['Unique_ID'] = df['PLC Name'] + "+" + df['Device Name']
# Reordenar columnas para mejor legibilidad
column_order = [
'PLC Name', 'Network Path', 'Network Type', 'Device Address', 'Device Name',
'Device Type', 'Order Number', 'Firmware Version', 'Position',
'Module Name', 'Module Type', 'Module Order Number',
'IO Input Start Address', 'IO Input Word Count', 'IO Output Start Address', 'IO Output Word Count',
'Total Input Bits', 'Total Output Bits', 'Unique_ID' # Agregar al final para compatibilidad
]
df = df.reindex(columns=column_order)
try:
# Convertir columnas numéricas específicas a int para evitar decimales
numeric_columns = [
'IO Input Start Address', 'IO Input Word Count',
'IO Output Start Address', 'IO Output Word Count',
'Total Input Bits', 'Total Output Bits'
]
for col in numeric_columns:
if col in df.columns:
# Convertir a int manteniendo N/A como string
df[col] = df[col].apply(lambda x: int(x) if isinstance(x, (int, float)) and pd.notna(x) and x != 'N/A' else x)
# Guardar como Excel con formato
with pd.ExcelWriter(excel_file_path, engine='openpyxl') as writer:
df.to_excel(writer, sheet_name='IO Report', index=False)
# Ajustar ancho de columnas
worksheet = writer.sheets['IO Report']
for column in worksheet.columns:
max_length = 0
column_letter = column[0].column_letter
for cell in column:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max_length + 2, 50) # Máximo 50 caracteres
worksheet.column_dimensions[column_letter].width = adjusted_width
print(f"Excel IO report saved to: {excel_file_path}")
print(f"Total rows in report: {len(excel_rows)}")
except Exception as e:
print(f"ERROR saving Excel file {excel_file_path}: {e}")
traceback.print_exc()
else:
print("No data to write to Excel file.")
# --- generate_markdown_tree function ---
def generate_markdown_tree(project_data, md_file_path, target_plc_id, output_root_path):
"""(Modified) Generates hierarchical Markdown for a specific PLC."""
@ -533,6 +1037,92 @@ def generate_markdown_tree(project_data, md_file_path, target_plc_id, output_roo
if firmware and firmware != "N/A":
markdown_lines.append(f"- **Firmware:** `{firmware}`")
# v32.5: Process PLC's local modules (modules in the same rack/structure as the PLC)
plc_local_modules_io = []
plc_parent_id = plc_info.get("parent_id")
if plc_parent_id:
# Find sibling modules in the same parent structure (rack)
for dev_id, dev_info in project_data.get("devices", {}).items():
if dev_info.get("parent_id") == plc_parent_id and dev_id != target_plc_id:
# This is a sibling module of the PLC
module_context = {
"id": dev_id,
"name": dev_info.get("name", dev_id),
"order_number": dev_info.get("order_number", "N/A"),
"type_name": dev_info.get("type_name", "N/A")
}
module_ios = find_io_recursively(dev_id, project_data, module_context)
if module_ios:
for addr_info in module_ios:
# Add to table data
length_bits = 0
siemens_addr = "FMT_ERROR"
try:
start_byte = int(addr_info.get("start", "0"))
length_bits = int(addr_info.get("length", "0"))
io_type = addr_info.get("type", "?")
length_bytes = math.ceil(length_bits / 8.0)
if length_bits > 0 and length_bytes == 0:
length_bytes = 1
end_byte = start_byte + length_bytes - 1
prefix = "P?"
if io_type.lower() == "input":
prefix = "EW"
elif io_type.lower() == "output":
prefix = "AW"
siemens_addr = f"{prefix} {start_byte}..{end_byte}"
except Exception:
siemens_addr = f"FMT_ERROR({addr_info.get('start', '?')},{addr_info.get('length', '?')})"
plc_local_modules_io.append({
"Network": "PLC Local Modules",
"Network Type": "Local I/O",
"Device Address": "Local",
"Device Name": f"PLC {plc_info.get('name', target_plc_id)}",
"Sub-Device": addr_info.get('module_name', '?'),
"Sub-Device OrderNo": addr_info.get('module_order_number', 'N/A'),
"Sub-Device Type": addr_info.get('module_type_name', 'N/A'),
"IO Type": addr_info.get("type", "?"),
"IO Address": siemens_addr,
"Number of Bits": length_bits,
"SortKey": (
"PLC Local Modules", # Network name for sorting
[0], # Device sort key (always first)
(
int(addr_info.get("module_pos", "9999"))
if str(addr_info.get("module_pos", "9999")).isdigit()
else 9999
),
addr_info.get("module_name", ""),
addr_info.get("type", ""),
(
int(addr_info.get("start", "0"))
if str(addr_info.get("start", "0")).isdigit()
else float("inf")
),
)
})
# Add PLC local modules to the main IO list
all_plc_io_for_table.extend(plc_local_modules_io)
# Show PLC local modules in markdown
if plc_local_modules_io:
markdown_lines.append("\n- **Local I/O Modules (same rack as PLC):**")
modules_by_name = {}
for io_data in plc_local_modules_io:
module_name = io_data["Sub-Device"]
if module_name not in modules_by_name:
modules_by_name[module_name] = []
modules_by_name[module_name].append(io_data)
for module_name in sorted(modules_by_name.keys()):
module_ios = modules_by_name[module_name]
first_io = module_ios[0]
markdown_lines.append(f" - **{module_name}** (Type: `{first_io['Sub-Device Type']}`, OrderNo: `{first_io['Sub-Device OrderNo']}`)")
for io_data in sorted(module_ios, key=lambda x: x['SortKey']):
markdown_lines.append(f" - `{io_data['IO Address']}` ({io_data['IO Type']}, {io_data['Number of Bits']} bits)")
plc_networks = plc_info.get("connected_networks", {})
markdown_lines.append("\n- **Networks:**")
if not plc_networks:
@ -1151,25 +1741,34 @@ def sanitize_filename(name):
# --- Main Execution ---
if __name__ == "__main__":
try:
configs = load_configuration()
working_directory = configs.get("working_directory")
except Exception as e:
print(f"Warning: Could not load configuration (frontend not running): {e}")
configs = {}
working_directory = None
script_version = "v31.1 - Corrected IO Summary Table Initialization" # Updated version
script_version = "v32.5 - Include PLC Local Modules in IO Summary" # Updated version
print(
f"--- AML (CAx Export) to Hierarchical JSON and Obsidian MD Converter ({script_version}) ---"
)
# Validate working directory
# Validate working directory with .debug fallback
if not working_directory or not os.path.isdir(working_directory):
print("ERROR: Working directory not set or invalid in configuration.")
print("Attempting to use script's directory as fallback.")
# Fallback to script's directory or current directory if needed
working_directory = os.path.dirname(os.path.abspath(__file__))
if not os.path.isdir(working_directory):
working_directory = os.getcwd()
print(f"Using fallback directory: {working_directory}")
# Optionally, prompt user to select a working directory here if critical
# output_dir = select_output_directory() # Keep this if you want user selection on failure
print("Working directory not set or invalid in configuration.")
print("Using .debug directory as fallback for direct script execution.")
# Fallback to .debug directory under script location
script_dir = os.path.dirname(os.path.abspath(__file__))
debug_dir = os.path.join(script_dir, ".debug")
# Create .debug directory if it doesn't exist
os.makedirs(debug_dir, exist_ok=True)
working_directory = debug_dir
print(f"Using debug directory: {working_directory}")
else:
print(f"Using configured working directory: {working_directory}")
# Use working_directory as the output directory
output_dir = working_directory
@ -1227,6 +1826,12 @@ if __name__ == "__main__":
print(f" Generating Hardware Tree for PLC '{plc_name_original}' (ID: {plc_id}) at: {output_plc_md_file.resolve()}")
# Pass output_path as the root directory for Hardware.md placement
generate_markdown_tree(project_data, str(output_plc_md_file), plc_id, str(output_path))
# Generate Excel IO report for this PLC
excel_io_filename = f"{input_path.stem}_IO_Report.xlsx"
output_excel_file = plc_doc_dir / excel_io_filename
print(f" Generating Excel IO Report for PLC '{plc_name_original}' (ID: {plc_id}) at: {output_excel_file.resolve()}")
generate_io_excel_report(project_data, str(output_excel_file), plc_id, str(output_path))
else:
print("\nFailed to process AML data. Halting before generating PLC-specific trees.")

View File

@ -0,0 +1,696 @@
"""
x7_update_CAx.py : Script que actualiza un archivo AML original basándose en las modificaciones
hechas en el archivo Excel de IOs generado por x2_process_CAx.py
Pipeline:
1. Lee el AML original
2. Lee el Excel modificado
3. Genera Excel de referencia desde el AML para comparar
4. Valida que la estructura base sea la misma (mismos nodos, nombres)
5. Identifica cambios en IOs, direcciones IP, etc.
6. Aplica los cambios al AML original
7. Genera un nuevo archivo AML con sufijo "_updated"
"""
import os
import sys
import tkinter as tk
from tkinter import filedialog, messagebox
import traceback
from lxml import etree as ET
import pandas as pd
from pathlib import Path
import re
import math
import tempfile
script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
sys.path.append(script_root)
from backend.script_utils import load_configuration
# Import functions from x2_process_CAx
from x2_process_CAx import (
extract_aml_data,
generate_io_excel_report,
sanitize_filename
)
def select_aml_file(title="Select Original AML File", initial_dir=None):
"""Abre un diálogo para seleccionar un archivo AML."""
root = tk.Tk()
root.withdraw()
file_path = filedialog.askopenfilename(
title=title,
filetypes=[("AML Files", "*.aml"), ("All Files", "*.*")],
initialdir=initial_dir
)
root.destroy()
if not file_path:
return None
return file_path
def select_excel_file(title="Select Modified Excel File", initial_dir=None):
"""Abre un diálogo para seleccionar un archivo Excel."""
root = tk.Tk()
root.withdraw()
file_path = filedialog.askopenfilename(
title=title,
filetypes=[("Excel Files", "*.xlsx"), ("Excel Files", "*.xls"), ("All Files", "*.*")],
initialdir=initial_dir
)
root.destroy()
if not file_path:
return None
return file_path
def generate_reference_excel_from_aml(aml_file_path, temp_dir):
"""
Genera un Excel de referencia desde el AML para comparar con el Excel modificado.
Retorna el path al Excel generado y los project_data.
"""
print("Generando Excel de referencia desde AML original...")
# Extraer datos del AML
try:
parser = ET.XMLParser(remove_blank_text=True, huge_tree=True)
tree = ET.parse(aml_file_path, parser)
root = tree.getroot()
project_data = extract_aml_data(root)
except Exception as e:
print(f"ERROR procesando archivo AML {aml_file_path}: {e}")
return None, None
if not project_data or not project_data.get("plcs"):
print("No se encontraron PLCs en el archivo AML.")
return None, None
# Generar Excel para cada PLC y combinar en uno solo
all_excel_rows = []
for plc_id, plc_data in project_data.get("plcs", {}).items():
# Crear archivo temporal para este PLC
temp_excel_path = os.path.join(temp_dir, f"temp_plc_{plc_id}.xlsx")
# Generar Excel para este PLC (reutilizamos la función existente)
generate_io_excel_report(project_data, temp_excel_path, plc_id, temp_dir)
# Leer el Excel generado y agregar a la lista combinada
if os.path.exists(temp_excel_path):
try:
df_plc = pd.read_excel(temp_excel_path, sheet_name='IO Report')
# Agregar columna de ID único
df_plc['Unique_ID'] = df_plc['PLC Name'] + "+" + df_plc['Device Name']
all_excel_rows.append(df_plc)
os.remove(temp_excel_path) # Limpiar archivo temporal
except Exception as e:
print(f"ERROR leyendo Excel temporal para PLC {plc_id}: {e}")
if not all_excel_rows:
print("No se pudieron generar datos de Excel de referencia.")
return None, None
# Combinar todos los DataFrames
combined_df = pd.concat(all_excel_rows, ignore_index=True)
# Guardar Excel de referencia
reference_excel_path = os.path.join(temp_dir, "reference_excel.xlsx")
combined_df.to_excel(reference_excel_path, sheet_name='IO Report', index=False)
return reference_excel_path, project_data
def compare_excel_files(reference_excel_path, modified_excel_path):
"""
Compara el Excel de referencia con el Excel modificado.
Retorna (is_valid, changes_dict) donde:
- is_valid: True si la estructura básica es la misma
- changes_dict: diccionario con los cambios detectados
"""
print("Comparando archivos Excel...")
try:
# Leer ambos archivos
df_ref = pd.read_excel(reference_excel_path, sheet_name='IO Report')
df_mod = pd.read_excel(modified_excel_path, sheet_name='IO Report')
# Agregar columna de ID único si no existe
if 'Unique_ID' not in df_ref.columns:
df_ref['Unique_ID'] = df_ref['PLC Name'] + "+" + df_ref['Device Name']
if 'Unique_ID' not in df_mod.columns:
df_mod['Unique_ID'] = df_mod['PLC Name'] + "+" + df_mod['Device Name']
except Exception as e:
print(f"ERROR leyendo archivos Excel: {e}")
return False, {}
# Validar estructura básica
if len(df_ref) != len(df_mod):
print(f"ERROR: Número de filas diferente. Referencia: {len(df_ref)}, Modificado: {len(df_mod)}")
return False, {}
# Verificar que todos los IDs únicos coincidan
ref_ids = set(df_ref['Unique_ID'].tolist())
mod_ids = set(df_mod['Unique_ID'].tolist())
if ref_ids != mod_ids:
missing_in_mod = ref_ids - mod_ids
extra_in_mod = mod_ids - ref_ids
print("ERROR: Los IDs únicos no coinciden entre archivos.")
if missing_in_mod:
print(f" Faltantes en modificado: {missing_in_mod}")
if extra_in_mod:
print(f" Extras en modificado: {extra_in_mod}")
return False, {}
print("Estructura básica validada correctamente.")
# Detectar cambios
changes = {}
columns_to_monitor = [
'Device Address', 'IO Input Start Address', 'IO Input Word Count',
'IO Output Start Address', 'IO Output Word Count',
'Network Type', 'Device Type', 'Order Number', 'Firmware Version'
]
for index, row_ref in df_ref.iterrows():
unique_id = row_ref['Unique_ID']
row_mod = df_mod[df_mod['Unique_ID'] == unique_id].iloc[0]
row_changes = {}
for col in columns_to_monitor:
if col in df_ref.columns and col in df_mod.columns:
# Manejo especial para valores numéricos
ref_raw = row_ref[col]
mod_raw = row_mod[col]
# Convertir a string, manejando floats apropiadamente
if pd.notna(ref_raw):
if isinstance(ref_raw, float) and ref_raw.is_integer():
ref_val = str(int(ref_raw))
else:
ref_val = str(ref_raw).strip()
else:
ref_val = 'N/A'
if pd.notna(mod_raw):
if isinstance(mod_raw, float) and mod_raw.is_integer():
mod_val = str(int(mod_raw))
else:
mod_val = str(mod_raw).strip()
else:
mod_val = 'N/A'
if ref_val != mod_val:
row_changes[col] = {
'original': ref_val,
'modified': mod_val
}
if row_changes:
changes[unique_id] = {
'plc_name': row_ref['PLC Name'],
'device_name': row_ref['Device Name'],
'changes': row_changes
}
print(f"Detectados {len(changes)} dispositivos con cambios.")
for unique_id, change_info in changes.items():
print(f" {unique_id}: {list(change_info['changes'].keys())}")
# Debug: mostrar los primeros cambios para verificar valores
if len(changes) <= 5: # Solo mostrar detalles si son pocos cambios
for field, change_data in change_info['changes'].items():
print(f" {field}: {change_data['original']}{change_data['modified']}")
return True, changes
def find_device_in_aml(project_data, plc_name, device_name):
"""
Encuentra el device_id correspondiente en los datos del AML basándose en PLC y device name.
"""
# Buscar el PLC por nombre
target_plc_id = None
for plc_id, plc_data in project_data.get("plcs", {}).items():
if plc_data.get('name', plc_id) == plc_name:
target_plc_id = plc_id
break
if not target_plc_id:
return None
# Buscar el dispositivo en las redes del PLC
plc_info = project_data["plcs"][target_plc_id]
plc_networks = plc_info.get("connected_networks", {})
for net_id, plc_addr_on_net in plc_networks.items():
net_info = project_data.get("networks", {}).get(net_id)
if not net_info:
continue
devices_on_net = net_info.get("devices_on_net", {})
# Identificar nodos que pertenecen al PLC
plc_interface_and_node_ids = set()
for node in plc_info.get("network_nodes", []):
plc_interface_and_node_ids.add(node["id"])
interface_id_lookup = project_data["devices"].get(node["id"], {}).get("parent_id")
if interface_id_lookup:
plc_interface_and_node_ids.add(interface_id_lookup)
plc_interface_and_node_ids.add(target_plc_id)
# Buscar en dispositivos de la red
for node_id, node_addr in devices_on_net.items():
if node_id in plc_interface_and_node_ids:
continue
node_info = project_data.get("devices", {}).get(node_id)
if not node_info:
continue
# Determinar información del dispositivo
interface_id = node_info.get("parent_id")
interface_info = None
actual_device_id = None
actual_device_info = None
if interface_id:
interface_info = project_data.get("devices", {}).get(interface_id)
if interface_info:
actual_device_id = interface_info.get("parent_id")
if actual_device_id:
actual_device_info = project_data.get("devices", {}).get(actual_device_id)
display_info = actual_device_info if actual_device_info else (interface_info if interface_info else node_info)
display_name = display_info.get("name", "Unknown")
if display_name == device_name:
return {
'node_id': node_id,
'display_id': actual_device_id if actual_device_info else (interface_id if interface_info else node_id),
'node_info': node_info,
'display_info': display_info,
'network_id': net_id
}
return None
def apply_changes_to_aml(aml_tree, project_data, changes_dict):
"""
Aplica los cambios detectados al árbol XML del AML.
"""
print("Aplicando cambios al archivo AML...")
root = aml_tree.getroot()
changes_applied = 0
for unique_id, change_info in changes_dict.items():
plc_name = change_info['plc_name']
device_name = change_info['device_name']
changes = change_info['changes']
print(f" Procesando cambios para: {unique_id}")
# Encontrar el dispositivo en los datos del AML
device_info = find_device_in_aml(project_data, plc_name, device_name)
if not device_info:
print(f" ERROR: No se pudo encontrar el dispositivo '{device_name}' del PLC '{plc_name}' en el AML")
continue
print(f" Debug: Encontrado dispositivo - node_id: {device_info['node_id']}, display_id: {device_info['display_id']}")
# Encontrar el elemento XML correspondiente
node_id = device_info['node_id']
display_id = device_info['display_id']
# Intentar primero con el display_id (el dispositivo real que contiene IOs)
xml_element = root.xpath(f".//*[@ID='{display_id}']")
if not xml_element:
# Si no se encuentra, intentar con el node_id
xml_element = root.xpath(f".//*[@ID='{node_id}']")
if not xml_element:
print(f" ERROR: No se pudo encontrar el elemento XML con ID {display_id} o {node_id}")
continue
device_element = xml_element[0]
print(f" Debug: Usando elemento XML con ID {device_element.get('ID', 'N/A')}")
# Aplicar cambios específicos
for field, change_data in changes.items():
original_val = change_data['original']
modified_val = change_data['modified']
if field == 'Device Address':
# Cambiar dirección de red del dispositivo
if apply_network_address_change(device_element, modified_val):
print(f" ✓ Dirección de red actualizada: {original_val} -> {modified_val}")
changes_applied += 1
else:
print(f" ✗ Error actualizando dirección de red")
elif field in ['IO Input Start Address', 'IO Input Word Count', 'IO Output Start Address', 'IO Output Word Count']:
# Cambiar direcciones IO específicas
if apply_io_address_change(device_element, field, original_val, modified_val, project_data, display_id):
print(f"{field} actualizada: {original_val} -> {modified_val}")
changes_applied += 1
else:
print(f" ✗ Error actualizando {field}")
elif field in ['Device Type', 'Order Number', 'Firmware Version']:
# Cambiar atributos del dispositivo
if apply_device_attribute_change(device_element, field, modified_val):
print(f"{field} actualizado: {original_val} -> {modified_val}")
changes_applied += 1
else:
print(f" ✗ Error actualizando {field}")
print(f"Total de cambios aplicados: {changes_applied}")
return changes_applied > 0
def apply_network_address_change(device_element, new_address):
"""
Aplica cambio de dirección de red a un elemento del dispositivo.
"""
try:
# Convertir new_address a string si es numérico
if isinstance(new_address, (int, float)):
address_str = str(new_address).rstrip('0').rstrip('.')
else:
address_str = str(new_address)
# Buscar atributo NetworkAddress
addr_attr = device_element.xpath("./*[local-name()='Attribute'][@Name='NetworkAddress']")
if addr_attr:
value_elem = addr_attr[0].xpath("./*[local-name()='Value']")
if value_elem:
value_elem[0].text = address_str
return True
# Si no existe, crear el atributo
attr_elem = ET.SubElement(device_element, "Attribute")
attr_elem.set("Name", "NetworkAddress")
value_elem = ET.SubElement(attr_elem, "Value")
value_elem.text = address_str
return True
except Exception as e:
print(f" Error aplicando cambio de dirección: {e}")
return False
def apply_device_attribute_change(device_element, field, new_value):
"""
Aplica cambio de atributo del dispositivo.
"""
try:
# Mapear campos a nombres de atributos XML
attr_mapping = {
'Device Type': 'TypeName',
'Order Number': 'OrderNumber',
'Firmware Version': 'FirmwareVersion'
}
attr_name = attr_mapping.get(field)
if not attr_name:
return False
# Buscar atributo existente
attr_elem = device_element.xpath(f"./*[local-name()='Attribute'][@Name='{attr_name}']")
if attr_elem:
value_elem = attr_elem[0].xpath("./*[local-name()='Value']")
if value_elem:
value_elem[0].text = new_value
return True
# Si no existe, crear el atributo
attr_elem = ET.SubElement(device_element, "Attribute")
attr_elem.set("Name", attr_name)
value_elem = ET.SubElement(attr_elem, "Value")
value_elem.text = new_value
return True
except Exception as e:
print(f" Error aplicando cambio de atributo {field}: {e}")
return False
def apply_io_address_change(device_element, field, original_val, modified_val, project_data, device_id):
"""
Aplica cambios a las direcciones IO usando Start Address + Word Count.
Esto es más simple y directo que calcular End Addresses.
"""
try:
# Validar que el nuevo valor sea numérico o N/A
if modified_val != 'N/A' and modified_val != '':
try:
# Manejar tanto floats como integers (pandas puede leer como float)
if isinstance(modified_val, str):
# Si es string, intentar convertir
new_value = int(float(modified_val))
else:
# Si ya es numérico, convertir directamente
new_value = int(modified_val)
except (ValueError, TypeError):
print(f" Error: Valor no válido: {modified_val} (tipo: {type(modified_val)})")
return False
else:
# Si es N/A, no hay dirección IO para este tipo
return True
# Determinar tipo de IO y si es start address o word count
is_input = "Input" in field
is_start_address = "Start Address" in field
is_word_count = "Word Count" in field
io_type = "Input" if is_input else "Output"
# Buscar la estructura IO en el dispositivo - con múltiples estrategias
print(f" Debug: Buscando elementos Address en dispositivo {device_element.get('Name', 'N/A')}")
# Estrategia 1: Buscar Address directamente en el elemento
address_elements = device_element.xpath("./*[local-name()='Attribute'][@Name='Address']")
if not address_elements:
# Estrategia 2: Buscar en elementos hijos
print(f" Debug: No se encontró Address directo, buscando en elementos hijos...")
address_elements = device_element.xpath(".//*[local-name()='Attribute'][@Name='Address']")
if not address_elements:
# Estrategia 3: Buscar en elementos padre (tal vez los IOs están en el parent)
print(f" Debug: No se encontró Address en hijos, buscando en elementos padre...")
parent_elements = device_element.xpath("parent::*")
if parent_elements:
address_elements = parent_elements[0].xpath(".//*[local-name()='Attribute'][@Name='Address']")
if not address_elements:
print(f" ERROR: No se encontró elemento Address en el dispositivo o sus alrededores")
print(f" Debug: Atributos disponibles en este elemento:")
for attr in device_element.xpath("./*[local-name()='Attribute']"):
attr_name = attr.get('Name', 'N/A')
print(f" - {attr_name}")
return False
print(f" Debug: Encontrados {len(address_elements)} elemento(s) Address")
# Buscar dentro del Address el sub-atributo correcto
target_io_element = None
# Buscar en todos los elementos Address encontrados
for address_element in address_elements:
print(f" Debug: Explorando Address element...")
io_subelements = address_element.xpath(f"./*[local-name()='Attribute']")
print(f" Debug: Encontrados {len(io_subelements)} sub-elementos en Address")
for io_sub in io_subelements:
sub_name = io_sub.get('Name', 'N/A')
print(f" Debug: Revisando sub-elemento: {sub_name}")
# Verificar si es el tipo correcto (Input/Output)
type_val = io_sub.xpath("./*[local-name()='Attribute'][@Name='IoType']/*[local-name()='Value']/text()")
if type_val:
found_type = type_val[0]
print(f" Debug: Encontrado IoType: {found_type}")
if found_type.lower() == io_type.lower():
target_io_element = io_sub
print(f" Debug: ¡Encontrado elemento {io_type} compatible!")
break
else:
# También revisar si el nombre del sub-elemento indica el tipo
if io_type.lower() in sub_name.lower():
print(f" Debug: Sub-elemento {sub_name} parece ser de tipo {io_type}")
target_io_element = io_sub
break
if target_io_element:
break
if not target_io_element:
print(f" ERROR: No se encontró elemento {io_type} en ningún Address")
print(f" Debug: Elementos Address disponibles:")
for addr_elem in address_elements:
sub_attrs = addr_elem.xpath(f"./*[local-name()='Attribute']")
for sub in sub_attrs:
sub_name = sub.get('Name', 'N/A')
type_vals = sub.xpath("./*[local-name()='Attribute'][@Name='IoType']/*[local-name()='Value']/text()")
type_info = type_vals[0] if type_vals else "No IoType"
print(f" - {sub_name} (IoType: {type_info})")
return False
if is_start_address:
# Actualizar StartAddress
start_attr = target_io_element.xpath("./*[local-name()='Attribute'][@Name='StartAddress']")
if start_attr:
value_elem = start_attr[0].xpath("./*[local-name()='Value']")
if value_elem:
value_elem[0].text = str(new_value)
return True
else:
# Crear StartAddress si no existe
start_attr_elem = ET.SubElement(target_io_element, "Attribute")
start_attr_elem.set("Name", "StartAddress")
value_elem = ET.SubElement(start_attr_elem, "Value")
value_elem.text = str(new_value)
return True
elif is_word_count:
# Actualizar Length basándose en Word Count
# Convertir words a bits (1 word = 16 bits)
length_bits = new_value * 16
# Actualizar Length
length_attr = target_io_element.xpath("./*[local-name()='Attribute'][@Name='Length']")
if length_attr:
value_elem = length_attr[0].xpath("./*[local-name()='Value']")
if value_elem:
value_elem[0].text = str(length_bits)
return True
else:
# Crear Length si no existe
length_attr_elem = ET.SubElement(target_io_element, "Attribute")
length_attr_elem.set("Name", "Length")
value_elem = ET.SubElement(length_attr_elem, "Value")
value_elem.text = str(length_bits)
return True
return False
except Exception as e:
print(f" Error aplicando cambio de dirección IO {field}: {e}")
traceback.print_exc()
return False
def save_updated_aml(aml_tree, original_aml_path):
"""
Guarda el árbol XML modificado como un nuevo archivo AML con sufijo "_updated".
"""
original_path = Path(original_aml_path)
updated_path = original_path.parent / f"{original_path.stem}_updated{original_path.suffix}"
try:
# Escribir el XML actualizado
aml_tree.write(
str(updated_path),
pretty_print=True,
xml_declaration=True,
encoding='utf-8'
)
print(f"Archivo AML actualizado guardado en: {updated_path}")
return str(updated_path)
except Exception as e:
print(f"ERROR guardando archivo AML actualizado: {e}")
return None
def main():
"""Función principal del script."""
try:
configs = load_configuration()
working_directory = configs.get("working_directory")
except Exception as e:
print(f"Warning: No se pudo cargar configuración: {e}")
configs = {}
working_directory = None
script_version = "v1.4 - Enhanced Address Element Search with Debug"
print(f"--- Actualizador de AML desde Excel Modificado ({script_version}) ---")
# Validar directorio de trabajo
if not working_directory or not os.path.isdir(working_directory):
print("Directorio de trabajo no configurado. Usando directorio actual.")
working_directory = os.getcwd()
print(f"Directorio de trabajo: {working_directory}")
# 1. Seleccionar archivo AML original
print("\n1. Seleccione el archivo AML original:")
aml_file_path = select_aml_file(initial_dir=working_directory)
if not aml_file_path:
print("No se seleccionó archivo AML. Saliendo.")
return
# 2. Seleccionar archivo Excel modificado
print("\n2. Seleccione el archivo Excel modificado:")
excel_file_path = select_excel_file(initial_dir=working_directory)
if not excel_file_path:
print("No se seleccionó archivo Excel. Saliendo.")
return
print(f"\nArchivo AML original: {aml_file_path}")
print(f"Archivo Excel modificado: {excel_file_path}")
# 3. Crear directorio temporal para archivos intermedios
with tempfile.TemporaryDirectory() as temp_dir:
print(f"\nUsando directorio temporal: {temp_dir}")
# 4. Generar Excel de referencia desde AML
reference_excel_path, project_data = generate_reference_excel_from_aml(aml_file_path, temp_dir)
if not reference_excel_path or not project_data:
print("ERROR: No se pudo generar Excel de referencia.")
return
# 5. Comparar archivos Excel
is_valid, changes_dict = compare_excel_files(reference_excel_path, excel_file_path)
if not is_valid:
print("ERROR: El archivo Excel modificado no es compatible con el AML original.")
return
if not changes_dict:
print("No se detectaron cambios en el Excel. No hay nada que actualizar.")
return
# 6. Cargar y parsear AML original
print("\nCargando archivo AML original...")
try:
parser = ET.XMLParser(remove_blank_text=True, huge_tree=True)
aml_tree = ET.parse(aml_file_path, parser)
except Exception as e:
print(f"ERROR parseando archivo AML: {e}")
return
# 7. Aplicar cambios al AML
success = apply_changes_to_aml(aml_tree, project_data, changes_dict)
if not success:
print("No se pudieron aplicar cambios al AML.")
return
# 8. Guardar AML actualizado
updated_aml_path = save_updated_aml(aml_tree, aml_file_path)
if updated_aml_path:
print(f"\n¡Proceso completado exitosamente!")
print(f"Archivo AML actualizado: {updated_aml_path}")
else:
print("ERROR: No se pudo guardar el archivo AML actualizado.")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,324 @@
"""
export_logic_from_tia : Script para exportar el software de un PLC desde TIA Portal en archivos XML y SCL.
"""
import tkinter as tk
from tkinter import filedialog
import os
import sys
import traceback
script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
sys.path.append(script_root)
from backend.script_utils import load_configuration
# --- Configuration ---
TIA_PORTAL_VERSION = "18.0" # Target TIA Portal version (e.g., "18.0")
EXPORT_OPTIONS = None # Use default export options
KEEP_FOLDER_STRUCTURE = (
True # Replicate TIA project folder structure in export directory
)
# --- TIA Scripting Import Handling ---
# Check if the TIA_SCRIPTING environment variable is set
if os.getenv("TIA_SCRIPTING"):
sys.path.append(os.getenv("TIA_SCRIPTING"))
else:
# Optional: Define a fallback path if the environment variable isn't set
# fallback_path = "C:\\path\\to\\your\\TIA_Scripting_binaries"
# if os.path.exists(fallback_path):
# sys.path.append(fallback_path)
pass # Allow import to fail if not found
try:
import siemens_tia_scripting as ts
EXPORT_OPTIONS = (
ts.Enums.ExportOptions.WithDefaults
) # Set default options now that 'ts' is imported
except ImportError:
print("ERROR: Failed to import 'siemens_tia_scripting'.")
print("Ensure:")
print(f"1. TIA Portal Openness for V{TIA_PORTAL_VERSION} is installed.")
print(
"2. The 'siemens_tia_scripting' Python module is installed (pip install ...) or"
)
print(
" the path to its binaries is set in the 'TIA_SCRIPTING' environment variable."
)
print(
"3. You are using a compatible Python version (e.g., 3.12.X as per documentation)."
)
sys.exit(1)
except Exception as e:
print(f"An unexpected error occurred during import: {e}")
traceback.print_exc()
sys.exit(1)
# --- Functions ---
def select_project_file():
"""Opens a dialog to select a TIA Portal project file."""
root = tk.Tk()
root.withdraw() # Hide the main tkinter window
file_path = filedialog.askopenfilename(
title="Select TIA Portal Project File",
filetypes=[
(
f"TIA Portal V{TIA_PORTAL_VERSION} Projects",
f"*.ap{TIA_PORTAL_VERSION.split('.')[0]}",
)
], # e.g. *.ap18
)
root.destroy()
if not file_path:
print("No project file selected. Exiting.")
sys.exit(0)
return file_path
def select_export_directory():
"""Opens a dialog to select the export directory."""
root = tk.Tk()
root.withdraw() # Hide the main tkinter window
dir_path = filedialog.askdirectory(title="Select Export Directory")
root.destroy()
if not dir_path:
print("No export directory selected. Exiting.")
sys.exit(0)
return dir_path
def export_plc_data(plc, export_base_dir):
"""Exports Blocks, UDTs, and Tag Tables from a given PLC."""
plc_name = plc.get_name()
print(f"\n--- Processing PLC: {plc_name} ---")
# Define base export path for this PLC
plc_export_dir = os.path.join(export_base_dir, plc_name)
os.makedirs(plc_export_dir, exist_ok=True)
# --- Export Program Blocks ---
blocks_exported = 0
blocks_skipped = 0
print(f"\n[PLC: {plc_name}] Exporting Program Blocks...")
xml_blocks_path = os.path.join(plc_export_dir, "ProgramBlocks_XML")
scl_blocks_path = os.path.join(plc_export_dir, "ProgramBlocks_SCL")
os.makedirs(xml_blocks_path, exist_ok=True)
os.makedirs(scl_blocks_path, exist_ok=True)
print(f" XML Target: {xml_blocks_path}")
print(f" SCL Target: {scl_blocks_path}")
try:
program_blocks = plc.get_program_blocks() #
print(f" Found {len(program_blocks)} program blocks.")
for block in program_blocks:
block_name = block.get_name() # Assuming get_name() exists
print(f" Processing block: {block_name}...")
try:
if not block.is_consistent(): #
print(f" Compiling block {block_name}...")
block.compile() #
if not block.is_consistent():
print(
f" WARNING: Block {block_name} inconsistent after compile. Skipping."
)
blocks_skipped += 1
continue
print(f" Exporting {block_name} as XML...")
block.export(
target_directory_path=xml_blocks_path, #
export_options=EXPORT_OPTIONS, #
export_format=ts.Enums.ExportFormats.SimaticML, #
keep_folder_structure=KEEP_FOLDER_STRUCTURE,
) #
try:
prog_language = block.get_property(name="ProgrammingLanguage")
if prog_language == "SCL":
print(f" Exporting {block_name} as SCL...")
block.export(
target_directory_path=scl_blocks_path,
export_options=EXPORT_OPTIONS,
export_format=ts.Enums.ExportFormats.ExternalSource, #
keep_folder_structure=KEEP_FOLDER_STRUCTURE,
)
except Exception as prop_ex:
print(
f" Could not get ProgrammingLanguage for {block_name}. Skipping SCL. Error: {prop_ex}"
)
blocks_exported += 1
except Exception as block_ex:
print(f" ERROR exporting block {block_name}: {block_ex}")
blocks_skipped += 1
print(
f" Program Blocks Export Summary: Exported={blocks_exported}, Skipped/Errors={blocks_skipped}"
)
except Exception as e:
print(f" ERROR processing Program Blocks: {e}")
traceback.print_exc()
# --- Export PLC Data Types (UDTs) ---
udts_exported = 0
udts_skipped = 0
print(f"\n[PLC: {plc_name}] Exporting PLC Data Types (UDTs)...")
udt_export_path = os.path.join(plc_export_dir, "PlcDataTypes")
os.makedirs(udt_export_path, exist_ok=True)
print(f" Target: {udt_export_path}")
try:
udts = plc.get_user_data_types() #
print(f" Found {len(udts)} UDTs.")
for udt in udts:
udt_name = udt.get_name() #
print(f" Processing UDT: {udt_name}...")
try:
if not udt.is_consistent(): #
print(f" Compiling UDT {udt_name}...")
udt.compile() #
if not udt.is_consistent():
print(
f" WARNING: UDT {udt_name} inconsistent after compile. Skipping."
)
udts_skipped += 1
continue
print(f" Exporting {udt_name}...")
udt.export(
target_directory_path=udt_export_path, #
export_options=EXPORT_OPTIONS, #
# export_format defaults to SimaticML for UDTs
keep_folder_structure=KEEP_FOLDER_STRUCTURE,
) #
udts_exported += 1
except Exception as udt_ex:
print(f" ERROR exporting UDT {udt_name}: {udt_ex}")
udts_skipped += 1
print(
f" UDT Export Summary: Exported={udts_exported}, Skipped/Errors={udts_skipped}"
)
except Exception as e:
print(f" ERROR processing UDTs: {e}")
traceback.print_exc()
# --- Export PLC Tag Tables ---
tags_exported = 0
tags_skipped = 0
print(f"\n[PLC: {plc_name}] Exporting PLC Tag Tables...")
tags_export_path = os.path.join(plc_export_dir, "PlcTags")
os.makedirs(tags_export_path, exist_ok=True)
print(f" Target: {tags_export_path}")
try:
tag_tables = plc.get_plc_tag_tables() #
print(f" Found {len(tag_tables)} Tag Tables.")
for table in tag_tables:
table_name = table.get_name() #
print(f" Processing Tag Table: {table_name}...")
try:
# Note: Consistency check might not be available/needed for tag tables like blocks/UDTs
print(f" Exporting {table_name}...")
table.export(
target_directory_path=tags_export_path, #
export_options=EXPORT_OPTIONS, #
# export_format defaults to SimaticML for Tag Tables
keep_folder_structure=KEEP_FOLDER_STRUCTURE,
) #
tags_exported += 1
except Exception as table_ex:
print(f" ERROR exporting Tag Table {table_name}: {table_ex}")
tags_skipped += 1
print(
f" Tag Table Export Summary: Exported={tags_exported}, Skipped/Errors={tags_skipped}"
)
except Exception as e:
print(f" ERROR processing Tag Tables: {e}")
traceback.print_exc()
print(f"\n--- Finished processing PLC: {plc_name} ---")
# --- Main Script ---
if __name__ == "__main__":
configs = load_configuration()
working_directory = configs.get("working_directory")
print("--- TIA Portal Data Exporter (Blocks, UDTs, Tags) ---")
# Validate working directory
if not working_directory or not os.path.isdir(working_directory):
print("ERROR: Working directory not set or invalid in configuration.")
print("Please configure the working directory using the main application.")
sys.exit(1)
# 1. Select Project File, Export Directory comes from config
project_file = select_project_file()
export_dir = working_directory # Use working directory from config
print(f"\nSelected Project: {project_file}")
print(f"Using Export Directory (Working Directory): {export_dir}")
portal_instance = None
project_object = None
try:
# 2. Connect to TIA Portal
print(f"\nConnecting to TIA Portal V{TIA_PORTAL_VERSION}...")
portal_instance = ts.open_portal(
version=TIA_PORTAL_VERSION,
portal_mode=ts.Enums.PortalMode.WithGraphicalUserInterface,
)
print("Connected to TIA Portal.")
print(f"Portal Process ID: {portal_instance.get_process_id()}") #
# 3. Open Project
print(f"Opening project: {os.path.basename(project_file)}...")
project_object = portal_instance.open_project(project_file_path=project_file) #
if project_object is None:
print("Project might already be open, attempting to get handle...")
project_object = portal_instance.get_project() #
if project_object is None:
raise Exception("Failed to open or get the specified project.")
print("Project opened successfully.")
# 4. Get PLCs
plcs = project_object.get_plcs() #
if not plcs:
print("No PLC devices found in the project.")
else:
print(f"Found {len(plcs)} PLC(s). Starting export process...")
# 5. Iterate and Export Data for each PLC
for plc_device in plcs:
export_plc_data(
plc=plc_device, export_base_dir=export_dir
) # Pass export_dir
print("\nExport process completed.")
except ts.TiaException as tia_ex:
print(f"\nTIA Portal Openness Error: {tia_ex}")
traceback.print_exc()
except FileNotFoundError:
print(f"\nERROR: Project file not found at {project_file}")
except Exception as e:
print(f"\nAn unexpected error occurred: {e}")
traceback.print_exc()
finally:
# 6. Cleanup
if portal_instance:
try:
print("\nClosing TIA Portal...")
portal_instance.close_portal() #
print("TIA Portal closed.")
except Exception as close_ex:
print(f"Error during TIA Portal cleanup: {close_ex}")
print("\nScript finished.")

View File

@ -0,0 +1,371 @@
"""
export_CAx_from_tia : Script que exporta los datos CAx de un proyecto de TIA Portal y genera un resumen en Markdown.
"""
import tkinter as tk
from tkinter import filedialog
import os
import sys
import traceback
import xml.etree.ElementTree as ET # Library to parse XML (AML)
from pathlib import Path # Import Path
script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
sys.path.append(script_root)
from backend.script_utils import load_configuration
# --- Configuration ---
# Supported TIA Portal versions mapping (extension -> version)
SUPPORTED_TIA_VERSIONS = {
".ap18": "18.0",
".ap19": "19.0",
".ap20": "20.0"
}
# --- TIA Scripting Import Handling ---
# (Same import handling as the previous script)
if os.getenv("TIA_SCRIPTING"):
sys.path.append(os.getenv("TIA_SCRIPTING"))
else:
pass
try:
import siemens_tia_scripting as ts
except ImportError:
print("ERROR: Failed to import 'siemens_tia_scripting'.")
print("Ensure TIA Openness, the module, and Python 3.12.X are set up.")
sys.exit(1)
except Exception as e:
print(f"An unexpected error occurred during import: {e}")
traceback.print_exc()
sys.exit(1)
# --- Functions ---
def get_supported_filetypes():
"""Returns the supported file types for TIA Portal projects."""
filetypes = []
for ext, version in SUPPORTED_TIA_VERSIONS.items():
version_major = version.split('.')[0]
filetypes.append((f"TIA Portal V{version_major} Projects", f"*{ext}"))
# Add option to show all supported files
all_extensions = " ".join([f"*{ext}" for ext in SUPPORTED_TIA_VERSIONS.keys()])
filetypes.insert(0, ("All TIA Portal Projects", all_extensions))
return filetypes
def detect_tia_version(project_file_path):
"""Detects TIA Portal version based on file extension."""
file_path = Path(project_file_path)
file_extension = file_path.suffix.lower()
if file_extension in SUPPORTED_TIA_VERSIONS:
detected_version = SUPPORTED_TIA_VERSIONS[file_extension]
print(f"Detected TIA Portal version: {detected_version} (from extension {file_extension})")
return detected_version
else:
print(f"WARNING: Unrecognized file extension '{file_extension}'. Supported extensions: {list(SUPPORTED_TIA_VERSIONS.keys())}")
# Default to version 18.0 for backward compatibility
print("Defaulting to TIA Portal V18.0")
return "18.0"
def select_project_file():
"""Opens a dialog to select a TIA Portal project file."""
root = tk.Tk()
root.withdraw()
file_path = filedialog.askopenfilename(
title="Select TIA Portal Project File",
filetypes=get_supported_filetypes(),
)
root.destroy()
if not file_path:
print("No project file selected. Exiting.")
sys.exit(0)
return file_path
def select_output_directory():
"""Opens a dialog to select the output directory."""
root = tk.Tk()
root.withdraw()
dir_path = filedialog.askdirectory(
title="Select Output Directory for AML and MD files"
)
root.destroy()
if not dir_path:
print("No output directory selected. Exiting.")
sys.exit(0)
return dir_path
def find_elements(element, path):
"""Helper to find elements using namespaces commonly found in AML."""
# AutomationML namespaces often vary slightly or might be default
# This basic approach tries common prefixes or no prefix
namespaces = {
"": (
element.tag.split("}")[0][1:] if "}" in element.tag else ""
), # Default namespace if present
"caex": "http://www.dke.de/CAEX", # Common CAEX namespace
# Add other potential namespaces if needed based on file inspection
}
# Try finding with common prefixes or the default namespace
for prefix, uri in namespaces.items():
# Construct path with namespace URI if prefix is defined
namespaced_path = path
if prefix:
parts = path.split("/")
namespaced_parts = [
f"{{{uri}}}{part}" if part != "." else part for part in parts
]
namespaced_path = "/".join(namespaced_parts)
# Try findall with the constructed path
found = element.findall(namespaced_path)
if found:
return found # Return first successful find
# Fallback: try finding without explicit namespace (might work if default ns is used throughout)
# This might require adjusting the path string itself depending on the XML structure
try:
# Simple attempt without namespace handling if the above fails
return element.findall(path)
except (
SyntaxError
): # Handle potential errors if path isn't valid without namespaces
return []
def parse_aml_to_markdown(aml_file_path, md_file_path):
"""Parses the AML file and generates a Markdown summary."""
print(f"Parsing AML file: {aml_file_path}")
try:
tree = ET.parse(aml_file_path)
root = tree.getroot()
markdown_lines = ["# Project CAx Data Summary (AutomationML)", ""]
# Find InstanceHierarchy - usually contains the project structure
# Note: Namespace handling in ElementTree can be tricky. Adjust '{...}' part if needed.
# We will use a helper function 'find_elements' to try common patterns
instance_hierarchies = find_elements(
root, ".//InstanceHierarchy"
) # Common CAEX tag
if not instance_hierarchies:
markdown_lines.append("Could not find InstanceHierarchy in the AML file.")
print("Warning: Could not find InstanceHierarchy element.")
else:
# Assuming the first InstanceHierarchy is the main one
ih = instance_hierarchies[0]
markdown_lines.append(f"## Instance Hierarchy: {ih.get('Name', 'N/A')}")
markdown_lines.append("")
# Look for InternalElements which represent devices/components
internal_elements = find_elements(
ih, ".//InternalElement"
) # Common CAEX tag
if not internal_elements:
markdown_lines.append(
"No devices (InternalElement) found in InstanceHierarchy."
)
print("Info: No InternalElement tags found under InstanceHierarchy.")
else:
markdown_lines.append(
f"Found {len(internal_elements)} device(s)/component(s):"
)
markdown_lines.append("")
markdown_lines.append(
"| Name | SystemUnitClass | RefBaseSystemUnitPath | Attributes |"
)
markdown_lines.append("|---|---|---|---|")
for elem in internal_elements:
name = elem.get("Name", "N/A")
ref_path = elem.get(
"RefBaseSystemUnitPath", "N/A"
) # Path to class definition
# Try to get the class name from the RefBaseSystemUnitPath or SystemUnitClassLib
su_class_path = find_elements(
elem, ".//SystemUnitClass"
) # Check direct child first
su_class = (
su_class_path[0].get("Path", "N/A")
if su_class_path
else ref_path.split("/")[-1]
) # Fallback to last part of path
attributes_md = ""
attributes = find_elements(elem, ".//Attribute") # Find attributes
attr_list = []
for attr in attributes:
attr_name = attr.get("Name", "")
attr_value_elem = find_elements(
attr, ".//Value"
) # Get Value element
attr_value = (
attr_value_elem[0].text
if attr_value_elem and attr_value_elem[0].text
else "N/A"
)
# Look for potential IP addresses (common attribute names)
if "Address" in attr_name or "IP" in attr_name:
attr_list.append(f"**{attr_name}**: {attr_value}")
else:
attr_list.append(f"{attr_name}: {attr_value}")
attributes_md = "<br>".join(attr_list) if attr_list else "None"
markdown_lines.append(
f"| {name} | {su_class} | `{ref_path}` | {attributes_md} |"
)
# Write to Markdown file
with open(md_file_path, "w", encoding="utf-8") as f:
f.write("\n".join(markdown_lines))
print(f"Markdown summary written to: {md_file_path}")
except ET.ParseError as xml_err:
print(f"ERROR parsing XML file {aml_file_path}: {xml_err}")
with open(md_file_path, "w", encoding="utf-8") as f:
f.write(
f"# Error\n\nFailed to parse AML file: {os.path.basename(aml_file_path)}\n\nError: {xml_err}"
)
except Exception as e:
print(f"ERROR processing AML file {aml_file_path}: {e}")
traceback.print_exc()
with open(md_file_path, "w", encoding="utf-8") as f:
f.write(
f"# Error\n\nAn unexpected error occurred while processing AML file: {os.path.basename(aml_file_path)}\n\nError: {e}"
)
# --- Main Script ---
if __name__ == "__main__":
configs = load_configuration()
working_directory = configs.get("working_directory")
print("--- TIA Portal Project CAx Exporter and Analyzer ---")
# Validate working directory
if not working_directory or not os.path.isdir(working_directory):
print("ERROR: Working directory not set or invalid in configuration.")
print("Please configure the working directory using the main application.")
sys.exit(1)
# 1. Select Project File, Output Directory comes from config
project_file = select_project_file()
output_dir = Path(
working_directory
) # Use working directory from config, ensure it's a Path object
print(f"\nSelected Project: {project_file}")
print(f"Using Output Directory (Working Directory): {output_dir}")
# 2. Detect TIA Portal version from project file
tia_version = detect_tia_version(project_file)
# Define output file names using Path object
project_path = Path(project_file)
project_base_name = project_path.stem # Get filename without extension
aml_file = output_dir / f"{project_base_name}_CAx_Export.aml"
md_file = output_dir / f"{project_base_name}_CAx_Summary.md"
log_file = (
output_dir / f"{project_base_name}_CAx_Export.log"
) # Log file for the export process
print(f"Will export CAx data to: {aml_file}")
print(f"Will generate summary to: {md_file}")
print(f"Export log file: {log_file}")
portal_instance = None
project_object = None
cax_export_successful = False
try:
# 3. Connect to TIA Portal with detected version
print(f"\nConnecting to TIA Portal V{tia_version}...")
portal_instance = ts.open_portal(
version=tia_version,
portal_mode=ts.Enums.PortalMode.WithGraphicalUserInterface,
)
print("Connected.")
# 4. Open Project
print(
f"Opening project: {project_path.name}..."
) # Use Path object's name attribute
project_object = portal_instance.open_project(
project_file_path=str(project_path)
) # Pass path as string
if project_object is None:
print("Project might already be open, attempting to get handle...")
project_object = portal_instance.get_project()
if project_object is None:
raise Exception("Failed to open or get the specified project.")
print("Project opened.")
# 5. Export CAx Data (Project Level)
print(f"Exporting CAx data for the project to {aml_file}...")
# Ensure output directory exists (Path.mkdir handles this implicitly if needed later, but good practice)
output_dir.mkdir(parents=True, exist_ok=True)
# Pass paths as strings to the TIA function
export_result = project_object.export_cax_data(
export_file_path=str(aml_file), log_file_path=str(log_file)
)
if export_result:
print("CAx data exported successfully.")
cax_export_successful = True
else:
print("CAx data export failed. Check the log file for details:")
print(f" Log file: {log_file}")
# Write basic error message to MD file if export fails
with open(md_file, "w", encoding="utf-8") as f:
f.write(
f"# Error\n\nCAx data export failed. Check log file: {log_file}"
)
except ts.TiaException as tia_ex:
print(f"\nTIA Portal Openness Error: {tia_ex}")
traceback.print_exc()
except FileNotFoundError:
print(f"\nERROR: Project file not found at {project_file}")
except Exception as e:
print(f"\nAn unexpected error occurred during TIA interaction: {e}")
traceback.print_exc()
finally:
# Close TIA Portal before processing the file (or detach)
if portal_instance:
try:
print("\nClosing TIA Portal...")
portal_instance.close_portal()
print("TIA Portal closed.")
except Exception as close_ex:
print(f"Error during TIA Portal cleanup: {close_ex}")
# 6. Parse AML and Generate Markdown (only if export was successful)
if cax_export_successful:
if aml_file.exists(): # Use Path object's exists() method
parse_aml_to_markdown(aml_file, md_file)
else:
print(
f"ERROR: Export was reported successful, but AML file not found at {aml_file}"
)
with open(md_file, "w", encoding="utf-8") as f:
f.write(
f"# Error\n\nExport was reported successful, but AML file not found:\n{aml_file}"
)
print("\nScript finished.")

File diff suppressed because it is too large Load Diff

View File

@ -5,5 +5,5 @@
},
"level2": {},
"level3": {},
"working_directory": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourcdSD"
"working_directory": "D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source"
}

View File

@ -1,6 +1,7 @@
{
"path": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourcdSD",
"path": "D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
"history": [
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourcdSD",
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourceXML"
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -15,5 +15,5 @@
"xref_source_subdir": "source"
},
"level3": {},
"working_directory": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourceXML"
"working_directory": "D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source"
}

View File

@ -1,6 +1,7 @@
{
"path": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourceXML",
"path": "D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
"history": [
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourceXML",
"C:\\Trabajo\\SIDEL\\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\\Reporte\\IOExport"
]

View File

@ -0,0 +1,33 @@
# Grupo de Ejemplo
Este es un archivo markdown de prueba para el **Launcher GUI**.
## Características
- Soporte para archivos `.md`
- Renderizado con markdown-it
- Visualización en modal
## Símbolos Unicode
Aquí tienes algunos símbolos que pueden causar problemas de encoding:
- Flecha derecha: →
- Flecha izquierda: ←
- Flecha doble: ⇒
- Caracteres especiales: ñ, á, é, í, ó, ú
- Emojis: 🚀 📄 ⚙️
## Código de Ejemplo
```python
def test_unicode():
print("Prueba de unicode → éxito")
return "Todo funcionando ✅"
```
## Lista de Verificación
- [x] Encoding UTF-8
- [x] Renderizado markdown
- [ ] Pruebas adicionales

View File

@ -7,6 +7,14 @@
"added_date": "2025-06-03T11:47:24.757186Z",
"execution_count": 0,
"last_executed": null
},
{
"id": "2_main.py",
"group_id": "2",
"script_name": "main.py",
"added_date": "2025-06-03T12:00:41.084138Z",
"execution_count": 0,
"last_executed": null
}
]
}

View File

@ -1,5 +1,409 @@
{
"history": [],
"history": [
{
"id": "a599effd",
"group_id": "4",
"script_name": "x1.py",
"executed_date": "2025-06-12T19:59:02.082348Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Siemens/Tia Portal Utils",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 36824,
"execution_time": null
},
{
"id": "d722b721",
"group_id": "4",
"script_name": "x1.py",
"executed_date": "2025-06-12T09:27:11.172727Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Siemens/Tia Portal Utils",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 24964,
"execution_time": null
},
{
"id": "0a84ec87",
"group_id": "1",
"script_name": "calc.py",
"executed_date": "2025-06-12T09:24:00.527248Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Calcv2",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 15548,
"execution_time": null
},
{
"id": "6297e689",
"group_id": "4",
"script_name": "x1.py",
"executed_date": "2025-06-12T09:16:08.148925Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Siemens/Tia Portal Utils",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 16892,
"execution_time": null
},
{
"id": "79f5a9e6",
"group_id": "4",
"script_name": "x1.py",
"executed_date": "2025-06-12T09:07:06.392550Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Siemens/Tia Portal Utils",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 45652,
"execution_time": null
},
{
"id": "22e264cd",
"group_id": "4",
"script_name": "x1.py",
"executed_date": "2025-06-12T09:07:06.280903Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Siemens/Tia Portal Utils",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 25984,
"execution_time": null
},
{
"id": "589be793",
"group_id": "4",
"script_name": "x1.py",
"executed_date": "2025-06-11T23:17:19.621521Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Siemens/Tia Portal Utils",
"python_env": "tia_scripting",
"executable_type": "python.exe",
"status": "success",
"pid": 25652,
"execution_time": 389.689278
},
{
"id": "6ca3fbde",
"group_id": "4",
"script_name": "x1.py",
"executed_date": "2025-06-11T23:17:06.940558Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Siemens/Tia Portal Utils",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 29496,
"execution_time": null
},
{
"id": "6f646ca2",
"group_id": "4",
"script_name": "x1.py",
"executed_date": "2025-06-11T23:16:59.013936Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Siemens/Tia Portal Utils",
"python_env": "tia_scripting",
"executable_type": "python.exe",
"status": "success",
"pid": 38516,
"execution_time": 3.471176
},
{
"id": "88e35e8b",
"group_id": "2",
"script_name": "main.py",
"executed_date": "2025-06-11T14:00:34.786377Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 404,
"execution_time": null
},
{
"id": "b373c77d",
"group_id": "2",
"script_name": "main.py",
"executed_date": "2025-06-11T13:51:11.750064Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 24472,
"execution_time": null
},
{
"id": "455a8271",
"group_id": "2",
"script_name": "main.py",
"executed_date": "2025-06-10T15:45:56.713179Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 16480,
"execution_time": null
},
{
"id": "a8b813b0",
"group_id": "2",
"script_name": "main.py",
"executed_date": "2025-06-10T15:40:39.728099Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 36640,
"execution_time": null
},
{
"id": "7924a173",
"group_id": "2",
"script_name": "main.py",
"executed_date": "2025-06-10T15:36:55.303738Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 15144,
"execution_time": null
},
{
"id": "c3796f4e",
"group_id": "2",
"script_name": "main.py",
"executed_date": "2025-06-10T15:36:18.822898Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 30656,
"execution_time": null
},
{
"id": "1040f2b6",
"group_id": "2",
"script_name": "main.py",
"executed_date": "2025-06-10T11:03:59.233659Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 26152,
"execution_time": null
},
{
"id": "6d4e8908",
"group_id": "1",
"script_name": "calc.py",
"executed_date": "2025-06-05T19:00:05.863426Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Calcv2",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 14212,
"execution_time": null
},
{
"id": "bc11fb82",
"group_id": "1",
"script_name": "calc.py",
"executed_date": "2025-06-05T18:55:33.372642Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Calcv2",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 30456,
"execution_time": null
},
{
"id": "7dda414a",
"group_id": "1",
"script_name": "calc.py",
"executed_date": "2025-06-05T18:55:25.022607Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Calcv2",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 31800,
"execution_time": null
},
{
"id": "ca0fdba4",
"group_id": "1",
"script_name": "calc.py",
"executed_date": "2025-06-05T18:55:20.165639Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Calcv2",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 27436,
"execution_time": null
},
{
"id": "8eeccfaf",
"group_id": "1",
"script_name": "calc.py",
"executed_date": "2025-06-05T18:36:56.208761Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Calcv2",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 11436,
"execution_time": null
},
{
"id": "be30fe5c",
"group_id": "1",
"script_name": "calc.py",
"executed_date": "2025-06-05T11:40:29.568449Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Calcv2",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 34348,
"execution_time": null
},
{
"id": "de046db1",
"group_id": "2",
"script_name": "main.py",
"executed_date": "2025-06-04T22:07:51.493125Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
"python_env": "tia_scripting",
"executable_type": "python.exe",
"status": "success",
"pid": 20460,
"execution_time": 37.787662
},
{
"id": "76b18be0",
"group_id": "2",
"script_name": "main.py",
"executed_date": "2025-06-04T22:06:04.365756Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 1036,
"execution_time": null
},
{
"id": "d3ad9738",
"group_id": "1",
"script_name": "calc.py",
"executed_date": "2025-06-04T18:56:27.720254Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Calcv2",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 43496,
"execution_time": null
},
{
"id": "b7796abb",
"group_id": "1",
"script_name": "calc.py",
"executed_date": "2025-06-04T18:56:17.950282Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Calcv2",
"python_env": "tia_scripting",
"executable_type": "python.exe",
"status": "success",
"pid": 36312,
"execution_time": 8.35251
},
{
"id": "c29e9b02",
"group_id": "2",
"script_name": "main.py",
"executed_date": "2025-06-04T12:20:30.402310Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 31416,
"execution_time": null
},
{
"id": "2a2caf05",
"group_id": "1",
"script_name": "test_symbolic.py",
"executed_date": "2025-06-04T11:15:06.230442Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Calcv2",
"python_env": "tia_scripting",
"executable_type": "python.exe",
"status": "success",
"pid": 15052,
"execution_time": 3.768344
},
{
"id": "5e008836",
"group_id": "1",
"script_name": "test_symbolic.py",
"executed_date": "2025-06-04T11:15:00.297979Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Calcv2",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 47804,
"execution_time": null
},
{
"id": "b7b7f4da",
"group_id": "1",
"script_name": "calc.py",
"executed_date": "2025-06-04T11:13:44.475321Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Calcv2",
"python_env": "tia_scripting",
"executable_type": "python.exe",
"status": "error",
"pid": 41924,
"execution_time": 27.585175
},
{
"id": "a8bf1045",
"group_id": "1",
"script_name": "calc.py",
"executed_date": "2025-06-04T11:13:24.997521Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Calcv2",
"python_env": "tia_scripting",
"executable_type": "python.exe",
"status": "error",
"pid": 29848,
"execution_time": 8.158545
}
],
"settings": {
"max_entries": 100,
"auto_cleanup_days": 30,

File diff suppressed because one or more lines are too long

View File

@ -7,11 +7,45 @@
"description": "",
"category": "Herramientas",
"version": "1.0",
"python_env": "tia_scripting",
"directory": "D:/Proyectos/Scripts/Calcv2",
"created_date": "2025-06-03T11:46:28.443910Z",
"updated_date": "2025-06-03T12:08:06.571266Z"
},
{
"id": "2",
"name": "ADAM Emulator - Gateway",
"description": "",
"category": "Desarrollo",
"version": "1.0",
"python_env": "tia_scripting",
"directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
"created_date": "2025-06-03T11:57:31.622922Z",
"updated_date": "2025-06-03T12:08:15.119223Z"
},
{
"id": "3",
"name": "Traducir Textos usando LLM",
"description": "",
"category": "Otros",
"version": "1.0",
"python_env": "tia_scripting",
"directory": "D:/Proyectos/Scripts/HMI Translate",
"created_date": "2025-06-03T12:31:19.529046Z",
"updated_date": "2025-06-03T12:44:24.651659Z"
},
{
"id": "4",
"name": "Tia Portal Utils",
"description": "",
"category": "Otros",
"version": "1.0",
"python_env": "tia_scripting",
"directory": "D:/Proyectos/Scripts/Siemens/Tia Portal Utils",
"author": "",
"tags": [],
"created_date": "2025-06-03T11:46:28.443910Z",
"updated_date": "2025-06-03T11:46:28.443910Z"
"created_date": "2025-06-11T22:55:41.635081Z",
"updated_date": "2025-06-11T22:55:41.635081Z"
}
],
"categories": {

View File

@ -0,0 +1,5 @@
[20:03:36] Iniciando ejecución de x1.py en D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\Source...
[20:03:37] --- TIA Portal Data Exporter (Blocks, UDTs, Tags) ---
[20:03:47] No project file selected. Exiting.
[20:03:47] Ejecución de x1.py finalizada (success). Duración: 0:00:10.497365.
[20:03:47] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\log_x1.txt

View File

@ -2,6 +2,8 @@ import os
import json
import subprocess
import sys
import threading
import time
from typing import Dict, Any, List, Optional
from datetime import datetime
import uuid
@ -12,6 +14,7 @@ class LauncherManager:
self.launcher_config_path = os.path.join(data_path, "launcher_scripts.json")
self.favorites_path = os.path.join(data_path, "launcher_favorites.json")
self.history_path = os.path.join(data_path, "launcher_history.json")
self.script_metadata_path = os.path.join(data_path, "launcher_script_metadata.json")
# Inicializar archivos si no existen
self._initialize_files()
@ -85,6 +88,15 @@ class LauncherManager:
with open(self.history_path, 'w', encoding='utf-8') as f:
json.dump(default_history, f, indent=2, ensure_ascii=False)
# Inicializar launcher_script_metadata.json
if not os.path.exists(self.script_metadata_path):
default_metadata = {
"version": "1.0",
"script_metadata": {}
}
with open(self.script_metadata_path, 'w', encoding='utf-8') as f:
json.dump(default_metadata, f, indent=2, ensure_ascii=False)
def get_launcher_groups(self) -> List[Dict[str, Any]]:
"""Obtener todos los grupos de scripts GUI"""
try:
@ -131,6 +143,7 @@ class LauncherManager:
group_data.setdefault("version", "1.0")
group_data.setdefault("author", "")
group_data.setdefault("tags", [])
group_data.setdefault("python_env", "base") # Entorno por defecto
group_data.setdefault("created_date", current_time)
group_data["updated_date"] = current_time
@ -197,13 +210,72 @@ class LauncherManager:
# Limpiar favoritos del grupo eliminado
self._cleanup_favorites_for_group(group_id)
# Limpiar metadatos de scripts del grupo eliminado
self._cleanup_script_metadata_for_group(group_id)
return {"status": "success", "message": "Grupo eliminado exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error eliminando grupo: {str(e)}"}
def get_group_scripts(self, group_id: str) -> List[Dict[str, Any]]:
"""Obtener scripts de un grupo específico"""
"""Obtener scripts de un grupo específico con metadatos"""
try:
print(f"[DEBUG] Loading scripts for group: {group_id}")
group = self.get_launcher_group(group_id)
if not group:
print(f"[DEBUG] Group {group_id} not found")
return []
directory = group["directory"]
print(f"[DEBUG] Group directory: {directory}")
if not os.path.isdir(directory):
print(f"[DEBUG] Directory {directory} does not exist or is not a directory")
return []
# Cargar metadatos de scripts
script_metadata = self._load_script_metadata()
scripts = []
python_files = [f for f in os.listdir(directory) if f.endswith('.py') and not f.startswith('_')]
print(f"[DEBUG] Found {len(python_files)} Python files: {python_files}")
for file in python_files:
script_path = os.path.join(directory, file)
if os.path.isfile(script_path):
# Clave para metadatos
metadata_key = f"{group_id}_{file}"
metadata = script_metadata.get(metadata_key, {})
# Verificar si está oculto
if metadata.get("hidden", False):
print(f"[DEBUG] Script {file} is hidden, skipping")
continue
script_info = {
"name": file,
"display_name": metadata.get("display_name", file[:-3]),
"description": metadata.get("description", ""),
"long_description": metadata.get("long_description", ""),
"path": script_path,
"size": os.path.getsize(script_path),
"modified": datetime.fromtimestamp(os.path.getmtime(script_path)).isoformat(),
"hidden": metadata.get("hidden", False)
}
scripts.append(script_info)
print(f"[DEBUG] Added script: {script_info['display_name']} ({file})")
print(f"[DEBUG] Returning {len(scripts)} scripts for group {group_id}")
return sorted(scripts, key=lambda x: x["display_name"])
except Exception as e:
print(f"Error getting scripts for group {group_id}: {e}")
import traceback
traceback.print_exc()
return []
def get_all_group_scripts(self, group_id: str) -> List[Dict[str, Any]]:
"""Obtener TODOS los scripts de un grupo (incluyendo ocultos) para gestión"""
try:
group = self.get_launcher_group(group_id)
if not group:
@ -213,28 +285,115 @@ class LauncherManager:
if not os.path.isdir(directory):
return []
# Cargar metadatos de scripts
script_metadata = self._load_script_metadata()
scripts = []
for file in os.listdir(directory):
if file.endswith('.py') and not file.startswith('_'):
script_path = os.path.join(directory, file)
if os.path.isfile(script_path):
# Clave para metadatos
metadata_key = f"{group_id}_{file}"
metadata = script_metadata.get(metadata_key, {})
scripts.append({
"name": file,
"display_name": file[:-3], # Sin extensión .py
"display_name": metadata.get("display_name", file[:-3]),
"description": metadata.get("description", ""),
"long_description": metadata.get("long_description", ""),
"path": script_path,
"size": os.path.getsize(script_path),
"modified": datetime.fromtimestamp(os.path.getmtime(script_path)).isoformat()
"modified": datetime.fromtimestamp(os.path.getmtime(script_path)).isoformat(),
"hidden": metadata.get("hidden", False)
})
return sorted(scripts, key=lambda x: x["name"])
except Exception as e:
print(f"Error getting scripts for group {group_id}: {e}")
print(f"Error getting all scripts for group {group_id}: {e}")
return []
def get_script_metadata(self, group_id: str, script_name: str) -> Dict[str, Any]:
"""Obtener metadatos de un script específico"""
try:
script_metadata = self._load_script_metadata()
metadata_key = f"{group_id}_{script_name}"
return script_metadata.get(metadata_key, {
"display_name": script_name[:-3] if script_name.endswith('.py') else script_name,
"description": "",
"long_description": "",
"hidden": False
})
except Exception as e:
print(f"Error getting script metadata for {group_id}/{script_name}: {e}")
return {}
def update_script_metadata(self, group_id: str, script_name: str, metadata: Dict[str, Any]) -> Dict[str, str]:
"""Actualizar metadatos de un script"""
try:
script_metadata = self._load_script_metadata()
metadata_key = f"{group_id}_{script_name}"
# Actualizar metadatos
script_metadata[metadata_key] = {
"display_name": metadata.get("display_name", script_name[:-3]),
"description": metadata.get("description", ""),
"long_description": metadata.get("long_description", ""),
"hidden": metadata.get("hidden", False),
"updated_date": datetime.now().isoformat() + "Z"
}
# Guardar
self._save_script_metadata(script_metadata)
return {"status": "success", "message": "Metadatos actualizados exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error actualizando metadatos: {str(e)}"}
def get_available_python_envs(self) -> List[Dict[str, str]]:
"""Obtener lista de entornos de Python/Miniconda disponibles"""
try:
envs = [{"name": "base", "display_name": "Base (Sistema)", "path": sys.executable}]
# Intentar encontrar Miniconda
miniconda_paths = [
r"C:\Users\migue\miniconda3",
r"C:\ProgramData\miniconda3",
r"C:\miniconda3",
os.path.expanduser("~/miniconda3"),
os.path.expanduser("~/anaconda3")
]
for base_path in miniconda_paths:
if os.path.exists(base_path):
envs_path = os.path.join(base_path, "envs")
if os.path.exists(envs_path):
for env_name in os.listdir(envs_path):
env_path = os.path.join(envs_path, env_name)
python_exe = os.path.join(env_path, "python.exe")
if os.path.exists(python_exe):
envs.append({
"name": env_name,
"display_name": f"{env_name} (Miniconda)",
"path": python_exe
})
break # Solo usar el primer Miniconda encontrado
return envs
except Exception as e:
print(f"Error getting Python environments: {e}")
return [{"name": "base", "display_name": "Base (Sistema)", "path": sys.executable}]
def execute_gui_script(self, group_id: str, script_name: str, script_args: List[str],
broadcast_func) -> Dict[str, Any]:
"""Ejecutar script GUI con argumentos opcionales"""
broadcast_func, working_dir: str = None, use_pythonw: bool = False) -> Dict[str, Any]:
"""Ejecutar script GUI con argumentos opcionales y entorno específico
Args:
use_pythonw: Si True, usa pythonw.exe (sin logging), si False usa python.exe (con logging)
"""
try:
group = self.get_launcher_group(group_id)
if not group:
@ -244,25 +403,152 @@ class LauncherManager:
if not os.path.isfile(script_path):
return {"status": "error", "message": "Script no encontrado"}
# Construir comando
cmd = [sys.executable, script_path] + script_args
working_dir = group["directory"] # Por defecto directorio del script
# Determinar el ejecutable de Python a usar
python_env = group.get("python_env", "base")
python_executable = self._get_python_executable(python_env, use_pythonw)
# Determinar directorio de trabajo
if working_dir and os.path.isdir(working_dir):
exec_working_dir = working_dir
else:
exec_working_dir = group["directory"] # Por defecto directorio del script
# Configurar variables de entorno para UTF-8
env = os.environ.copy()
env['PYTHONIOENCODING'] = 'utf-8'
env['PYTHONLEGACYWINDOWSSTDIO'] = '0'
# Variables adicionales para encoding
env['LANG'] = 'en_US.UTF-8'
env['LC_ALL'] = 'en_US.UTF-8'
env['PYTHONUNBUFFERED'] = '1'
# Forzar UTF-8 en consola de Windows
if sys.platform == "win32":
env['PYTHONUTF8'] = '1'
env['PYTHONLEGACYWINDOWSFSENCODING'] = '0'
# Construir comando con flag UTF-8
if use_pythonw:
cmd = [python_executable, script_path] + script_args
else:
# Para python.exe, agregar flag UTF-8 explícitamente
if python_executable.endswith('python.exe'):
cmd = [python_executable, '-X', 'utf8', script_path] + script_args
else:
cmd = [python_executable, script_path] + script_args
broadcast_func(f"Ejecutando script GUI: {script_name}")
broadcast_func(f"Entorno Python: {python_env}")
broadcast_func(f"Ejecutable: {'pythonw.exe (sin logging)' if use_pythonw else 'python.exe (con logging)'}")
broadcast_func(f"Comando: {' '.join(cmd)}")
broadcast_func(f"Directorio: {working_dir}")
broadcast_func(f"Directorio: {exec_working_dir}")
broadcast_func(f"Encoding: UTF-8 (PYTHONUTF8=1, PYTHONIOENCODING=utf-8)")
if '-X' in cmd and 'utf8' in cmd:
broadcast_func("Usando flag -X utf8 para forzar UTF-8")
broadcast_func("=" * 50)
# Ejecutar script
start_time = datetime.now()
if use_pythonw:
# Con pythonw.exe: No capturar salida, solo ejecutar
try:
process = subprocess.Popen(
cmd,
cwd=working_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
cwd=exec_working_dir,
env=env,
creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == "win32" else 0
)
broadcast_func("Script GUI ejecutado sin logging (pythonw.exe)")
broadcast_func(f"PID: {process.pid}")
broadcast_func("=" * 50)
status = "running"
except subprocess.SubprocessError as e:
error_msg = f"Error ejecutando con pythonw.exe: {str(e)}"
broadcast_func(error_msg)
return {"status": "error", "message": error_msg}
else:
# Con python.exe: Capturar salida para logging
try:
process = subprocess.Popen(
cmd,
cwd=exec_working_dir,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, # Combinar stderr con stdout
text=True,
encoding='utf-8', # Forzar UTF-8
universal_newlines=True,
bufsize=1 # Line buffered
)
# Leer salida en tiempo real
def read_output():
try:
for line in iter(process.stdout.readline, ''):
if line.strip(): # Solo enviar líneas no vacías
broadcast_func(line.rstrip())
process.stdout.close()
except Exception as e:
broadcast_func(f"Error leyendo salida: {str(e)}")
# Iniciar thread para leer salida
output_thread = threading.Thread(target=read_output, daemon=True)
output_thread.start()
# Esperar más tiempo para capturar salida inicial y detectar finalización
time.sleep(2)
# Verificar si el proceso sigue corriendo
poll_result = process.poll()
if poll_result is not None:
# El proceso terminó
execution_time = (datetime.now() - start_time).total_seconds()
# Esperar a que termine el thread de lectura
output_thread.join(timeout=1)
if poll_result == 0:
broadcast_func("=" * 50)
broadcast_func(f"Script completado exitosamente en {execution_time:.2f}s")
status = "success"
else:
broadcast_func("=" * 50)
broadcast_func(f"Script terminó con código de error: {poll_result}")
status = "error"
else:
# El proceso sigue corriendo (típico para GUIs)
broadcast_func("=" * 50)
broadcast_func(f"Script GUI iniciado correctamente (PID: {process.pid})")
broadcast_func("Nota: El script sigue ejecutándose en segundo plano")
status = "running"
# Iniciar un thread separado para monitorear la finalización
def monitor_completion():
try:
final_code = process.wait() # Esperar hasta que termine
end_time = datetime.now()
final_execution_time = (end_time - start_time).total_seconds()
# Actualizar el historial cuando termine
self._update_history_status(execution_id, final_code, final_execution_time)
if final_code == 0:
broadcast_func(f"[{end_time.strftime('%H:%M:%S')}] Script {script_name} completado exitosamente ({final_execution_time:.2f}s)")
else:
broadcast_func(f"[{end_time.strftime('%H:%M:%S')}] Script {script_name} terminó con error (código: {final_code})")
except Exception as e:
broadcast_func(f"Error monitoreando finalización: {str(e)}")
monitor_thread = threading.Thread(target=monitor_completion, daemon=True)
monitor_thread.start()
except subprocess.SubprocessError as e:
error_msg = f"Error ejecutando con python.exe: {str(e)}"
broadcast_func(error_msg)
return {"status": "error", "message": error_msg}
# Registrar en historial
execution_id = str(uuid.uuid4())[:8]
self._add_to_history({
@ -271,12 +557,14 @@ class LauncherManager:
"script_name": script_name,
"executed_date": start_time.isoformat() + "Z",
"arguments": script_args,
"working_directory": working_dir,
"status": "running",
"pid": process.pid
"working_directory": exec_working_dir,
"python_env": python_env,
"executable_type": "pythonw.exe" if use_pythonw else "python.exe",
"status": status,
"pid": process.pid,
"execution_time": (datetime.now() - start_time).total_seconds() if status != "running" else None
})
broadcast_func(f"Script GUI ejecutado con PID: {process.pid}")
broadcast_func(f"ID de ejecución: {execution_id}")
return {
@ -291,6 +579,87 @@ class LauncherManager:
broadcast_func(error_msg)
return {"status": "error", "message": error_msg}
def _get_python_executable(self, env_name: str, use_pythonw: bool = False) -> str:
"""Obtener el ejecutable de Python para un entorno específico
Args:
env_name: Nombre del entorno Python
use_pythonw: Si True, usa pythonw.exe (sin consola), si False usa python.exe (con logging)
"""
if env_name == "base":
# Para el sistema base
base_dir = os.path.dirname(sys.executable)
if use_pythonw:
pythonw_path = os.path.join(base_dir, "pythonw.exe")
if os.path.exists(pythonw_path):
return pythonw_path
python_path = os.path.join(base_dir, "python.exe")
if os.path.exists(python_path):
return python_path
return sys.executable
# Buscar en entornos de Miniconda
miniconda_paths = [
r"C:\Users\migue\miniconda3",
r"C:\ProgramData\miniconda3",
r"C:\miniconda3",
os.path.expanduser("~/miniconda3"),
os.path.expanduser("~/anaconda3")
]
for base_path in miniconda_paths:
if use_pythonw:
# Intentar pythonw.exe para GUI sin consola
env_pythonw_path = os.path.join(base_path, "envs", env_name, "pythonw.exe")
if os.path.exists(env_pythonw_path):
return env_pythonw_path
# Intentar python.exe para logging
env_python_path = os.path.join(base_path, "envs", env_name, "python.exe")
if os.path.exists(env_python_path):
return env_python_path
# Fallback final al sistema
base_dir = os.path.dirname(sys.executable)
if use_pythonw:
pythonw_path = os.path.join(base_dir, "pythonw.exe")
if os.path.exists(pythonw_path):
return pythonw_path
return sys.executable
def _load_script_metadata(self) -> Dict[str, Any]:
"""Cargar metadatos de scripts"""
try:
with open(self.script_metadata_path, 'r', encoding='utf-8') as f:
data = json.load(f)
return data.get("script_metadata", {})
except Exception as e:
print(f"Error loading script metadata: {e}")
return {}
def _save_script_metadata(self, metadata: Dict[str, Any]):
"""Guardar metadatos de scripts"""
try:
data = {
"version": "1.0",
"script_metadata": metadata,
"updated_date": datetime.now().isoformat() + "Z"
}
with open(self.script_metadata_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error saving script metadata: {e}")
def _cleanup_script_metadata_for_group(self, group_id: str):
"""Limpiar metadatos de scripts de un grupo eliminado"""
try:
script_metadata = self._load_script_metadata()
# Filtrar metadatos del grupo eliminado
filtered_metadata = {k: v for k, v in script_metadata.items() if not k.startswith(f"{group_id}_")}
self._save_script_metadata(filtered_metadata)
except Exception as e:
print(f"Error cleaning up script metadata for group {group_id}: {e}")
def get_favorites(self) -> List[Dict[str, Any]]:
"""Obtener lista de favoritos"""
try:
@ -414,3 +783,236 @@ class LauncherManager:
except Exception as e:
print(f"Error cleaning up favorites for group {group_id}: {e}")
def _update_history_status(self, execution_id: str, final_code: int, final_execution_time: float):
"""Actualizar el estado del historial de ejecución"""
try:
with open(self.history_path, 'r', encoding='utf-8') as f:
data = json.load(f)
history = data.get("history", [])
for i, entry in enumerate(history):
if entry["id"] == execution_id:
history[i]["status"] = "success" if final_code == 0 else "error"
history[i]["execution_time"] = final_execution_time
break
data["history"] = history
with open(self.history_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error updating history status: {e}")
def focus_process(self, pid: int) -> Dict[str, str]:
"""Activar el foco de un proceso por su PID"""
try:
if sys.platform == "win32":
import ctypes
from ctypes import wintypes
# Funciones de Windows API
user32 = ctypes.windll.user32
kernel32 = ctypes.windll.kernel32
# Encontrar ventana por PID
def enum_windows_proc(hwnd, pid):
window_pid = wintypes.DWORD()
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(window_pid))
if window_pid.value == pid:
# Traer ventana al frente
user32.SetForegroundWindow(hwnd)
user32.ShowWindow(hwnd, 9) # SW_RESTORE
return False # Detener enumeración
return True
# Enumeración de ventanas
WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, wintypes.HWND, wintypes.LPARAM)
enum_proc = WNDENUMPROC(enum_windows_proc)
user32.EnumWindows(enum_proc, pid)
return {"status": "success", "message": f"Proceso {pid} activado"}
else:
return {"status": "error", "message": "Función solo disponible en Windows"}
except Exception as e:
return {"status": "error", "message": f"Error activando proceso: {str(e)}"}
def terminate_process(self, pid: int) -> Dict[str, str]:
"""Cerrar un proceso por su PID"""
try:
import psutil
process = psutil.Process(pid)
process_name = process.name()
# Intentar cerrar suavemente primero
process.terminate()
# Esperar un momento para que cierre
time.sleep(1)
# Si sigue corriendo, forzar cierre
if process.is_running():
process.kill()
return {"status": "success", "message": f"Proceso {process_name} (PID: {pid}) cerrado exitosamente"}
except ImportError:
# Fallback sin psutil
try:
if sys.platform == "win32":
subprocess.run(["taskkill", "/F", "/PID", str(pid)], check=True)
else:
subprocess.run(["kill", "-9", str(pid)], check=True)
return {"status": "success", "message": f"Proceso {pid} cerrado"}
except subprocess.CalledProcessError as e:
return {"status": "error", "message": f"Error cerrando proceso: {str(e)}"}
except Exception as e:
return {"status": "error", "message": f"Error: {str(e)}"}
def get_running_processes(self) -> List[Dict[str, Any]]:
"""Obtener lista de procesos en ejecución del historial"""
try:
history = self.get_history()
running_processes = []
for entry in history:
if entry.get("status") == "running" and entry.get("pid"):
try:
# Verificar si el proceso sigue corriendo
if sys.platform == "win32":
result = subprocess.run(
["tasklist", "/FI", f"PID eq {entry['pid']}", "/FO", "CSV"],
capture_output=True, text=True
)
if str(entry['pid']) in result.stdout:
running_processes.append(entry)
else:
# Linux/Mac
result = subprocess.run(
["ps", "-p", str(entry['pid'])],
capture_output=True, text=True
)
if result.returncode == 0:
running_processes.append(entry)
except Exception:
continue
return running_processes
except Exception as e:
print(f"Error getting running processes: {e}")
return []
def get_markdown_files(self, group_id: str) -> List[Dict[str, Any]]:
"""Obtener archivos Markdown de un grupo (root y subdirectorios hasta 10 archivos)"""
try:
print(f"[DEBUG] Looking for markdown files in group: {group_id}")
group = self.get_launcher_group(group_id)
if not group:
print(f"[DEBUG] Group {group_id} not found for markdown search")
return []
directory = group["directory"]
print(f"[DEBUG] Searching markdown files in directory: {directory}")
if not os.path.isdir(directory):
print(f"[DEBUG] Directory {directory} does not exist for markdown search")
return []
markdown_files = []
# Buscar en directorio root
all_files = os.listdir(directory)
print(f"[DEBUG] All files in directory: {all_files}")
for file in all_files:
if file.lower().endswith('.md'):
file_path = os.path.join(directory, file)
if os.path.isfile(file_path):
print(f"[DEBUG] Found markdown file: {file}")
markdown_files.append({
"name": file,
"display_name": file[:-3], # Sin extensión .md
"path": file_path,
"relative_path": file,
"level": 0, # Root level
"size": os.path.getsize(file_path),
"modified": datetime.fromtimestamp(os.path.getmtime(file_path)).isoformat()
})
else:
print(f"[DEBUG] {file} is not a file, skipping")
# Buscar en subdirectorios (máximo 1 nivel)
try:
for subdir in all_files:
subdir_path = os.path.join(directory, subdir)
if os.path.isdir(subdir_path) and (not subdir.startswith('.') or subdir.startswith('.doc')):
print(f"[DEBUG] Searching in subdirectory: {subdir}")
subdir_files = os.listdir(subdir_path)
for file in subdir_files:
if file.lower().endswith('.md'):
file_path = os.path.join(subdir_path, file)
if os.path.isfile(file_path):
print(f"[DEBUG] Found markdown file in subdir: {subdir}/{file}")
markdown_files.append({
"name": file,
"display_name": f"{subdir}/{file[:-3]}",
"path": file_path,
"relative_path": f"{subdir}/{file}",
"level": 1, # Subdirectory level
"size": os.path.getsize(file_path),
"modified": datetime.fromtimestamp(os.path.getmtime(file_path)).isoformat()
})
# Limitar a 10 archivos total
if len(markdown_files) >= 10:
break
if len(markdown_files) >= 10:
break
except PermissionError as e:
print(f"[DEBUG] Permission error accessing subdirectories: {e}")
# Ignorar subdirectorios sin permisos
pass
# Ordenar por modificación (más recientes primero) y limitar a 10
markdown_files.sort(key=lambda x: x["modified"], reverse=True)
print(f"[DEBUG] Found {len(markdown_files)} total markdown files")
return markdown_files[:10]
except Exception as e:
print(f"Error getting markdown files for group {group_id}: {e}")
import traceback
traceback.print_exc()
return []
def read_markdown_file(self, group_id: str, relative_path: str) -> Dict[str, Any]:
"""Leer contenido de un archivo Markdown"""
try:
group = self.get_launcher_group(group_id)
if not group:
return {"status": "error", "message": "Grupo no encontrado"}
file_path = os.path.join(group["directory"], relative_path)
# Verificar que el archivo existe y está dentro del directorio del grupo (seguridad)
if not os.path.isfile(file_path):
return {"status": "error", "message": "Archivo no encontrado"}
if not file_path.startswith(group["directory"]):
return {"status": "error", "message": "Acceso denegado"}
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
return {
"status": "success",
"content": content,
"file_name": os.path.basename(file_path),
"file_path": relative_path,
"size": len(content),
"modified": datetime.fromtimestamp(os.path.getmtime(file_path)).isoformat()
}
except Exception as e:
return {"status": "error", "message": f"Error leyendo archivo: {str(e)}"}

View File

@ -14,3 +14,11 @@ pypandoc==1.15
Requests==2.32.3
siemens_tia_scripting==1.0.7
sympy==1.13.3
pillow
pystray
# Nota: Para evitar problemas de encoding, configura estas variables de entorno:
# PYTHONIOENCODING=utf-8
# PYTHONLEGACYWINDOWSSTDIO=0
#
# O ejecuta Python con: python -X utf8 app.py

12
run_utf8.bat Normal file
View File

@ -0,0 +1,12 @@
@echo off
echo Configurando encoding UTF-8 para ParamManager...
set PYTHONIOENCODING=utf-8
set PYTHONLEGACYWINDOWSSTDIO=0
echo.
echo Variables de entorno configuradas:
echo PYTHONIOENCODING=%PYTHONIOENCODING%
echo PYTHONLEGACYWINDOWSSTDIO=%PYTHONLEGACYWINDOWSSTDIO%
echo.
echo Iniciando ParamManager...
python app.py
pause

View File

@ -221,8 +221,8 @@
}
.group-icon-small {
width: 24px;
height: 24px;
width: 20px;
height: 20px;
border-radius: 4px;
object-fit: cover;
border: 1px solid #E5E7EB;
@ -234,7 +234,7 @@
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
font-size: 10px;
}
/* Category Styles */
@ -291,12 +291,10 @@
.favorite-star {
cursor: pointer;
transition: all 0.2s ease;
color: #D1D5DB;
}
.favorite-star:hover {
transform: scale(1.1);
color: #F59E0B;
}
.favorite-star.active {
@ -305,16 +303,11 @@
/* History Item */
.history-item {
padding: 1rem;
margin-bottom: 0.5rem;
border-radius: 0.5rem;
border: 1px solid #E5E7EB;
border-radius: 6px;
padding: 0.75rem;
background: #F9FAFB;
transition: all 0.2s ease;
}
.history-item:hover {
background: #F3F4F6;
border-color: #D1D5DB;
background: white;
}
.history-item.success {
@ -327,6 +320,19 @@
.history-item.running {
border-left: 4px solid #3B82F6;
background: #F0F9FF;
}
/* Process control buttons */
.history-item button {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
transition: all 0.2s ease;
}
.history-item button:hover {
background-color: rgba(0, 0, 0, 0.1);
}
/* Favorites Panel */
@ -338,6 +344,19 @@
display: none;
}
/* Favorite Cards - Diseño más compacto */
.favorites-card {
transition: all 0.2s ease;
border: 1px solid #FCD34D;
background: white;
}
.favorites-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-color: #F59E0B;
}
/* Group List Item */
.group-list-item {
display: flex;
@ -534,3 +553,172 @@
outline: 2px solid #3B82F6;
outline-offset: 2px;
}
/* Description Modal Styles */
.prose {
color: #374151;
line-height: 1.75;
}
.prose h1,
.prose h2,
.prose h3,
.prose h4,
.prose h5,
.prose h6 {
color: #111827;
font-weight: 600;
margin-top: 1.5em;
margin-bottom: 0.5em;
}
.prose h1 {
font-size: 1.875rem;
border-bottom: 1px solid #E5E7EB;
padding-bottom: 0.5rem;
}
.prose h2 {
font-size: 1.5rem;
}
.prose h3 {
font-size: 1.25rem;
}
.prose p {
margin-bottom: 1rem;
}
.prose ul,
.prose ol {
margin-bottom: 1rem;
padding-left: 1.5rem;
}
.prose li {
margin-bottom: 0.25rem;
}
.prose code {
background-color: #F3F4F6;
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
font-size: 0.875em;
color: #EF4444;
}
.prose pre {
background-color: #F9FAFB;
border: 1px solid #E5E7EB;
border-radius: 0.5rem;
padding: 1rem;
overflow-x: auto;
margin: 1rem 0;
}
.prose pre code {
background: none;
padding: 0;
color: #374151;
}
.prose blockquote {
border-left: 4px solid #E5E7EB;
padding-left: 1rem;
font-style: italic;
color: #6B7280;
margin: 1rem 0;
}
.prose table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
}
.prose th,
.prose td {
border: 1px solid #E5E7EB;
padding: 0.5rem;
text-align: left;
}
.prose th {
background-color: #F9FAFB;
font-weight: 600;
}
.prose a {
color: #3B82F6;
text-decoration: underline;
}
.prose a:hover {
color: #1D4ED8;
}
/* Script Card Button Improvements */
.script-card .flex.gap-1 button {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
transition: all 0.2s ease;
}
.script-card .flex.gap-1 button:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Markdown Files */
.markdown-file-card {
transition: all 0.2s ease;
border: 1px solid #E5E7EB;
}
.markdown-file-card:hover {
border-color: #3B82F6;
transform: translateY(-2px);
}
/* Responsive adjustments for favorites grid */
@media (max-width: 640px) {
.favorites-list.grid {
grid-template-columns: 1fr;
}
}
@media (min-width: 641px) and (max-width: 1023px) {
.favorites-list.grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.favorites-list.grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* Button styles for favorite cards */
.favorites-card button {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
transition: all 0.2s ease;
border: none;
cursor: pointer;
}
.favorites-card button:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
/* Truncate text utilities */
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

BIN
static/icons/cursor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
static/icons/vscode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

File diff suppressed because it is too large Load Diff

View File

@ -876,10 +876,14 @@ function toggleConfig(sectionId) {
}
async function clearLogs() {
// Eliminada la confirmación - directamente procede a limpiar
const response = await fetch('/api/logs', { method: 'DELETE' });
const result = await response.json();
if (result.status === 'success') {
document.getElementById('log-area').innerHTML = '';
showToast ? showToast('Logs borrados correctamente', 'success') : console.log('Logs cleared');
} else {
showToast ? showToast('Error al borrar los logs', 'error') : alert('Error al borrar los logs');
}
}
@ -1188,26 +1192,6 @@ function fetchLogs() {
.catch(error => console.error('Error fetching logs:', error));
}
function clearLogs() {
if (confirm("¿Estás seguro de que quieres borrar los logs?")) {
fetch('/api/logs', { method: 'DELETE' })
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// Limpiar el área de log visualmente AHORA
document.getElementById('log-area').innerHTML = '';
showToast('Logs borrados correctamente.');
} else {
showToast('Error al borrar los logs.', 'error');
}
})
.catch(error => {
console.error('Error clearing logs:', error);
showToast('Error de red al borrar los logs.', 'error');
});
}
}
// Necesitarás una función showToast o similar si la usas
function showToast(message, type = 'success') {
// Implementa tu lógica de Toast aquí
@ -1276,28 +1260,37 @@ async function saveConfig(level) {
}
}
async function openGroupInVsCode() {
if (!currentGroup) {
async function openGroupInEditor(editorCode, groupSystem, groupId) {
// groupId is already the currentGroup string from the select
if (!groupId) {
alert('Por favor, seleccione un grupo de scripts primero');
return;
}
const editorName = editorCode.toUpperCase();
try {
const response = await fetch(`/api/open-vscode/${currentGroup}`, {
const response = await fetch(`/api/open-editor/${editorCode}/${groupSystem}/${groupId}`, {
method: 'POST'
});
const result = await response.json();
if (result.status === 'success') {
console.log('VS Code opened successfully');
if (!response.ok) {
// If response is not OK, it might not be JSON (e.g., Flask error page)
const errorText = await response.text();
console.error(`Error al abrir ${editorName}: HTTP ${response.status}`, errorText);
alert(`Error al abrir ${editorName}: ${response.status} ${response.statusText}\n${errorText.substring(0, 200)}...`); // Show limited error text
} else {
console.error('Error opening VS Code:', result.message);
alert(`Error al abrir VS Code: ${result.message}`);
const result = await response.json(); // Now it's safer to parse JSON
if (result.status === 'success') {
console.log(`${editorName} opened successfully`);
} else {
console.error(`Error al abrir ${editorName}:`, result.message);
alert(`Error al abrir ${editorName}: ${result.message}`);
}
}
} catch (error) {
console.error('Error calling open-vscode API:', error);
alert('Error al intentar abrir VS Code');
console.error(`Error en la llamada fetch para open-editor (${editorName}):`, error);
alert(`Error de red o del cliente al intentar abrir ${editorName}.`);
}
}
@ -1357,3 +1350,68 @@ async function openCurrentWorkingDirectoryInExplorer() {
showToast("Error de red al intentar abrir el explorador.", "error");
}
}
async function openGroupFolder(groupSystem, groupId) {
if (!groupId) {
alert('Por favor, seleccione un grupo de scripts primero');
return;
}
try {
const response = await fetch(`/api/open-group-folder/${groupSystem}/${groupId}`, {
method: 'POST'
});
const result = await response.json();
if (result.status === 'success') {
console.log(`Carpeta abierta: ${result.path}`);
} else {
alert(`Error al abrir carpeta: ${result.message}`);
}
} catch (error) {
console.error('Error opening folder:', error);
alert('Error al comunicarse con el servidor');
}
}
async function copyGroupPath(groupSystem, groupId) {
if (!groupId) {
alert('Por favor, seleccione un grupo de scripts primero');
return;
}
try {
const response = await fetch(`/api/get-group-path/${groupSystem}/${groupId}`);
const result = await response.json();
if (result.status === 'success') {
// Copiar al portapapeles usando la API moderna
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(result.path);
showToast ? showToast('Path copiado al portapapeles', 'success') : alert('Path copiado al portapapeles');
} else {
// Fallback para navegadores más antiguos
const textArea = document.createElement('textarea');
textArea.value = result.path;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
showToast ? showToast('Path copiado al portapapeles', 'success') : alert('Path copiado al portapapeles');
} catch (err) {
console.error('Error copying to clipboard:', err);
alert(`Error al copiar. Path: ${result.path}`);
}
document.body.removeChild(textArea);
}
} else {
alert(`Error al obtener path: ${result.message}`);
}
} catch (error) {
console.error('Error getting path:', error);
alert('Error al comunicarse con el servidor');
}
}

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Script Parameter Manager</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}?v=1.1">
</head>
<body class="bg-gray-100">
@ -140,14 +140,33 @@
</path>
</svg>
</button>
<button onclick="openGroupInVsCode()" class="bg-blue-500 text-white p-2 rounded mb-2"
<button onclick="openGroupInEditor('vscode', 'config', currentGroup)"
class="bg-blue-500 text-white p-2 rounded mb-2" id="vscode-config-btn"
title="Abrir grupo en VS Code">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
<img src="{{ url_for('static', filename='icons/vscode.png') }}" class="w-5 h-5"
alt="VS Code Icon">
</button>
<!-- Nuevo botón para Cursor -->
<button onclick="openGroupInEditor('cursor', 'config', currentGroup)"
class="bg-blue-500 text-white p-2 rounded mb-2" id="cursor-config-btn"
title="Abrir grupo en Cursor">
<img src="{{ url_for('static', filename='icons/cursor.png') }}" class="w-5 h-5"
alt="Cursor Icon">
</button>
<!-- Botón para abrir carpeta del grupo -->
<button onclick="openGroupFolder('config', currentGroup)"
class="bg-green-500 text-white p-2 rounded mb-2" id="folder-config-btn"
title="Abrir carpeta del grupo">
📁
</button>
<!-- Botón para copiar path del grupo -->
<button onclick="copyGroupPath('config', currentGroup)"
class="bg-gray-500 text-white p-2 rounded mb-2" id="copy-path-config-btn"
title="Copiar path del grupo">
📋
</button>
</div>
<p id="group-description" class="text-gray-600 text-sm italic"></p>
<div class="text-xs text-gray-500 mt-2">
<span id="group-version"></span>
@ -223,17 +242,47 @@
<!-- Group Selector -->
<div class="mb-4">
<label class="block text-sm font-medium mb-2">Seleccionar Grupo de Scripts</label>
<div class="relative">
<div class="flex gap-2">
<div class="relative flex-1">
<select id="launcher-group-select" class="w-full p-3 border rounded-lg pl-12"
onchange="loadLauncherScripts()">
<option value="">-- Seleccionar Grupo --</option>
</select>
<div class="absolute left-3 top-1/2 transform -translate-y-1/2">
<div id="selected-group-icon"
class="w-6 h-6 bg-gray-200 rounded flex items-center justify-center text-sm">📁</div>
class="w-6 h-6 bg-gray-200 rounded flex items-center justify-center text-sm">📁
</div>
</div>
</div>
<button
onclick="launcherManager.openGroupInEditor('vscode', 'launcher', launcherManager.currentGroup.id)"
class="bg-purple-500 text-white px-4 py-3 rounded-lg hover:bg-purple-600"
id="vscode-launcher-btn" style="display: none;" title="Abrir grupo en VS Code">
<img src="{{ url_for('static', filename='icons/vscode.png') }}" class="w-5 h-5"
alt="VS Code Icon">
</button>
<!-- Nuevo botón para Cursor -->
<button
onclick="launcherManager.openGroupInEditor('cursor', 'launcher', launcherManager.currentGroup.id)"
class="bg-purple-500 text-white px-4 py-3 rounded-lg hover:bg-purple-600"
id="cursor-launcher-btn" style="display: none;" title="Abrir grupo en Cursor">
<img src="{{ url_for('static', filename='icons/cursor.png') }}" class="w-5 h-5"
alt="Cursor Icon">
</button>
<!-- Botón para abrir carpeta del grupo launcher -->
<button onclick="launcherManager.openGroupFolder()"
class="bg-green-500 text-white px-4 py-3 rounded-lg hover:bg-green-600"
id="folder-launcher-btn" style="display: none;" title="Abrir carpeta del grupo">
📁
</button>
<!-- Botón para copiar path del grupo launcher -->
<button onclick="launcherManager.copyGroupPath()"
class="bg-gray-500 text-white px-4 py-3 rounded-lg hover:bg-gray-600"
id="copy-path-launcher-btn" style="display: none;" title="Copiar path del grupo">
📋
</button>
</div>
</div>
<!-- Category Filter -->
<div class="mb-4">
@ -276,21 +325,43 @@
<!-- Scripts Grid -->
<div class="mb-6 bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Scripts Disponibles</h2>
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Scripts Disponibles</h2>
<button onclick="openScriptManager()"
class="bg-purple-500 text-white px-4 py-2 rounded hover:bg-purple-600" id="manage-scripts-btn"
style="display: none;">
Gestionar Scripts
</button>
</div>
<div id="launcher-scripts-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Scripts cards dinámicos -->
</div>
</div>
<!-- Markdown Files Section -->
<div id="markdown-files-section" class="mb-6 bg-white p-6 rounded-lg shadow" style="display: none;">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">📄 Documentación (Markdown)</h2>
<span class="text-sm text-gray-500">Archivos .md en el directorio del grupo</span>
</div>
<div id="markdown-files-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
<!-- Markdown files cards dinámicos -->
</div>
</div>
<!-- History Panel -->
<div class="mb-6 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<div class="flex justify-between items-center mb-4 cursor-pointer" onclick="toggleHistoryPanel()">
<div class="flex items-center">
<span id="history-toggle-icon" class="mr-2"></span>
<h3 class="text-lg font-semibold">📝 Historial Reciente</h3>
<button onclick="clearLauncherHistory()" class="text-red-500 hover:text-red-700 text-sm">
</div>
<button onclick="event.stopPropagation(); clearLauncherHistory()"
class="text-red-500 hover:text-red-700 text-sm">
Limpiar Historial
</button>
</div>
<div id="history-list" class="space-y-2 max-h-64 overflow-y-auto">
<div id="history-list" class="space-y-2 max-h-64 overflow-y-auto hidden">
<!-- Lista dinámica de historial -->
</div>
</div>
@ -441,7 +512,7 @@
<textarea id="group-description" class="w-full p-2 border rounded h-20"></textarea>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium mb-1">Categoría</label>
<select id="group-category" class="w-full p-2 border rounded">
@ -457,6 +528,12 @@
<label class="block text-sm font-medium mb-1">Versión</label>
<input type="text" id="group-version" class="w-full p-2 border rounded" value="1.0">
</div>
<div>
<label class="block text-sm font-medium mb-1">Entorno Python</label>
<select id="group-python-env" class="w-full p-2 border rounded">
<option value="base">Cargando...</option>
</select>
</div>
</div>
<div>
@ -490,6 +567,90 @@
</div>
</div>
<!-- Script Manager Modal (NUEVO) -->
<div id="script-manager-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-screen overflow-y-auto">
<div class="p-6">
<h3 class="text-xl font-semibold mb-6">Gestionar Scripts del Grupo</h3>
<p class="text-gray-600 mb-4" id="script-manager-group-info">Selecciona un grupo para gestionar sus
scripts</p>
<!-- Lista de scripts -->
<div class="space-y-3" id="script-manager-list">
<!-- Lista dinámica de scripts -->
</div>
<div class="flex justify-end gap-3 pt-4 mt-6 border-t">
<button type="button" onclick="closeScriptManager()"
class="px-4 py-2 text-gray-600 hover:text-gray-800">
Cerrar
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Script Metadata Editor Modal (NUEVO) -->
<div id="script-metadata-editor-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-lg w-full max-h-screen overflow-y-auto">
<div class="p-6">
<h3 class="text-lg font-semibold mb-4">Editar Metadatos del Script</h3>
<form id="script-metadata-form" class="space-y-4">
<input type="hidden" id="edit-meta-group-id">
<input type="hidden" id="edit-meta-script-name">
<div>
<label class="block text-sm font-bold mb-1">Nombre del Archivo</label>
<p id="edit-meta-filename-display"
class="text-sm text-gray-600 bg-gray-100 p-2 rounded border"></p>
</div>
<div>
<label for="edit-meta-display-name" class="block text-sm font-bold mb-2">Nombre a
Mostrar</label>
<input type="text" id="edit-meta-display-name" class="w-full p-2 border rounded" required>
</div>
<div>
<label for="edit-meta-description" class="block text-sm font-bold mb-2">Descripción
Corta</label>
<input type="text" id="edit-meta-description" class="w-full p-2 border rounded">
</div>
<div>
<label for="edit-meta-long-description" class="block text-sm font-bold mb-2">Descripción
Larga / Ayuda</label>
<textarea id="edit-meta-long-description" class="w-full p-2 border rounded"
rows="5"></textarea>
<p class="text-xs text-gray-500 mt-1">Usa Markdown. Doble Enter para párrafo nuevo, dos
espacios + Enter para salto de línea simple.</p>
</div>
<div class="flex items-center">
<input type="checkbox" id="edit-meta-hidden" class="form-checkbox h-5 w-5 mr-2">
<label for="edit-meta-hidden" class="text-sm font-bold">Ocultar script (no aparecerá en la
lista de ejecución)</label>
</div>
<div class="flex justify-end gap-3 pt-4">
<button type="button" onclick="closeScriptMetadataEditor()"
class="px-4 py-2 text-gray-600 hover:text-gray-800">
Cancelar
</button>
<button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Guardar Cambios
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Script Arguments Modal (NUEVO) -->
<div id="script-args-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
<div class="flex items-center justify-center min-h-screen p-4">
@ -512,6 +673,39 @@
Separar argumentos con espacios. Usar comillas para valores con espacios.
</p>
</div>
<div>
<label class="block text-sm font-medium mb-1">
Directorio de Ejecución
</label>
<div class="flex gap-2">
<input type="text" id="script-working-dir" class="flex-1 p-2 border rounded"
placeholder="Por defecto: directorio del script">
<button type="button" onclick="browseWorkingDirectory()"
class="bg-gray-500 text-white px-3 py-2 rounded text-sm">
📁
</button>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">
Tipo de Ejecución
</label>
<div class="flex gap-2">
<label class="flex items-center">
<input type="radio" name="execution-type" value="false" checked class="mr-2">
<span class="text-sm">🖥️ Con Log (python.exe)</span>
</label>
<label class="flex items-center">
<input type="radio" name="execution-type" value="true" class="mr-2">
<span class="text-sm">🚀 Sin Log (pythonw.exe)</span>
</label>
</div>
<p class="text-xs text-gray-500 mt-1">
Con Log: Muestra la salida del script. Sin Log: No muestra ventana de consola.
</p>
</div>
</div>
<div class="flex justify-end gap-3 mt-6">
@ -528,10 +722,71 @@
</div>
</div>
<!-- Script Description Modal (NUEVO) -->
<div id="script-description-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[80vh] overflow-hidden">
<div class="p-6 border-b">
<div class="flex justify-between items-start">
<div>
<h3 class="text-lg font-semibold" id="desc-modal-script-name">Descripción del Script</h3>
<p class="text-sm text-gray-600" id="desc-modal-script-file"></p>
</div>
<button onclick="closeDescriptionModal()"
class="text-gray-500 hover:text-gray-700 text-2xl">&times;</button>
</div>
</div>
<div class="p-6 overflow-y-auto max-h-[60vh]">
<div id="script-description-content" class="prose prose-sm max-w-none">
<!-- Contenido markdown renderizado -->
</div>
</div>
<div class="p-4 border-t bg-gray-50 flex justify-end">
<button onclick="closeDescriptionModal()"
class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600">
Cerrar
</button>
</div>
</div>
</div>
</div>
<!-- Markdown Viewer Modal (NUEVO) -->
<div id="markdown-viewer-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden">
<div class="p-6 border-b">
<div class="flex justify-between items-start">
<div>
<h3 class="text-lg font-semibold" id="markdown-viewer-title">Documento Markdown</h3>
<p class="text-sm text-gray-600" id="markdown-viewer-path"></p>
</div>
<button onclick="closeMarkdownViewer()"
class="text-gray-500 hover:text-gray-700 text-2xl">&times;</button>
</div>
</div>
<div class="p-6 overflow-y-auto max-h-[75vh]">
<div id="markdown-viewer-content" class="prose prose-lg max-w-none">
<!-- Contenido markdown renderizado -->
</div>
</div>
<div class="p-4 border-t bg-gray-50 flex justify-end">
<button onclick="closeMarkdownViewer()"
class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600">
Cerrar
</button>
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
<script src="{{ url_for('static', filename='js/scripts.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/launcher.js') }}" defer></script>
<script>
// Inicializar markdown-it globalmente
window.markdownit = window.markdownit || markdownit;
window.addEventListener('load', () => {
console.log('Window loaded, initializing app...');
if (typeof initializeApp === 'function') {