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.
This commit is contained in:
Miguel 2025-06-06 16:35:11 +02:00
parent 71a2a63de4
commit 88806ee4e4
20 changed files with 2158 additions and 245 deletions

222
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
@ -609,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:
@ -690,8 +693,225 @@ 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
if __name__ == "__main__":
# --- Start Flask in a background thread ---

View File

@ -1,35 +1,36 @@
--- 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 16:30:18
Fin: 2025-06-06 16:33:55
Duración: 0:03:37.052535
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
Selected Project: D:/Trabajo/VM/44 - 98050 - Fiera/InLavoro/PLC/98050_PLC_01/98050_PLC_01.ap19
Using Output Directory (Working Directory): D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia
Detected TIA Portal version: 19.0 (from extension .ap19)
Will export CAx data to: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_CAx_Export.aml
Will generate summary to: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_CAx_Summary.md
Export log file: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_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
Connecting to TIA Portal V19.0...
2025-06-06 16:30:25,861 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Global OpenPortal - Start TIA Portal, please acknowledge the security dialog.
2025-06-06 16:30:25,895 [1] INFO Siemens.TiaPortal.OpennessApi19.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
Opening project: 98050_PLC_01.ap19...
2025-06-06 16:32:34,404 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Portal OpenProject - Open project... D:\Trabajo\VM\44 - 98050 - Fiera\InLavoro\PLC\98050_PLC_01\98050_PLC_01.ap19
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...
Exporting CAx data for the project to D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_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
2025-06-06 16:33:52,008 [1] INFO Siemens.TiaPortal.OpennessApi19.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
Parsing AML file: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_CAx_Export.aml
Markdown summary written to: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_CAx_Summary.md
Script finished.

View File

@ -1,48 +1,67 @@
--- 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-06 16:34:01
Fin: 2025-06-06 16:34:09
Duración: 0:00:08.316649
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...
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_CAx_Export.aml
Output Directory: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia
Output JSON: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_CAx_Export.hierarchical.json
Output IO Debug Tree MD: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_CAx_Export_IO_Upward_Debug.md
Processing AML file: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_CAx_Export.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 (fcbafb48-c53d-4af5-8143-794df99be4d6) Type: Unknown
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:ffef8b7b-b6bf-46aa-b3b5-f5083c420563, Addr:10.1.30.11) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:221c248d-83d8-464c-9425-c949086b34e7, Addr:10.1.30.58) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:5c3bf795-ecec-47e7-a962-4d54c95b9887, Addr:10.1.30.59) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:4100c829-d889-489a-971c-a69207333e2f, Addr:10.1.30.60) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:a6aed467-1a4c-4a5a-bf3d-e7ce46e94a9f, Addr:10.1.30.61) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:b3d71b04-f99a-4af8-aa69-2ca02a30d391, Addr:10.1.30.62) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:eb59daf8-f59c-435e-bfe4-8e7afd0937bd, Addr:10.1.30.63) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:3e931f82-08a5-44de-89a5-b907f5aaeb24, Addr:10.1.30.64) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:f673d611-7a73-4a8e-912d-ea16c294d51b, Addr:10.1.30.65) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:09a2569a-4948-4830-9dd8-315ae638e391, Addr:10.1.30.66) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:719a9643-c903-454e-8325-0f03cf871c35, Addr:10.1.30.31) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:ef8c3829-2363-43b3-ae20-7e903c2517a5, Addr:10.1.30.32) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:7cece978-67aa-4707-b23c-375ebddfc2bc, Addr:10.1.30.170) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:ad86d223-8f60-41c0-8208-c88d68e42154, Addr:10.1.30.33) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:fa86bb0c-6142-41ce-ba6c-565e1e1f810e, Addr:10.1.30.34) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:441a0b56-5830-4595-ab8d-b073b6db8545, Addr:10.1.30.35) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:8dbc0c6b-a8f5-491c-832b-4af0757c259d, Addr:10.1.30.36) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:d06cdd21-62c3-4cff-9389-12a4e21869a2, Addr:10.1.30.40) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:f63db18d-dc19-48b8-afaa-be557db4d13e, Addr:10.1.30.44) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:21589a71-d4dd-4534-b457-deb72f3f7660, Addr:10.1.30.41) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:230ddec2-fa03-44d9-8350-0c0eeb463400, Addr:10.1.30.42) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:b2647d59-ed85-44c5-813f-167c41ea1ca1, Addr:10.1.30.43) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:9933a2a3-47db-448a-a724-ed583d5994bb, Addr:10.1.30.37) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:9b45edaa-0640-4e86-ba59-1b991c1a85ca, Addr:10.1.30.45) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:a5fbeadb-a8ab-42e0-b58f-296e44633ce5, Addr:10.1.30.46) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:67d1d9ad-cf1d-43aa-9dc4-9e574cda5e5b, Addr:10.1.30.47) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:08689cb8-1b00-403f-bc20-c5911a20e655, Addr:10.1.30.48) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:a06a0e94-b651-4510-bbe0-8a0c3b717283, Addr:10.1.30.49) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:cf339d67-1758-4462-85f0-9e6a002d1c3c, Addr:10.1.30.70) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:d80a9fcf-5e0d-45a9-b617-362a9cb4121a, Addr:10.1.30.71) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:7c28d11a-9673-4957-931d-8840a589da85, Addr:10.1.30.72) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:ad792043-fdb8-4695-a10a-aa2cad996cf0, Addr:10.1.30.74) to Network 'PN/IE_1'
Mapping Device/Node 'IE1' (NodeID:b30940b8-6cb5-4143-854f-9b569f081703, 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_CAx_Export.hierarchical.json
JSON data written successfully.
IO upward debug tree written to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\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_CAx_Export_IO_Upward_Debug.md
Found 1 PLC(s). Generating individual hardware trees...
Generating Hardware Tree for PLC 'PLC' (ID: a48e038f-0bcc-4b48-8373-033da316c62b) at: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\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_CAx_Export_Hardware_Tree.md
Markdown tree summary written to: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\A40510\Documentation\98050_PLC_01_CAx_Export_Hardware_Tree.md
Script finished.

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

@ -1,6 +1,7 @@
{
"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",
"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

@ -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

@ -1,47 +1,199 @@
{
"history": [
{
"id": "03b026d2",
"id": "6d4e8908",
"group_id": "1",
"script_name": "calc.py",
"executed_date": "2025-06-03T12:09:48.856960Z",
"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": 47056
"pid": 14212,
"execution_time": null
},
{
"id": "b27ada90",
"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-03T12:09:12.887585Z",
"executed_date": "2025-06-04T22:07:51.493125Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
"python_env": "tia_scripting",
"status": "running",
"pid": 18248
"executable_type": "python.exe",
"status": "success",
"pid": 20460,
"execution_time": 37.787662
},
{
"id": "fb227680",
"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-03T12:08:27.391884Z",
"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": 38664
"pid": 43496,
"execution_time": null
},
{
"id": "f0b71d84",
"id": "b7796abb",
"group_id": "1",
"script_name": "calc.py",
"executed_date": "2025-06-03T11:48:50.783603Z",
"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": 29560
"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": {

View File

@ -47,8 +47,8 @@
"display_name": "test_symbolic",
"description": "",
"long_description": "",
"hidden": true,
"updated_date": "2025-06-03T12:06:00.100424Z"
"hidden": false,
"updated_date": "2025-06-03T13:44:26.045048Z"
},
"1_tl_bracket_parser.py": {
"display_name": "tl_bracket_parser",
@ -77,7 +77,133 @@
"long_description": "## Descripción\n\nSistema de Álgebra Computacional (CAS) que combina SymPy con clases especializadas para networking, programación y análisis numérico. Incluye sistema de tipos dinámico con auto-descubrimiento.\n\n## Características Principales\n\n- **Motor SymPy completo**: Cálculo simbólico y numérico integrado\n- **Sistema de tipos dinámico**: Auto-descubrimiento desde `custom_types/`\n- **Sintaxis simplificada**: `Tipo[args]` en lugar de `Tipo(\"args\")`\n- **Detección automática de ecuaciones**: Sin sintaxis especial\n- **Contexto limpio por evaluación**: Cada modificación evalúa todo desde cero, garantizando comportamiento predecible\n- **Resultados interactivos**: Elementos clickeables para plots, matrices y listas\n- **Autocompletado inteligente**: Basado en tipos registrados dinámicamente",
"hidden": false,
"updated_date": "2025-06-03T12:07:20.450126Z"
},
"2_main.py": {
"display_name": "Simulador ADAM",
"description": "Simulador - Gateway - Sniffer",
"long_description": "",
"hidden": false,
"updated_date": "2025-06-03T12:34:27.531689Z"
},
"2_config_manager.py": {
"display_name": "config_manager",
"description": "",
"long_description": "",
"hidden": true,
"updated_date": "2025-06-03T12:34:33.310121Z"
},
"2_connection_manager.py": {
"display_name": "connection_manager",
"description": "",
"long_description": "",
"hidden": true,
"updated_date": "2025-06-03T12:34:36.597956Z"
},
"2_maselli_app.py": {
"display_name": "maselli_app",
"description": "",
"long_description": "",
"hidden": true,
"updated_date": "2025-06-03T12:34:39.560628Z"
},
"2_protocol_handler.py": {
"display_name": "protocol_handler",
"description": "",
"long_description": "",
"hidden": true,
"updated_date": "2025-06-03T12:34:42.028505Z"
},
"2_utils.py": {
"display_name": "utils",
"description": "",
"long_description": "",
"hidden": true,
"updated_date": "2025-06-03T12:34:44.270726Z"
},
"3_google_api_key.py": {
"display_name": "google_api_key",
"description": "",
"long_description": "",
"hidden": true,
"updated_date": "2025-06-03T12:35:47.564659Z"
},
"3_openai_api_key.py": {
"display_name": "openai_api_key",
"description": "",
"long_description": "",
"hidden": true,
"updated_date": "2025-06-03T12:35:55.946088Z"
},
"3_test.py": {
"display_name": "test",
"description": "",
"long_description": "",
"hidden": true,
"updated_date": "2025-06-03T12:36:06.694912Z"
},
"3_translation_config.py": {
"display_name": "translation_config",
"description": "",
"long_description": "",
"hidden": true,
"updated_date": "2025-06-03T12:36:02.818575Z"
},
"3_x1_importar_to_master.py": {
"display_name": "x1_importar_to_master",
"description": "",
"long_description": "",
"hidden": true,
"updated_date": "2025-06-03T12:36:11.404143Z"
},
"3_x2_master_export2translate.py": {
"display_name": "x2_master_export2translate",
"description": "",
"long_description": "",
"hidden": true,
"updated_date": "2025-06-03T12:36:13.715080Z"
},
"3_x3_llm_auto_translate.py": {
"display_name": "x3_llm_auto_translate",
"description": "",
"long_description": "",
"hidden": true,
"updated_date": "2025-06-03T12:36:17.369339Z"
},
"3_x4B_integrate_manual_translates_to_master.py": {
"display_name": "x4B_integrate_manual_translates_to_master",
"description": "",
"long_description": "",
"hidden": true,
"updated_date": "2025-06-03T12:36:19.661939Z"
},
"3_x4_integrate_translates_to_master.py": {
"display_name": "x4_integrate_translates_to_master",
"description": "",
"long_description": "",
"hidden": true,
"updated_date": "2025-06-03T12:36:21.890504Z"
},
"3_x5_complete_empty_cells_master.py": {
"display_name": "x5_complete_empty_cells_master",
"description": "",
"long_description": "",
"hidden": true,
"updated_date": "2025-06-03T12:36:24.140382Z"
},
"3_x6_update_from_master.py": {
"display_name": "x6_update_from_master",
"description": "",
"long_description": "",
"hidden": true,
"updated_date": "2025-06-03T12:36:30.996433Z"
},
"3_menu_pasos_traduccion.py": {
"display_name": "Menu para Traducir x1-x6",
"description": "Menu para Traducir Textos de Siemens o Allenbradley",
"long_description": "",
"hidden": false,
"updated_date": "2025-06-03T12:37:14.675034Z"
}
},
"updated_date": "2025-06-03T12:07:20.450126Z"
"updated_date": "2025-06-03T13:44:26.045048Z"
}

View File

@ -22,6 +22,17 @@
"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"
}
],
"categories": {

View File

@ -1,6 +1,82 @@
[12:09:48] Ejecutando script GUI: calc.py
[12:09:48] Entorno Python: tia_scripting
[12:09:48] Comando: C:\Users\migue\miniconda3\envs\tia_scripting\python.exe D:/Proyectos/Scripts/Calcv2\calc.py
[12:09:48] Directorio: D:/Proyectos/Scripts/Calcv2
[12:09:48] Script GUI ejecutado con PID: 47056
[12:09:48] ID de ejecución: 03b026d2
[16:30:18] Iniciando ejecución de x1_export_CAx.py en D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia...
[16:30:19] --- TIA Portal Project CAx Exporter and Analyzer ---
[16:30:24] Selected Project: D:/Trabajo/VM/44 - 98050 - Fiera/InLavoro/PLC/98050_PLC_01/98050_PLC_01.ap19
[16:30:24] Using Output Directory (Working Directory): D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia
[16:30:24] Detected TIA Portal version: 19.0 (from extension .ap19)
[16:30:24] Will export CAx data to: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_CAx_Export.aml
[16:30:24] Will generate summary to: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_CAx_Summary.md
[16:30:24] Export log file: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_CAx_Export.log
[16:30:24] Connecting to TIA Portal V19.0...
[16:30:25] 2025-06-06 16:30:25,861 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Global OpenPortal - Start TIA Portal, please acknowledge the security dialog.
[16:30:25] 2025-06-06 16:30:25,895 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Global OpenPortal - With user interface
[16:32:34] Connected.
[16:32:34] Opening project: 98050_PLC_01.ap19...
[16:32:34] 2025-06-06 16:32:34,404 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Portal OpenProject - Open project... D:\Trabajo\VM\44 - 98050 - Fiera\InLavoro\PLC\98050_PLC_01\98050_PLC_01.ap19
[16:33:05] Project opened.
[16:33:05] Exporting CAx data for the project to D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_CAx_Export.aml...
[16:33:52] CAx data exported successfully.
[16:33:52] Closing TIA Portal...
[16:33:52] 2025-06-06 16:33:52,008 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Portal ClosePortal - Close TIA Portal
[16:33:52] TIA Portal closed.
[16:33:52] Parsing AML file: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_CAx_Export.aml
[16:33:52] Markdown summary written to: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_CAx_Summary.md
[16:33:52] Script finished.
[16:33:55] Ejecución de x1_export_CAx.py finalizada (success). Duración: 0:03:37.052535.
[16:33:55] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\IO_adaptation\log_x1_export_CAx.txt
[16:34:01] Iniciando ejecución de x2_process_CAx.py en D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia...
[16:34:01] --- AML (CAx Export) to Hierarchical JSON and Obsidian MD Converter (v31.1 - Corrected IO Summary Table Initialization) ---
[16:34:01] Using Working Directory for Output: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia
[16:34:09] Input AML: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_CAx_Export.aml
[16:34:09] Output Directory: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia
[16:34:09] Output JSON: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_CAx_Export.hierarchical.json
[16:34:09] Output IO Debug Tree MD: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_CAx_Export_IO_Upward_Debug.md
[16:34:09] Processing AML file: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_CAx_Export.aml
[16:34:09] Pass 1: Found 363 InternalElement(s). Populating device dictionary...
[16:34:09] Pass 2: Identifying PLCs and Networks (Refined v2)...
[16:34:09] Identified Network: PN/IE_1 (fcbafb48-c53d-4af5-8143-794df99be4d6) Type: Unknown
[16:34:09] Identified PLC: A40510 (fc0d3bac-267e-488a-8dcf-7dc8599d80e8) - Type: CPU 1514SP T-2 PN OrderNo: 6ES7 514-2VN03-0AB0
[16:34:09] Pass 3: Processing InternalLinks (Robust Network Mapping & IO)...
[16:34:09] Found 103 InternalLink(s).
[16:34:09] Mapping Device/Node 'E1' (NodeID:ffef8b7b-b6bf-46aa-b3b5-f5083c420563, Addr:10.1.30.11) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:221c248d-83d8-464c-9425-c949086b34e7, Addr:10.1.30.58) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:5c3bf795-ecec-47e7-a962-4d54c95b9887, Addr:10.1.30.59) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:4100c829-d889-489a-971c-a69207333e2f, Addr:10.1.30.60) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:a6aed467-1a4c-4a5a-bf3d-e7ce46e94a9f, Addr:10.1.30.61) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:b3d71b04-f99a-4af8-aa69-2ca02a30d391, Addr:10.1.30.62) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:eb59daf8-f59c-435e-bfe4-8e7afd0937bd, Addr:10.1.30.63) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:3e931f82-08a5-44de-89a5-b907f5aaeb24, Addr:10.1.30.64) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:f673d611-7a73-4a8e-912d-ea16c294d51b, Addr:10.1.30.65) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:09a2569a-4948-4830-9dd8-315ae638e391, Addr:10.1.30.66) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:719a9643-c903-454e-8325-0f03cf871c35, Addr:10.1.30.31) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:ef8c3829-2363-43b3-ae20-7e903c2517a5, Addr:10.1.30.32) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:7cece978-67aa-4707-b23c-375ebddfc2bc, Addr:10.1.30.170) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:ad86d223-8f60-41c0-8208-c88d68e42154, Addr:10.1.30.33) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:fa86bb0c-6142-41ce-ba6c-565e1e1f810e, Addr:10.1.30.34) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:441a0b56-5830-4595-ab8d-b073b6db8545, Addr:10.1.30.35) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:8dbc0c6b-a8f5-491c-832b-4af0757c259d, Addr:10.1.30.36) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:d06cdd21-62c3-4cff-9389-12a4e21869a2, Addr:10.1.30.40) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:f63db18d-dc19-48b8-afaa-be557db4d13e, Addr:10.1.30.44) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:21589a71-d4dd-4534-b457-deb72f3f7660, Addr:10.1.30.41) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:230ddec2-fa03-44d9-8350-0c0eeb463400, Addr:10.1.30.42) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:b2647d59-ed85-44c5-813f-167c41ea1ca1, Addr:10.1.30.43) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:9933a2a3-47db-448a-a724-ed583d5994bb, Addr:10.1.30.37) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:9b45edaa-0640-4e86-ba59-1b991c1a85ca, Addr:10.1.30.45) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:a5fbeadb-a8ab-42e0-b58f-296e44633ce5, Addr:10.1.30.46) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:67d1d9ad-cf1d-43aa-9dc4-9e574cda5e5b, Addr:10.1.30.47) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:08689cb8-1b00-403f-bc20-c5911a20e655, Addr:10.1.30.48) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:a06a0e94-b651-4510-bbe0-8a0c3b717283, Addr:10.1.30.49) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:cf339d67-1758-4462-85f0-9e6a002d1c3c, Addr:10.1.30.70) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:d80a9fcf-5e0d-45a9-b617-362a9cb4121a, Addr:10.1.30.71) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:7c28d11a-9673-4957-931d-8840a589da85, Addr:10.1.30.72) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:ad792043-fdb8-4695-a10a-aa2cad996cf0, Addr:10.1.30.74) to Network 'PN/IE_1'
[16:34:09] Mapping Device/Node 'IE1' (NodeID:b30940b8-6cb5-4143-854f-9b569f081703, Addr:10.1.30.73) to Network 'PN/IE_1'
[16:34:09] Data extraction and structuring complete.
[16:34:09] Generating JSON output: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_CAx_Export.hierarchical.json
[16:34:09] JSON data written successfully.
[16:34:09] IO upward debug tree written to: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\98050_PLC_01_CAx_Export_IO_Upward_Debug.md
[16:34:09] Found 1 PLC(s). Generating individual hardware trees...
[16:34:09] 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_CAx_Export_Hardware_Tree.md
[16:34:09] Markdown tree summary written to: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\A40510\Documentation\98050_PLC_01_CAx_Export_Hardware_Tree.md
[16:34:09] Script finished.
[16:34:09] Ejecución de x2_process_CAx.py finalizada (success). Duración: 0:00:08.316649.
[16:34:09] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\IO_adaptation\log_x2_process_CAx.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
@ -219,45 +221,57 @@ class LauncherManager:
def get_group_scripts(self, group_id: str) -> List[Dict[str, Any]]:
"""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 = []
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, {})
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}")
# Verificar si está oculto
if metadata.get("hidden", False):
continue
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, {})
scripts.append({
"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)
})
# 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]]:
@ -374,8 +388,12 @@ class LauncherManager:
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 y entorno específico"""
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:
@ -387,27 +405,149 @@ class LauncherManager:
# Determinar el ejecutable de Python a usar
python_env = group.get("python_env", "base")
python_executable = self._get_python_executable(python_env)
python_executable = self._get_python_executable(python_env, use_pythonw)
# Construir comando
cmd = [python_executable, script_path] + script_args
working_dir = group["directory"] # Por defecto directorio del script
# 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()
process = subprocess.Popen(
cmd,
cwd=working_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == "win32" else 0
)
if use_pythonw:
# Con pythonw.exe: No capturar salida, solo ejecutar
try:
process = subprocess.Popen(
cmd,
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]
@ -417,13 +557,14 @@ class LauncherManager:
"script_name": script_name,
"executed_date": start_time.isoformat() + "Z",
"arguments": script_args,
"working_directory": working_dir,
"working_directory": exec_working_dir,
"python_env": python_env,
"status": "running",
"pid": process.pid
"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 {
@ -438,9 +579,23 @@ class LauncherManager:
broadcast_func(error_msg)
return {"status": "error", "message": error_msg}
def _get_python_executable(self, env_name: str) -> str:
"""Obtener el ejecutable de Python para un entorno específico"""
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
@ -453,11 +608,23 @@ class LauncherManager:
]
for base_path in miniconda_paths:
env_path = os.path.join(base_path, "envs", env_name, "python.exe")
if os.path.exists(env_path):
return env_path
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
# Fallback al sistema
# 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]:
@ -616,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

View File

@ -11,6 +11,7 @@ class LauncherManager {
this.currentFilter = 'all';
this.currentEditingGroup = null;
this.pythonEnvs = [];
this.markdownFiles = [];
}
async init() {
@ -71,7 +72,7 @@ class LauncherManager {
const response = await fetch('/api/launcher-favorites');
const data = await response.json();
this.favorites = new Set(data.favorites.map(f => `${f.group_id}_${f.script_name}`));
this.renderFavorites(data.favorites);
await this.renderFavorites(data.favorites);
} catch (error) {
console.error('Error loading favorites:', error);
}
@ -118,9 +119,15 @@ class LauncherManager {
const selector = document.getElementById('launcher-group-select');
if (!selector) return;
// Guardar la selección actual
const currentSelection = this.getCurrentGroupSelection();
selector.innerHTML = '<option value="">-- Seleccionar Grupo --</option>';
this.groups.forEach(group => {
// Ordenar grupos alfabéticamente por nombre
const sortedGroups = [...this.groups].sort((a, b) => a.name.localeCompare(b.name));
sortedGroups.forEach(group => {
const option = document.createElement('option');
option.value = group.id;
option.textContent = group.name;
@ -128,6 +135,32 @@ class LauncherManager {
option.dataset.description = group.description;
selector.appendChild(option);
});
// Restaurar la selección guardada
this.restoreGroupSelection(currentSelection);
}
// Nuevo método para obtener la selección actual del localStorage
getCurrentGroupSelection() {
const currentValue = document.getElementById('launcher-group-select')?.value;
return localStorage.getItem('launcher-selected-group') || currentValue || '';
}
// Nuevo método para restaurar la selección desde localStorage
restoreGroupSelection(groupId) {
const selector = document.getElementById('launcher-group-select');
if (!selector || !groupId) return;
// Verificar que el grupo aún existe
const groupExists = this.groups.some(g => g.id === groupId);
if (groupExists) {
selector.value = groupId;
// Cargar scripts del grupo restaurado automáticamente
this.loadLauncherScripts();
} else {
// Si el grupo ya no existe, limpiar localStorage
localStorage.removeItem('launcher-selected-group');
}
}
renderCategoryFilter() {
@ -152,26 +185,60 @@ class LauncherManager {
async loadLauncherScripts() {
const groupId = document.getElementById('launcher-group-select').value;
// Guardar la selección en localStorage
if (groupId) {
localStorage.setItem('launcher-selected-group', groupId);
} else {
localStorage.removeItem('launcher-selected-group');
}
if (!groupId) {
this.scripts = [];
this.markdownFiles = [];
this.renderScripts();
this.renderMarkdownFiles();
this.updateManageScriptsButton(false);
this.updateEditorButtons(false);
return;
}
// Cargar scripts (parte crítica)
try {
const response = await fetch(`/api/launcher-scripts/${groupId}`);
this.scripts = await response.json();
this.currentGroup = this.groups.find(g => g.id === groupId);
this.updateGroupIcon();
this.renderScripts();
this.updateManageScriptsButton(true);
if (response.ok) {
this.scripts = await response.json();
} else {
console.error('Error loading scripts:', response.status, response.statusText);
this.scripts = [];
}
} catch (error) {
console.error('Error loading launcher scripts:', error);
this.scripts = [];
this.renderScripts();
this.updateManageScriptsButton(false);
}
// Cargar archivos markdown (parte opcional)
try {
const markdownResponse = await fetch(`/api/launcher-markdown/${groupId}`);
if (markdownResponse.ok) {
const markdownData = await markdownResponse.json();
this.markdownFiles = markdownData.files || [];
} else {
console.warn('No markdown files available for group:', groupId);
this.markdownFiles = [];
}
} catch (error) {
console.warn('Error loading markdown files (non-critical):', error);
this.markdownFiles = [];
}
// Actualizar interfaz
this.currentGroup = this.groups.find(g => g.id === groupId);
this.updateGroupIcon();
this.renderScripts();
this.renderMarkdownFiles();
this.updateManageScriptsButton(this.scripts.length > 0);
this.updateEditorButtons(true); // Usar la nueva función
}
updateManageScriptsButton(show) {
@ -181,6 +248,18 @@ class LauncherManager {
}
}
// Función actualizada para mostrar/ocultar ambos botones de editor
updateEditorButtons(show) {
const vscodeButton = document.getElementById('vscode-launcher-btn');
const cursorButton = document.getElementById('cursor-launcher-btn');
if (vscodeButton) {
vscodeButton.style.display = show ? 'inline-flex' : 'none'; // Usar inline-flex para iconos+texto si aplica
}
if (cursorButton) {
cursorButton.style.display = show ? 'inline-flex' : 'none'; // Usar inline-flex para iconos+texto si aplica
}
}
updateGroupIcon() {
const iconElement = document.getElementById('selected-group-icon');
if (!iconElement || !this.currentGroup) return;
@ -230,7 +309,9 @@ class LauncherManager {
let filteredScripts = this.scripts;
if (this.currentFilter !== 'all' && this.currentGroup) {
if (this.currentGroup.category !== this.currentFilter) {
// Filtrar solo si la categoría del grupo no coincide con el filtro
const groupCategory = this.currentGroup.category;
if (groupCategory !== this.currentFilter) {
filteredScripts = [];
}
}
@ -253,15 +334,30 @@ class LauncherManager {
<p class="text-sm text-gray-600 mb-3 line-clamp-2">${script.description || 'Script: ' + script.name}</p>
<div class="flex justify-between items-center">
<span class="category-badge">${this.currentGroup.category}</span>
<div class="space-x-2">
<button class="text-blue-500 hover:underline text-sm"
onclick="launcherManager.showArgsModal('${script.name}', '${script.display_name}')">
Con Argumentos
</button>
<button class="bg-blue-500 text-white px-3 py-1 rounded text-sm hover:bg-blue-600"
onclick="launcherManager.executeScript('${script.name}')">
Ejecutar
</button>
<div class="flex flex-col gap-1 text-xs">
<div class="flex gap-1">
${script.long_description && script.long_description.trim() ? `
<button class="text-green-600 hover:underline"
onclick="launcherManager.showDescriptionModal('${script.name}', '${this.escapeHtml(script.display_name)}')">
Descripción
</button>` : ''}
<button class="text-blue-500 hover:underline"
onclick="launcherManager.showArgsModal('${script.name}', '${this.escapeHtml(script.display_name)}')">
Con Argumentos
</button>
</div>
<div class="flex gap-1">
<button class="bg-blue-500 text-white px-2 py-1 rounded hover:bg-blue-600 text-xs"
onclick="launcherManager.executeScript('${script.name}', [], null, false)"
title="Ejecutar con python.exe (muestra logs)">
🖥 Con Log
</button>
<button class="bg-green-500 text-white px-2 py-1 rounded hover:bg-green-600 text-xs"
onclick="launcherManager.executeScript('${script.name}', [], null, true)"
title="Ejecutar con pythonw.exe (sin ventana de consola)">
🚀 Sin Log
</button>
</div>
</div>
</div>
`;
@ -269,6 +365,111 @@ class LauncherManager {
});
}
renderMarkdownFiles() {
const container = document.getElementById('markdown-files-section');
if (!container) return;
if (!this.currentGroup || this.markdownFiles.length === 0) {
container.style.display = 'none';
return;
}
container.style.display = 'block';
const grid = document.getElementById('markdown-files-grid');
if (!grid) return;
grid.innerHTML = '';
this.markdownFiles.forEach(file => {
const card = document.createElement('div');
card.className = 'markdown-file-card bg-white border rounded-lg p-3 hover:shadow-md transition-shadow cursor-pointer';
card.innerHTML = `
<div class="flex items-start justify-between">
<div class="flex-1">
<h4 class="font-medium text-gray-900 text-sm">${file.display_name}</h4>
<p class="text-xs text-gray-500 mt-1">
📄 ${(file.size / 1024).toFixed(1)} KB
${file.level === 1 ? ' • Subdirectorio' : ''}
</p>
</div>
<span class="text-xs text-gray-400">${this.getTimeAgo(file.modified)}</span>
</div>
`;
card.onclick = () => this.openMarkdownViewer(file.relative_path, file.display_name);
grid.appendChild(card);
});
}
async openMarkdownViewer(relativePath, displayName) {
if (!this.currentGroup) return;
try {
const response = await fetch(`/api/launcher-markdown-content/${this.currentGroup.id}/${encodeURIComponent(relativePath)}`);
const result = await response.json();
if (result.status === 'success') {
const modal = document.getElementById('markdown-viewer-modal');
const titleElement = document.getElementById('markdown-viewer-title');
const pathElement = document.getElementById('markdown-viewer-path');
const contentElement = document.getElementById('markdown-viewer-content');
if (modal && titleElement && contentElement) {
titleElement.textContent = displayName;
pathElement.textContent = `Archivo: ${relativePath}`;
// Renderizar markdown
const md = window.markdownit();
contentElement.innerHTML = md.render(result.content);
modal.classList.remove('hidden');
}
} else {
alert(`Error: ${result.message}`);
}
} catch (error) {
console.error('Error loading markdown file:', error);
alert('Error cargando archivo Markdown');
}
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
async showDescriptionModal(scriptName, displayName) {
if (!this.currentGroup) return;
try {
// Cargar metadatos del script para obtener la descripción larga
const response = await fetch(`/api/launcher-script-metadata/${this.currentGroup.id}/${scriptName}`);
const metadata = await response.json();
const modal = document.getElementById('script-description-modal');
const scriptNameElement = document.getElementById('desc-modal-script-name');
const scriptFileElement = document.getElementById('desc-modal-script-file');
const contentElement = document.getElementById('script-description-content');
if (modal && scriptNameElement && contentElement) {
scriptNameElement.textContent = displayName;
scriptFileElement.textContent = `Archivo: ${scriptName}`;
// Renderizar markdown
if (metadata.long_description && metadata.long_description.trim()) {
const md = window.markdownit();
contentElement.innerHTML = md.render(metadata.long_description);
} else {
contentElement.innerHTML = '<p class="text-gray-500 italic">No hay descripción disponible para este script.</p>';
}
modal.classList.remove('hidden');
}
} catch (error) {
console.error('Error loading script description:', error);
alert('Error cargando la descripción del script');
}
}
// === GESTIÓN DE SCRIPTS INDIVIDUALES ===
async openScriptManager() {
@ -525,11 +726,45 @@ class LauncherManager {
const timeAgo = this.getTimeAgo(entry.executed_date);
const statusClass = entry.status === 'success' ? 'success' :
entry.status === 'error' ? 'error' : 'running';
const statusIcon = entry.status === 'success' ? '✅' :
entry.status === 'error' ? '❌' : '🔄';
// Información del entorno Python
// Iconos y mensajes más descriptivos por status
let statusIcon, statusText;
switch (entry.status) {
case 'success':
statusIcon = '✅';
statusText = 'Completado';
break;
case 'error':
statusIcon = '❌';
statusText = 'Error';
break;
case 'running':
statusIcon = '🔄';
statusText = 'En ejecución (GUI activa)';
break;
default:
statusIcon = '❓';
statusText = entry.status;
}
// Información del entorno Python y ejecutable
const envInfo = entry.python_env ? `${entry.python_env}` : '';
const executableType = entry.executable_type || 'python.exe';
const executableIcon = executableType.includes('pythonw') ? '🚀' : '🖥️';
// Botones para procesos en ejecución
const processButtons = entry.status === 'running' && entry.pid ? `
<div class="flex gap-1 mt-2">
<button onclick="launcherManager.focusProcess(${entry.pid})"
class="text-blue-500 hover:underline text-xs">
📍 Activar
</button>
<button onclick="launcherManager.terminateProcess(${entry.pid})"
class="text-red-500 hover:underline text-xs">
Cerrar
</button>
</div>
` : '';
const item = document.createElement('div');
item.className = `history-item ${statusClass}`;
@ -542,10 +777,14 @@ class LauncherManager {
<span class="text-xs text-gray-400">${timeAgo}</span>
</div>
<div class="text-sm text-gray-600 mt-1">
${statusIcon} ${entry.status.charAt(0).toUpperCase() + entry.status.slice(1)}
${entry.execution_time ? ` - ${entry.execution_time}s` : ''}
${entry.arguments && entry.arguments.length > 0 ? ` - Con argumentos` : ''}
${statusIcon} ${statusText}
${entry.execution_time ? ` ${entry.execution_time.toFixed(1)}s` : ''}
${entry.arguments && entry.arguments.length > 0 ? ` Con argumentos` : ''}
</div>
<div class="text-xs text-gray-500 mt-1">
${executableIcon} ${executableType}${entry.working_directory ? `${entry.working_directory}` : ''}
</div>
${processButtons}
`;
historyList.appendChild(item);
});
@ -608,20 +847,46 @@ class LauncherManager {
}
}
async executeScript(scriptName, args = []) {
showArgsModal(scriptName, displayName) {
const modal = document.getElementById('script-args-modal');
const scriptDisplayElement = document.getElementById('script-display-name');
const argsInput = document.getElementById('script-args-input');
const workingDirInput = document.getElementById('script-working-dir');
if (modal && scriptDisplayElement && argsInput) {
scriptDisplayElement.textContent = displayName;
argsInput.value = '';
workingDirInput.value = ''; // Limpiar directorio de trabajo
modal.classList.remove('hidden');
// Guardar datos para uso posterior
modal.dataset.scriptName = scriptName;
modal.dataset.groupId = this.currentGroup.id;
}
}
async executeScript(scriptName, args = [], workingDir = null, usePythonw = false) {
if (!this.currentGroup) return;
try {
const requestData = {
group_id: this.currentGroup.id,
script_name: scriptName,
args: args,
use_pythonw: usePythonw
};
// Agregar directorio de trabajo si se especifica
if (workingDir && workingDir.trim()) {
requestData.working_dir = workingDir.trim();
}
const response = await fetch('/api/execute-gui-script', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
group_id: this.currentGroup.id,
script_name: scriptName,
args: args
})
body: JSON.stringify(requestData)
});
const result = await response.json();
@ -634,23 +899,21 @@ class LauncherManager {
}
}
showArgsModal(scriptName, displayName) {
const modal = document.getElementById('script-args-modal');
const scriptDisplayElement = document.getElementById('script-display-name');
const argsInput = document.getElementById('script-args-input');
if (modal && scriptDisplayElement && argsInput) {
scriptDisplayElement.textContent = displayName;
argsInput.value = '';
modal.classList.remove('hidden');
// Guardar datos para uso posterior
modal.dataset.scriptName = scriptName;
modal.dataset.groupId = this.currentGroup.id;
}
// Función para buscar directorio de trabajo
browseWorkingDirectory() {
fetch('/api/browse-directories')
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
document.getElementById('script-working-dir').value = data.path;
}
})
.catch(error => {
console.error('Error browsing directory:', error);
});
}
renderFavorites(favorites) {
async renderFavorites(favorites) {
const favoritesList = document.getElementById('favorites-list');
const favoritesPanel = document.getElementById('favorites-panel');
@ -664,35 +927,154 @@ class LauncherManager {
favoritesPanel.classList.remove('empty');
favoritesList.innerHTML = '';
favorites.slice(0, 5).forEach(fav => {
const group = this.groups.find(g => g.id === fav.group_id);
if (!group) return;
// Cambiar a diseño de grid para cards más compactas
favoritesList.className = 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3';
const item = document.createElement('div');
item.className = 'flex items-center justify-between p-2 bg-white rounded border';
item.innerHTML = `
<div class="flex items-center">
<div class="group-icon-small default mr-2">
${this.getDefaultIconForCategory(group.category)}
// Obtener metadatos de todos los scripts favoritos
for (const fav of favorites.slice(0, 6)) { // Mostrar hasta 6 favoritos en grid
const group = this.groups.find(g => g.id === fav.group_id);
if (!group) continue;
try {
// Obtener metadatos del script para mostrar el display_name
const response = await fetch(`/api/launcher-script-metadata/${fav.group_id}/${fav.script_name}`);
const metadata = await response.json();
const displayName = metadata.display_name || fav.script_name.replace('.py', '');
const card = document.createElement('div');
card.className = 'bg-white border border-yellow-300 rounded-lg p-3 hover:shadow-md transition-all duration-200 hover:border-yellow-400';
// Crear contenedor para el icono que se actualizará dinámicamente
const iconId = `fav-icon-${fav.group_id}`;
card.innerHTML = `
<div class="flex items-start justify-between mb-2">
<div class="flex items-center min-w-0 flex-1">
<div id="${iconId}" class="group-icon-small default mr-2 flex-shrink-0">
${this.getDefaultIconForCategory(group.category)}
</div>
<div class="min-w-0 flex-1">
<div class="font-medium text-sm text-gray-900 truncate" title="${displayName}">
${displayName}
</div>
<div class="text-xs text-gray-500 truncate" title="${group.name}">
${group.name}
</div>
</div>
</div>
<button class="favorite-star active text-yellow-500 ml-2 flex-shrink-0"
onclick="launcherManager.toggleFavorite('${fav.group_id}', '${fav.script_name}')"
title="Quitar de favoritos">
</button>
</div>
<div>
<div class="font-medium text-sm">${fav.script_name.replace('.py', '')}</div>
<div class="text-xs text-gray-500">${group.name}</div>
<div class="flex gap-1">
<button class="bg-blue-500 text-white px-2 py-1 rounded text-xs hover:bg-blue-600 flex-1"
onclick="launcherManager.executeFavoriteScript('${fav.group_id}', '${fav.script_name}')"
title="Ejecutar script">
🖥 Ejecutar
</button>
<button class="bg-green-500 text-white px-2 py-1 rounded text-xs hover:bg-green-600 flex-1"
onclick="launcherManager.executeFavoriteScriptSilent('${fav.group_id}', '${fav.script_name}')"
title="Ejecutar sin ventana de consola">
🔇 Silencioso
</button>
</div>
</div>
<button class="text-blue-500 hover:underline text-sm"
onclick="launcherManager.executeFavoriteScript('${fav.group_id}', '${fav.script_name}')">
Ejecutar
</button>
`;
favoritesList.appendChild(item);
});
`;
favoritesList.appendChild(card);
// Intentar cargar el icono personalizado del grupo después de agregar la card
this.loadGroupIconForFavorite(iconId, fav.group_id, group.category);
} catch (error) {
console.error('Error loading script metadata for favorite:', error);
// Fallback al nombre del archivo
const card = document.createElement('div');
card.className = 'bg-white border border-yellow-300 rounded-lg p-3 hover:shadow-md transition-all duration-200 hover:border-yellow-400';
const iconId = `fav-icon-${fav.group_id}`;
card.innerHTML = `
<div class="flex items-start justify-between mb-2">
<div class="flex items-center min-w-0 flex-1">
<div id="${iconId}" class="group-icon-small default mr-2 flex-shrink-0">
${this.getDefaultIconForCategory(group.category)}
</div>
<div class="min-w-0 flex-1">
<div class="font-medium text-sm text-gray-900 truncate" title="${fav.script_name.replace('.py', '')}">
${fav.script_name.replace('.py', '')}
</div>
<div class="text-xs text-gray-500 truncate" title="${group.name}">
${group.name}
</div>
</div>
</div>
<button class="favorite-star active text-yellow-500 ml-2 flex-shrink-0"
onclick="launcherManager.toggleFavorite('${fav.group_id}', '${fav.script_name}')"
title="Quitar de favoritos">
</button>
</div>
<div class="flex gap-1">
<button class="bg-blue-500 text-white px-2 py-1 rounded text-xs hover:bg-blue-600 flex-1"
onclick="launcherManager.executeFavoriteScript('${fav.group_id}', '${fav.script_name}')"
title="Ejecutar script">
🖥 Ejecutar
</button>
<button class="bg-green-500 text-white px-2 py-1 rounded text-xs hover:bg-green-600 flex-1"
onclick="launcherManager.executeFavoriteScriptSilent('${fav.group_id}', '${fav.script_name}')"
title="Ejecutar sin ventana de consola">
🔇 Silencioso
</button>
</div>
`;
favoritesList.appendChild(card);
// Intentar cargar el icono personalizado del grupo
this.loadGroupIconForFavorite(iconId, fav.group_id, group.category);
}
}
}
// Nuevo método para cargar iconos específicos de grupo en favoritos
loadGroupIconForFavorite(iconElementId, groupId, fallbackCategory) {
const iconElement = document.getElementById(iconElementId);
if (!iconElement) return;
// Intentar cargar icono personalizado
const img = document.createElement('img');
img.src = `/api/group-icon/launcher/${groupId}`;
img.className = 'w-5 h-5 rounded object-cover';
img.onerror = () => {
// Fallback a icono por defecto basado en categoría
iconElement.innerHTML = this.getDefaultIconForCategory(fallbackCategory);
iconElement.className = 'group-icon-small default mr-2 flex-shrink-0';
};
img.onload = () => {
// Reemplazar con imagen personalizada
iconElement.innerHTML = '';
iconElement.className = 'mr-2 flex-shrink-0 flex items-center justify-center';
iconElement.appendChild(img);
};
}
// Nuevo método para ejecutar favoritos en modo silencioso
async executeFavoriteScriptSilent(groupId, scriptName) {
// Cambiar al grupo correcto si no está seleccionado
if (!this.currentGroup || this.currentGroup.id !== groupId) {
document.getElementById('launcher-group-select').value = groupId;
localStorage.setItem('launcher-selected-group', groupId);
await this.loadLauncherScripts();
}
this.executeScript(scriptName, [], null, true); // true para usar pythonw
}
async executeFavoriteScript(groupId, scriptName) {
// Cambiar al grupo correcto si no está seleccionado
if (!this.currentGroup || this.currentGroup.id !== groupId) {
document.getElementById('launcher-group-select').value = groupId;
localStorage.setItem('launcher-selected-group', groupId);
await this.loadLauncherScripts();
}
@ -788,6 +1170,7 @@ class LauncherManager {
this.scripts = [];
this.renderScripts();
this.updateManageScriptsButton(false);
this.updateEditorButtons(false); // Ocultar botones de editor
}
} else {
alert(`Error: ${result.message}`);
@ -811,6 +1194,83 @@ class LauncherManager {
console.error('Error browsing directory:', error);
});
}
// Nueva función genérica para abrir en editor
async openGroupInEditor(editorCode, groupSystem, groupId) {
if (!groupId) {
alert('Selecciona un grupo primero');
return;
}
const editorName = editorCode.toUpperCase();
try {
const response = await fetch(`/api/open-editor/${editorCode}/${groupSystem}/${groupId}`, {
method: 'POST'
});
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 {
const result = await response.json(); // Now it's safer to parse JSON
if (result.status === 'success') {
console.log(result.message);
// Opcional: mostrar un toast/notificación de éxito
// showToast(result.message, 'success');
} else {
console.error(`Error al abrir ${editorName}:`, result.message);
alert(`Error: ${result.message || 'Error desconocido.'}`);
}
}
} catch (error) {
console.error(`Error en la llamada fetch para open-editor (${editorName}):`, error);
alert(`Error de red o del cliente al intentar abrir ${editorName}.`);
}
}
async focusProcess(pid) {
try {
const response = await fetch(`/api/launcher-process-focus/${pid}`, {
method: 'POST'
});
const result = await response.json();
if (result.status === 'success') {
console.log(result.message);
} else {
alert(`Error: ${result.message}`);
}
} catch (error) {
console.error('Error focusing process:', error);
alert('Error activando proceso');
}
}
async terminateProcess(pid) {
if (!confirm('¿Estás seguro de que quieres cerrar este proceso?')) return;
try {
const response = await fetch(`/api/launcher-process-terminate/${pid}`, {
method: 'POST'
});
const result = await response.json();
if (result.status === 'success') {
console.log(result.message);
// Recargar historial para actualizar estado
await this.loadHistory();
} else {
alert(`Error: ${result.message}`);
}
} catch (error) {
console.error('Error terminating process:', error);
alert('Error cerrando proceso');
}
}
}
// === FUNCIONES GLOBALES ===
@ -909,16 +1369,49 @@ function closeArgsModal() {
function executeWithArgs() {
const modal = document.getElementById('script-args-modal');
const argsInput = document.getElementById('script-args-input');
const workingDirInput = document.getElementById('script-working-dir');
const executionTypeInputs = document.getElementsByName('execution-type');
if (modal && argsInput && window.launcherManager) {
const scriptName = modal.dataset.scriptName;
const args = argsInput.value.trim().split(/\s+/).filter(arg => arg.length > 0);
const workingDir = workingDirInput.value.trim();
window.launcherManager.executeScript(scriptName, args);
// Leer el tipo de ejecución seleccionado
let usePythonw = false;
for (const input of executionTypeInputs) {
if (input.checked) {
usePythonw = input.value === 'true';
break;
}
}
window.launcherManager.executeScript(scriptName, args, workingDir, usePythonw);
closeArgsModal();
}
}
function browseWorkingDirectory() {
if (window.launcherManager) {
window.launcherManager.browseWorkingDirectory();
}
}
// Funciones para modal de descripción
function closeDescriptionModal() {
const modal = document.getElementById('script-description-modal');
if (modal) {
modal.classList.add('hidden');
}
}
function closeMarkdownViewer() {
const modal = document.getElementById('markdown-viewer-modal');
if (modal) {
modal.classList.add('hidden');
}
}
// Inicialización cuando se carga la página
document.addEventListener('DOMContentLoaded', function () {
console.log('Launcher JS loaded');

View File

@ -1276,28 +1276,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}.`);
}
}

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,21 @@
</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>
</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,15 +230,34 @@
<!-- Group Selector -->
<div class="mb-4">
<label class="block text-sm font-medium mb-2">Seleccionar Grupo de Scripts</label>
<div class="relative">
<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>
<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>
</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>
</div>
</div>
@ -289,6 +315,17 @@
</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">
@ -609,6 +646,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">
@ -625,10 +695,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') {